From 8d05813c97f730304d7f573bdebebc3682459a36 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Oct 2021 23:08:34 +0200 Subject: [PATCH 0001/1452] Bump version to 2021.12.0dev0 (#58546) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85f7ad1bd6e..587e3bb9509 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Final MAJOR_VERSION: Final = 2021 -MINOR_VERSION: Final = 11 +MINOR_VERSION: Final = 12 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From b34eb539144630043d52a3a4652f72849005c050 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 27 Oct 2021 23:29:28 +0200 Subject: [PATCH 0002/1452] Register LCN devices in device registry (#53143) --- homeassistant/components/lcn/__init__.py | 64 ++++++-- homeassistant/components/lcn/config_flow.py | 9 ++ homeassistant/components/lcn/helpers.py | 153 +++++++++++++++++++- homeassistant/components/lcn/sensor.py | 6 +- tests/components/lcn/conftest.py | 6 + tests/components/lcn/test_config_flow.py | 3 +- tests/components/lcn/test_init.py | 24 ++- tests/fixtures/lcn/config.json | 5 + tests/fixtures/lcn/config_entry_pchk.json | 16 ++ 9 files changed, 258 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 48a63a50fa9..d019c156f37 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -8,6 +8,8 @@ import pypck from homeassistant import config_entries from homeassistant.const import ( + CONF_ADDRESS, + CONF_DOMAIN, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, @@ -16,16 +18,27 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType -from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN, PLATFORMS +from .const import ( + CONF_DIM_MODE, + CONF_DOMAIN_DATA, + CONF_SK_NUM_TRIES, + CONNECTION, + DOMAIN, + PLATFORMS, +) from .helpers import ( + AddressType, DeviceConnectionType, InputType, + async_update_config_entry, generate_unique_id, + get_device_model, import_lcn_config, + register_lcn_address_devices, + register_lcn_host_device, ) from .schemas import CONFIG_SCHEMA # noqa: F401 from .services import SERVICES @@ -96,12 +109,12 @@ async def async_setup_entry( hass.data[DOMAIN][config_entry.entry_id] = { CONNECTION: lcn_connection, } + # Update config_entry with LCN device serials + await async_update_config_entry(hass, config_entry) - # remove orphans from entity registry which are in ConfigEntry but were removed - # from configuration.yaml - if config_entry.source == config_entries.SOURCE_IMPORT: - entity_registry = await er.async_get_registry(hass) - entity_registry.async_clear_config_entry(config_entry.entry_id) + # register/update devices for host, modules and groups in device registry + register_lcn_host_device(hass, config_entry) + register_lcn_address_devices(hass, config_entry) # forward config_entry to components hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -150,17 +163,38 @@ class LcnEntity(Entity): self._unregister_for_inputs: Callable | None = None self._name: str = config[CONF_NAME] + @property + def address(self) -> AddressType: + """Return LCN address.""" + return ( + self.device_connection.seg_id, + self.device_connection.addr_id, + self.device_connection.is_group, + ) + @property def unique_id(self) -> str: """Return a unique ID.""" - unique_device_id = generate_unique_id( - ( - self.device_connection.seg_id, - self.device_connection.addr_id, - self.device_connection.is_group, - ) + return generate_unique_id( + self.entry_id, self.address, self.config[CONF_RESOURCE] ) - return f"{self.entry_id}-{unique_device_id}-{self.config[CONF_RESOURCE]}" + + @property + def device_info(self) -> DeviceInfo | None: + """Return device specific attributes.""" + address = f"{'g' if self.address[2] else 'm'}{self.address[0]:03d}{self.address[1]:03d}" + model = f"LCN {get_device_model(self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA])}" + + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": f"{address}.{self.config[CONF_RESOURCE]}", + "model": model, + "manufacturer": "Issendorff", + "via_device": ( + DOMAIN, + generate_unique_id(self.entry_id, self.config[CONF_ADDRESS]), + ), + } @property def should_poll(self) -> bool: diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 905da4d005c..20549ea32a3 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN @@ -93,6 +94,14 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entry = get_config_entry(self.hass, data) if entry: entry.source = config_entries.SOURCE_IMPORT + + # Cleanup entity and device registry, if we imported from configuration.yaml to + # remove orphans when entities were removed from configuration + entity_registry = er.async_get(self.hass) + entity_registry.async_clear_config_entry(entry.entry_id) + device_registry = dr.async_get(self.hass) + device_registry.async_clear_config_entry(entry.entry_id) + self.hass.config_entries.async_update_entry(entry, data=data) return self.async_abort(reason="existing_configuration_updated") diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index b62f8474470..2834cc1940e 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -1,6 +1,8 @@ """Helpers for LCN component.""" from __future__ import annotations +import asyncio +from itertools import chain import re from typing import Tuple, Type, Union, cast @@ -22,19 +24,23 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_SENSORS, + CONF_SOURCE, CONF_SWITCHES, CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import ( + BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE, CONF_DOMAIN_DATA, CONF_HARDWARE_SERIAL, CONF_HARDWARE_TYPE, + CONF_OUTPUT, CONF_RESOURCE, CONF_SCENES, CONF_SK_NUM_TRIES, @@ -42,6 +48,13 @@ from .const import ( CONNECTION, DEFAULT_NAME, DOMAIN, + LED_PORTS, + LOGICOP_PORTS, + OUTPUT_PORTS, + S0_INPUTS, + SETPOINTS, + THRESHOLDS, + VARIABLES, ) # typing @@ -92,10 +105,43 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str: raise ValueError("Unknown domain") -def generate_unique_id(address: AddressType) -> str: +def get_device_model(domain_name: str, domain_data: ConfigType) -> str: + """Return the model for the specified domain_data.""" + if domain_name in ("switch", "light"): + return "Output" if domain_data[CONF_OUTPUT] in OUTPUT_PORTS else "Relay" + if domain_name in ("binary_sensor", "sensor"): + if domain_data[CONF_SOURCE] in BINSENSOR_PORTS: + return "Binary Sensor" + if domain_data[CONF_SOURCE] in chain( + VARIABLES, SETPOINTS, THRESHOLDS, S0_INPUTS + ): + return "Variable" + if domain_data[CONF_SOURCE] in LED_PORTS: + return "Led" + if domain_data[CONF_SOURCE] in LOGICOP_PORTS: + return "Logical Operation" + return "Key" + if domain_name == "cover": + return "Motor" + if domain_name == "climate": + return "Regulator" + if domain_name == "scene": + return "Scene" + raise ValueError("Unknown domain") + + +def generate_unique_id( + entry_id: str, + address: AddressType, + resource: str | None = None, +) -> str: """Generate a unique_id from the given parameters.""" + unique_id = entry_id is_group = "g" if address[2] else "m" - return f"{is_group}{address[0]:03d}{address[1]:03d}" + unique_id += f"-{is_group}{address[0]:03d}{address[1]:03d}" + if resource: + unique_id += f"-{resource}".lower() + return unique_id def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]: @@ -200,6 +246,109 @@ def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]: return list(data.values()) +def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Register LCN host for given config_entry in device registry.""" + device_registry = dr.async_get(hass) + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Issendorff", + name=config_entry.title, + model="PCHK", + ) + + +def register_lcn_address_devices( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Register LCN modules and groups defined in config_entry as devices in device registry. + + The name of all given device_connections is collected and the devices + are updated. + """ + device_registry = dr.async_get(hass) + + host_identifiers = (DOMAIN, config_entry.entry_id) + + for device_config in config_entry.data[CONF_DEVICES]: + address = device_config[CONF_ADDRESS] + device_name = device_config[CONF_NAME] + identifiers = {(DOMAIN, generate_unique_id(config_entry.entry_id, address))} + + if device_config[CONF_ADDRESS][2]: # is group + device_model = f"LCN group (g{address[0]:03d}{address[1]:03d})" + sw_version = None + else: # is module + hardware_type = device_config[CONF_HARDWARE_TYPE] + if hardware_type in pypck.lcn_defs.HARDWARE_DESCRIPTIONS: + hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[hardware_type] + else: + hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[-1] + device_model = f"{hardware_name} (m{address[0]:03d}{address[1]:03d})" + sw_version = f"{device_config[CONF_SOFTWARE_SERIAL]:06X}" + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers=identifiers, + via_device=host_identifiers, + manufacturer="Issendorff", + sw_version=sw_version, + name=device_name, + model=device_model, + ) + + +async def async_update_device_config( + device_connection: DeviceConnectionType, device_config: ConfigType +) -> None: + """Fill missing values in device_config with infos from LCN bus.""" + is_group = device_config[CONF_ADDRESS][2] + + # fetch serial info if device is module + if not is_group: # is module + await device_connection.serial_known + if device_config[CONF_HARDWARE_SERIAL] == -1: + device_config[CONF_HARDWARE_SERIAL] = device_connection.hardware_serial + if device_config[CONF_SOFTWARE_SERIAL] == -1: + device_config[CONF_SOFTWARE_SERIAL] = device_connection.software_serial + if device_config[CONF_HARDWARE_TYPE] == -1: + device_config[CONF_HARDWARE_TYPE] = device_connection.hardware_type.value + + # fetch name if device is module + if device_config[CONF_NAME] != "": + return + + device_name = "" + if not is_group: + device_name = await device_connection.request_name() + if is_group or device_name == "": + module_type = "Group" if is_group else "Module" + device_name = ( + f"{module_type} " + f"{device_config[CONF_ADDRESS][0]:03d}/" + f"{device_config[CONF_ADDRESS][1]:03d}" + ) + device_config[CONF_NAME] = device_name + + +async def async_update_config_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Fill missing values in config_entry with infos from LCN bus.""" + coros = [] + for device_config in config_entry.data[CONF_DEVICES]: + device_connection = get_device_connection( + hass, device_config[CONF_ADDRESS], config_entry + ) + coros.append(async_update_device_config(device_connection, device_config)) + + await asyncio.gather(*coros) + + # schedule config_entry for save + hass.config_entries.async_update_entry(config_entry) + + def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]: """Validate that all connection names are unique. diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 965e9626f66..66321c79a1b 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,6 +1,7 @@ """Support for LCN sensors.""" from __future__ import annotations +from itertools import chain from typing import cast import pypck @@ -38,9 +39,8 @@ def create_lcn_sensor_entity( hass, entity_config[CONF_ADDRESS], config_entry ) - if ( - entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] - in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS + if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in chain( + VARIABLES, SETPOINTS, THRESHOLDS, S0_INPUTS ): return LcnVariableSensor( entity_config, config_entry.entry_id, device_connection diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index aae4acfa914..81c2fdc68e4 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -21,8 +21,14 @@ class MockModuleConnection(ModuleConnection): status_request_handler = AsyncMock() activate_status_request_handler = AsyncMock() cancel_status_request_handler = AsyncMock() + request_name = AsyncMock(return_value="TestModule") send_command = AsyncMock(return_value=True) + def __init__(self, *args, **kwargs): + """Construct ModuleConnection instance.""" + super().__init__(*args, **kwargs) + self.serials_request_handler.serial_known.set() + class MockGroupConnection(GroupConnection): """Fake a LCN group connection.""" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 5f83ab27762..49351b023b6 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -76,8 +76,7 @@ async def test_step_import_existing_host(hass): ], ) async def test_step_import_error(hass, error, reason): - """Test for authentication error is handled correctly.""" - + """Test for error in import is handled correctly.""" with patch( "pypck.connection.PchkConnectionManager.async_connect", side_effect=error ): diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 79f0eed4e46..e4fb5beef0d 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -10,7 +10,7 @@ from pypck.connection import ( from homeassistant import config_entries from homeassistant.components.lcn.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import MockPchkConnectionManager, init_integration, setup_component @@ -53,19 +53,31 @@ async def test_async_setup_entry_update(hass, entry): """Test a successful setup entry if entry with same id already exists.""" # setup first entry entry.source = config_entries.SOURCE_IMPORT + entry.add_to_hass(hass) # create dummy entity for LCN platform as an orphan - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) dummy_entity = entity_registry.async_get_or_create( "switch", DOMAIN, "dummy", config_entry=entry ) + + # create dummy device for LCN platform as an orphan + device_registry = dr.async_get(hass) + dummy_device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.entry_id, 0, 7, False)}, + via_device=(DOMAIN, entry.entry_id), + ) + assert dummy_entity in entity_registry.entities.values() + assert dummy_device in device_registry.devices.values() - # add entity to hass and setup (should cleanup dummy entity) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + # setup new entry with same data via import step (should cleanup dummy device) + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=entry.data + ) + assert dummy_device not in device_registry.devices.values() assert dummy_entity not in entity_registry.entities.values() diff --git a/tests/fixtures/lcn/config.json b/tests/fixtures/lcn/config.json index 50a1ca05e29..3cbb66b4e31 100644 --- a/tests/fixtures/lcn/config.json +++ b/tests/fixtures/lcn/config.json @@ -25,6 +25,11 @@ "name": "Switch_Output1", "address": "s0.m7", "output": "output1" + }, + { + "name": "Switch_Group5", + "address": "s0.g5", + "output": "relay1" } ] } diff --git a/tests/fixtures/lcn/config_entry_pchk.json b/tests/fixtures/lcn/config_entry_pchk.json index 3058389a95d..a4f78c16b41 100644 --- a/tests/fixtures/lcn/config_entry_pchk.json +++ b/tests/fixtures/lcn/config_entry_pchk.json @@ -13,6 +13,13 @@ "hardware_serial": -1, "software_serial": -1, "hardware_type": -1 + }, + { + "address": [0, 5, true], + "name": "", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 } ], "entities": [ @@ -24,6 +31,15 @@ "domain_data": { "output": "OUTPUT1" } + }, + { + "address": [0, 5, true], + "name": "Switch_Group5", + "resource": "relay1", + "domain": "switch", + "domain_data": { + "output": "RELAY1" + } } ] } From e1e864d2b6576da8493dad0ddc794fd0cf226065 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Oct 2021 15:58:14 -0700 Subject: [PATCH 0003/1452] Get the registry using the callback method (#58542) --- homeassistant/components/agent_dvr/__init__.py | 2 +- homeassistant/components/apple_tv/__init__.py | 7 ++++--- homeassistant/components/bond/__init__.py | 2 +- homeassistant/components/broadlink/device.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/denonavr/__init__.py | 2 +- homeassistant/components/esphome/__init__.py | 7 ++++--- homeassistant/components/firmata/__init__.py | 2 +- .../components/homematicip_cloud/__init__.py | 11 ++++++----- .../components/homematicip_cloud/generic_entity.py | 10 +++++----- homeassistant/components/huawei_lte/__init__.py | 2 +- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/isy994/__init__.py | 7 ++++--- homeassistant/components/isy994/services.py | 5 +++-- homeassistant/components/konnected/panel.py | 2 +- homeassistant/components/lutron_caseta/__init__.py | 14 ++++++++------ homeassistant/components/mobile_app/__init__.py | 2 +- .../components/mobile_app/binary_sensor.py | 2 +- homeassistant/components/mobile_app/config_flow.py | 2 +- homeassistant/components/mobile_app/sensor.py | 2 +- homeassistant/components/mobile_app/webhook.py | 6 +++--- homeassistant/components/motion_blinds/__init__.py | 2 +- homeassistant/components/motioneye/__init__.py | 2 +- homeassistant/components/netgear/__init__.py | 2 +- homeassistant/components/nightscout/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 14 ++++++++------ homeassistant/components/plugwise/gateway.py | 2 +- homeassistant/components/roon/__init__.py | 2 +- homeassistant/components/sia/__init__.py | 2 +- homeassistant/components/sia/hub.py | 7 ++++--- homeassistant/components/simplisafe/__init__.py | 9 ++++----- homeassistant/components/smarttub/controller.py | 8 +++++--- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/syncthru/__init__.py | 2 +- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- homeassistant/components/xiaomi_miio/__init__.py | 2 +- 38 files changed, 84 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index 5b765da7f8e..bb464d09723 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry): hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client} - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index fbf02fcfdff..b710a753da9 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -317,7 +317,7 @@ class AppleTVManager: self._dispatch_send(SIGNAL_CONNECTED, self.atv) self._address_updated(str(conf.address)) - await self._async_setup_device_registry() + self._async_setup_device_registry() self._connection_attempts = 0 if self._connection_was_lost: @@ -327,7 +327,8 @@ class AppleTVManager: ) self._connection_was_lost = False - async def _async_setup_device_registry(self): + @callback + def _async_setup_device_registry(self): attrs = { ATTR_IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)}, ATTR_MANUFACTURER: "Apple", @@ -351,7 +352,7 @@ class AppleTVManager: if dev_info.mac: attrs[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} - device_registry = await dr.async_get_registry(self.hass) + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, **attrs ) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 20b6b0a2ea5..bf40d4c6066 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert hub.bond_id is not None hub_name = hub.name or hub.bond_id - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry_id, identifiers={(DOMAIN, hub.bond_id)}, diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 2686b3dd9ed..aada9ace84a 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -62,7 +62,7 @@ class BroadlinkDevice: Triggered when the device is renamed on the frontend. """ - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index e57abfa3b73..d593f759eb3 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _, model, mac_address = controller_unique_id.split("_", 3) entry_data[CONF_DIRECTOR_MODEL] = model.upper() - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, controller_unique_id)}, diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 818c005b1cd..75bd69cf2e3 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -72,7 +72,7 @@ async def async_unload_entry( hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() # Remove zone2 and zone3 entities if needed - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) unique_id = config_entry.unique_id or config_entry.entry_id zone2_id = f"{unique_id}-Zone2" diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 97b459ae0cd..f985394c8e9 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -267,7 +267,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True - device_id = await _async_setup_device_registry( + device_id = _async_setup_device_registry( hass, entry, entry_data.device_info ) entry_data.async_update_device_state(hass) @@ -320,14 +320,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_setup_device_registry( +@callback +def _async_setup_device_registry( hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo ) -> str: """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index d98866f900b..d147d84b341 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -189,7 +189,7 @@ async def async_setup_entry( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) ) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={}, diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 14c80f56b1a..2f7d8d86012 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False await async_setup_services(hass) - await async_remove_obsolete_entities(hass, entry, hap) + _async_remove_obsolete_entities(hass, entry, hap) # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection hap.reset_connection_listener = hass.bus.async_listen_once( @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Register hap as device in registry. - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) home = hap.home hapname = home.label if home.label != entry.unique_id else f"Home-{home.label}" @@ -118,7 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hap.async_reset() -async def async_remove_obsolete_entities( +@callback +def _async_remove_obsolete_entities( hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP ): """Remove obsolete entities from entity registry.""" @@ -126,7 +127,7 @@ async def async_remove_obsolete_entities( if hap.home.currentAPVersion < "2.2.12": return - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) er_entries = async_entries_for_config_entry(entity_registry, entry.entry_id) for er_entry in er_entries: if er_entry.unique_id.startswith("HomematicipAccesspointStatus"): diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index ecf0549d8b8..d3a3b1e34a4 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -139,13 +139,13 @@ class HomematicipGenericEntity(Entity): if self.hmip_device_removed: try: del self._hap.hmip_device_by_entity_id[self.entity_id] - await self.async_remove_from_registries() + self.async_remove_from_registries() except KeyError as err: _LOGGER.debug("Error removing HMIP device from registry: %s", err) - async def async_remove_from_registries(self) -> None: + @callback + def async_remove_from_registries(self) -> None: """Remove entity/device from registry.""" - # Remove callback from device. self._device.remove_callback(self._async_device_changed) self._device.remove_callback(self._async_device_removed) @@ -155,7 +155,7 @@ class HomematicipGenericEntity(Entity): if device_id := self.registry_entry.device_id: # Remove from device registry. - device_registry = await dr.async_get_registry(self.hass) + device_registry = dr.async_get(self.hass) if device_id in device_registry.devices: # This will also remove associated entities from entity registry. device_registry.async_remove_device(device_id) @@ -163,7 +163,7 @@ class HomematicipGenericEntity(Entity): # Remove from entity registry. # Only relevant for entities that do not belong to a device. if entity_id := self.registry_entry.entity_id: - entity_registry = await er.async_get_registry(self.hass) + entity_registry = er.async_get(self.hass) if entity_id in entity_registry.entities: entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 1314344f48f..c159b6530fb 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -447,7 +447,7 @@ async def async_setup_entry( # noqa: C901 ) if sw_version: device_info[ATTR_SW_VERSION] = sw_version - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, **device_info, diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 71b62e22d33..8e0f194e904 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -100,7 +100,7 @@ async def async_setup_entry( hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) return False - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, config.mac)}, diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 65be6a74c19..9250b567d1b 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -200,7 +200,7 @@ async def async_setup_entry( _LOGGER.info(repr(isy.clock)) hass_isy_data[ISY994_ISY] = isy - await _async_get_or_create_isy_device_in_registry(hass, entry, isy) + _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -259,10 +259,11 @@ def _async_isy_to_configuration_url(isy: ISY) -> str: return f"{proto}://{connection_info['addr']}:{connection_info['port']}" -async def _async_get_or_create_isy_device_in_registry( +@callback +def _async_get_or_create_isy_device_in_registry( hass: HomeAssistant, entry: config_entries.ConfigEntry, isy ) -> None: - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index c34e8a1c67b..5b18d6cd33a 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -274,9 +274,10 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 return _LOGGER.error("Could not set variable value; not found or enabled on the ISY") - async def async_cleanup_registry_entries(service) -> None: + @callback + def async_cleanup_registry_entries(service) -> None: """Remove extra entities that are no longer part of the integration.""" - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) config_ids = [] current_unique_ids = [] diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 137fdada8c5..a9999df3bc0 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -151,7 +151,7 @@ class AlarmPanel: self.port, ) - device_registry = await dr.async_get_registry(self.hass) + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, self.status.get("mac"))}, diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 786e21f2d0b..0d6c29047a9 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass, config_entry): devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] - await _async_register_bridge_device(hass, config_entry.entry_id, bridge_device) + _async_register_bridge_device(hass, config_entry.entry_id, bridge_device) # Store this bridge (keyed by entry_id) so it can be retrieved by the # platforms we're setting up. hass.data[DOMAIN][config_entry.entry_id] = { @@ -164,7 +164,7 @@ async def async_setup_lip(hass, config_entry, lip_devices): _LOGGER.debug("Connected to Lutron Caseta bridge via LIP at %s:23", host) button_devices_by_lip_id = _async_merge_lip_leap_data(lip_devices, bridge) - button_devices_by_dr_id = await _async_register_button_devices( + button_devices_by_dr_id = _async_register_button_devices( hass, config_entry_id, bridge_device, button_devices_by_lip_id ) _async_subscribe_pico_remote_events(hass, lip, button_devices_by_lip_id) @@ -200,9 +200,10 @@ def _async_merge_lip_leap_data(lip_devices, bridge): return button_devices_by_id -async def _async_register_bridge_device(hass, config_entry_id, bridge_device): +@callback +def _async_register_bridge_device(hass, config_entry_id, bridge_device): """Register the bridge device in the device registry.""" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( name=bridge_device["name"], manufacturer=MANUFACTURER, @@ -212,11 +213,12 @@ async def _async_register_bridge_device(hass, config_entry_id, bridge_device): ) -async def _async_register_button_devices( +@callback +def _async_register_button_devices( hass, config_entry_id, bridge_device, button_devices_by_id ): """Register button devices (Pico Remotes) in the device registry.""" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) button_devices_by_dr_id = {} for device in button_devices_by_id.values(): diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 73775f23e6d..47e21d53515 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] = entry - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 616cd97a775..94bcd3b1c0d 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) for entry in entries: if entry.domain != ENTITY_TYPE or entry.disabled_by: diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index e8efbd92898..246c433672b 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -35,7 +35,7 @@ class MobileAppFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") # Register device tracker entity and add to person registering app - entity_registry = await er.async_get_registry(self.hass) + entity_registry = er.async_get(self.hass) devt_entry = entity_registry.async_get_or_create( "device_tracker", DOMAIN, diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 9d56e55a106..533e33e84bb 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) for entry in entries: if entry.domain != ENTITY_TYPE or entry.disabled_by: diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7d8b6ad4b53..9a0d391373c 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -344,7 +344,7 @@ async def webhook_update_registration(hass, config_entry, data): """Handle an update registration webhook.""" new_registration = {**config_entry.data, **data} - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -433,7 +433,7 @@ async def webhook_register_sensor(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) existing_sensor = entity_registry.async_get_entity_id( entity_type, DOMAIN, unique_store_key ) @@ -498,7 +498,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) if not entity_registry.async_get_entity_id( entity_type, DOMAIN, unique_store_key ): diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 14bdeae817b..904e5cecbbe 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -157,7 +157,7 @@ async def async_setup_entry( else: version = f"Protocol: {motion_gateway.protocol}" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index ec501f9f112..56c95115cb9 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -325,7 +325,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } current_cameras: set[tuple[str, str]] = set() - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) @callback def _async_process_motioneye_cameras() -> None: diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 301fb780c1b..657e7e06880 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool entry.async_on_unload(entry.add_update_listener(update_listener)) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.unique_id)}, diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 69d79d1cecb..6bf1a7f1929 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, server_url)}, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 74fd2b90117..d06827ffd7d 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for item in result: if attr == "bridges" and item["id"] not in data["bridges"]: # If a new bridge is discovered, register it: - hass.async_create_task(async_register_new_bridge(hass, item, entry)) + _async_register_new_bridge(hass, item, entry) data[attr][item["id"]] = item return data @@ -115,11 +115,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_register_new_bridge( +@callback +def _async_register_new_bridge( hass: HomeAssistant, bridge: dict, entry: ConfigEntry ) -> None: """Register a new bridge.""" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, bridge["hardware_id"])}, @@ -175,7 +176,8 @@ class NotionEntity(CoordinatorEntity): and self._task_id in self.coordinator.data["tasks"] ) - async def _async_update_bridge_id(self) -> None: + @callback + def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. Sensors can move to other bridges based on signal strength, etc. @@ -193,7 +195,7 @@ class NotionEntity(CoordinatorEntity): self._bridge_id = sensor["bridge"]["id"] - device_registry = await dr.async_get_registry(self.hass) + device_registry = dr.async_get(self.hass) this_device = device_registry.async_get_device( {(DOMAIN, sensor["hardware_id"])} ) @@ -218,7 +220,7 @@ class NotionEntity(CoordinatorEntity): def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" if self._task_id in self.coordinator.data["tasks"]: - self.hass.async_create_task(self._async_update_bridge_id()) + self._async_update_bridge_id() self._async_update_from_latest_data() self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 05d8925aeb0..8cf007ca82c 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -123,7 +123,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, api.gateway_id)}, diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index c9dbe86ee4b..94c51abf705 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass, entry): return False hass.data[DOMAIN][entry.entry_id] = roonserver - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.entry_id)}, diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index 9bca9a5f5b2..dbbb12f29ce 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -11,7 +11,7 @@ from .hub import SIAHub async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up sia from a config entry.""" hub: SIAHub = SIAHub(hass, entry) - await hub.async_setup_hub() + hub.async_setup_hub() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 387c2273606..7db432256f9 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -9,7 +9,7 @@ from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEve from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,10 +50,11 @@ class SIAHub: self.sia_accounts: list[SIAAccount] | None = None self.sia_client: SIAClient = None - async def async_setup_hub(self) -> None: + @callback + def async_setup_hub(self) -> None: """Add a device to the device_registry, register shutdown listener, load reactions.""" self.update_accounts() - device_registry = await dr.async_get_registry(self._hass) + device_registry = dr.async_get(self._hass) for acc in self._accounts: account = acc[CONF_ACCOUNT] device_registry.async_get_or_create( diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index ae5c3cd9527..ddaf70cc071 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -201,11 +201,12 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> hass.config_entries.async_update_entry(entry, **entry_updates) -async def async_register_base_station( +@callback +def _async_register_base_station( hass: HomeAssistant, entry: ConfigEntry, system: SystemV2 | SystemV3 ) -> None: """Register a new bridge.""" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, system.system_id)}, @@ -473,9 +474,7 @@ class SimpliSafe: for system in self.systems.values(): self._system_notifications[system.system_id] = set() - self._hass.async_create_task( - async_register_base_station(self._hass, self.entry, system) - ) + _async_register_base_station(self._hass, self.entry, system) # Future events will come from the websocket, but since subscription to the # websocket doesn't provide the most recent event, we grab it from the REST diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index adb7f3bf720..e62819f122c 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -10,6 +10,7 @@ from smarttub import APIError, LoginFailed, SmartTub from smarttub.api import Account from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -75,7 +76,7 @@ class SmartTubController: await self.coordinator.async_refresh() - await self.async_register_devices(entry) + self.async_register_devices(entry) return True @@ -107,9 +108,10 @@ class SmartTubController: ATTR_ERRORS: errors, } - async def async_register_devices(self, entry): + @callback + def async_register_devices(self, entry): """Register devices with the device registry for all spas.""" - device_registry = await dr.async_get_registry(self._hass) + device_registry = dr.async_get(self._hass) for spa in self.spas: device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 5efd4bfaa3a..a6bd320edd6 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator.update_interval = SCAN_INTERVAL_ALL_ASSUMED_STATE - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) hubs = [ device diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index ef3e8c4419d..da45350836c 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # and the config should simply be discarded return False - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=device_connections(printer), diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index f05c480aede..372eeb47096 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -102,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Register device for the Meter Adapter, since it will have no entities. - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={ diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 3982296b419..36b6278d968 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Create device registry entry. - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_UPNP, device.udn)}, diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index b365dbb1bee..7aff6ece0e1 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -172,7 +172,7 @@ async def async_setup_entry( entry.data[CONF_HOST], ) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.unique_id)}, diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index de5baf69683..5513485f51e 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -377,7 +377,7 @@ async def async_setup_gateway_entry( gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)}, From 366a4d24f4631011493de6c16bec6c3775179cbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Oct 2021 16:28:10 -0700 Subject: [PATCH 0004/1452] Add entity category to ZHA battery (#58553) --- homeassistant/components/zha/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 8e8a92c099a..d7c3e0797b8 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -223,6 +224,7 @@ class Battery(Sensor): _device_class = DEVICE_CLASS_BATTERY _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @staticmethod def formatter(value: int) -> int: From 11cb04822e3dd3e1750234c8aa9df10fc3a28626 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 28 Oct 2021 00:11:45 +0000 Subject: [PATCH 0005/1452] [ci skip] Translation update --- .../aurora_abb_powerone/translations/bg.json | 10 ++++++++ .../aurora_abb_powerone/translations/ca.json | 8 +++++-- .../aurora_abb_powerone/translations/nl.json | 23 +++++++++++++++++++ .../aurora_abb_powerone/translations/no.json | 23 +++++++++++++++++++ .../binary_sensor/translations/bg.json | 11 +++++++++ .../binary_sensor/translations/ca.json | 18 +++++++++++++++ .../binary_sensor/translations/hu.json | 13 +++++++++++ .../binary_sensor/translations/nl.json | 21 +++++++++++++++++ .../binary_sensor/translations/no.json | 21 +++++++++++++++++ .../binary_sensor/translations/ru.json | 13 +++++++++++ .../components/demo/translations/de.json | 6 ----- .../components/directv/translations/de.json | 4 ---- .../components/konnected/translations/de.json | 4 +--- .../components/netatmo/translations/bg.json | 3 ++- .../components/netatmo/translations/nl.json | 5 ++++ .../components/netatmo/translations/no.json | 5 ++++ .../components/roku/translations/de.json | 4 ---- .../tuya/translations/select.bg.json | 18 +++++++++++++++ .../tuya/translations/sensor.ca.json | 8 ++++++- .../tuya/translations/sensor.nl.json | 13 +++++++++++ .../tuya/translations/sensor.no.json | 15 ++++++++++++ .../components/unifi/translations/de.json | 6 ----- .../components/venstar/translations/bg.json | 14 +++++++++++ .../components/venstar/translations/nl.json | 13 +++++++++++ .../components/venstar/translations/no.json | 23 +++++++++++++++++++ .../components/wallbox/translations/en.json | 3 ++- .../components/wallbox/translations/nl.json | 10 +------- .../components/watttime/translations/nl.json | 10 ++++++++ .../components/watttime/translations/no.json | 10 ++++++++ .../components/yeelight/translations/ca.json | 2 +- .../components/yeelight/translations/ru.json | 2 +- 31 files changed, 300 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/aurora_abb_powerone/translations/bg.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/nl.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/no.json create mode 100644 homeassistant/components/tuya/translations/sensor.nl.json create mode 100644 homeassistant/components/tuya/translations/sensor.no.json create mode 100644 homeassistant/components/venstar/translations/bg.json create mode 100644 homeassistant/components/venstar/translations/nl.json create mode 100644 homeassistant/components/venstar/translations/no.json diff --git a/homeassistant/components/aurora_abb_powerone/translations/bg.json b/homeassistant/components/aurora_abb_powerone/translations/bg.json new file mode 100644 index 00000000000..88f52d84269 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/bg.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/ca.json b/homeassistant/components/aurora_abb_powerone/translations/ca.json index 6976430a9b3..98f77dad13b 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ca.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ca.json @@ -5,14 +5,18 @@ "no_serial_ports": "No s'han trobat ports COM. Es necessita un dispositiu de comunicaci\u00f3 RS485 v\u00e0lid." }, "error": { + "cannot_connect": "No s'ha pogut connectar, comprova el port s\u00e8rie, l'adre\u00e7a, la connexi\u00f3 el\u00e8ctrica i que l'inversor estigui enc\u00e8s", "cannot_open_serial_port": "No s'ha pogut obrir el port s\u00e8rie, comprova'l i torna-ho a provar", + "invalid_serial_port": "El port s\u00e8rie no t\u00e9 un dispositiu v\u00e0lid o no s'ha pogut obrir", "unknown": "Error inesperat" }, "step": { "user": { "data": { - "address": "Adre\u00e7a de l'inversor" - } + "address": "Adre\u00e7a de l'inversor", + "port": "Port RS485 o adaptador USB-RS485" + }, + "description": "L'inversor ha d'estar connectat mitjan\u00e7ant un adaptador RS485. Selecciona el port s\u00e8rie i l'adre\u00e7a de l'inversor tal com estan configurats a la pantalla LCD de l'aparell" } } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/nl.json b/homeassistant/components/aurora_abb_powerone/translations/nl.json new file mode 100644 index 00000000000..d70113e9c19 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_serial_ports": "Geen com-poorten gevonden. Een geldig RS485-apparaat is nodig om te communiceren." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken, controleer de seri\u00eble poort, het adres, de elektrische aansluiting en of de omvormer aan staat (bij daglicht)", + "cannot_open_serial_port": "Kan seri\u00eble poort niet openen, controleer en probeer het opnieuw", + "invalid_serial_port": "Seri\u00eble poort is geen geldig apparaat of kan niet worden geopend", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "address": "Omvormer adres", + "port": "RS485 of USB-RS485 adapter poort" + }, + "description": "De omvormer moet worden aangesloten via een RS485-adapter, selecteer de seri\u00eble poort en het adres van de omvormer zoals geconfigureerd op het LCD-paneel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/no.json b/homeassistant/components/aurora_abb_powerone/translations/no.json new file mode 100644 index 00000000000..9d4cd656f45 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_serial_ports": "Ingen com-porter funnet. Trenger en gyldig RS485-enhet for \u00e5 kommunisere." + }, + "error": { + "cannot_connect": "Kan ikke koble til, sjekk seriell port, adresse, elektrisk tilkobling og at omformeren er p\u00e5 (i dagslys)", + "cannot_open_serial_port": "Kan ikke \u00e5pne serieporten, sjekk og pr\u00f8v igjen", + "invalid_serial_port": "Seriell port er ikke en gyldig enhet eller kunne ikke \u00e5pnes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "address": "Inverter adresse", + "port": "RS485- eller USB-RS485-adapterport" + }, + "description": "Omformeren m\u00e5 kobles til via en RS485-adapter, velg seriell port og omformerens adresse som konfigurert p\u00e5 LCD-panelet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/bg.json b/homeassistant/components/binary_sensor/translations/bg.json index b1b3d766dc4..621625cb457 100644 --- a/homeassistant/components/binary_sensor/translations/bg.json +++ b/homeassistant/components/binary_sensor/translations/bg.json @@ -93,6 +93,17 @@ "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" } }, + "device_class": { + "cold": "\u0441\u0442\u0443\u0434", + "gas": "\u0433\u0430\u0437", + "heat": "\u0442\u043e\u043f\u043b\u0438\u043d\u0430", + "moisture": "\u0432\u043b\u0430\u0433\u0430", + "motion": "\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "problem": "\u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "smoke": "\u0434\u0438\u043c", + "sound": "\u0437\u0432\u0443\u043a", + "vibration": "\u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044f" + }, "state": { "_": { "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 17c22571c14..b68400fd679 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", "is_not_powered": "{entity_name} no est\u00e0 alimentat", "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_running": "{entity_name} no est\u00e0 funcionant", "is_not_tampered": "{entity_name} no detecta manipulaci\u00f3", "is_not_unsafe": "{entity_name} \u00e9s segur", "is_occupied": "{entity_name} est\u00e0 ocupat", @@ -41,6 +42,7 @@ "is_powered": "{entity_name} est\u00e0 alimentat", "is_present": "{entity_name} est\u00e0 present", "is_problem": "{entity_name} est\u00e0 detectant un problema", + "is_running": "{entity_name} est\u00e0 funcionant", "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", "is_tampered": "{entity_name} detecta manipulaci\u00f3", @@ -81,6 +83,7 @@ "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", + "not_running": "{entity_name} para de funcionar", "not_unsafe": "{entity_name} es torna segur", "occupied": "{entity_name} s'ocupa", "opened": "{entity_name} s'ha obert", @@ -88,6 +91,7 @@ "powered": "{entity_name} alimentat", "present": "{entity_name} present", "problem": "{entity_name} ha comen\u00e7at a detectar un problema", + "running": "{entity_name} comen\u00e7a a funcionar", "smoke": "{entity_name} ha comen\u00e7at a detectar fum", "sound": "{entity_name} ha comen\u00e7at a detectar so", "turned_off": "{entity_name} apagat", @@ -97,6 +101,19 @@ "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" } }, + "device_class": { + "cold": "fred", + "gas": "gas", + "heat": "calor", + "moisture": "humitat", + "motion": "moviment", + "occupancy": "ocupaci\u00f3", + "power": "pot\u00e8ncia", + "problem": "problema", + "smoke": "fum", + "sound": "so", + "vibration": "vibraci\u00f3" + }, "state": { "_": { "off": "off", @@ -175,6 +192,7 @@ "on": "Problema" }, "running": { + "off": "No funcionant", "on": "En funcionament" }, "safety": { diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 9c95cc67d93..016b0d71072 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -101,6 +101,19 @@ "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" } }, + "device_class": { + "cold": "hideg", + "gas": "g\u00e1z", + "heat": "f\u0171t\u00e9s", + "moisture": "nedvess\u00e9g", + "motion": "mozg\u00e1s", + "occupancy": "foglalts\u00e1g", + "power": "teljes\u00edtm\u00e9ny", + "problem": "probl\u00e9ma", + "smoke": "f\u00fcst", + "sound": "hang", + "vibration": "rezg\u00e9s" + }, "state": { "_": { "off": "Ki", diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 1abf0b86bca..3f5aff1585e 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} is niet aangesloten", "is_not_powered": "{entity_name} is niet van stroom voorzien...", "is_not_present": "{entity_name} is niet aanwezig", + "is_not_running": "{entity_name} is niet langer actief", "is_not_unsafe": "{entity_name} is veilig", "is_occupied": "{entity_name} bezet is", "is_off": "{entity_name} is uitgeschakeld", @@ -40,6 +41,7 @@ "is_powered": "{entity_name} is van stroom voorzien....", "is_present": "{entity_name} is aanwezig", "is_problem": "{entity_name} detecteert een probleem", + "is_running": "{entity_name} is actief", "is_smoke": "{entity_name} detecteert rook", "is_sound": "{entity_name} detecteert geluid", "is_unsafe": "{entity_name} is onveilig", @@ -78,6 +80,7 @@ "not_plugged_in": "{entity_name} niet verbonden", "not_powered": "{entity_name} niet ingeschakeld", "not_present": "{entity_name} is niet aanwezig", + "not_running": "{entity_name} is niet langer actief", "not_unsafe": "{entity_name} werd veilig", "occupied": "{entity_name} werd bezet", "opened": "{entity_name} geopend", @@ -85,6 +88,7 @@ "powered": "{entity_name} heeft vermogen", "present": "{entity_name} aanwezig", "problem": "{entity_name} begonnen met het detecteren van een probleem", + "running": "{entity_name} is actief geworden", "smoke": "{entity_name} begon rook te detecteren", "sound": "{entity_name} begon geluid te detecteren", "turned_off": "{entity_name} uitgeschakeld", @@ -94,6 +98,19 @@ "vibration": "{entity_name} begon trillingen te detecteren" } }, + "device_class": { + "cold": "koud", + "gas": "gas", + "heat": "warmte", + "moisture": "vochtigheid", + "motion": "beweging", + "occupancy": "bezetting", + "power": "power", + "problem": "probleem", + "smoke": "rook", + "sound": "geluid", + "vibration": "trilling" + }, "state": { "_": { "off": "Uit", @@ -171,6 +188,10 @@ "off": "OK", "on": "Probleem" }, + "running": { + "off": "Niet actief", + "on": "Actief" + }, "safety": { "off": "Veilig", "on": "Onveilig" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 7dd6243edf8..da2fea944c4 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} er koblet fra", "is_not_powered": "{entity_name} er spenningsl\u00f8s", "is_not_present": "{entity_name} er ikke tilstede", + "is_not_running": "{entity_name} kj\u00f8rer ikke", "is_not_tampered": "{entity_name} oppdager ikke manipulering", "is_not_unsafe": "{entity_name} er trygg", "is_occupied": "{entity_name} er opptatt", @@ -41,6 +42,7 @@ "is_powered": "{entity_name} er spenningssatt", "is_present": "{entity_name} er tilstede", "is_problem": "{entity_name} registrerer et problem", + "is_running": "{entity_name} kj\u00f8rer", "is_smoke": "{entity_name} registrerer r\u00f8yk", "is_sound": "{entity_name} registrerer lyd", "is_tampered": "{entity_name} oppdager manipulering", @@ -81,6 +83,7 @@ "not_plugged_in": "{entity_name} koblet fra", "not_powered": "{entity_name} spenningsl\u00f8s", "not_present": "{entity_name} ikke til stede", + "not_running": "{entity_name} kj\u00f8rer ikke lenger", "not_unsafe": "{entity_name} ble trygg", "occupied": "{entity_name} ble opptatt", "opened": "{entity_name} \u00e5pnet", @@ -88,6 +91,7 @@ "powered": "{entity_name} spenningssatt", "present": "{entity_name} tilstede", "problem": "{entity_name} begynte \u00e5 registrere et problem", + "running": "{entity_name} begynte \u00e5 kj\u00f8re", "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", "sound": "{entity_name} begynte \u00e5 registrere lyd", "turned_off": "{entity_name} sl\u00e5tt av", @@ -97,6 +101,19 @@ "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" } }, + "device_class": { + "cold": "kald", + "gas": "Gass", + "heat": "varme", + "moisture": "fuktighet", + "motion": "bevegelse", + "occupancy": "Bruk", + "power": "kraft", + "problem": "problem", + "smoke": "r\u00f8yk", + "sound": "lyd", + "vibration": "vibrasjon" + }, "state": { "_": { "off": "Av", @@ -174,6 +191,10 @@ "off": "", "on": "" }, + "running": { + "off": "Kj\u00f8rer ikke", + "on": "Kj\u00f8rer" + }, "safety": { "off": "Sikker", "on": "Usikker" diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 09a9da61e20..bb8ec9fdadb 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -101,6 +101,19 @@ "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" } }, + "device_class": { + "cold": "\u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "gas": "\u0433\u0430\u0437", + "heat": "\u043d\u0430\u0433\u0440\u0435\u0432", + "moisture": "\u0432\u043b\u0430\u0433\u0430", + "motion": "\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "occupancy": "\u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "power": "\u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c", + "problem": "\u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430", + "smoke": "\u0434\u044b\u043c", + "sound": "\u0437\u0432\u0443\u043a", + "vibration": "\u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044f" + }, "state": { "_": { "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index 74178521138..fd6239fa787 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,12 +1,6 @@ { "options": { "step": { - "init": { - "data": { - "one": "eins", - "other": "andere" - } - }, "options_1": { "data": { "bool": "Optionaler Boolescher Wert", diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json index 5f06a68e715..5fb4d2e7b5b 100644 --- a/homeassistant/components/directv/translations/de.json +++ b/homeassistant/components/directv/translations/de.json @@ -10,10 +10,6 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { - "data": { - "one": "eins", - "other": "andere" - }, "description": "M\u00f6chtest du {name} einrichten?" }, "user": { diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index fd307e90f20..11cfee75ef4 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -32,9 +32,7 @@ "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t" }, "error": { - "bad_host": "Ung\u00fcltige Override-API-Host-URL", - "one": "eins", - "other": "andere" + "bad_host": "Ung\u00fcltige Override-API-Host-URL" }, "step": { "options_binary": { diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 83b32fc7c85..a27d52d9559 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index dc811b63534..1a740f9d5b1 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Time-out genereren autorisatie-URL.", "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "Kies de verificatiemethode" + }, + "reauth_confirm": { + "description": "De Netatmo-integratie moet uw account opnieuw verifi\u00ebren", + "title": "Verifieer de integratie opnieuw" } } }, diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 9e3e24d5771..dc751d3a4b5 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Netatmo-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" } } }, diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index ce8ec9e4595..5ff560809c8 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -15,10 +15,6 @@ "title": "Roku" }, "ssdp_confirm": { - "data": { - "one": "eins", - "other": "andere" - }, "description": "M\u00f6chtest du {name} einrichten?", "title": "Roku" }, diff --git a/homeassistant/components/tuya/translations/select.bg.json b/homeassistant/components/tuya/translations/select.bg.json index 0d9652389ea..9b166dbd262 100644 --- a/homeassistant/components/tuya/translations/select.bg.json +++ b/homeassistant/components/tuya/translations/select.bg.json @@ -1,5 +1,18 @@ { "state": { + "tuya__basic_anti_flickr": { + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "1": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "2": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "tuya__decibel_sensitivity": { + "0": "\u041d\u0438\u0441\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442", + "1": "\u0412\u0438\u0441\u043e\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442" + }, "tuya__led_type": { "halogen": "\u0425\u0430\u043b\u043e\u0433\u0435\u043d\u043d\u0438", "incandescent": "\u0421 \u043d\u0430\u0436\u0435\u0436\u0430\u0435\u043c\u0430 \u0436\u0438\u0447\u043a\u0430", @@ -8,6 +21,11 @@ "tuya__light_mode": { "none": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, + "tuya__motion_sensitivity": { + "0": "\u041d\u0438\u0441\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442", + "1": "\u0421\u0440\u0435\u0434\u043d\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442", + "2": "\u0412\u0438\u0441\u043e\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442" + }, "tuya__relay_status": { "last": "\u0417\u0430\u043f\u043e\u043c\u043d\u044f\u043d\u0435 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "memory": "\u0417\u0430\u043f\u043e\u043c\u043d\u044f\u043d\u0435 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", diff --git a/homeassistant/components/tuya/translations/sensor.ca.json b/homeassistant/components/tuya/translations/sensor.ca.json index c3cfc0c9d08..681ae04107a 100644 --- a/homeassistant/components/tuya/translations/sensor.ca.json +++ b/homeassistant/components/tuya/translations/sensor.ca.json @@ -3,7 +3,13 @@ "tuya__status": { "boiling_temp": "Temperatura d'ebullici\u00f3", "cooling": "Refredant", - "heating": "Escalfant" + "heating": "Escalfant", + "heating_temp": "Temperatura d'escalfament", + "reserve_1": "Reserva 1", + "reserve_2": "Reserva 2", + "reserve_3": "Reserva 3", + "standby": "En espera", + "warm": "Conservaci\u00f3 de calor" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.nl.json b/homeassistant/components/tuya/translations/sensor.nl.json new file mode 100644 index 00000000000..286befbede0 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.nl.json @@ -0,0 +1,13 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Kooktemperatuur", + "cooling": "Koeling", + "heating": "Verwarming", + "heating_temp": "Verwarmingstemperatuur", + "reserve_1": "Reserve 1", + "reserve_2": "Reserve 2", + "reserve_3": "Reserve 3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.no.json b/homeassistant/components/tuya/translations/sensor.no.json new file mode 100644 index 00000000000..2992fcb2f4b --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.no.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Kokende temperatur", + "cooling": "Kj\u00f8ling", + "heating": "Oppvarming", + "heating_temp": "Oppvarmingstemperatur", + "reserve_1": "Reserver 1", + "reserve_2": "Reserver 2", + "reserve_3": "Reserver 3", + "standby": "Avventer", + "warm": "Varmebevaring" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index ab7f9bb9b16..04fc14845c9 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -48,12 +48,6 @@ "description": "Konfiguriere die Ger\u00e4teverfolgung", "title": "UniFi-Optionen 1/3" }, - "init": { - "data": { - "one": "eins", - "other": "andere" - } - }, "simple_options": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", diff --git a/homeassistant/components/venstar/translations/bg.json b/homeassistant/components/venstar/translations/bg.json new file mode 100644 index 00000000000..9b2acbd7c42 --- /dev/null +++ b/homeassistant/components/venstar/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/nl.json b/homeassistant/components/venstar/translations/nl.json new file mode 100644 index 00000000000..d2c837d2f91 --- /dev/null +++ b/homeassistant/components/venstar/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ssl": "Gebruik een SSL-certificaat", + "username": "Gebruikersnaam" + }, + "title": "Maak verbinding met de Venstar-thermostaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/no.json b/homeassistant/components/venstar/translations/no.json new file mode 100644 index 00000000000..6e77da8d723 --- /dev/null +++ b/homeassistant/components/venstar/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "pin": "PIN kode", + "ssl": "Bruker et SSL-sertifikat", + "username": "Brukernavn" + }, + "title": "Koble til Venstar-termostaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json index 3d75e0bc276..52dcf8530d4 100644 --- a/homeassistant/components/wallbox/translations/en.json +++ b/homeassistant/components/wallbox/translations/en.json @@ -17,5 +17,6 @@ } } } - } + }, + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index dd406ea3b90..6ba03e7ee99 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -16,13 +15,6 @@ "station": "Station Serienummer", "username": "Gebruikersnaam" } - }, - "reauth_confirm": { - "data": { - "password": "Wachtwoord", - "station": "Station Serienummer", - "username": "Gebruikersnaam" - } } } }, diff --git a/homeassistant/components/watttime/translations/nl.json b/homeassistant/components/watttime/translations/nl.json index 045533d2336..72758f4a0d7 100644 --- a/homeassistant/components/watttime/translations/nl.json +++ b/homeassistant/components/watttime/translations/nl.json @@ -38,5 +38,15 @@ "description": "Voer uw gebruikersnaam en wachtwoord in:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Toon gemonitorde locatie op de kaart" + }, + "title": "Configureer WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/no.json b/homeassistant/components/watttime/translations/no.json index bef57982658..19ec82e863c 100644 --- a/homeassistant/components/watttime/translations/no.json +++ b/homeassistant/components/watttime/translations/no.json @@ -38,5 +38,15 @@ "description": "Skriv inn brukernavn og passord:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Vis overv\u00e5ket plassering p\u00e5 kartet" + }, + "title": "Konfigurer WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ca.json b/homeassistant/components/yeelight/translations/ca.json index 2732806de85..0b62bebf1be 100644 --- a/homeassistant/components/yeelight/translations/ca.json +++ b/homeassistant/components/yeelight/translations/ca.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (opcional)", + "model": "Model", "nightlight_switch": "Utilitza l'interruptor NightLight", "save_on_change": "Desa l'estat en canviar", "transition": "Temps de transici\u00f3 (ms)", diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json index 34e3c4d2c8a..70694147baa 100644 --- a/homeassistant/components/yeelight/translations/ru.json +++ b/homeassistant/components/yeelight/translations/ru.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "\u041c\u043e\u0434\u0435\u043b\u044c (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c", "nightlight_switch": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043d\u043e\u0447\u043d\u0438\u043a\u0430", "save_on_change": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", "transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 (\u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", From 0456a896e3898a1ff96d9ce07d3ae6a55d075e04 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Oct 2021 08:13:32 +0200 Subject: [PATCH 0006/1452] Avoid service call in MQTT async_publish function (#58441) * Avoid service call in MQTT async_publish function * Tweak * Fix integrations + tests --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 31 +------- .../components/mqtt/alarm_control_panel.py | 16 ++-- homeassistant/components/mqtt/climate.py | 54 ++++++++------ homeassistant/components/mqtt/cover.py | 14 ++-- homeassistant/components/mqtt/fan.py | 10 +-- homeassistant/components/mqtt/humidifier.py | 8 +- .../components/mqtt/light/schema_basic.py | 42 +++++------ .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 4 +- homeassistant/components/mqtt/lock.py | 4 +- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/scene.py | 2 +- homeassistant/components/mqtt/select.py | 2 +- homeassistant/components/mqtt/switch.py | 4 +- .../components/mqtt/vacuum/schema_legacy.py | 18 ++--- .../components/mqtt/vacuum/schema_state.py | 16 ++-- .../components/mqtt_eventstream/__init__.py | 9 +-- .../components/mqtt_statestream/__init__.py | 25 ++++--- homeassistant/components/mysensors/gateway.py | 4 +- homeassistant/components/ozw/__init__.py | 2 +- homeassistant/components/snips/__init__.py | 26 +++---- homeassistant/components/tasmota/__init__.py | 6 +- tests/components/mqtt/test_init.py | 73 +++++++++++-------- tests/components/ozw/test_websocket_api.py | 10 ++- tests/components/snips/test_init.py | 30 ++++---- 26 files changed, 207 insertions(+), 211 deletions(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index a94b1013782..4eb30028e8b 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -452,6 +452,6 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): """Publish state change to MQTT.""" if (new_state := event.data.get("new_state")) is None: return - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 994ae2d108a..f42663ac4a8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -245,39 +245,16 @@ def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: return data -@bind_hass -def publish(hass: HomeAssistant, topic, payload, qos=None, retain=None) -> None: +def publish(hass: HomeAssistant, topic, payload, qos=0, retain=False) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish, hass, topic, payload, qos, retain) -@callback -@bind_hass -def async_publish( - hass: HomeAssistant, topic: Any, payload, qos=None, retain=None +async def async_publish( + hass: HomeAssistant, topic: Any, payload, qos=0, retain=False ) -> None: """Publish message to an MQTT topic.""" - data = _build_publish_data(topic, qos, retain) - data[ATTR_PAYLOAD] = payload - hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) - - -@bind_hass -def publish_template( - hass: HomeAssistant, topic, payload_template, qos=None, retain=None -) -> None: - """Publish message to an MQTT topic.""" - hass.add_job(async_publish_template, hass, topic, payload_template, qos, retain) - - -@bind_hass -def async_publish_template( - hass: HomeAssistant, topic, payload_template, qos=None, retain=None -) -> None: - """Publish message to an MQTT topic using a template payload.""" - data = _build_publish_data(topic, qos, retain) - data[ATTR_PAYLOAD_TEMPLATE] = payload_template - hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) + await hass.data[DATA_MQTT].async_publish(topic, str(payload), qos, retain) AsyncDeprecatedMessageCallbackType = Callable[ diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index dfdf2dbfb26..5ac6e5ea89a 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -228,7 +228,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "disarming"): return payload = self._config[CONF_PAYLOAD_DISARM] - self._publish(code, payload) + await self._publish(code, payload) async def async_alarm_arm_home(self, code=None): """Send arm home command. @@ -239,7 +239,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "arming home"): return action = self._config[CONF_PAYLOAD_ARM_HOME] - self._publish(code, action) + await self._publish(code, action) async def async_alarm_arm_away(self, code=None): """Send arm away command. @@ -250,7 +250,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "arming away"): return action = self._config[CONF_PAYLOAD_ARM_AWAY] - self._publish(code, action) + await self._publish(code, action) async def async_alarm_arm_night(self, code=None): """Send arm night command. @@ -261,7 +261,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "arming night"): return action = self._config[CONF_PAYLOAD_ARM_NIGHT] - self._publish(code, action) + await self._publish(code, action) async def async_alarm_arm_vacation(self, code=None): """Send arm vacation command. @@ -272,7 +272,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "arming vacation"): return action = self._config[CONF_PAYLOAD_ARM_VACATION] - self._publish(code, action) + await self._publish(code, action) async def async_alarm_arm_custom_bypass(self, code=None): """Send arm custom bypass command. @@ -283,14 +283,14 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if code_required and not self._validate_code(code, "arming custom bypass"): return action = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] - self._publish(code, action) + await self._publish(code, action) - def _publish(self, code, action): + async def _publish(self, code, action): """Publish via mqtt.""" command_template = self._config[CONF_COMMAND_TEMPLATE] values = {"action": action, "code": code} payload = command_template.async_render(**values, parse_result=False) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], payload, diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 16d4ae695c1..5b186ff9126 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -659,9 +659,9 @@ class MqttClimate(MqttEntity, ClimateEntity): """Return the list of available fan modes.""" return self._config[CONF_FAN_MODE_LIST] - def _publish(self, topic, payload): + async def _publish(self, topic, payload): if self._topic[topic] is not None: - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[topic], payload, @@ -669,7 +669,9 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_RETAIN], ) - def _set_temperature(self, temp, cmnd_topic, cmnd_template, state_topic, attr): + async def _set_temperature( + self, temp, cmnd_topic, cmnd_template, state_topic, attr + ): if temp is not None: if self._topic[state_topic] is None: # optimistic mode @@ -680,7 +682,7 @@ class MqttClimate(MqttEntity, ClimateEntity): or self._current_operation != HVAC_MODE_OFF ): payload = self._command_templates[cmnd_template](temp) - self._publish(cmnd_topic, payload) + await self._publish(cmnd_topic, payload) async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" @@ -688,7 +690,7 @@ class MqttClimate(MqttEntity, ClimateEntity): operation_mode = kwargs.get(ATTR_HVAC_MODE) await self.async_set_hvac_mode(operation_mode) - self._set_temperature( + await self._set_temperature( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, @@ -696,7 +698,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_target_temp", ) - self._set_temperature( + await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, @@ -704,7 +706,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_target_temp_low", ) - self._set_temperature( + await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, @@ -721,7 +723,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode ) - self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) + await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = swing_mode @@ -731,7 +733,7 @@ class MqttClimate(MqttEntity, ClimateEntity): """Set new target temperature.""" if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) - self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) + await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._current_fan_mode = fan_mode @@ -740,12 +742,14 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new operation mode.""" if self._current_operation == HVAC_MODE_OFF and hvac_mode != HVAC_MODE_OFF: - self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON]) + await self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON]) elif self._current_operation != HVAC_MODE_OFF and hvac_mode == HVAC_MODE_OFF: - self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF]) + await self._publish( + CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF] + ) payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) - self._publish(CONF_MODE_COMMAND_TOPIC, payload) + await self._publish(CONF_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_MODE_STATE_TOPIC] is None: self._current_operation = hvac_mode @@ -770,26 +774,28 @@ class MqttClimate(MqttEntity, ClimateEntity): optimistic_update = False if self._away: - optimistic_update = optimistic_update or self._set_away_mode(False) + optimistic_update = optimistic_update or await self._set_away_mode(False) elif preset_mode == PRESET_AWAY: if self._hold: - self._set_hold_mode(None) - optimistic_update = optimistic_update or self._set_away_mode(True) + await self._set_hold_mode(None) + optimistic_update = optimistic_update or await self._set_away_mode(True) else: hold_mode = preset_mode if preset_mode == PRESET_NONE: hold_mode = None - optimistic_update = optimistic_update or self._set_hold_mode(hold_mode) + optimistic_update = optimistic_update or await self._set_hold_mode( + hold_mode + ) if optimistic_update: self.async_write_ha_state() - def _set_away_mode(self, state): + async def _set_away_mode(self, state): """Set away mode. Returns if we should optimistically write the state. """ - self._publish( + await self._publish( CONF_AWAY_MODE_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], ) @@ -800,7 +806,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._away = state return True - def _set_hold_mode(self, hold_mode): + async def _set_hold_mode(self, hold_mode): """Set hold mode. Returns if we should optimistically write the state. @@ -808,7 +814,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_HOLD_COMMAND_TEMPLATE]( hold_mode or "off" ) - self._publish(CONF_HOLD_COMMAND_TOPIC, payload) + await self._publish(CONF_HOLD_COMMAND_TOPIC, payload) if self._topic[CONF_HOLD_STATE_TOPIC] is not None: return False @@ -816,8 +822,8 @@ class MqttClimate(MqttEntity, ClimateEntity): self._hold = hold_mode return True - def _set_aux_heat(self, state): - self._publish( + async def _set_aux_heat(self, state): + await self._publish( CONF_AUX_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], ) @@ -828,11 +834,11 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" - self._set_aux_heat(True) + await self._set_aux_heat(True) async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" - self._set_aux_heat(False) + await self._set_aux_heat(False) @property def supported_features(self): diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 0af9d7d3739..859d9811617 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -528,7 +528,7 @@ class MqttCover(MqttEntity, CoverEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_OPEN], @@ -549,7 +549,7 @@ class MqttCover(MqttEntity, CoverEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_CLOSE], @@ -570,7 +570,7 @@ class MqttCover(MqttEntity, CoverEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_STOP], @@ -580,7 +580,7 @@ class MqttCover(MqttEntity, CoverEntity): async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_TILT_COMMAND_TOPIC), self._config[CONF_TILT_OPEN_POSITION], @@ -595,7 +595,7 @@ class MqttCover(MqttEntity, CoverEntity): async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_TILT_COMMAND_TOPIC), self._config[CONF_TILT_CLOSED_POSITION], @@ -626,7 +626,7 @@ class MqttCover(MqttEntity, CoverEntity): } tilt = template.async_render(parse_result=False, variables=variables) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_TILT_COMMAND_TOPIC), tilt, @@ -655,7 +655,7 @@ class MqttCover(MqttEntity, CoverEntity): } position = template.async_render(parse_result=False, variables=variables) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config.get(CONF_SET_POSITION_TOPIC), position, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f950a4d2c60..34e5d66a5e7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -520,7 +520,7 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], mqtt_payload, @@ -541,7 +541,7 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], mqtt_payload, @@ -561,7 +561,7 @@ class MqttFan(MqttEntity, FanEntity): percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload, @@ -584,7 +584,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload, @@ -610,7 +610,7 @@ class MqttFan(MqttEntity, FanEntity): self._payload["OSCILLATE_OFF_PAYLOAD"] ) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload, diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index e8bbbc7fd4b..e4f578ef94c 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -385,7 +385,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], mqtt_payload, @@ -402,7 +402,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], mqtt_payload, @@ -419,7 +419,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): This method is a coroutine. """ mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], mqtt_payload, @@ -442,7 +442,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mqtt_payload = self._command_templates[ATTR_MODE](mode) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_MODE_COMMAND_TOPIC], mqtt_payload, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 5dd3f13af25..1909d7e136b 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -811,9 +811,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): should_update = False on_command_type = self._config[CONF_ON_COMMAND_TYPE] - def publish(topic, payload): + async def publish(topic, payload): """Publish an MQTT message.""" - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[topic], payload, @@ -859,7 +859,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return True if on_command_type == "first": - publish(CONF_COMMAND_TOPIC, self._payload["on"]) + await publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True # If brightness is being used instead of an on command, make sure @@ -881,13 +881,13 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # Legacy mode: Convert HS to RGB rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) - publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic( ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR ) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: - publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") + await publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, COLOR_MODE_HS) if ( @@ -897,7 +897,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ): # Legacy mode: Convert HS to XY xy_color = color_util.color_hs_to_xy(*hs_color) - publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") should_update |= set_optimistic( ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR ) @@ -909,7 +909,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ): scaled = scale_rgbx(rgb) rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) - publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, COLOR_MODE_RGB) if ( @@ -919,7 +919,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ): scaled = scale_rgbx(rgbw) rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) - publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, COLOR_MODE_RGBW) if ( @@ -929,7 +929,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ): scaled = scale_rgbx(rgbww) rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) - publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, COLOR_MODE_RGBWW) if ( @@ -937,7 +937,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and self._topic[CONF_XY_COMMAND_TOPIC] is not None and not self._legacy_mode ): - publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, COLOR_MODE_XY) if ( @@ -951,7 +951,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) - publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) + await publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs @@ -964,7 +964,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): brightness = kwargs[ATTR_BRIGHTNESS] rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) - publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs @@ -975,7 +975,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) - publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs @@ -988,7 +988,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) - publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs @@ -1001,7 +1001,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) - publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs @@ -1012,7 +1012,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if tpl: color_temp = tpl({"value": color_temp}) - publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) + await publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) should_update |= set_optimistic( ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP], COLOR_MODE_COLOR_TEMP ) @@ -1020,14 +1020,14 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] if effect in self._config.get(CONF_EFFECT_LIST): - publish(CONF_EFFECT_COMMAND_TOPIC, effect) + await publish(CONF_EFFECT_COMMAND_TOPIC, effect) should_update |= set_optimistic(ATTR_EFFECT, effect) if ATTR_WHITE in kwargs and self._topic[CONF_WHITE_COMMAND_TOPIC] is not None: percent_white = float(kwargs[ATTR_WHITE]) / 255 white_scale = self._config[CONF_WHITE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) - publish(CONF_WHITE_COMMAND_TOPIC, device_white_value) + await publish(CONF_WHITE_COMMAND_TOPIC, device_white_value) should_update |= set_optimistic( ATTR_BRIGHTNESS, kwargs[ATTR_WHITE], @@ -1041,11 +1041,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 white_scale = self._config[CONF_WHITE_VALUE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) - publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) + await publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) should_update |= set_optimistic(ATTR_WHITE_VALUE, kwargs[ATTR_WHITE_VALUE]) if on_command_type == "last": - publish(CONF_COMMAND_TOPIC, self._payload["on"]) + await publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True if self._optimistic: @@ -1061,7 +1061,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload["off"], diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 9915c2455df..412205ea9cf 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -621,7 +621,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), @@ -646,7 +646,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._set_flash_and_transition(message, **kwargs) - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 6b7396846e1..0a19fbb9836 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -374,7 +374,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topics[CONF_COMMAND_TOPIC], self._templates[CONF_COMMAND_ON_TEMPLATE].async_render( @@ -399,7 +399,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._topics[CONF_COMMAND_TOPIC], self._templates[CONF_COMMAND_OFF_TEMPLATE].async_render( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index fdcd4294b8c..8de4c4431b9 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -149,7 +149,7 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_LOCK], @@ -166,7 +166,7 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_UNLOCK], diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 902d828d911..e3ff03b2fbe 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -231,7 +231,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._current_number = current_number self.async_write_ha_state() - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], current_number, diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 6cf953ccf44..c2b201e20e6 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -121,7 +121,7 @@ class MqttScene( This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index b289bd53f0d..6ef0dbc3776 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -168,7 +168,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): self._attr_current_option = option self.async_write_ha_state() - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], option, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 3a593adf1e3..13a241f7ab1 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -165,7 +165,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], @@ -182,7 +182,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): This method is a coroutine. """ - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OFF], diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index d827853d603..63bb47ce21b 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -380,7 +380,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_TURN_ON == 0: return - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_TURN_ON], @@ -395,7 +395,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_TURN_OFF == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_TURN_OFF], @@ -410,7 +410,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_STOP == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_STOP], @@ -425,7 +425,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_CLEAN_SPOT], @@ -440,7 +440,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_LOCATE == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_LOCATE], @@ -455,7 +455,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_PAUSE == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_START_PAUSE], @@ -470,7 +470,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if self.supported_features & SUPPORT_RETURN_HOME == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], @@ -487,7 +487,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ) or fan_speed not in self._fan_speed_list: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._set_fan_speed_topic, fan_speed, self._qos, self._retain ) self._status = f"Setting fan to {fan_speed}..." @@ -503,7 +503,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): message = json.dumps(message) else: message = command - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._send_command_topic, message, self._qos, self._retain ) self._status = f"Sending command {message}..." diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 80f566ff5de..8c654a526e4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -240,7 +240,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Start the vacuum.""" if self.supported_features & SUPPORT_START == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_START], @@ -252,7 +252,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Pause the vacuum.""" if self.supported_features & SUPPORT_PAUSE == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_PAUSE], @@ -264,7 +264,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_STOP], @@ -278,7 +278,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): fan_speed not in self._fan_speed_list ): return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._set_fan_speed_topic, fan_speed, @@ -290,7 +290,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_RETURN_TO_BASE], @@ -302,7 +302,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_CLEAN_SPOT], @@ -314,7 +314,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return None - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._command_topic, self._config[CONF_PAYLOAD_LOCATE], @@ -332,7 +332,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): message = json.dumps(message) else: message = command - mqtt.async_publish( + await mqtt.async_publish( self.hass, self._send_command_topic, message, diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index d31d6d1cd53..6e6596b3425 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -3,6 +3,7 @@ import json import voluptuous as vol +from homeassistant.components import mqtt from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.const import ( ATTR_SERVICE_DATA, @@ -53,15 +54,13 @@ BLOCKED_EVENTS = [ async def async_setup(hass, config): """Set up the MQTT eventstream component.""" - mqtt = hass.components.mqtt conf = config.get(DOMAIN, {}) pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) ignore_event = conf.get(CONF_IGNORE_EVENT) ignore_event.append(EVENT_TIME_CHANGED) - @callback - def _event_publisher(event): + async def _event_publisher(event): """Handle events by publishing them on the MQTT queue.""" if event.origin != EventOrigin.local: return @@ -82,7 +81,7 @@ async def async_setup(hass, config): event_info = {"event_type": event.event_type, "event_data": event.data} msg = json.dumps(event_info, cls=JSONEncoder) - mqtt.async_publish(pub_topic, msg) + await mqtt.async_publish(hass, pub_topic, msg) # Only listen for local events if you are going to publish them. if pub_topic: @@ -117,6 +116,6 @@ async def async_setup(hass, config): # Only subscribe if you specified a topic. if sub_topic: - await mqtt.async_subscribe(sub_topic, _event_receiver) + await mqtt.async_subscribe(hass, sub_topic, _event_receiver) return True diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index d7c971b7d35..d0a13a7384b 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -3,9 +3,9 @@ import json import voluptuous as vol +from homeassistant.components import mqtt from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import MATCH_ALL -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -44,8 +44,7 @@ async def async_setup(hass, config): if not base_topic.endswith("/"): base_topic = f"{base_topic}/" - @callback - def _state_publisher(entity_id, old_state, new_state): + async def _state_publisher(entity_id, old_state, new_state): if new_state is None: return @@ -55,22 +54,30 @@ async def async_setup(hass, config): payload = new_state.state mybase = f"{base_topic}{entity_id.replace('.', '/')}/" - hass.components.mqtt.async_publish(f"{mybase}state", payload, 1, True) + await mqtt.async_publish(hass, f"{mybase}state", payload, 1, True) if publish_timestamps: if new_state.last_updated: - hass.components.mqtt.async_publish( - f"{mybase}last_updated", new_state.last_updated.isoformat(), 1, True + await mqtt.async_publish( + hass, + f"{mybase}last_updated", + new_state.last_updated.isoformat(), + 1, + True, ) if new_state.last_changed: - hass.components.mqtt.async_publish( - f"{mybase}last_changed", new_state.last_changed.isoformat(), 1, True + await mqtt.async_publish( + hass, + f"{mybase}last_changed", + new_state.last_changed.isoformat(), + 1, + True, ) if publish_attributes: for key, val in new_state.attributes.items(): encoded_val = json.dumps(val, cls=JSONEncoder) - hass.components.mqtt.async_publish(mybase + key, encoded_val, 1, True) + await mqtt.async_publish(hass, mybase + key, encoded_val, 1, True) async_track_state_change(hass, MATCH_ALL, _state_publisher) return True diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index e7f97792493..41311f45b03 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -185,7 +185,9 @@ async def _get_gateway( def pub_callback(topic: str, payload: str, qos: int, retain: bool) -> None: """Call MQTT publish function.""" - mqtt.async_publish(topic, payload, qos, retain) + hass.async_create_task( + mqtt.async_publish(hass, topic, payload, qos, retain) + ) def sub_callback( topic: str, sub_cb: Callable[[str, ReceivePayloadType, int], None], qos: int diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 238e7dcd8cd..04c7c3854bc 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -106,7 +106,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER.error("MQTT integration is not set up") return - mqtt.async_publish(hass, topic, json.dumps(payload)) + hass.async_create_task(mqtt.async_publish(hass, topic, json.dumps(payload))) manager_options["send_message"] = send_message diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 256a4ae8719..4c65bd30a77 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -6,7 +6,6 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, intent DOMAIN = "snips" @@ -90,22 +89,17 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema( async def async_setup(hass, config): """Activate Snips component.""" - @callback - def async_set_feedback(site_ids, state): + async def async_set_feedback(site_ids, state): """Set Feedback sound state.""" site_ids = site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS) topic = FEEDBACK_ON_TOPIC if state else FEEDBACK_OFF_TOPIC for site_id in site_ids: payload = json.dumps({"siteId": site_id}) - hass.components.mqtt.async_publish( - FEEDBACK_ON_TOPIC, "", qos=0, retain=False - ) - hass.components.mqtt.async_publish( - topic, payload, qos=int(state), retain=state - ) + await mqtt.async_publish(hass, FEEDBACK_ON_TOPIC, "", qos=0, retain=False) + await mqtt.async_publish(hass, topic, payload, qos=int(state), retain=state) if CONF_FEEDBACK in config[DOMAIN]: - async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK]) + await async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK]) async def message_received(msg): """Handle new messages on MQTT.""" @@ -153,7 +147,7 @@ async def async_setup(hass, config): notification["text"] = intent_response.speech["plain"]["speech"] _LOGGER.debug("send_response %s", json.dumps(notification)) - mqtt.async_publish( + await mqtt.async_publish( hass, "hermes/dialogueManager/endSession", json.dumps(notification) ) except intent.UnknownIntent: @@ -163,7 +157,7 @@ async def async_setup(hass, config): except intent.IntentError: _LOGGER.exception("Error while handling intent: %s", intent_type) - await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received) + await mqtt.async_subscribe(hass, INTENT_TOPIC, message_received) async def snips_say(call): """Send a Snips notification message.""" @@ -172,7 +166,7 @@ async def async_setup(hass, config): "customData": call.data.get(ATTR_CUSTOM_DATA, ""), "init": {"type": "notification", "text": call.data.get(ATTR_TEXT)}, } - mqtt.async_publish( + await mqtt.async_publish( hass, "hermes/dialogueManager/startSession", json.dumps(notification) ) return @@ -189,18 +183,18 @@ async def async_setup(hass, config): "intentFilter": call.data.get(ATTR_INTENT_FILTER, []), }, } - mqtt.async_publish( + await mqtt.async_publish( hass, "hermes/dialogueManager/startSession", json.dumps(notification) ) return async def feedback_on(call): """Turn feedback sounds on.""" - async_set_feedback(call.data.get(ATTR_SITE_ID), True) + await async_set_feedback(call.data.get(ATTR_SITE_ID), True) async def feedback_off(call): """Turn feedback sounds off.""" - async_set_feedback(call.data.get(ATTR_SITE_ID), False) + await async_set_feedback(call.data.get(ATTR_SITE_ID), False) hass.services.async_register( DOMAIN, SERVICE_SAY, snips_say, schema=SERVICE_SCHEMA_SAY diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index fd156e20c3c..f33359347b8 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -55,7 +55,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: qos: int | None = None, retain: bool | None = None, ) -> None: - mqtt.async_publish(hass, topic, payload, qos, retain) + if qos is None: + qos = 0 + if retain is None: + retain = False + hass.async_create_task(mqtt.async_publish(hass, topic, payload, qos, retain)) async def _subscribe_topics(sub_state: dict | None, topics: dict) -> dict: # Optionally mark message handlers as callback diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7c6d482c7ec..26ceb583818 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.const import ( - EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, @@ -92,29 +91,51 @@ async def test_mqtt_disconnects_on_home_assistant_stop(hass, mqtt_mock): assert mqtt_mock.async_disconnect.called -async def test_publish_calls_service(hass, mqtt_mock, calls, record_calls): - """Test the publishing of call to services.""" - hass.bus.async_listen_once(EVENT_CALL_SERVICE, record_calls) - - mqtt.async_publish(hass, "test-topic", "test-payload") +async def test_publish_(hass, mqtt_mock): + """Test the publish function.""" + await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0] == ( + "test-topic", + "test-payload", + 0, + False, + ) + mqtt_mock.reset_mock() - assert len(calls) == 1 - assert calls[0][0].data["service_data"][mqtt.ATTR_TOPIC] == "test-topic" - assert calls[0][0].data["service_data"][mqtt.ATTR_PAYLOAD] == "test-payload" - assert mqtt.ATTR_QOS not in calls[0][0].data["service_data"] - assert mqtt.ATTR_RETAIN not in calls[0][0].data["service_data"] - - hass.bus.async_listen_once(EVENT_CALL_SERVICE, record_calls) - - mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) + await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) await hass.async_block_till_done() + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0] == ( + "test-topic", + "test-payload", + 2, + True, + ) + mqtt_mock.reset_mock() - assert len(calls) == 2 - assert calls[1][0].data["service_data"][mqtt.ATTR_TOPIC] == "test-topic" - assert calls[1][0].data["service_data"][mqtt.ATTR_PAYLOAD] == "test-payload" - assert calls[1][0].data["service_data"][mqtt.ATTR_QOS] == 2 - assert calls[1][0].data["service_data"][mqtt.ATTR_RETAIN] is True + mqtt.publish(hass, "test-topic2", "test-payload2") + await hass.async_block_till_done() + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0] == ( + "test-topic2", + "test-payload2", + 0, + False, + ) + mqtt_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) + await hass.async_block_till_done() + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0] == ( + "test-topic2", + "test-payload2", + 2, + True, + ) + mqtt_mock.reset_mock() async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): @@ -134,18 +155,6 @@ async def test_service_call_with_template_payload_renders_template(hass, mqtt_mo If 'payload_template' is provided and 'payload' is not, then render it. """ - mqtt.publish_template(hass, "test/topic", "{{ 1+1 }}") - await hass.async_block_till_done() - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == "2" - mqtt_mock.reset_mock() - - mqtt.async_publish_template(hass, "test/topic", "{{ 2+2 }}") - await hass.async_block_till_done() - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == "4" - mqtt_mock.reset_mock() - await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index ad3b568b62a..2cbe69f1c98 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -43,7 +43,7 @@ from homeassistant.components.websocket_api.const import ( from .common import MQTTMessage, setup_ozw -async def test_websocket_api(hass, generic_data, hass_ws_client): +async def test_websocket_api(hass, generic_data, hass_ws_client, mqtt_mock): """Test the ozw websocket api.""" await setup_ozw(hass, fixture=generic_data) client = await hass_ws_client(hass) @@ -280,7 +280,7 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result["code"] == ERR_NOT_FOUND -async def test_ws_locks(hass, lock_data, hass_ws_client): +async def test_ws_locks(hass, lock_data, hass_ws_client, mqtt_mock): """Test lock websocket apis.""" await setup_ozw(hass, fixture=lock_data) client = await hass_ws_client(hass) @@ -319,7 +319,9 @@ async def test_ws_locks(hass, lock_data, hass_ws_client): assert msg["success"] -async def test_refresh_node(hass, generic_data, sent_messages, hass_ws_client): +async def test_refresh_node( + hass, generic_data, sent_messages, hass_ws_client, mqtt_mock +): """Test the ozw refresh node api.""" receive_message = await setup_ozw(hass, fixture=generic_data) client = await hass_ws_client(hass) @@ -368,7 +370,7 @@ async def test_refresh_node(hass, generic_data, sent_messages, hass_ws_client): assert result["node_query_stage"] == "versions" -async def test_refresh_node_unsubscribe(hass, generic_data, hass_ws_client): +async def test_refresh_node_unsubscribe(hass, generic_data, hass_ws_client, mqtt_mock): """Test unsubscribing the ozw refresh node api.""" await setup_ozw(hass, fixture=generic_data) client = await hass_ws_client(hass) diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 82811b61925..14e58d54ebe 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -6,7 +6,6 @@ import pytest import voluptuous as vol from homeassistant.bootstrap import async_setup_component -from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips from homeassistant.helpers.intent import ServiceIntentHandler, async_register @@ -47,38 +46,36 @@ async def test_snips_bad_config(hass, mqtt_mock): async def test_snips_config_feedback_on(hass, mqtt_mock): """Test Snips Config.""" - calls = async_mock_service(hass, "mqtt", "publish", MQTT_PUBLISH_SCHEMA) result = await async_setup_component( hass, "snips", {"snips": {"feedback_sounds": True}} ) assert result await hass.async_block_till_done() - assert len(calls) == 2 - topic = calls[0].data["topic"] + assert mqtt_mock.async_publish.call_count == 2 + topic = mqtt_mock.async_publish.call_args_list[0][0][0] assert topic == "hermes/feedback/sound/toggleOn" - topic = calls[1].data["topic"] + topic = mqtt_mock.async_publish.call_args_list[1][0][0] assert topic == "hermes/feedback/sound/toggleOn" - assert calls[1].data["qos"] == 1 - assert calls[1].data["retain"] + assert mqtt_mock.async_publish.call_args_list[1][0][2] == 1 + assert mqtt_mock.async_publish.call_args_list[1][0][3] async def test_snips_config_feedback_off(hass, mqtt_mock): """Test Snips Config.""" - calls = async_mock_service(hass, "mqtt", "publish", MQTT_PUBLISH_SCHEMA) result = await async_setup_component( hass, "snips", {"snips": {"feedback_sounds": False}} ) assert result await hass.async_block_till_done() - assert len(calls) == 2 - topic = calls[0].data["topic"] + assert mqtt_mock.async_publish.call_count == 2 + topic = mqtt_mock.async_publish.call_args_list[0][0][0] assert topic == "hermes/feedback/sound/toggleOn" - topic = calls[1].data["topic"] + topic = mqtt_mock.async_publish.call_args_list[1][0][0] assert topic == "hermes/feedback/sound/toggleOff" - assert calls[1].data["qos"] == 0 - assert not calls[1].data["retain"] + assert mqtt_mock.async_publish.call_args_list[1][0][2] == 0 + assert not mqtt_mock.async_publish.call_args_list[1][0][3] async def test_snips_config_no_feedback(hass, mqtt_mock): @@ -232,7 +229,6 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): async def test_intent_speech_response(hass, mqtt_mock): """Test intent speech response via Snips.""" - calls = async_mock_service(hass, "mqtt", "publish", MQTT_PUBLISH_SCHEMA) result = await async_setup_component(hass, "snips", {"snips": {}}) assert result result = await async_setup_component( @@ -261,9 +257,9 @@ async def test_intent_speech_response(hass, mqtt_mock): async_fire_mqtt_message(hass, "hermes/intent/spokenIntent", payload) await hass.async_block_till_done() - assert len(calls) == 1 - payload = json.loads(calls[0].data["payload"]) - topic = calls[0].data["topic"] + assert mqtt_mock.async_publish.call_count == 1 + payload = json.loads(mqtt_mock.async_publish.call_args[0][1]) + topic = mqtt_mock.async_publish.call_args[0][0] assert payload["sessionId"] == "abcdef0123456789" assert payload["text"] == "I am speaking to you" assert topic == "hermes/dialogueManager/endSession" From feda48f59907cc68b721a84487f6daee153f1b87 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 00:47:54 -0600 Subject: [PATCH 0007/1452] Add additional MAC address pattern for Guardian DHCP discovery (#58562) --- homeassistant/components/guardian/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index baa7eb50e7a..cbdd8fe3ba8 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -12,6 +12,10 @@ "hostname": "gvc*", "macaddress": "30AEA4*" }, + { + "hostname": "gvc*", + "macaddress": "B4E62D*" + }, { "hostname": "guardian*", "macaddress": "30AEA4*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 1bb37e6c736..03adeed3ed2 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -166,6 +166,11 @@ DHCP = [ "hostname": "gvc*", "macaddress": "30AEA4*" }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "B4E62D*" + }, { "domain": "guardian", "hostname": "guardian*", From bcd4ffdef3cc140b4a106342494a3c6bf58aacdc Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 28 Oct 2021 08:56:38 +0200 Subject: [PATCH 0008/1452] Add `configuration_url` to Freebox integration (#58555) --- homeassistant/components/freebox/router.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 2eef38d7d1e..e352146915e 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -184,6 +184,7 @@ class FreeboxRouter: def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url=f"https://{self._host}:{self._port}/", connections={(CONNECTION_NETWORK_MAC, self.mac)}, identifiers={(DOMAIN, self.mac)}, manufacturer="Freebox SAS", From f28de7891d3dd2b7db6e2f7f5d9ab7c107d0e56b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 28 Oct 2021 09:03:53 +0200 Subject: [PATCH 0009/1452] Add `configuration_url` to iCloud integration (#58557) --- homeassistant/components/icloud/device_tracker.py | 1 + homeassistant/components/icloud/sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index d255d29b6ee..4c6dcf37297 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -116,6 +116,7 @@ class IcloudTrackerEntity(TrackerEntity): def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url="https://icloud.com/", identifiers={(DOMAIN, self._device.unique_id)}, manufacturer="Apple", model=self._device.device_model, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6de8a49daf1..699f3e9baa3 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -94,6 +94,7 @@ class IcloudDeviceBatterySensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url="https://icloud.com/", identifiers={(DOMAIN, self._device.unique_id)}, manufacturer="Apple", model=self._device.device_model, From 892df608e9f9f1407740adf7a62ccfafc86a8ec1 Mon Sep 17 00:00:00 2001 From: Dave Lowper Date: Thu, 28 Oct 2021 09:30:06 +0200 Subject: [PATCH 0010/1452] Fix ZeroDivisionError on freebox/sensor (#57077) Co-authored-by: Martin Hjelmare --- homeassistant/components/freebox/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 814c2ea402f..016434ac89f 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -177,6 +177,9 @@ class FreeboxDiskSensor(FreeboxSensor): @callback def async_update_state(self) -> None: """Update the Freebox disk sensor.""" - self._attr_native_value = round( - self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 - ) + value = None + if self._partition.get("total_bytes"): + value = round( + self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 + ) + self._attr_native_value = value From f7797328e6574d14558997329ab2fa30b830e5fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:38:53 +0200 Subject: [PATCH 0011/1452] Use DeviceInfo in soma (#58572) Co-authored-by: epenet --- homeassistant/components/soma/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 77ecd3af96b..532e6204ad9 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import API, DOMAIN, HOST, PORT @@ -89,13 +89,13 @@ class SomaEntity(Entity): return self.device["name"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes. Implemented by platform classes. """ - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Wazombi Labs", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Wazombi Labs", + name=self.name, + ) From 3e4d3884916a8318c7c1f6f9ee4100b53ddf9395 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:39:12 +0200 Subject: [PATCH 0012/1452] Use DeviceInfo in somfy-mylink (#58573) Co-authored-by: epenet --- homeassistant/components/somfy_mylink/cover.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 2725e2da9c7..b4eb847a5e0 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -8,6 +8,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -115,13 +116,13 @@ class SomfyShade(RestoreEntity, CoverEntity): return self._closed @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._target_id)}, - "name": self._name, - "manufacturer": MANUFACTURER, - } + return DeviceInfo( + identifiers={(DOMAIN, self._target_id)}, + manufacturer=MANUFACTURER, + name=self._name, + ) async def async_close_cover(self, **kwargs): """Close the cover.""" From b175f424d69952dbe8d93aca25c59721cb59fb9c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:39:42 +0200 Subject: [PATCH 0013/1452] Use DeviceInfo in spider (#58575) Co-authored-by: epenet --- homeassistant/components/spider/climate.py | 15 +++++++------- homeassistant/components/spider/sensor.py | 24 +++++++++++----------- homeassistant/components/spider/switch.py | 15 +++++++------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index fbae603a239..72ae67c7600 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -8,6 +8,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN @@ -46,14 +47,14 @@ class SpiderThermostat(ClimateEntity): self.support_hvac.append(SPIDER_STATE_TO_HA[operation_value]) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.thermostat.id)}, - "name": self.thermostat.name, - "manufacturer": self.thermostat.manufacturer, - "model": self.thermostat.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.thermostat.id)}, + manufacturer=self.thermostat.manufacturer, + model=self.thermostat.model, + name=self.thermostat.name, + ) @property def supported_features(self): diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index 8b38fdbe6f6..c390e060194 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -42,12 +42,12 @@ class SpiderPowerPlugEnergy(SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.power_plug.id)}, - "name": self.power_plug.name, - "manufacturer": self.power_plug.manufacturer, - "model": self.power_plug.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.power_plug.id)}, + manufacturer=self.power_plug.manufacturer, + model=self.power_plug.model, + name=self.power_plug.name, + ) @property def unique_id(self) -> str: @@ -84,12 +84,12 @@ class SpiderPowerPlugPower(SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.power_plug.id)}, - "name": self.power_plug.name, - "manufacturer": self.power_plug.manufacturer, - "model": self.power_plug.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.power_plug.id)}, + manufacturer=self.power_plug.manufacturer, + model=self.power_plug.model, + name=self.power_plug.name, + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index ceb814b234a..4569105b8f4 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -1,5 +1,6 @@ """Support for Spider switches.""" from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN @@ -24,14 +25,14 @@ class SpiderPowerPlug(SwitchEntity): self.power_plug = power_plug @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.power_plug.id)}, - "name": self.power_plug.name, - "manufacturer": self.power_plug.manufacturer, - "model": self.power_plug.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.power_plug.id)}, + manufacturer=self.power_plug.manufacturer, + model=self.power_plug.model, + name=self.power_plug.name, + ) @property def unique_id(self): From 1a5333f376798ef928eb83509e3276d10e43a777 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:40:01 +0200 Subject: [PATCH 0014/1452] Use DeviceInfo in starline (#58576) Co-authored-by: epenet --- homeassistant/components/starline/account.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 9033375ce90..920c9214aec 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -129,13 +129,13 @@ class StarlineAccount: @staticmethod def device_info(device: StarlineDevice) -> DeviceInfo: """Device information for entities.""" - return { - "identifiers": {(DOMAIN, device.device_id)}, - "manufacturer": "StarLine", - "name": device.name, - "sw_version": device.fw_version, - "model": device.typename, - } + return DeviceInfo( + identifiers={(DOMAIN, device.device_id)}, + manufacturer="StarLine", + model=device.typename, + name=device.name, + sw_version=device.fw_version, + ) @staticmethod def gps_attrs(device: StarlineDevice) -> dict[str, Any]: From d579d90f10b9fcee7ed0940ae38a0f6b76b6dacc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:40:35 +0200 Subject: [PATCH 0015/1452] Use DeviceInfo in switchbot (#58578) Co-authored-by: epenet --- homeassistant/components/switchbot/entity.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index d6e88174d79..688cfea6a86 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -28,12 +28,12 @@ class SwitchbotEntity(CoordinatorEntity, Entity): self._idx = idx self._mac = mac self._attr_name = name - self._attr_device_info: DeviceInfo = { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, - "name": name, - "model": self.data["modelName"], - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, + manufacturer=MANUFACTURER, + model=self.data["modelName"], + name=name, + ) @property def data(self) -> dict[str, Any]: From e99bef7b6e8506690023aa3fe4d9bd21b8773f3e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:41:07 +0200 Subject: [PATCH 0016/1452] Use DeviceInfo in songpal (#58574) Co-authored-by: epenet --- .../components/songpal/media_player.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 1746d5ece0d..b13ca99cd5b 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_platform, ) +from homeassistant.helpers.entity import DeviceInfo from .const import CONF_ENDPOINT, DOMAIN, SET_SOUND_SETTING @@ -205,16 +206,16 @@ class SongpalEntity(MediaPlayerEntity): return self._sysinfo.macAddr @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)}, - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": "Sony Corporation", - "name": self.name, - "sw_version": self._sysinfo.version, - "model": self._model, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)}, + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Sony Corporation", + model=self._model, + name=self.name, + sw_version=self._sysinfo.version, + ) @property def available(self): From 7a728997bbae1be786eb9a79aaab01b30c95c8a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Oct 2021 11:05:02 +0200 Subject: [PATCH 0017/1452] Remove incorrect use of iscoroutinefunction from duckdns (#58585) --- homeassistant/components/duckdns/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 76353415d4f..2271b107b36 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,5 +1,4 @@ """Integrate with DuckDNS.""" -from asyncio import iscoroutinefunction from datetime import timedelta import logging @@ -102,10 +101,6 @@ async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False) @bind_hass def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" - if not iscoroutinefunction: - _LOGGER.error("Action needs to be a coroutine and return True/False") - return - if not isinstance(intervals, (list, tuple)): intervals = (intervals,) remove = None From 6a3c23d02a036798487d1f9561132a374f176251 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Oct 2021 02:20:28 -0700 Subject: [PATCH 0018/1452] Fix default value for host in octoprint config flow (#58568) --- homeassistant/components/octoprint/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 5962aedc89f..9674806d49f 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -22,7 +22,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def _schema_with_defaults(username="", host=None, port=80, path="/", ssl=False): +def _schema_with_defaults(username="", host="", port=80, path="/", ssl=False): return vol.Schema( { vol.Required(CONF_USERNAME, default=username): cv.string, From ea028e38d5eb285b305720aad349413e44fe061c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 28 Oct 2021 23:11:54 +1300 Subject: [PATCH 0019/1452] Allow configuration_url to be removed/nullified from device registry (#58564) * Allow configuration_url to be removed from device registry * Add test * Check for None before stringifying and url parsing * Add type to dict to remove mypy error on assigning None --- homeassistant/helpers/entity_platform.py | 29 ++++++++------- tests/helpers/test_entity_platform.py | 45 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 1864111c0ed..c44eb96026d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -458,7 +458,9 @@ class EntityPlatform: device_id = None if config_entry_id is not None and device_info is not None: - processed_dev_info = {"config_entry_id": config_entry_id} + processed_dev_info: dict[str, str | None] = { + "config_entry_id": config_entry_id + } for key in ( "connections", "default_manufacturer", @@ -477,18 +479,21 @@ class EntityPlatform: processed_dev_info[key] = device_info[key] # type: ignore[misc] if "configuration_url" in device_info: - configuration_url = str(device_info["configuration_url"]) - if urlparse(configuration_url).scheme in [ - "http", - "https", - "homeassistant", - ]: - processed_dev_info["configuration_url"] = configuration_url + if device_info["configuration_url"] is None: + processed_dev_info["configuration_url"] = None else: - _LOGGER.warning( - "Ignoring invalid device configuration_url '%s'", - configuration_url, - ) + configuration_url = str(device_info["configuration_url"]) + if urlparse(configuration_url).scheme in [ + "http", + "https", + "homeassistant", + ]: + processed_dev_info["configuration_url"] = configuration_url + else: + _LOGGER.warning( + "Ignoring invalid device configuration_url '%s'", + configuration_url, + ) try: device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 636ce7a764b..a151a3b7ef3 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1011,6 +1011,51 @@ async def test_device_info_homeassistant_url(hass, caplog): assert device.configuration_url == "homeassistant://config/mqtt" +async def test_device_info_change_to_no_url(hass, caplog): + """Test device info changes to no URL.""" + registry = dr.async_get(hass) + registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("mqtt", "via-id")}, + manufacturer="manufacturer", + model="via", + configuration_url="homeassistant://config/mqtt", + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + # Valid device info, with homeassistant url + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("mqtt", "1234")}, + "configuration_url": None, + }, + ), + ] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + device = registry.async_get_device({("mqtt", "1234")}) + assert device is not None + assert device.identifiers == {("mqtt", "1234")} + assert device.configuration_url is None + + async def test_entity_disabled_by_integration(hass): """Test entity disabled by integration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) From 5851d5246e55b1f7148c3912d5b56eebae1e629a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Oct 2021 14:23:26 +0200 Subject: [PATCH 0020/1452] Bump hatasmota to 0.3.0 (#58592) --- homeassistant/components/tasmota/__init__.py | 32 ++++++++++--------- homeassistant/components/tasmota/cover.py | 8 ++--- homeassistant/components/tasmota/discovery.py | 6 ++-- homeassistant/components/tasmota/fan.py | 4 +-- homeassistant/components/tasmota/light.py | 4 +-- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/mixins.py | 5 ++- homeassistant/components/tasmota/switch.py | 4 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 3 ++ tests/components/tasmota/test_common.py | 14 ++++++-- tests/components/tasmota/test_cover.py | 4 +++ tests/components/tasmota/test_fan.py | 3 ++ tests/components/tasmota/test_light.py | 22 +++++++++++++ tests/components/tasmota/test_sensor.py | 10 ++++++ tests/components/tasmota/test_switch.py | 2 ++ 17 files changed, 90 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index f33359347b8..f8dcd4035df 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -49,17 +49,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websocket_api.async_register_command(hass, websocket_remove_device) hass.data[DATA_UNSUB] = [] - def _publish( + async def _publish( topic: str, payload: mqtt.PublishPayloadType, - qos: int | None = None, - retain: bool | None = None, + qos: int | None, + retain: bool | None, ) -> None: - if qos is None: - qos = 0 - if retain is None: - retain = False - hass.async_create_task(mqtt.async_publish(hass, topic, payload, qos, retain)) + await mqtt.async_publish(hass, topic, payload, qos, retain) async def _subscribe_topics(sub_state: dict | None, topics: dict) -> dict: # Optionally mark message handlers as callback @@ -75,9 +71,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = await hass.helpers.device_registry.async_get_registry() - def async_discover_device(config: TasmotaDeviceConfig, mac: str) -> None: + async def async_discover_device(config: TasmotaDeviceConfig, mac: str) -> None: """Discover and add a Tasmota device.""" - async_setup_device(hass, mac, config, entry, tasmota_mqtt, device_registry) + await async_setup_device( + hass, mac, config, entry, tasmota_mqtt, device_registry + ) async def async_device_removed(event: Event) -> None: """Handle the removal of a device.""" @@ -92,7 +90,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: macs = [c[1] for c in device.connections if c[0] == CONNECTION_NETWORK_MAC] for mac in macs: - clear_discovery_topic(mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt) + await clear_discovery_topic( + mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt + ) hass.data[DATA_UNSUB].append( hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) @@ -143,7 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _remove_device( +async def _remove_device( hass: HomeAssistant, config_entry: ConfigEntry, mac: str, @@ -158,7 +158,9 @@ def _remove_device( _LOGGER.debug("Removing tasmota device %s", mac) device_registry.async_remove_device(device.id) - clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt) + await clear_discovery_topic( + mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt + ) def _update_device( @@ -180,7 +182,7 @@ def _update_device( ) -def async_setup_device( +async def async_setup_device( hass: HomeAssistant, mac: str, config: TasmotaDeviceConfig, @@ -190,7 +192,7 @@ def async_setup_device( ) -> None: """Set up the Tasmota device.""" if not config: - _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry) + await _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry) else: _update_device(hass, config_entry, config, device_registry) diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py index 458c712ae3d..0b67e469929 100644 --- a/homeassistant/components/tasmota/cover.py +++ b/homeassistant/components/tasmota/cover.py @@ -111,17 +111,17 @@ class TasmotaCover( async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - self._tasmota_entity.open() + await self._tasmota_entity.open() async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - self._tasmota_entity.close() + await self._tasmota_entity.close() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[cover.ATTR_POSITION] - self._tasmota_entity.set_position(position) + await self._tasmota_entity.set_position(position) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - self._tasmota_entity.stop() + await self._tasmota_entity.stop() diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 37b373d30a1..5ba4a4032f8 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Callable +from typing import Awaitable, Callable from hatasmota.discovery import ( TasmotaDiscovery, @@ -34,7 +34,7 @@ TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}" TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}" TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance" -SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], None] +SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], Awaitable[None]] def clear_discovery_hash( @@ -119,7 +119,7 @@ async def async_start( _LOGGER.debug("Received discovery data for tasmota device: %s", mac) tasmota_device_config = tasmota_get_device_config(payload) - setup_device(tasmota_device_config, mac) + await setup_device(tasmota_device_config, mac) if not payload: return diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 92399fa1bbc..6aabfb05091 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -109,7 +109,7 @@ class TasmotaFan( tasmota_speed = percentage_to_ordered_list_item( ORDERED_NAMED_FAN_SPEEDS, percentage ) - self._tasmota_entity.set_speed(tasmota_speed) + await self._tasmota_entity.set_speed(tasmota_speed) async def async_turn_on( self, @@ -129,4 +129,4 @@ class TasmotaFan( async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" - self._tasmota_entity.set_speed(tasmota_const.FAN_SPEED_OFF) + await self._tasmota_entity.set_speed(tasmota_const.FAN_SPEED_OFF) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index c09b4c71948..e739967b48b 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -275,7 +275,7 @@ class TasmotaLight( if ATTR_EFFECT in kwargs: attributes["effect"] = kwargs[ATTR_EFFECT] - self._tasmota_entity.set_state(True, attributes) + await self._tasmota_entity.set_state(True, attributes) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" @@ -284,4 +284,4 @@ class TasmotaLight( if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] - self._tasmota_entity.set_state(False, attributes) + await self._tasmota_entity.set_state(False, attributes) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 592f833fd12..9ea06bea545 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.21"], + "requirements": ["hatasmota==0.3.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index a07e48b53a7..8c7ef9ba703 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -123,10 +123,9 @@ class TasmotaAvailability(TasmotaEntity): ) await super().async_added_to_hass() - @callback - def availability_updated(self, available: bool) -> None: + async def availability_updated(self, available: bool) -> None: """Handle updated availability.""" - self._tasmota_entity.poll_status() + await self._tasmota_entity.poll_status() self._available = available self.async_write_ha_state() diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py index 50319abac56..8ee4d2f47ee 100644 --- a/homeassistant/components/tasmota/switch.py +++ b/homeassistant/components/tasmota/switch.py @@ -58,8 +58,8 @@ class TasmotaSwitch( async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self._tasmota_entity.set_state(True) + await self._tasmota_entity.set_state(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self._tasmota_entity.set_state(False) + await self._tasmota_entity.set_state(False) diff --git a/requirements_all.txt b/requirements_all.txt index 1a2a3b7d493..8eba0757a04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ hass-nabucasa==0.50.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.21 +hatasmota==0.3.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82199eadaf1..ceac7475402 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -482,7 +482,7 @@ hangups==0.4.14 hass-nabucasa==0.50.0 # homeassistant.components.tasmota -hatasmota==0.2.21 +hatasmota==0.3.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 6b13dcc89ec..2ee40428293 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -56,6 +56,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -121,6 +122,7 @@ async def test_controlling_state_via_mqtt_switchname(hass, mqtt_mock, setup_tasm assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("binary_sensor.custom_name") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -179,6 +181,7 @@ async def test_pushon_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota) assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 9174060ef93..6c296a78006 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -130,7 +130,7 @@ async def help_test_availability_when_connection_lost( get_topic_tele_will(config), config_get_state_online(config), ) - + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE @@ -158,6 +158,7 @@ async def help_test_availability_when_connection_lost( get_topic_tele_will(config), config_get_state_online(config), ) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE @@ -196,7 +197,7 @@ async def help_test_availability( get_topic_tele_will(config), config_get_state_online(config), ) - + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE @@ -205,7 +206,7 @@ async def help_test_availability( get_topic_tele_will(config), config_get_state_offline(config), ) - + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state == STATE_UNAVAILABLE @@ -258,10 +259,12 @@ async def help_test_availability_discovery_update( assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, online1) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, offline1) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state == STATE_UNAVAILABLE @@ -273,11 +276,13 @@ async def help_test_availability_discovery_update( async_fire_mqtt_message(hass, availability_topic1, online1) async_fire_mqtt_message(hass, availability_topic1, online2) async_fire_mqtt_message(hass, availability_topic2, online1) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state == STATE_UNAVAILABLE # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, availability_topic2, online2) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE @@ -575,10 +580,12 @@ async def help_test_entity_id_update_discovery_update( await hass.async_block_till_done() async_fire_mqtt_message(hass, topic, config_get_state_online(config)) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, topic, config_get_state_offline(config)) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.{entity_id}") assert state.state == STATE_UNAVAILABLE @@ -597,5 +604,6 @@ async def help_test_entity_id_update_discovery_update( topic = get_topic_tele_will(config) async_fire_mqtt_message(hass, topic, config_get_state_online(config)) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.milk") assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 131f95842a5..54ac192f7c1 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -53,6 +53,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("cover.tasmota_cover_1") assert state.state == STATE_UNKNOWN assert ( @@ -215,6 +216,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("cover.tasmota_cover_1") assert state.state == STATE_UNKNOWN assert ( @@ -383,6 +385,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("cover.test_cover_1") assert state.state == STATE_UNKNOWN await hass.async_block_till_done() @@ -446,6 +449,7 @@ async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("cover.test_cover_1") assert state.state == STATE_UNKNOWN await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 202a6a5386b..bb2610d466d 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -50,6 +50,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["percentage"] is None @@ -101,6 +102,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -167,6 +169,7 @@ async def test_invalid_fan_speed_percentage(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 411567208db..ec3d67ef8f2 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -46,6 +46,7 @@ async def test_attributes_on_off(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -72,6 +73,7 @@ async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -97,6 +99,7 @@ async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -122,6 +125,7 @@ async def test_attributes_ct(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -148,6 +152,7 @@ async def test_attributes_ct_reduced(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -173,6 +178,7 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -207,6 +213,7 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -241,6 +248,7 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -276,6 +284,7 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") @@ -316,6 +325,7 @@ async def test_controlling_state_via_mqtt_on_off(hass, mqtt_mock, setup_tasmota) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -364,6 +374,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -426,6 +437,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -524,6 +536,7 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -625,6 +638,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -729,6 +743,7 @@ async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -770,6 +785,7 @@ async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -817,6 +833,7 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -923,6 +940,7 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -1029,6 +1047,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -1114,6 +1133,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -1164,6 +1184,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() @@ -1345,6 +1366,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c6f27f0193c..0cd18c89435 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -155,6 +155,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert entry.entity_category is None async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_dht11_temperature") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -200,6 +201,7 @@ async def test_nested_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_tx23_speed_act") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -245,6 +247,7 @@ async def test_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_energy_totaltariff_1") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -293,6 +296,7 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): ) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_energy_total") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -343,6 +347,7 @@ async def test_indexed_sensor_state_via_mqtt3(hass, mqtt_mock, setup_tasmota): ) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_energy_total_1") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -396,6 +401,7 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota) assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_energy_apparentpower_0") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -507,6 +513,7 @@ async def test_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_status") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -566,6 +573,7 @@ async def test_single_shot_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_t assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_status") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -650,6 +658,7 @@ async def test_restart_time_status_sensor_state_via_mqtt( assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_status") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -863,6 +872,7 @@ async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("sensor.tasmota_signal") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 00b0a922e0a..7c5bf66db45 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -48,6 +48,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -87,6 +88,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_OFF await hass.async_block_till_done() From f2169ba111feb08d092d4e08e160dc893cff00f6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 28 Oct 2021 08:27:01 -0400 Subject: [PATCH 0021/1452] Fix missing config string in sense (#58597) --- homeassistant/components/sense/strings.json | 3 ++- homeassistant/components/sense/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json index 44a45f07ce9..29e85c98fc2 100644 --- a/homeassistant/components/sense/strings.json +++ b/homeassistant/components/sense/strings.json @@ -5,7 +5,8 @@ "title": "Connect to your Sense Energy Monitor", "data": { "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "timeout": "Timeout" } } }, diff --git a/homeassistant/components/sense/translations/en.json b/homeassistant/components/sense/translations/en.json index 5582a8424a6..24cde7411a8 100644 --- a/homeassistant/components/sense/translations/en.json +++ b/homeassistant/components/sense/translations/en.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Email", - "password": "Password" + "password": "Password", + "timeout": "Timeout" }, "title": "Connect to your Sense Energy Monitor" } From 383a820ae10e62a3dbd9de8e9ff6cbfa34d50d8d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 14:29:37 +0200 Subject: [PATCH 0022/1452] Use DeviceInfo in switcher-kis (#58579) Co-authored-by: epenet --- homeassistant/components/switcher_kis/switch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 8b93e422e2a..620a742f4e3 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -23,6 +23,7 @@ from homeassistant.helpers import ( entity_platform, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -92,11 +93,11 @@ class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity): # Entity class attributes self._attr_name = coordinator.name self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" - self._attr_device_info = { - "connections": { + self._attr_device_info = DeviceInfo( + connections={ (device_registry.CONNECTION_NETWORK_MAC, coordinator.mac_address) } - } + ) @callback def _handle_coordinator_update(self) -> None: From 03100693b0a57ccf509cb5df9beadfb28214d535 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 14:30:02 +0200 Subject: [PATCH 0023/1452] Use DeviceInfo in syncthing (#58580) Co-authored-by: epenet --- homeassistant/components/syncthing/sensor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index e88636b814b..192b4c5c395 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -130,15 +131,15 @@ class FolderSensor(SensorEntity): return False @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self._server_id)}, - "name": f"Syncthing ({self._syncthing.url})", - "manufacturer": "Syncthing Team", - "sw_version": self._version, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._server_id)}, + manufacturer="Syncthing Team", + name=f"Syncthing ({self._syncthing.url})", + sw_version=self._version, + ) async def async_update_status(self): """Request folder status and update state.""" From d7edb5b11c5b2cc2f3925425c5d581aa8b9922a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 14:31:11 +0200 Subject: [PATCH 0024/1452] Use DeviceInfo in syncthru (#58581) Co-authored-by: epenet --- homeassistant/components/syncthru/binary_sensor.py | 10 ++++++++-- homeassistant/components/syncthru/sensor.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 1c402fbf836..18780e9225e 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Samsung Printers with SyncThru web interface.""" +from __future__ import annotations from pysyncthru import SyncThru, SyncthruState @@ -8,6 +9,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -63,9 +65,13 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information.""" - return {"identifiers": device_identifiers(self.syncthru)} + if (identifiers := device_identifiers(self.syncthru)) is None: + return None + return DeviceInfo( + identifiers=identifiers, + ) class SyncThruOnlineSensor(SyncThruBinarySensor): diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index cc832f77f0a..a3e24f9ffd8 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,4 +1,5 @@ """Support for Samsung Printers with SyncThru web interface.""" +from __future__ import annotations import logging @@ -9,6 +10,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -129,9 +131,13 @@ class SyncThruSensor(CoordinatorEntity, SensorEntity): return self._unit_of_measurement @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information.""" - return {"identifiers": device_identifiers(self.syncthru)} + if (identifiers := device_identifiers(self.syncthru)) is None: + return None + return DeviceInfo( + identifiers=identifiers, + ) class SyncThruMainSensor(SyncThruSensor): From 52b0107a771de942a924bb60936db37591641ef7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 14:31:30 +0200 Subject: [PATCH 0025/1452] Use DeviceInfo in system-bridge (#58582) Co-authored-by: epenet --- homeassistant/components/system_bridge/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index d1da463816f..faea8b8418c 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -262,10 +262,10 @@ class SystemBridgeDeviceEntity(SystemBridgeEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this System Bridge instance.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, - "manufacturer": self._manufacturer, - "model": self._model, - "name": self._hostname, - "sw_version": self._version, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, + manufacturer=self._manufacturer, + model=self._model, + name=self._hostname, + sw_version=self._version, + ) From 438d1b2f15a82c7a87e18a893f6241eb51b1b333 Mon Sep 17 00:00:00 2001 From: Robert Chmielowiec Date: Thu, 28 Oct 2021 14:32:20 +0200 Subject: [PATCH 0026/1452] Add `configuration_url` to Huawei LTE integration (#58584) --- homeassistant/components/huawei_lte/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c159b6530fb..f63b84254fc 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -431,6 +431,7 @@ async def async_setup_entry( # noqa: C901 # Set up device registry if router.device_identifiers or router.device_connections: device_info = DeviceInfo( + configuration_url=router.url, connections=router.device_connections, identifiers=router.device_identifiers, name=router.device_name, From 8c5832ae826d810eb782a191883b40d938d85ad7 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Thu, 28 Oct 2021 14:32:53 +0200 Subject: [PATCH 0027/1452] Add service configuration URL to Doorbird (#58549) --- homeassistant/components/doorbird/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 73404e7c199..2cf97aa4b57 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -28,6 +28,7 @@ class DoorBirdEntity(Entity): firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] return DeviceInfo( + configuration_url="https://webadmin.doorbird.com/", connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, manufacturer=MANUFACTURER, model=self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], From a0a8b9db26f665023f58326a826a939ae769800b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Oct 2021 14:36:41 +0200 Subject: [PATCH 0028/1452] Add entity category 'system' (#58595) * Add entity category 'hidden' * Update cloud * Update Google assistant * Update Alexa * Fix tests * Add ENTITY_CATEGORIES constant * Rename ENTITY_CATEGORY_HIDDEN to ENTITY_CATEGORY_SYSTEM * Correct import in motioneye --- .../components/alexa/smart_home_http.py | 12 ++-------- .../components/cloud/alexa_config.py | 11 ++------- .../components/cloud/google_config.py | 11 ++------- homeassistant/components/energy/sensor.py | 2 ++ .../components/google_assistant/http.py | 11 ++------- homeassistant/components/motioneye/switch.py | 2 +- homeassistant/const.py | 10 ++++++++ homeassistant/helpers/entity.py | 10 ++------ homeassistant/helpers/service.py | 8 ++----- tests/components/cloud/test_alexa_config.py | 22 +++++++++++++----- tests/components/cloud/test_google_config.py | 23 ++++++++++++++----- .../google_assistant/test_google_assistant.py | 8 +++++++ 12 files changed, 66 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index df4f95f12f2..237828987e8 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -3,12 +3,7 @@ import logging from homeassistant import core from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, ENTITY_CATEGORIES from homeassistant.helpers import entity_registry as er from .auth import Auth @@ -71,10 +66,7 @@ class AlexaConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category in ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ) + auxiliary_entity = registry_entry.entity_category in ENTITY_CATEGORIES else: auxiliary_entity = False return not auxiliary_entity diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 41bab5e0bd4..a6c30a5a79b 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -16,11 +16,7 @@ from homeassistant.components.alexa import ( errors as alexa_errors, state_report as alexa_state_report, ) -from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORIES from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry as er, start from homeassistant.helpers.event import async_call_later @@ -135,10 +131,7 @@ class AlexaConfig(alexa_config.AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category in ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ) + auxiliary_entity = registry_entry.entity_category in ENTITY_CATEGORIES else: auxiliary_entity = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index f3f5a64bbd6..9ecd76302b7 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -8,11 +8,7 @@ from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig -from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORIES from homeassistant.core import CoreState, split_entity_id from homeassistant.helpers import entity_registry as er, start from homeassistant.setup import async_setup_component @@ -133,10 +129,7 @@ class CloudGoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category in ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ) + auxiliary_entity = registry_entry.entity_category in ENTITY_CATEGORIES else: auxiliary_entity = False diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 8cd5702deb7..bfcc2817633 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + ENTITY_CATEGORY_SYSTEM, VOLUME_CUBIC_METERS, ) from homeassistant.core import ( @@ -215,6 +216,7 @@ class EnergyCostSensor(SensorEntity): utility. """ + _attr_entity_category = ENTITY_CATEGORY_SYSTEM _wrong_state_class_reported = False _wrong_unit_reported = False diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index ba7dc2597bc..e7a73351f60 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -11,11 +11,7 @@ import jwt # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORIES from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -117,10 +113,7 @@ class GoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) registry_entry = entity_registry.async_get(state.entity_id) if registry_entry: - auxiliary_entity = registry_entry.entity_category in ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ) + auxiliary_entity = registry_entry.entity_category in ENTITY_CATEGORIES else: auxiliary_entity = False diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 9a6fe27441f..695cde842a7 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -17,8 +17,8 @@ from motioneye_client.const import ( from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import ENTITY_CATEGORY_CONFIG from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/const.py b/homeassistant/const.py index 587e3bb9509..229fe704348 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -714,3 +714,13 @@ CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" ENTITY_CATEGORY_CONFIG: Final = "config" ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" +ENTITY_CATEGORY_SYSTEM: Final = "system" + +# Entity categories which will: +# - Not be exposed to cloud, alexa, or google_home components +# - Not be included in indirect service calls to devices or areas +ENTITY_CATEGORIES: Final[list[str]] = [ + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_SYSTEM, +] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a05d2c7c2fa..3b2a12c687e 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -26,8 +26,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORIES, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -56,11 +55,6 @@ SOURCE_PLATFORM_CONFIG = "platform_config" FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 -ENTITY_CATEGORIES: Final[list[str]] = [ - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, -] - ENTITY_CATEGORIES_SCHEMA: Final = vol.In(ENTITY_CATEGORIES) @@ -193,7 +187,7 @@ class EntityDescription: key: str device_class: str | None = None - entity_category: Literal["config", "diagnostic"] | None = None + entity_category: Literal["config", "diagnostic", "system"] | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index c2720c02f47..00369d43536 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,8 +20,7 @@ from homeassistant.const import ( CONF_SERVICE_DATA, CONF_SERVICE_TEMPLATE, CONF_TARGET, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORIES, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, ) @@ -367,10 +366,7 @@ def async_extract_referenced_entity_ids( for ent_entry in ent_reg.entities.values(): # Do not add config or diagnostic entities referenced by areas or devices - if ent_entry.entity_category in ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ): + if ent_entry.entity_category in ENTITY_CATEGORIES: continue if ( diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 9b0075db09d..8d08e924be7 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -22,19 +22,26 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): entity_registry = mock_registry(hass) entity_entry1 = entity_registry.async_get_or_create( - "switch", + "light", "test", - "switch_config_id", - suggested_object_id="config_switch", + "light_config_id", + suggested_object_id="config_light", entity_category="config", ) entity_entry2 = entity_registry.async_get_or_create( - "switch", + "light", "test", - "switch_diagnostic_id", - suggested_object_id="diagnostic_switch", + "light_diagnostic_id", + suggested_object_id="diagnostic_light", entity_category="diagnostic", ) + entity_entry3 = entity_registry.async_get_or_create( + "light", + "test", + "light_system_id", + suggested_object_id="system_light", + entity_category="system", + ) entity_conf = {"should_expose": False} await cloud_prefs.async_update( @@ -50,18 +57,21 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose("light.kitchen") assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) + assert not conf.should_expose(entity_entry3.entity_id) entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") # config and diagnostic entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) + assert not conf.should_expose(entity_entry3.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") # config and diagnostic entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) + assert not conf.should_expose(entity_entry3.entity_id) assert "alexa" not in hass.config.components await cloud_prefs.async_update( diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 99fa24a6cb9..478fa22f66c 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -223,19 +223,26 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): entity_registry = mock_registry(hass) entity_entry1 = entity_registry.async_get_or_create( - "switch", + "light", "test", - "switch_config_id", - suggested_object_id="config_switch", + "light_config_id", + suggested_object_id="config_light", entity_category="config", ) entity_entry2 = entity_registry.async_get_or_create( - "switch", + "light", "test", - "switch_diagnostic_id", - suggested_object_id="diagnostic_switch", + "light_diagnostic_id", + suggested_object_id="diagnostic_light", entity_category="diagnostic", ) + entity_entry3 = entity_registry.async_get_or_create( + "light", + "test", + "light_system_id", + suggested_object_id="system_light", + entity_category="system", + ) entity_conf = {"should_expose": False} await cloud_prefs.async_update( @@ -246,22 +253,26 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): state = State("light.kitchen", "on") state_config = State(entity_entry1.entity_id, "on") state_diagnostic = State(entity_entry2.entity_id, "on") + state_system = State(entity_entry3.entity_id, "on") assert not mock_conf.should_expose(state) assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) + assert not mock_conf.should_expose(state_system) entity_conf["should_expose"] = True assert mock_conf.should_expose(state) # config and diagnostic entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) + assert not mock_conf.should_expose(state_system) entity_conf["should_expose"] = None assert mock_conf.should_expose(state) # config and diagnostic entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) + assert not mock_conf.should_expose(state_system) await cloud_prefs.async_update( google_default_expose=["sensor"], diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index ded7429bdee..0a916c1e184 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -144,10 +144,18 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): suggested_object_id="diagnostic_switch", entity_category="diagnostic", ) + entity_entry3 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_system_id", + suggested_object_id="system_switch", + entity_category="system", + ) # These should not show up in the sync request hass_fixture.states.async_set(entity_entry1.entity_id, "on") hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") + hass_fixture.states.async_set(entity_entry3.entity_id, "blah") reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} From d27c91b9fed08970685d0908ad25397f5d16defb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 16:07:37 +0200 Subject: [PATCH 0029/1452] Use DeviceInfo in tasmota (#58604) Co-authored-by: epenet --- homeassistant/components/tasmota/mixins.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 8c7ef9ba703..1cac7fc2d4b 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -62,7 +62,9 @@ class TasmotaEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self._tasmota_entity.mac)}} + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._tasmota_entity.mac)} + ) @property def name(self) -> str | None: From d3bafce1570b6efc3fcbf5db8ffe7c318f217566 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:09:28 +0200 Subject: [PATCH 0030/1452] Use constants in acmeda config flow (#58590) Co-authored-by: epenet --- homeassistant/components/acmeda/config_flow.py | 13 +++++++------ homeassistant/data_entry_flow.py | 4 +++- tests/components/acmeda/test_config_flow.py | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 1f288e84bc7..460357baa78 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -8,7 +8,8 @@ import aiopulse import async_timeout import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import CONF_HOST, CONF_ID from .const import DOMAIN @@ -27,9 +28,9 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if ( user_input is not None and self.discovered_hubs is not None - and user_input["id"] in self.discovered_hubs + and user_input[CONF_ID] in self.discovered_hubs ): - return await self.async_create(self.discovered_hubs[user_input["id"]]) + return await self.async_create(self.discovered_hubs[user_input[CONF_ID]]) # Already configured hosts already_configured = { @@ -52,10 +53,10 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_hubs = {hub.id: hub for hub in hubs} return self.async_show_form( - step_id="user", + step_id=data_entry_flow.STEP_ID_USER, data_schema=vol.Schema( { - vol.Required("id"): vol.In( + vol.Required(CONF_ID): vol.In( {hub.id: f"{hub.id} {hub.host}" for hub in hubs} ) } @@ -65,4 +66,4 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_create(self, hub): """Create the Acmeda Hub entry.""" await self.async_set_unique_id(hub.id, raise_on_progress=False) - return self.async_create_entry(title=hub.id, data={"host": hub.host}) + return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host}) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c1f798fcc32..b5a35929459 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ import abc import asyncio from collections.abc import Iterable, Mapping from types import MappingProxyType -from typing import Any, TypedDict +from typing import Any, Final, TypedDict import uuid import voluptuous as vol @@ -21,6 +21,8 @@ RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" RESULT_TYPE_SHOW_PROGRESS = "progress" RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" +STEP_ID_USER: Final = "user" + # Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 269a72cd839..6355d1b1adb 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -69,7 +69,7 @@ async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { - "host": DUMMY_HOST1, + CONF_HOST: DUMMY_HOST1, } # Check we performed the discovery @@ -92,7 +92,7 @@ async def test_show_form_two_hubs(hass, mock_hub_discover): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["step_id"] == data_entry_flow.STEP_ID_USER # Check we performed the discovery assert len(mock_hub_discover.mock_calls) == 1 @@ -120,7 +120,7 @@ async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { - "host": DUMMY_HOST2, + CONF_HOST: DUMMY_HOST2, } From 11661454acec81129de4cad7db69d112f42a2925 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 11:13:21 -0500 Subject: [PATCH 0031/1452] Add tplink KP303 to discovery (#58548) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index c82eafb96d8..4db98d680d3 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,10 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + { + "hostname": "k[lp]*", + "macaddress": "005F67*" + }, { "hostname": "k[lp]*", "macaddress": "1027F5*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 03adeed3ed2..9e99e512d9e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -356,6 +356,11 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "005F67*" + }, { "domain": "tplink", "hostname": "k[lp]*", From 6cfa5b2a28fb1d44b5cf5c57fedcc685e3c9646b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:19:46 +0200 Subject: [PATCH 0032/1452] Use DeviceInfo in subaru (#58577) Co-authored-by: epenet --- homeassistant/components/subaru/entity.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/subaru/entity.py b/homeassistant/components/subaru/entity.py index 559feeea303..2bdb1425b2d 100644 --- a/homeassistant/components/subaru/entity.py +++ b/homeassistant/components/subaru/entity.py @@ -1,4 +1,5 @@ """Base class for all Subaru Entities.""" +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN @@ -25,10 +26,10 @@ class SubaruEntity(CoordinatorEntity): return f"{self.vin}_{self.entity_type}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.vin)}, - "name": self.car_name, - "manufacturer": MANUFACTURER, - } + return DeviceInfo( + identifiers={(DOMAIN, self.vin)}, + manufacturer=MANUFACTURER, + name=self.car_name, + ) From e64bc67bec6e98f0cc187775acfc0bc77cce68fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:20:09 +0200 Subject: [PATCH 0033/1452] Use DeviceInfo in tado (#58603) Co-authored-by: epenet --- homeassistant/components/tado/entity.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 95c0643191b..7827564afa5 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -42,14 +42,14 @@ class TadoHomeEntity(Entity): self.home_id = tado.home_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.home_id)}, - "name": self.home_name, - "manufacturer": DEFAULT_NAME, - "model": TADO_HOME, - } + return DeviceInfo( + identifiers={(DOMAIN, self.home_id)}, + manufacturer=DEFAULT_NAME, + model=TADO_HOME, + name=self.home_name, + ) class TadoZoneEntity(Entity): From a0b3a58d1c1ae321709c38569adcce55a950064b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:20:41 +0200 Subject: [PATCH 0034/1452] Use DeviceInfo in toon (#58605) Co-authored-by: epenet --- homeassistant/components/toon/models.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 116613640ad..57db44beb6b 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -23,15 +23,13 @@ class ToonDisplayDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this thermostat.""" agreement = self.coordinator.data.agreement - model = agreement.display_hardware_version.rpartition("/")[0] - sw_version = agreement.display_software_version.rpartition("/")[-1] - return { - "identifiers": {(DOMAIN, agreement.agreement_id)}, - "name": "Toon Display", - "manufacturer": "Eneco", - "model": model, - "sw_version": sw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, agreement.agreement_id)}, + manufacturer="Eneco", + model=agreement.display_hardware_version.rpartition("/")[0], + name="Toon Display", + sw_version=agreement.display_software_version.rpartition("/")[-1], + ) class ToonElectricityMeterDeviceEntity(ToonEntity): From 2dcb429c9541bda5c990fd0929ed73d360a0fef2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:23:16 +0200 Subject: [PATCH 0035/1452] Use DeviceInfo in tplink (#58606) Co-authored-by: epenet --- homeassistant/components/tplink/entity.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index b331f70c5bb..30c0fd60add 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -48,14 +48,14 @@ class CoordinatedTPLinkEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "name": self.device.alias, - "model": self.device.model, - "manufacturer": "TP-Link", - "identifiers": {(DOMAIN, str(self.device.device_id))}, - "connections": {(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, - "sw_version": self.device.hw_info["sw_ver"], - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, + identifiers={(DOMAIN, str(self.device.device_id))}, + manufacturer="TP-Link", + model=self.device.model, + name=self.device.alias, + sw_version=self.device.hw_info["sw_ver"], + ) @property def is_on(self) -> bool: From d065ddc5c19d6193494a4f3426a4a43090bdfc7d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:23:41 +0200 Subject: [PATCH 0036/1452] Use DeviceInfo in twentemilieu (#58608) Co-authored-by: epenet --- .../components/twentemilieu/sensor.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 89c750ec865..14568fa5d87 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -12,13 +12,7 @@ from twentemilieu import ( from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, - CONF_ID, - DEVICE_CLASS_DATE, -) +from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -153,8 +147,8 @@ class TwenteMilieuSensor(SensorEntity): @property def device_info(self) -> DeviceInfo: """Return device information about Twente Milieu.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, - ATTR_NAME: "Twente Milieu", - ATTR_MANUFACTURER: "Twente Milieu", - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer="Twente Milieu", + name="Twente Milieu", + ) From 7e9a67194a2fed7d1e651fc48ada10998d64b0cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:24:20 +0200 Subject: [PATCH 0037/1452] Use DeviceInfo in twinkly (#58609) Co-authored-by: epenet --- homeassistant/components/twinkly/light.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index b0f94a1c52f..2c742ba93f5 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -108,12 +108,12 @@ class TwinklyLight(LightEntity): def device_info(self) -> DeviceInfo | None: """Get device specific attributes.""" return ( - { - "identifiers": {(DOMAIN, self._id)}, - "name": self.name, - "manufacturer": "LEDWORKS", - "model": self.model, - } + DeviceInfo( + identifiers={(DOMAIN, self._id)}, + manufacturer="LEDWORKS", + model=self.model, + name=self.name, + ) if self._id else None # device_info is available only for entities configured from the UI ) From 7d235cb9bbadda232a85d2ef001391e9ff5ab81b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:24:40 +0200 Subject: [PATCH 0038/1452] Use DeviceInfo in vizio (#58617) Co-authored-by: epenet --- homeassistant/components/vizio/media_player.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index c60ae4582ad..0bff362ad31 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -194,13 +195,13 @@ class VizioDevice(MediaPlayerEntity): self._attr_available = True if not self._attr_device_info: - self._attr_device_info = { - "identifiers": {(DOMAIN, self._attr_unique_id)}, - "name": self._attr_name, - "manufacturer": "VIZIO", - "model": await self._device.get_model_name(log_api_exception=False), - "sw_version": await self._device.get_version(log_api_exception=False), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="VIZIO", + model=await self._device.get_model_name(log_api_exception=False), + name=self._attr_name, + sw_version=await self._device.get_version(log_api_exception=False), + ) if not is_on: self._attr_state = STATE_OFF From 8ed4e500d31e72bc242863ddce0729f08b58d332 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:25:10 +0200 Subject: [PATCH 0039/1452] Use DeviceInfo in vlc-telnet (#58618) Co-authored-by: epenet --- homeassistant/components/vlc_telnet/media_player.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 624234ce712..cb231f62861 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -36,6 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -140,12 +141,12 @@ class VlcDevice(MediaPlayerEntity): self._media_title: str | None = None config_entry_id = config_entry.entry_id self._attr_unique_id = config_entry_id - self._attr_device_info = { - "name": name, - "identifiers": {(DOMAIN, config_entry_id)}, - "manufacturer": "VideoLAN", - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, config_entry_id)}, + manufacturer="VideoLAN", + name=name, + ) @catch_vlc_errors async def async_update(self) -> None: From 1c7fbb754093aad646c62e1916e42677fd164c23 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:25:40 +0200 Subject: [PATCH 0040/1452] Use DeviceInfo in volumio (#58619) Co-authored-by: epenet --- .../components/volumio/media_player.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 86747519149..06ee0346b7f 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -31,6 +31,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from .browse_media import browse_node, browse_top_level @@ -99,15 +100,15 @@ class Volumio(MediaPlayerEntity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Volumio", - "sw_version": self._info["systemversion"], - "model": self._info["hardware"], - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Volumio", + model=self._info["hardware"], + name=self.name, + sw_version=self._info["systemversion"], + ) @property def media_content_type(self): From 6391376d6f5b51dd923847b2d6f651d8eb0565b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:26:21 +0200 Subject: [PATCH 0041/1452] Use DeviceInfo in unifi (#58620) Co-authored-by: epenet --- .../components/unifi/device_tracker.py | 18 ++++++++++-------- homeassistant/components/unifi/switch.py | 14 +++++++------- homeassistant/components/unifi/unifi_client.py | 10 +++++----- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ecb549e89ef..abf37a02fa8 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -18,10 +18,12 @@ from aiounifi.events import ( from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.const import ATTR_NAME from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -405,17 +407,17 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): return not self.device.disabled and self.controller.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - info = { - "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "manufacturer": ATTR_MANUFACTURER, - "model": self.device.model, - "sw_version": self.device.version, - } + info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=self.device.model, + sw_version=self.device.version, + ) if self.device.name: - info["name"] = self.device.name + info[ATTR_NAME] = self.device.name return info diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 03cd9056830..3c002642564 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -369,10 +369,10 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): @property def device_info(self) -> DeviceInfo: """Return a service description for device registry.""" - return { - "identifiers": {(DOMAIN, f"unifi_controller_{self._item.site_id}")}, - "name": "UniFi Controller", - "manufacturer": ATTR_MANUFACTURER, - "model": "UniFi Controller", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, f"unifi_controller_{self._item.site_id}")}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Controller", + name="UniFi Controller", + ) diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 3340616dada..9e90eef518a 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -47,8 +47,8 @@ class UniFiClient(UniFiBase): @property def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}, - "default_name": self.name, - "default_manufacturer": self.client.oui, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.client.mac)}, + default_manufacturer=self.client.oui, + default_name=self.name, + ) From d214bfec479836d3c9c2fc83c1e4ec898f348570 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:26:53 +0200 Subject: [PATCH 0042/1452] Use DeviceInfo in upb (#58621) Co-authored-by: epenet --- homeassistant/components/upb/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index a3c3016dc05..90d7c35cf64 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -4,7 +4,7 @@ import upb_lib from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( ATTR_ADDRESS, @@ -119,12 +119,12 @@ class UpbAttachedEntity(UpbEntity): """Base class for UPB attached entities.""" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for the entity.""" - return { - "name": self._element.name, - "identifiers": {(DOMAIN, self._element.index)}, - "sw_version": self._element.version, - "manufacturer": self._element.manufacturer, - "model": self._element.product, - } + return DeviceInfo( + identifiers={(DOMAIN, self._element.index)}, + manufacturer=self._element.manufacturer, + model=self._element.product, + name=self._element.name, + sw_version=self._element.version, + ) From 0c2f12601206b5c93fdc7c207f3db37dc00dc858 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 29 Oct 2021 03:27:40 +1100 Subject: [PATCH 0043/1452] Return the real MAC address for LIFX bulbs with newer firmware (#58511) --- homeassistant/components/lifx/light.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index bdb0337c1a1..106c66c8900 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -7,6 +7,7 @@ import math import aiolifx as aiolifx_module import aiolifx_effects as aiolifx_effects_module +from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import util @@ -66,6 +67,8 @@ MESSAGE_TIMEOUT = 1.0 MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 +FIX_MAC_FW = AwesomeVersion("3.70") + SERVICE_LIFX_SET_STATE = "set_state" ATTR_INFRARED = "infrared" @@ -455,20 +458,34 @@ class LIFXLight(LightEntity): self.postponed_update = None self.lock = asyncio.Lock() + def get_mac_addr(self): + """Increment the last byte of the mac address by one for FW>3.70.""" + if ( + self.bulb.host_firmware_version + and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW + ): + octets = [int(octet, 16) for octet in self.bulb.mac_addr.split(":")] + octets[5] = (octets[5] + 1) % 256 + return ":".join(f"{octet:02x}" for octet in octets) + return self.bulb.mac_addr + @property def device_info(self) -> DeviceInfo: """Return information about the device.""" _map = aiolifx().products.product_map + info = DeviceInfo( identifiers={(LIFX_DOMAIN, self.unique_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr)}, + connections={(dr.CONNECTION_NETWORK_MAC, self.get_mac_addr())}, manufacturer="LIFX", name=self.name, ) - if model := (_map.get(self.bulb.product) or self.bulb.product) is not None: + + if (model := (_map.get(self.bulb.product) or self.bulb.product)) is not None: info[ATTR_MODEL] = str(model) if (version := self.bulb.host_firmware_version) is not None: info[ATTR_SW_VERSION] = version + return info @property From 669e36caece7a49cfb463e51172d49df36343849 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 11:07:58 -0600 Subject: [PATCH 0044/1452] Add DHCP Discovery for SimpliSafe (#58560) * Add DHCP Discovery for SimpliSafe * Fix tests * Docstring * Code review --- homeassistant/components/simplisafe/manifest.json | 8 +++++++- homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 956157a237d..97968e124b1 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -5,5 +5,11 @@ "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": ["simplisafe-python==12.0.2"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "dhcp": [ + { + "hostname": "simplisafe*", + "macaddress": "30AEA4*" + } + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9e99e512d9e..92222cf092b 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -307,6 +307,11 @@ DHCP = [ "hostname": "sense-*", "macaddress": "A4D578*" }, + { + "domain": "simplisafe", + "hostname": "simplisafe*", + "macaddress": "30AEA4*" + }, { "domain": "smartthings", "hostname": "st*", From 05353f8e13ac7aeabbe7489c86c416c82d9a1d67 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 28 Oct 2021 21:06:04 +0200 Subject: [PATCH 0045/1452] Add ROCKROBO_S5_MAX to xiaomi_miio vacuum models (#58591) * Add ROCKROBO_S5_MAX to xiaomi_miio vacuum models. https://github.com/home-assistant/core/issues/58550 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * disable pylint for todo Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Minor refactor Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> --- homeassistant/components/xiaomi_miio/const.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index e27fb4d2110..a1f2414b713 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -195,8 +195,25 @@ MODELS_LIGHT = ( + MODELS_LIGHT_BULB + MODELS_LIGHT_MONO ) -MODELS_VACUUM = [ROCKROBO_V1, ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] -MODELS_VACUUM_WITH_MOP = [ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] + +# TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme +ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +MODELS_VACUUM = [ + ROCKROBO_V1, + ROCKROBO_S5, + ROCKROBO_S5_MAX, + ROCKROBO_S6, + ROCKROBO_S6_MAXV, + ROCKROBO_S7, +] +MODELS_VACUUM_WITH_MOP = [ + ROCKROBO_S5, + ROCKROBO_S5_MAX, + ROCKROBO_S6, + ROCKROBO_S6_MAXV, + ROCKROBO_S7, +] + MODELS_AIR_MONITOR = [ MODEL_AIRQUALITYMONITOR_V1, MODEL_AIRQUALITYMONITOR_B1, From 5f36fd2a805d359bb5380ff917469fb2e7f6de6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 14:27:19 -0500 Subject: [PATCH 0046/1452] Fix uncaught exception in sense and retry later (#58623) --- homeassistant/components/sense/__init__.py | 25 ++++++++++++++-------- homeassistant/components/sense/const.py | 3 +++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 5c8028f2525..92a4e29108c 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -3,11 +3,7 @@ import asyncio from datetime import timedelta import logging -from sense_energy import ( - ASyncSenseable, - SenseAPITimeoutException, - SenseAuthenticationException, -) +from sense_energy import ASyncSenseable, SenseAuthenticationException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,6 +26,7 @@ from .const import ( SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, + SENSE_EXCEPTIONS, SENSE_TIMEOUT_EXCEPTIONS, SENSE_TRENDS_COORDINATOR, ) @@ -76,14 +73,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Could not authenticate with sense server") return False except SENSE_TIMEOUT_EXCEPTIONS as err: - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + str(err) or "Timed out during authentication" + ) from err + except SENSE_EXCEPTIONS as err: + raise ConfigEntryNotReady(str(err) or "Error during authentication") from err sense_devices_data = SenseDevicesData() try: sense_discovered_devices = await gateway.get_discovered_device_data() await gateway.update_realtime() except SENSE_TIMEOUT_EXCEPTIONS as err: - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + str(err) or "Timed out during realtime update" + ) from err + except SENSE_EXCEPTIONS as err: + raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err trends_coordinator = DataUpdateCoordinator( hass, @@ -114,8 +119,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Retrieve latest state.""" try: await gateway.update_realtime() - except SenseAPITimeoutException: - _LOGGER.error("Timeout retrieving data") + except SENSE_TIMEOUT_EXCEPTIONS as ex: + _LOGGER.error("Timeout retrieving data: %s", ex) + except SENSE_EXCEPTIONS as ex: + _LOGGER.error("Failed to update data: %s", ex) data = gateway.get_realtime() if "devices" in data: diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index af8454bbeab..bb323151950 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -1,8 +1,10 @@ """Constants for monitoring a Sense energy sensor.""" import asyncio +import socket from sense_energy import SenseAPITimeoutException +from sense_energy.sense_exceptions import SenseWebsocketException DOMAIN = "sense" DEFAULT_TIMEOUT = 10 @@ -37,6 +39,7 @@ SOLAR_POWERED_ID = "solar_powered" ICON = "mdi:flash" SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) +SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) MDI_ICONS = { "ac": "air-conditioner", From 2b175a37a76b00501391f69ef238fe3692eecce4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 14:32:22 -0500 Subject: [PATCH 0047/1452] Add package constraint to websockets (#58626) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2699d02ed0..2e2b9657800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -79,3 +79,7 @@ regex==2021.8.28 # anyio has a bug that was fixed in 3.3.1 # can remove after httpx/httpcore updates its anyio version pin anyio>=3.3.1 + +# websockets 10.0 is broken with AWS +# https://github.com/aaugustin/websockets/issues/1065 +websockets==9.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 939806d379e..3d2ace4c240 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,6 +106,10 @@ regex==2021.8.28 # anyio has a bug that was fixed in 3.3.1 # can remove after httpx/httpcore updates its anyio version pin anyio>=3.3.1 + +# websockets 10.0 is broken with AWS +# https://github.com/aaugustin/websockets/issues/1065 +websockets==9.1 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 808e067c22cc72ac118691d546fad37add43d7f2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Oct 2021 21:32:38 +0200 Subject: [PATCH 0048/1452] Update frontend to 20211028.0 (#58629) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d66d9138e24..9f15dc4fc5a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211027.0" + "home-assistant-frontend==20211028.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e2b9657800..c4f1a31f5c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8eba0757a04..dbbd93deb1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceac7475402..7dff0f66554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 9f30cd7826f91978e1ee50ecaf14183f22e2839b Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 28 Oct 2021 21:33:06 +0200 Subject: [PATCH 0049/1452] Add configuration_url to devolo Home Control (#58594) --- homeassistant/components/devolo_home_control/devolo_device.py | 2 ++ tests/components/devolo_home_control/mocks.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 7fdd53d0d87..f4f2432aa6e 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl @@ -33,6 +34,7 @@ class DevoloDeviceEntity(Entity): self._attr_should_poll = False self._attr_unique_id = element_uid self._attr_device_info = DeviceInfo( + configuration_url=f"https://{urlparse(device_instance.href).netloc}", identifiers={(DOMAIN, self._device_instance.uid)}, manufacturer=device_instance.brand, model=device_instance.name, diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 7700d30b1dd..6651215251a 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -45,6 +45,7 @@ class DeviceMock(Zwave): self.name = "Test Device" self.uid = "Test" self.settings_property = {"general_device_settings": SettingsMock()} + self.href = "https://www.mydevolo.com" class BinarySensorMock(DeviceMock): From e32fdfec844844a23be0ed971b07ca00b7e0c47a Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:34:26 +0300 Subject: [PATCH 0050/1452] Add entity category for load sensors to AsusWRT (#58625) --- homeassistant/components/asuswrt/sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 4b865bfb0e3..8beb5d3d9ee 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -11,7 +11,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_RATE_MEGABITS_PER_SECOND, + ENTITY_CATEGORY_DIAGNOSTIC, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -89,6 +93,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (1m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -98,6 +103,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (5m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -107,6 +113,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (15m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, From 0f2590030997dec7d8524a2e81723763270964ff Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 13:52:06 -0600 Subject: [PATCH 0051/1452] Fix missing triggered state in SimpliSafe alarm control panel (#58628) --- .../simplisafe/alarm_control_panel.py | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 6643bd3a1a1..bc2e2a8ac74 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -151,22 +151,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY self._last_event = None - if system.alarm_going_off: - self._attr_state = STATE_ALARM_TRIGGERED - elif system.state == SystemStates.away: - self._attr_state = STATE_ALARM_ARMED_AWAY - elif system.state in ( - SystemStates.away_count, - SystemStates.exit_delay, - SystemStates.home_count, - ): - self._attr_state = STATE_ALARM_ARMING - elif system.state == SystemStates.home: - self._attr_state = STATE_ALARM_ARMED_HOME - elif system.state == SystemStates.off: - self._attr_state = STATE_ALARM_DISARMED - else: - self._attr_state = None + self._set_state_from_system_data() @callback def _is_code_valid(self, code: str | None, state: str) -> bool: @@ -182,6 +167,17 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return True + @callback + def _set_state_from_system_data(self) -> None: + """Set the state based on the latest REST API data.""" + if self._system.alarm_going_off: + self._attr_state = STATE_ALARM_TRIGGERED + elif state := STATE_MAP_FROM_REST_API.get(self._system.state): + self._attr_state = state + else: + LOGGER.error("Unknown system state (REST API): %s", self._system.state) + self._attr_state = None + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._is_code_valid(code, STATE_ALARM_DISARMED): @@ -266,11 +262,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._errors = 0 - if state := STATE_MAP_FROM_REST_API.get(self._system.state): - self._attr_state = state - else: - LOGGER.error("Unknown system state (REST API): %s", self._system.state) - self._attr_state = None + self._set_state_from_system_data() @callback def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: From b3684764298068f39670de387b1645844cb41173 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Thu, 28 Oct 2021 22:14:50 +0200 Subject: [PATCH 0052/1452] Allow initialized callback to have arguments (#58129) --- homeassistant/components/hdmi_cec/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 47bd8d160c6..9dfd68d4a4f 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,9 +1,10 @@ """Support for HDMI CEC.""" from __future__ import annotations -from functools import partial, reduce +from functools import reduce import logging import multiprocessing +from typing import Any from pycec.cec import CecAdapter from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand @@ -41,7 +42,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -222,9 +223,12 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) adapter.init() - hdmi_network.set_initialized_callback( - partial(event.async_call_later, hass, WATCHDOG_INTERVAL, _adapter_watchdog) - ) + @callback + def _async_initialized_callback(*_: Any): + """Add watchdog on initialization.""" + return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + + hdmi_network.set_initialized_callback(_async_initialized_callback) def _volume(call): """Increase/decrease volume and mute/unmute system.""" From 1e8ccb47ce68055b1ef886704f9ba926e64f32ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:15:30 +0200 Subject: [PATCH 0053/1452] Use constants in control4 config flow (#58602) Co-authored-by: epenet --- .../components/control4/config_flow.py | 17 +++++++++-------- homeassistant/data_entry_flow.py | 1 + tests/components/control4/test_config_flow.py | 5 +++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index d13fe31601f..660d265758f 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import STEP_ID_INIT, STEP_ID_USER from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -96,9 +97,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: hub = Control4Validator( - user_input["host"], - user_input["username"], - user_input["password"], + user_input[CONF_HOST], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], self.hass, ) try: @@ -123,15 +124,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=controller_unique_id, data={ - CONF_HOST: user_input["host"], - CONF_USERNAME: user_input["username"], - CONF_PASSWORD: user_input["password"], + CONF_HOST: user_input[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_CONTROLLER_UNIQUE_ID: controller_unique_id, }, ) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id=STEP_ID_USER, data_schema=DATA_SCHEMA, errors=errors ) @staticmethod @@ -163,7 +164,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), } ) - return self.async_show_form(step_id="init", data_schema=data_schema) + return self.async_show_form(step_id=STEP_ID_INIT, data_schema=data_schema) class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index b5a35929459..83135373b9b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -21,6 +21,7 @@ RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" RESULT_TYPE_SHOW_PROGRESS = "progress" RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" +STEP_ID_INIT: Final = "init" STEP_ID_USER: Final = "user" # Event that is fired when a flow is progressed via external or progress source. diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 8a4791ab579..1ae69dbe36d 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) +from homeassistant.data_entry_flow import STEP_ID_INIT from tests.common import MockConfigEntry @@ -166,7 +167,7 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" - assert result["step_id"] == "init" + assert result["step_id"] == STEP_ID_INIT result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -186,7 +187,7 @@ async def test_option_flow_defaults(hass): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" - assert result["step_id"] == "init" + assert result["step_id"] == STEP_ID_INIT result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} From 37a07acce0c8f96c518944ee0205a6036f632c00 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:16:31 +0200 Subject: [PATCH 0054/1452] Adding newly supported language codes to Google TTS (#58607) --- homeassistant/components/google_cloud/tts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index af4e6771795..f402e2a7a06 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -55,8 +55,11 @@ SUPPORTED_LANGUAGES = [ "ko-KR", "lv-LV", "ml-IN", + "ms-MY", "nb-NO", + "nl-BE", "nl-NL", + "pa-IN", "pl-PL", "pt-BR", "pt-PT", From 2b7fe06b160f80cef239fc156633fa7623580086 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 14:29:25 -0600 Subject: [PATCH 0055/1452] Fix incorrect RainMachine service helper (#58633) --- homeassistant/components/rainmachine/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d8eea8b3df5..1879b33e949 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -127,9 +127,11 @@ def async_get_controller_for_service_call( device_registry = dr.async_get(hass) if device_entry := device_registry.async_get(device_id): - for entry_id in device_entry.config_entries: - if controller := hass.data[DOMAIN][entry_id][DATA_CONTROLLER]: - return cast(Controller, controller) + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.entry_id in device_entry.config_entries: + return cast( + Controller, hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] + ) raise ValueError(f"No controller for device ID: {device_id}") From 3705f2f7f10acd8c7ad816de540f6a6e8d4f084f Mon Sep 17 00:00:00 2001 From: Paul Frank Date: Thu, 28 Oct 2021 22:30:34 +0200 Subject: [PATCH 0056/1452] Open and close tilt for Fibaro devices in zwave_js (#58435) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/cover.py | 92 +- .../components/zwave_js/discovery.py | 24 +- .../zwave_js/discovery_data_template.py | 25 + tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_cover.py | 81 ++ .../zwave_js/cover_fibaro_fgr222_state.json | 1133 +++++++++++++++++ 6 files changed, 1363 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index e9759dbb171..1dee8b0bad3 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,8 +1,9 @@ """Support for Z-Wave cover devices.""" from __future__ import annotations +import asyncio import logging -from typing import Any +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY @@ -19,13 +20,19 @@ from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -35,6 +42,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo +from .discovery_data_template import CoverTiltDataTemplate from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) @@ -54,6 +62,8 @@ async def async_setup_entry( entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "motorized_barrier": entities.append(ZwaveMotorizedBarrier(config_entry, client, info)) + elif info.platform_hint == "window_shutter_tilt": + entities.append(ZWaveTiltCover(config_entry, client, info)) else: entities.append(ZWaveCover(config_entry, client, info)) async_add_entities(entities) @@ -77,6 +87,26 @@ def percent_to_zwave_position(value: int) -> int: return 0 +def percent_to_zwave_tilt(value: int) -> int: + """Convert position in 0-100 scale to 0-99 scale. + + `value` -- (int) Position byte value from 0-100. + """ + if value > 0: + return round((value / 100) * 99) + return 0 + + +def zwave_tilt_to_percent(value: int) -> int: + """Convert 0-99 scale to position in 0-100 scale. + + `value` -- (int) Position byte value from 0-99. + """ + if value > 0: + return round((value / 99) * 100) + return 0 + + class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" @@ -91,7 +121,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): # Entity class attributes self._attr_device_class = DEVICE_CLASS_WINDOW - if self.info.platform_hint == "window_shutter": + if self.info.platform_hint in ("window_shutter", "window_shutter_tilt"): self._attr_device_class = DEVICE_CLASS_SHUTTER if self.info.platform_hint == "window_blind": self._attr_device_class = DEVICE_CLASS_BLIND @@ -150,6 +180,64 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): await self.info.node.async_set_value(close_value, False) +class ZWaveTiltCover(ZWaveCover): + """Representation of a Fibaro Z-Wave cover device.""" + + _attr_supported_features = ( + SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_SET_TILT_POSITION + ) + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveCover entity.""" + super().__init__(config_entry, client, info) + self.data_template = cast( + CoverTiltDataTemplate, self.info.platform_data_template + ) + + @property + def current_cover_tilt_position(self) -> int | None: + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + value = self.data_template.current_tilt_value(self.info.platform_data) + return zwave_tilt_to_percent(value.value) if value else None + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the cover tilt to a specific position.""" + tilt_value = self.data_template.current_tilt_value(self.info.platform_data) + if tilt_value: + await self.info.node.async_set_value( + tilt_value, + percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]), + ) + # The following 2 lines are a workaround for this issue: + # https://github.com/zwave-js/node-zwave-js/issues/3611 + # As soon as the issue is fixed, and minimum server schema is bumped + # the 2 lines should be removed. + await asyncio.sleep(2.5) + await self.info.node.async_refresh_cc_values(tilt_value.command_class) + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + await self.async_set_cover_tilt_position(tilt_position=100) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + await self.async_set_cover_tilt_position(tilt_position=0) + + class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave motorized barrier device.""" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 23053804aae..80cd6f023fb 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -44,6 +44,7 @@ from homeassistant.helpers.device_registry import DeviceEntry from .const import LOGGER from .discovery_data_template import ( BaseDiscoverySchemaDataTemplate, + CoverTiltDataTemplate, DynamicCurrentTempClimateDataTemplate, NumericSensorDataTemplate, ZwaveValueID, @@ -258,14 +259,29 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), ), - # Fibaro Shutter Fibaro FGS222 + # Fibaro Shutter Fibaro FGR222 ZWaveDiscoverySchema( platform="cover", - hint="window_shutter", + hint="window_shutter_tilt", manufacturer_id={0x010F}, - product_id={0x1000}, - product_type={0x0302}, + product_id={0x1000, 0x1001}, + product_type={0x0301, 0x0302}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + data_template=CoverTiltDataTemplate( + tilt_value_id=ZwaveValueID( + "fibaro", + CommandClass.MANUFACTURER_PROPRIETARY, + endpoint=0, + property_key="venetianBlindsTilt", + ) + ), + required_values=[ + ZWaveValueDiscoverySchema( + command_class={CommandClass.MANUFACTURER_PROPRIETARY}, + property={"fibaro"}, + property_key={"venetianBlindsTilt"}, + ) + ], ), # Qubino flush shutter ZWaveDiscoverySchema( diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 7b76465d60e..a550c683ea2 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -226,3 +226,28 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): return key return None + + +@dataclass +class TiltValueMix: + """Mixin data class for the tilt_value.""" + + tilt_value_id: ZwaveValueID + + +@dataclass +class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): + """Tilt data template class for Z-Wave Cover entities.""" + + def resolve_data(self, value: ZwaveValue) -> dict[str, Any]: + """Resolve helper class data for a discovered value.""" + return {"tilt_value": self._get_value_from_id(value.node, self.tilt_value_id)} + + def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + """Return list of all ZwaveValues resolved by helper that should be watched.""" + return [resolved_data["tilt_value"]] + + @staticmethod + def current_tilt_value(resolved_data: dict[str, Any]) -> ZwaveValue | None: + """Get current tilt ZwaveValue from resolved data.""" + return resolved_data["tilt_value"] diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 422f4b55c16..be9dec3b6bc 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -356,6 +356,12 @@ def aeotec_nano_shutter_state_fixture(): return json.loads(load_fixture("zwave_js/cover_aeotec_nano_shutter_state.json")) +@pytest.fixture(name="fibaro_fgr222_shutter_state", scope="session") +def fibaro_fgr222_shutter_state_fixture(): + """Load the Fibaro FGR222 node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_fibaro_fgr222_state.json")) + + @pytest.fixture(name="aeon_smart_switch_6_state", scope="session") def aeon_smart_switch_6_state_fixture(): """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data.""" @@ -743,6 +749,14 @@ def aeotec_nano_shutter_cover_fixture(client, aeotec_nano_shutter_state): return node +@pytest.fixture(name="fibaro_fgr222_shutter") +def fibaro_fgr222_shutter_cover_fixture(client, fibaro_fgr222_shutter_state): + """Mock a Fibaro FGR222 Shutter node.""" + node = Node(client, copy.deepcopy(fibaro_fgr222_shutter_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeon_smart_switch_6") def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state): """Mock an AEON Labs (ZW096) Smart Switch 6 node.""" diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 1afe7a114da..d6d3376d0e6 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -3,6 +3,7 @@ from zwave_js_server.event import Event from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHUTTER, @@ -25,6 +26,7 @@ GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" SHUTTER_COVER_ENTITY = "cover.flush_shutter" AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" +FIBARO_SHUTTER_COVER_ENTITY = "cover.fgr_222_test_cover" async def test_window_cover(hass, client, chain_actuator_zws12, integration): @@ -307,6 +309,85 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert state.state == "closed" +async def test_fibaro_FGR222_shutter_cover( + hass, client, fibaro_fgr222_shutter, integration +): + """Test tilt function of the Fibaro Shutter devices.""" + state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY) + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SHUTTER + + assert state.state == "open" + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + # Test opening tilts + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": FIBARO_SHUTTER_COVER_ENTITY}, + 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"] == 42 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 145, + "commandClassName": "Manufacturer Proprietary", + "property": "fibaro", + "propertyKey": "venetianBlindsTilt", + "propertyName": "fibaro", + "propertyKeyName": "venetianBlindsTilt", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Venetian blinds tilt", + "min": 0, + "max": 99, + }, + "value": 0, + } + assert args["value"] == 99 + + client.async_send_command.reset_mock() + # Test closing tilts + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": FIBARO_SHUTTER_COVER_ENTITY}, + 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"] == 42 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 145, + "commandClassName": "Manufacturer Proprietary", + "property": "fibaro", + "propertyKey": "venetianBlindsTilt", + "propertyName": "fibaro", + "propertyKeyName": "venetianBlindsTilt", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Venetian blinds tilt", + "min": 0, + "max": 99, + }, + "value": 0, + } + assert args["value"] == 0 + + async def test_aeotec_nano_shutter_cover( hass, client, aeotec_nano_shutter, integration ): diff --git a/tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json b/tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json new file mode 100644 index 00000000000..59dff945846 --- /dev/null +++ b/tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json @@ -0,0 +1,1133 @@ +{ + "nodeId": 42, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": "unknown", + "manufacturerId": 271, + "productId": 4096, + "productType": 770, + "firmwareVersion": "25.25", + "name": "fgr 222 test cover", + "location": "test location", + "deviceConfig": { + "filename": "/usr/src/app/store/.config-db/devices/0x010f/fgr222_24.24.json", + "isEmbedded": true, + "manufacturer": "Fibargroup", + "manufacturerId": 271, + "label": "FGR222", + "description": "Roller Shutter 2", + "devices": [ + { + "productType": 769, + "productId": 4097 + }, + { + "productType": 770, + "productId": 4096 + }, + { + "productType": 770, + "productId": 12288 + }, + { + "productType": 770, + "productId": 16384 + }, + { + "productType": 768, + "productId": 258 + } + ], + "firmwareVersion": { + "min": "24.24", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "proprietary": { + "fibaroCCs": [ + 38 + ] + } + }, + "label": "FGR222", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 42, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + } + } + ], + "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": true + }, + { + "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, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 99 + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 3, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 96 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Open", + "propertyName": "Open", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Open)", + "ccSpecific": { + "switchType": 3 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Close", + "propertyName": "Close", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Close)", + "ccSpecific": { + "switchType": 3 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Power", + "propertyName": "Power", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power", + "ccSpecific": { + "sensorType": 4, + "scale": 0 + }, + "unit": "W" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" + }, + "value": 0.48 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Reports type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "value should be set to 1 if the module operates in Venetian Blind mode.", + "label": "Reports type", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Blind position using Z-Wave Command", + "1": "Blind position via Fibar Command" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Roller Shutter operating modes", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Roller Shutter operating modes", + "default": 1, + "min": 0, + "max": 4, + "states": { + "0": "Roller Blind Mode, without positioning", + "1": "Roller Blind Mode, with positioning", + "2": "Venetian Blind Mode, with positioning", + "3": "Gate Mode, without positioning", + "4": "Gate Mode, with positioning" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Turning time/ delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "depending on mode, turning time or delay time", + "label": "Turning time/ delay time", + "default": 150, + "min": 0, + "max": 65535, + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 83 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Lamellas positioning mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Parameter influences the lamellas positioning in venetian blind mode", + "label": "Lamellas positioning mode", + "default": 2, + "min": 0, + "max": 2, + "states": { + "0": "only in case of the main controller operation", + "1": "default - controller+switchlimit", + "2": "like 1 + STOP control frame" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Delay time after S2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "delay till auto turned off or auto gate close", + "label": "Delay time after S2", + "default": 10, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Motor operation detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Power threshold to be interpreted as reaching a limit switch.", + "label": "Motor operation detection", + "default": 10, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "Motor operation time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Time period for the motor to continue operation.", + "label": "Motor operation time", + "default": 10, + "min": 0, + "max": 65535, + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 240 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyName": "Forced Roller Shutter calibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "set to 1 will enter calibration mode", + "label": "Forced Roller Shutter calibration", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Deactivated", + "1": "Start calibration process" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyName": "Response to General Alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Response to General Alarm", + "default": 2, + "min": 0, + "max": 2, + "states": { + "0": "No response to alarm frames", + "1": "Open Blind", + "2": "Close Blind" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyName": "Response to Water Flood Alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Response to Water Flood Alarm", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No response to alarm frames", + "1": "Open Blind", + "2": "Close Blind" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyName": "Response to Smoke, CO, CO2 Alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Response to Smoke, CO, CO2 Alarm", + "default": 1, + "min": 0, + "max": 2, + "states": { + "0": "No response to alarm frames", + "1": "Open Blind", + "2": "Close Blind" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyName": "Response to Temperature Alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Response to Temperature Alarm", + "default": 1, + "min": 0, + "max": 2, + "states": { + "0": "No response to alarm frames", + "1": "Open Blind", + "2": "Close Blind" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 35, + "propertyName": "Managing lamellas in response to alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "0 no change, 1 extreme position", + "label": "Managing lamellas in response to alarm", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Do not change lamellas position", + "1": "Set lamellas to their extreme position" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Power reports", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "change that needs to occur to trigger the power report", + "label": "Power reports", + "default": 10, + "min": 0, + "max": 100, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Periodic power or energy reports", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Time to the next report. Value of 0 means the reports are turned off.", + "label": "Periodic power or energy reports", + "default": 3600, + "min": 0, + "max": 65534, + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 43, + "propertyName": "Energy reports", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Energy threshold to trigger report", + "label": "Energy reports", + "default": 10, + "min": 0, + "max": 254, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 44, + "propertyName": "Self-measurement", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "if power and energy reports are to sent to the main controller", + "label": "Self-measurement", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Activated" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyName": "Scenes/ Associations activation", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "whether scenes or associations are activated by the switch keys", + "label": "Scenes/ Associations activation", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Associations Active", + "1": "Scenes Active" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Switch type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "either Toggle switches or a single, momentary switch.", + "label": "Switch type", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "Momentary switches", + "1": "Toggle switches", + "2": "Single, momentary switch." + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 271 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 770 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 4096 + }, + { + "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", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "rf", + "propertyName": "rf", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection state", + "states": { + "0": "Unprotected", + "1": "NoControl" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "exclusiveControlNodeId", + "propertyName": "exclusiveControlNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "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": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.52" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "25.25" + ] + }, + { + "endpoint": 0, + "commandClass": 145, + "commandClassName": "Manufacturer Proprietary", + "property": "fibaro", + "propertyKey": "venetianBlindsPosition", + "propertyName": "fibaro", + "propertyKeyName": "venetianBlindsPosition", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Venetian blinds position", + "min": 0, + "max": 99 + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 145, + "commandClassName": "Manufacturer Proprietary", + "property": "fibaro", + "propertyKey": "venetianBlindsTilt", + "propertyName": "fibaro", + "propertyKeyName": "venetianBlindsTilt", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Venetian blinds tilt", + "min": 0, + "max": 99 + }, + "value": 0 + } + ], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [ + 40000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "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": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 2, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0302:0x1000:25.25", + "statistics": { + "commandsTX": 24, + "commandsRX": 350, + "commandsDroppedRX": 1, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } +} \ No newline at end of file From f1884d34e9a68993d0461e99c612cbf1030a48f2 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 28 Oct 2021 22:42:10 +0200 Subject: [PATCH 0057/1452] Add devolo home network integration (#45866) Co-authored-by: Markus Bong <2Fake1987@gmail.com> Co-authored-by: Markus Bong --- .strict-typing | 1 + CODEOWNERS | 1 + .../devolo_home_network/__init__.py | 122 +++++++++++++ .../devolo_home_network/config_flow.py | 108 +++++++++++ .../components/devolo_home_network/const.py | 17 ++ .../components/devolo_home_network/entity.py | 37 ++++ .../devolo_home_network/manifest.json | 11 ++ .../components/devolo_home_network/sensor.py | 122 +++++++++++++ .../devolo_home_network/strings.json | 25 +++ .../devolo_home_network/translations/en.json | 25 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../devolo_home_network/__init__.py | 31 ++++ .../devolo_home_network/conftest.py | 41 +++++ tests/components/devolo_home_network/const.py | 64 +++++++ .../devolo_home_network/test_config_flow.py | 172 ++++++++++++++++++ .../devolo_home_network/test_init.py | 61 +++++++ .../devolo_home_network/test_sensor.py | 148 +++++++++++++++ 21 files changed, 1007 insertions(+) create mode 100644 homeassistant/components/devolo_home_network/__init__.py create mode 100644 homeassistant/components/devolo_home_network/config_flow.py create mode 100644 homeassistant/components/devolo_home_network/const.py create mode 100644 homeassistant/components/devolo_home_network/entity.py create mode 100644 homeassistant/components/devolo_home_network/manifest.json create mode 100644 homeassistant/components/devolo_home_network/sensor.py create mode 100644 homeassistant/components/devolo_home_network/strings.json create mode 100644 homeassistant/components/devolo_home_network/translations/en.json create mode 100644 tests/components/devolo_home_network/__init__.py create mode 100644 tests/components/devolo_home_network/conftest.py create mode 100644 tests/components/devolo_home_network/const.py create mode 100644 tests/components/devolo_home_network/test_config_flow.py create mode 100644 tests/components/devolo_home_network/test_init.py create mode 100644 tests/components/devolo_home_network/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 685c87aa094..8977c9f68c8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -32,6 +32,7 @@ homeassistant.components.crownstone.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* +homeassistant.components.devolo_home_network.* homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* diff --git a/CODEOWNERS b/CODEOWNERS index 2612cc3fd18..680424fbdea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -118,6 +118,7 @@ homeassistant/components/denonavr/* @ol-iver @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun +homeassistant/components/devolo_home_network/* @2Fake @Shutgun homeassistant/components/dexcom/* @gagebenne homeassistant/components/dhcp/* @bdraco homeassistant/components/dht/* @thegardenmonkey diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py new file mode 100644 index 00000000000..f427e5acbfc --- /dev/null +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -0,0 +1,122 @@ +"""The devolo Home Network integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import async_timeout +from devolo_plc_api.device import Device +from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable + +from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONNECTED_PLC_DEVICES, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + LONG_UPDATE_INTERVAL, + NEIGHBORING_WIFI_NETWORKS, + PLATFORMS, + SHORT_UPDATE_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up devolo Home Network from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + zeroconf_instance = await zeroconf.async_get_async_instance(hass) + async_client = get_async_client(hass) + + try: + device = Device( + ip=entry.data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance + ) + await device.async_connect(session_instance=async_client) + except DeviceNotFound as err: + raise ConfigEntryNotReady( + f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" + ) from err + + async def async_update_connected_plc_devices() -> dict[str, Any]: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(10): + return await device.plcnet.async_get_network_overview() # type: ignore[no-any-return, union-attr] + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + + async def async_update_wifi_connected_station() -> dict[str, Any]: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(10): + return await device.device.async_get_wifi_connected_station() # type: ignore[no-any-return, union-attr] + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + + async def async_update_wifi_neighbor_access_points() -> dict[str, Any]: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(30): + return await device.device.async_get_wifi_neighbor_access_points() # type: ignore[no-any-return, union-attr] + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + + async def disconnect(event: Event) -> None: + """Disconnect from device.""" + await device.async_disconnect() + + coordinators: dict[str, DataUpdateCoordinator] = {} + if device.plcnet: + coordinators[CONNECTED_PLC_DEVICES] = DataUpdateCoordinator( + hass, + _LOGGER, + name=CONNECTED_PLC_DEVICES, + update_method=async_update_connected_plc_devices, + update_interval=LONG_UPDATE_INTERVAL, + ) + if device.device and "wifi1" in device.device.features: + coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( + hass, + _LOGGER, + name=CONNECTED_WIFI_CLIENTS, + update_method=async_update_wifi_connected_station, + update_interval=SHORT_UPDATE_INTERVAL, + ) + coordinators[NEIGHBORING_WIFI_NETWORKS] = DataUpdateCoordinator( + hass, + _LOGGER, + name=NEIGHBORING_WIFI_NETWORKS, + update_method=async_update_wifi_neighbor_access_points, + update_interval=LONG_UPDATE_INTERVAL, + ) + + hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} + + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) + ) + + 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: + await hass.data[DOMAIN][entry.entry_id]["device"].async_disconnect() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py new file mode 100644 index 00000000000..fa0ee983b69 --- /dev/null +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -0,0 +1,108 @@ +"""Config flow for devolo Home Network integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from devolo_plc_api.device import Device +from devolo_plc_api.exceptions.device import DeviceNotFound +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components import zeroconf +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + zeroconf_instance = await zeroconf.async_get_instance(hass) + async_client = get_async_client(hass) + + device = Device(data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance) + + await device.async_connect(session_instance=async_client) + await device.async_disconnect() + + return { + SERIAL_NUMBER: str(device.serial_number), + TITLE: device.hostname.split(".")[0], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for devolo Home Network.""" + + VERSION = 1 + + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + """Handle the initial step.""" + errors: dict = {} + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + try: + info = await validate_input(self.hass, user_input) + except DeviceNotFound: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[SERIAL_NUMBER], raise_on_progress=False) + self._abort_if_unique_id_configured() + 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 + ) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle zerooconf discovery.""" + if discovery_info["properties"]["MT"] in ["2600", "2601"]: + return self.async_abort(reason="home_control") + + await self.async_set_unique_id(discovery_info["properties"]["SN"]) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context[CONF_HOST] = discovery_info["host"] + self.context["title_placeholders"] = { + PRODUCT: discovery_info["properties"]["Product"], + CONF_NAME: discovery_info["hostname"].split(".")[0], + } + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: ConfigType | None = None + ) -> FlowResult: + """Handle a flow initiated by zeroconf.""" + title = self.context["title_placeholders"][CONF_NAME] + if user_input is not None: + data = { + CONF_IP_ADDRESS: self.context[CONF_HOST], + } + return self.async_create_entry(title=title, data=data) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"host_name": title}, + ) diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py new file mode 100644 index 00000000000..9276acebe41 --- /dev/null +++ b/homeassistant/components/devolo_home_network/const.py @@ -0,0 +1,17 @@ +"""Constants for the devolo Home Network integration.""" + +from datetime import timedelta + +DOMAIN = "devolo_home_network" +PLATFORMS = ["sensor"] + +PRODUCT = "product" +SERIAL_NUMBER = "serial_number" +TITLE = "title" + +LONG_UPDATE_INTERVAL = timedelta(minutes=5) +SHORT_UPDATE_INTERVAL = timedelta(seconds=15) + +CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" +NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py new file mode 100644 index 00000000000..dbfe0e4035a --- /dev/null +++ b/homeassistant/components/devolo_home_network/entity.py @@ -0,0 +1,37 @@ +"""Generic platform.""" +from __future__ import annotations + +from devolo_plc_api.device import Device + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class DevoloEntity(CoordinatorEntity): + """Representation of a devolo home network device.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, device: Device, device_name: str + ) -> None: + """Initialize a devolo home network device.""" + super().__init__(coordinator) + + self._device = device + self._device_name = device_name + + self._attr_device_info = DeviceInfo( + configuration_url=f"http://{self._device.ip}", + identifiers={(DOMAIN, str(self._device.serial_number))}, + manufacturer="devolo", + model=self._device.product, + name=self._device_name, + sw_version=self._device.firmware_version, + ) + self._attr_unique_id = ( + f"{self._device.serial_number}_{self.entity_description.key}" + ) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json new file mode 100644 index 00000000000..987211ca631 --- /dev/null +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "devolo_home_network", + "name": "devolo Home Network", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", + "requirements": ["devolo-plc-api==0.6.2"], + "zeroconf": ["_dvl-deviceapi._tcp.local."], + "codeowners": ["@2Fake", "@Shutgun"], + "quality_scale": "platinum", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py new file mode 100644 index 00000000000..3b0175d8c31 --- /dev/null +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -0,0 +1,122 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from devolo_plc_api.device import Device + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + CONNECTED_PLC_DEVICES, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + NEIGHBORING_WIFI_NETWORKS, +) +from .entity import DevoloEntity + + +@dataclass +class DevoloSensorRequiredKeysMixin: + """Mixin for required keys.""" + + value_func: Callable[[dict[str, Any]], int] + + +@dataclass +class DevoloSensorEntityDescription( + SensorEntityDescription, DevoloSensorRequiredKeysMixin +): + """Describes devolo sensor entity.""" + + +SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { + CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription( + key=CONNECTED_PLC_DEVICES, + entity_registry_enabled_default=False, + icon="mdi:lan", + name="Connected PLC devices", + value_func=lambda data: len( + {device["mac_address_from"] for device in data["network"]["data_rates"]} + ), + ), + CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription( + key=CONNECTED_WIFI_CLIENTS, + entity_registry_enabled_default=True, + icon="mdi:wifi", + name="Connected Wifi clients", + value_func=lambda data: len(data["connected_stations"]), + ), + NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( + key=NEIGHBORING_WIFI_NETWORKS, + entity_registry_enabled_default=False, + icon="mdi:wifi-marker", + name="Neighboring Wifi networks", + value_func=lambda data: len(data["neighbor_aps"]), + ), +} + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + + entities: list[DevoloSensorEntity] = [] + if device.plcnet: + entities.append( + DevoloSensorEntity( + coordinators[CONNECTED_PLC_DEVICES], + SENSOR_TYPES[CONNECTED_PLC_DEVICES], + device, + entry.title, + ) + ) + if device.device and "wifi1" in device.device.features: + entities.append( + DevoloSensorEntity( + coordinators[CONNECTED_WIFI_CLIENTS], + SENSOR_TYPES[CONNECTED_WIFI_CLIENTS], + device, + entry.title, + ) + ) + entities.append( + DevoloSensorEntity( + coordinators[NEIGHBORING_WIFI_NETWORKS], + SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS], + device, + entry.title, + ) + ) + async_add_entities(entities) + + +class DevoloSensorEntity(DevoloEntity, SensorEntity): + """Representation of a devolo sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: DevoloSensorEntityDescription, + device: Device, + device_name: str, + ) -> None: + """Initialize entity.""" + self.entity_description: DevoloSensorEntityDescription = description + super().__init__(coordinator, device, device_name) + + @property + def native_value(self) -> int: + """State of the sensor.""" + return self.entity_description.value_func(self.coordinator.data) diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json new file mode 100644 index 00000000000..685e139d2b8 --- /dev/null +++ b/homeassistant/components/devolo_home_network/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "flow_title": "{product} ({name})", + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?", + "title": "Discovered devolo home network device" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "home_control": "The devolo Home Control Central Unit does not work with this integration." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/en.json b/homeassistant/components/devolo_home_network/translations/en.json new file mode 100644 index 00000000000..52e51d953c1 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP Address" + }, + "description": "Do you want to start set up?" + }, + "zeroconf_confirm": { + "description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?", + "title": "Discovered devolo home network device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index aef37105170..b45c06b10b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -60,6 +60,7 @@ FLOWS = [ "deconz", "denonavr", "devolo_home_control", + "devolo_home_network", "dexcom", "dialogflow", "directv", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8ab7a7f8e31..aec93dd36c9 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -58,6 +58,9 @@ ZEROCONF = { "_dvl-deviceapi._tcp.local.": [ { "domain": "devolo_home_control" + }, + { + "domain": "devolo_home_network" } ], "_easylink._tcp.local.": [ diff --git a/mypy.ini b/mypy.ini index 5e9501d5407..4192e1a10ea 100644 --- a/mypy.ini +++ b/mypy.ini @@ -363,6 +363,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.devolo_home_network.*] +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.dlna_dmr.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index dbbd93deb1b..866ebdc8d29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,6 +536,9 @@ denonavr==0.10.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.4 +# homeassistant.components.devolo_home_network +devolo-plc-api==0.6.2 + # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dff0f66554..a11eb5c9fff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,6 +335,9 @@ denonavr==0.10.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.4 +# homeassistant.components.devolo_home_network +devolo-plc-api==0.6.2 + # homeassistant.components.directv directv==0.4.0 diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py new file mode 100644 index 00000000000..b9026d8453b --- /dev/null +++ b/tests/components/devolo_home_network/__init__.py @@ -0,0 +1,31 @@ +"""Tests for the devolo Home Network integration.""" + +from typing import Any + +from devolo_plc_api.device_api.deviceapi import DeviceApi +from devolo_plc_api.plcnet_api.plcnetapi import PlcNetApi + +from homeassistant.components.devolo_home_network.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .const import DISCOVERY_INFO, IP + +from tests.common import MockConfigEntry + + +def configure_integration(hass: HomeAssistant) -> MockConfigEntry: + """Configure the integration.""" + config = { + CONF_IP_ADDRESS: IP, + } + entry = MockConfigEntry(domain=DOMAIN, data=config) + entry.add_to_hass(hass) + + return entry + + +async def async_connect(self, session_instance: Any = None): + """Give a mocked device the needed properties.""" + self.plcnet = PlcNetApi(IP, None, DISCOVERY_INFO) + self.device = DeviceApi(IP, None, DISCOVERY_INFO) diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py new file mode 100644 index 00000000000..ab798cd5cfd --- /dev/null +++ b/tests/components/devolo_home_network/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from . import async_connect +from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NEIGHBOR_ACCESS_POINTS, PLCNET + + +@pytest.fixture() +def mock_device(): + """Mock connecting to a devolo home network device.""" + with patch("devolo_plc_api.device.Device.async_connect", async_connect), patch( + "devolo_plc_api.device.Device.async_disconnect" + ), patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=CONNECTED_STATIONS), + ), patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", + new=AsyncMock(return_value=NEIGHBOR_ACCESS_POINTS), + ), patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + new=AsyncMock(return_value=PLCNET), + ): + yield + + +@pytest.fixture(name="info") +def mock_validate_input(): + """Mock setup entry and user input.""" + info = { + "serial_number": DISCOVERY_INFO["properties"]["SN"], + "title": DISCOVERY_INFO["properties"]["Product"], + } + + with patch( + "homeassistant.components.devolo_home_network.config_flow.validate_input", + return_value=info, + ): + yield info diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py new file mode 100644 index 00000000000..e9b2113c4a6 --- /dev/null +++ b/tests/components/devolo_home_network/const.py @@ -0,0 +1,64 @@ +"""Constants used for mocking data.""" + +IP = "1.1.1.1" + +CONNECTED_STATIONS = { + "connected_stations": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "vap_type": "WIFI_VAP_MAIN_AP", + "band": "WIFI_BAND_5G", + "rx_rate": 87800, + "tx_rate": 87800, + } + ], +} + +DISCOVERY_INFO = { + "host": IP, + "port": 14791, + "hostname": "test.local.", + "type": "_dvl-deviceapi._tcp.local.", + "name": "dLAN pro 1200+ WiFi ac._dvl-deviceapi._tcp.local.", + "properties": { + "Path": "abcdefghijkl/deviceapi", + "Version": "v0", + "Product": "dLAN pro 1200+ WiFi ac", + "Features": "reset,update,led,intmtg,wifi1", + "MT": "2730", + "SN": "1234567890", + "FirmwareVersion": "5.6.1", + "FirmwareDate": "2020-10-23", + "PS": "", + "PlcMacAddress": "AA:BB:CC:DD:EE:FF", + }, +} + +DISCOVERY_INFO_WRONG_DEVICE = {"properties": {"MT": "2600"}} + +NEIGHBOR_ACCESS_POINTS = { + "neighbor_aps": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "ssid": "wifi", + "band": "WIFI_BAND_2G", + "channel": 1, + "signal": -73, + "signal_bars": 1, + } + ] +} + +PLCNET = { + "network": { + "data_rates": [ + { + "mac_address_from": "AA:BB:CC:DD:EE:FF", + "mac_address_to": "11:22:33:44:55:66", + "rx_rate": 0.0, + "tx_rate": 0.0, + }, + ], + "devices": [], + } +} diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py new file mode 100644 index 00000000000..0be07be9a00 --- /dev/null +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -0,0 +1,172 @@ +"""Test the devolo Home Network config flow.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +from devolo_plc_api.exceptions.device import DeviceNotFound +import pytest + +from homeassistant import config_entries +from homeassistant.components.devolo_home_network import config_flow +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + SERIAL_NUMBER, + TITLE, +) +from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP + + +async def test_form(hass: HomeAssistant, info: dict[str, Any]): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: IP, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["result"].unique_id == info["serial_number"] + assert result2["title"] == info["title"] + assert result2["data"] == { + CONF_IP_ADDRESS: IP, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "exception_type, expected_error", + [[DeviceNotFound, "cannot_connect"], [Exception, "unknown"]], +) +async def test_form_error(hass: HomeAssistant, exception_type, expected_error): + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.devolo_home_network.config_flow.validate_input", + side_effect=exception_type, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: IP, + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {CONF_BASE: expected_error} + + +async def test_zeroconf(hass: HomeAssistant): + """Test that the zeroconf form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["description_placeholders"] == {"host_name": "test"} + + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + + assert ( + context["title_placeholders"][CONF_NAME] + == DISCOVERY_INFO["hostname"].split(".", maxsplit=1)[0] + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["title"] == "test" + assert result2["data"] == { + CONF_IP_ADDRESS: IP, + } + + +async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): + """Test we abort zeroconf for wrong devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO_WRONG_DEVICE, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "home_control" + + +@pytest.mark.usefixtures("info") +async def test_abort_if_configued(hass: HomeAssistant): + """Test we abort config flow if already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: IP, + }, + ) + await hass.async_block_till_done() + + # Abort on concurrent user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: IP, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + # Abort on concurrent zeroconf discovery flow + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_validate_input(hass: HomeAssistant): + """Test input validaton.""" + info = await config_flow.validate_input(hass, {CONF_IP_ADDRESS: IP}) + assert SERIAL_NUMBER in info + assert TITLE in info diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py new file mode 100644 index 00000000000..66d32e8974d --- /dev/null +++ b/tests/components/devolo_home_network/test_init.py @@ -0,0 +1,61 @@ +"""Test the devolo Home Network integration setup.""" +from unittest.mock import patch + +from devolo_plc_api.exceptions.device import DeviceNotFound +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_setup_entry(hass: HomeAssistant): + """Test setup entry.""" + entry = configure_integration(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ), patch("homeassistant.core.EventBus.async_listen_once"): + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + +@pytest.mark.usefixtures("mock_zeroconf") +async def test_setup_device_not_found(hass: HomeAssistant): + """Test setup entry.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_network.Device.async_connect", + side_effect=DeviceNotFound, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_unload_entry(hass: HomeAssistant): + """Test unload entry.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_hass_stop(hass: HomeAssistant): + """Test homeassistant stop event.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_network.Device.async_disconnect" + ) as async_disconnect: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + assert async_disconnect.assert_called_once diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py new file mode 100644 index 00000000000..100db9005aa --- /dev/null +++ b/tests/components/devolo_home_network/test_sensor.py @@ -0,0 +1,148 @@ +"""Tests for the devolo Home Network sensors.""" +from unittest.mock import patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.devolo_home_network.const import ( + LONG_UPDATE_INTERVAL, + SHORT_UPDATE_INTERVAL, +) +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import configure_integration + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_sensor_setup(hass: HomeAssistant): + """Test default setup of the sensor component.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{DOMAIN}.connected_wifi_clients") is not None + assert hass.states.get(f"{DOMAIN}.connected_plc_devices") is None + assert hass.states.get(f"{DOMAIN}.neighboring_wifi_networks") is None + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_update_connected_wifi_clients(hass: HomeAssistant): + """Test state change of a connected_wifi_clients sensor device.""" + state_key = f"{DOMAIN}.connected_wifi_clients" + + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_update_neighboring_wifi_networks(hass: HomeAssistant): + """Test state change of a neighboring_wifi_networks sensor device.""" + state_key = f"{DOMAIN}.neighboring_wifi_networks" + entry = configure_integration(hass) + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_update_connected_plc_devices(hass: HomeAssistant): + """Test state change of a connected_plc_devices sensor device.""" + state_key = f"{DOMAIN}.connected_plc_devices" + entry = configure_integration(hass) + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + # Emulate device failure + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + await hass.config_entries.async_unload(entry.entry_id) From f619a8e4a028d3867b4891b268ee9803076b3d1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 15:44:28 -0500 Subject: [PATCH 0058/1452] Reduce rainmachine intervals to avoid device overload (#58319) --- homeassistant/components/rainmachine/__init__.py | 10 +++++++++- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 1879b33e949..b72fe0fb25d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -55,6 +55,14 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] +UPDATE_INTERVALS = { + DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_PROGRAMS: timedelta(seconds=30), + DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), + DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), + DATA_ZONES: timedelta(seconds=15), +} + # Constants expected by the RainMachine API for Service Data CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -230,7 +238,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, LOGGER, name=f'{controller.name} ("{api_category}")', - update_interval=DEFAULT_UPDATE_INTERVAL, + update_interval=UPDATE_INTERVALS[api_category], update_method=partial(async_update, api_category), ) controller_init_tasks.append(coordinator.async_refresh()) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 307cd7681f5..b6dc4e5c1db 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==3.2.0"], + "requirements": ["regenmaschine==2021.10.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 866ebdc8d29..79515f9abec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2040,7 +2040,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==3.2.0 +regenmaschine==2021.10.0 # homeassistant.components.renault renault-api==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a11eb5c9fff..091a8cebbbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.rainmachine -regenmaschine==3.2.0 +regenmaschine==2021.10.0 # homeassistant.components.renault renault-api==0.1.4 From 806242093d2f5d12f8b897f683204a4998be0524 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 14:55:14 -0600 Subject: [PATCH 0059/1452] Add missing SimpliSafe config flow test (#58563) --- .../components/simplisafe/test_config_flow.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 4546b7d3383..99943497556 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -180,6 +180,45 @@ async def test_step_reauth_new_format(hass, mock_async_from_auth): assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} +async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): + """Test the re-auth step returning a different account from this one.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_USER_ID: "12345", + CONF_TOKEN: "token123", + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USER_ID: "12345", CONF_TOKEN: "token123"}, + ) + assert result["step_id"] == "user" + + # Simulate the next auth call returning a different user ID than the one we've + # identified as this entry's unique ID: + api.user_id = "67890" + + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_account" + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.unique_id == "12345" + + async def test_step_user(hass, mock_async_from_auth): """Test the user step.""" result = await hass.config_entries.flow.async_init( From 3f50e444ca71cd698ce05cbe3d73c3078d5630bd Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 28 Oct 2021 22:58:28 +0200 Subject: [PATCH 0060/1452] Improve ViCare energy units (#58630) --- homeassistant/components/vicare/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 89c30f8b570..044632b6244 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -22,6 +22,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -83,7 +84,7 @@ SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin): """Describes ViCare sensor entity.""" - unit_getter: Callable[[Device], bool | None] | None = None + unit_getter: Callable[[Device], str | None] | None = None GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( @@ -185,7 +186,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), - device_class=DEVICE_CLASS_ENERGY, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), ViCareSensorEntityDescription( @@ -312,7 +313,7 @@ def _build_entity(name, vicare_api, device_config, sensor): try: sensor.value_getter(vicare_api) - if callable(sensor.unit_getter): + if sensor.unit_getter: with suppress(PyViCareNotSupportedFeatureError): vicare_unit = sensor.unit_getter(vicare_api) if vicare_unit is not None: From 6cdc372dcb8a30d8b308c77f6bcac2dddc9b85b3 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 29 Oct 2021 08:44:41 +1100 Subject: [PATCH 0061/1452] Add more dlna_dmr media_player services and attributes (#57827) --- homeassistant/components/dlna_dmr/const.py | 113 +++++ .../components/dlna_dmr/media_player.py | 183 +++++++- .../components/dlna_dmr/test_media_player.py | 431 +++++++++++++++--- 3 files changed, 640 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index f3217fdafff..20a978f9fda 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -5,6 +5,8 @@ from collections.abc import Mapping import logging from typing import Final +from async_upnp_client.profiles.dlna import PlayMode as _PlayMode + from homeassistant.components.media_player import const as _mp_const LOGGER = logging.getLogger(__package__) @@ -58,3 +60,114 @@ MEDIA_TYPE_MAP: Mapping[str, str] = { "object.container.storageFolder": _mp_const.MEDIA_TYPE_PLAYLIST, "object.container.bookmarkFolder": _mp_const.MEDIA_TYPE_PLAYLIST, } + +# Map media_player media_content_type to UPnP class. Not everything will map +# directly, in which case it's not specified and other defaults will be used. +MEDIA_UPNP_CLASS_MAP: Mapping[str, str] = { + _mp_const.MEDIA_TYPE_ALBUM: "object.container.album.musicAlbum", + _mp_const.MEDIA_TYPE_ARTIST: "object.container.person.musicArtist", + _mp_const.MEDIA_TYPE_CHANNEL: "object.item.videoItem.videoBroadcast", + _mp_const.MEDIA_TYPE_CHANNELS: "object.container.channelGroup", + _mp_const.MEDIA_TYPE_COMPOSER: "object.container.person.musicArtist", + _mp_const.MEDIA_TYPE_CONTRIBUTING_ARTIST: "object.container.person.musicArtist", + _mp_const.MEDIA_TYPE_EPISODE: "object.item.epgItem.videoProgram", + _mp_const.MEDIA_TYPE_GENRE: "object.container.genre", + _mp_const.MEDIA_TYPE_IMAGE: "object.item.imageItem", + _mp_const.MEDIA_TYPE_MOVIE: "object.item.videoItem.movie", + _mp_const.MEDIA_TYPE_MUSIC: "object.item.audioItem.musicTrack", + _mp_const.MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", + _mp_const.MEDIA_TYPE_PODCAST: "object.item.audioItem.audioBook", + _mp_const.MEDIA_TYPE_SEASON: "object.item.epgItem.videoProgram", + _mp_const.MEDIA_TYPE_TRACK: "object.item.audioItem.musicTrack", + _mp_const.MEDIA_TYPE_TVSHOW: "object.item.videoItem.videoBroadcast", + _mp_const.MEDIA_TYPE_URL: "object.item.bookmarkItem", + _mp_const.MEDIA_TYPE_VIDEO: "object.item.videoItem", +} + +# Translation of MediaMetadata keys to DIDL-Lite keys. +# See https://developers.google.com/cast/docs/reference/messages#MediaData via +# https://www.home-assistant.io/integrations/media_player/ for HA keys. +# See http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v4-Service.pdf for +# DIDL-Lite keys. +MEDIA_METADATA_DIDL: Mapping[str, str] = { + "subtitle": "longDescription", + "releaseDate": "date", + "studio": "publisher", + "season": "episodeSeason", + "episode": "episodeNumber", + "albumName": "album", + "trackNumber": "originalTrackNumber", +} + +# For (un)setting repeat mode, map a combination of shuffle & repeat to a list +# of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any +# case. NOTE: This list is slightly different to that in SHUFFLE_PLAY_MODES, +# due to fallback behaviour when turning on repeat modes. +REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { + (False, _mp_const.REPEAT_MODE_OFF): [ + _PlayMode.NORMAL, + ], + (False, _mp_const.REPEAT_MODE_ONE): [ + _PlayMode.REPEAT_ONE, + _PlayMode.REPEAT_ALL, + _PlayMode.NORMAL, + ], + (False, _mp_const.REPEAT_MODE_ALL): [ + _PlayMode.REPEAT_ALL, + _PlayMode.REPEAT_ONE, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_OFF): [ + _PlayMode.SHUFFLE, + _PlayMode.RANDOM, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_ONE): [ + _PlayMode.REPEAT_ONE, + _PlayMode.RANDOM, + _PlayMode.SHUFFLE, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_ALL): [ + _PlayMode.RANDOM, + _PlayMode.REPEAT_ALL, + _PlayMode.SHUFFLE, + _PlayMode.NORMAL, + ], +} + +# For (un)setting shuffle mode, map a combination of shuffle & repeat to a list +# of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any +# case. +SHUFFLE_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { + (False, _mp_const.REPEAT_MODE_OFF): [ + _PlayMode.NORMAL, + ], + (False, _mp_const.REPEAT_MODE_ONE): [ + _PlayMode.REPEAT_ONE, + _PlayMode.REPEAT_ALL, + _PlayMode.NORMAL, + ], + (False, _mp_const.REPEAT_MODE_ALL): [ + _PlayMode.REPEAT_ALL, + _PlayMode.REPEAT_ONE, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_OFF): [ + _PlayMode.SHUFFLE, + _PlayMode.RANDOM, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_ONE): [ + _PlayMode.RANDOM, + _PlayMode.SHUFFLE, + _PlayMode.REPEAT_ONE, + _PlayMode.NORMAL, + ], + (True, _mp_const.REPEAT_MODE_ALL): [ + _PlayMode.RANDOM, + _PlayMode.SHUFFLE, + _PlayMode.REPEAT_ALL, + _PlayMode.NORMAL, + ], +} diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 2835117e57c..2b699735108 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping, Sequence +import contextlib from datetime import datetime, timedelta import functools from typing import Any, Callable, TypeVar, cast @@ -10,7 +11,7 @@ from typing import Any, Callable, TypeVar, cast from async_upnp_client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpError, UpnpResponseError -from async_upnp_client.profiles.dlna import DmrDevice, TransportState +from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState from async_upnp_client.utils import async_get_local_ip import voluptuous as vol @@ -18,12 +19,19 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( + ATTR_MEDIA_EXTRA, + REPEAT_MODE_ALL, + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, SUPPORT_SEEK, + SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, @@ -51,7 +59,11 @@ from .const import ( CONF_POLL_AVAILABILITY, DOMAIN, LOGGER as _LOGGER, + MEDIA_METADATA_DIDL, MEDIA_TYPE_MAP, + MEDIA_UPNP_CLASS_MAP, + REPEAT_PLAY_MODES, + SHUFFLE_PLAY_MODES, ) from .data import EventListenAddr, get_domain_data @@ -250,11 +262,9 @@ class DlnaDmrEntity(MediaPlayerEntity): if self._bootid is not None and self._bootid == bootid: # Store the new value (because our old value matches) so that we # can ignore subsequent ssdp:alive messages - try: + with contextlib.suppress(KeyError, ValueError): next_bootid_str = info[ssdp.ATTR_SSDP_NEXTBOOTID] self._bootid = int(next_bootid_str, 10) - except (KeyError, ValueError): - pass # Nothing left to do until ssdp:alive comes through return @@ -445,7 +455,21 @@ class DlnaDmrEntity(MediaPlayerEntity): if not state_variables: # Indicates a failure to resubscribe, check if device is still available self.check_available = True - self.async_write_ha_state() + + force_refresh = False + + if service.service_id == "urn:upnp-org:serviceId:AVTransport": + for state_variable in state_variables: + # Force a state refresh when player begins or pauses playback + # to update the position info. + if ( + state_variable.name == "TransportState" + and state_variable.value + in (TransportState.PLAYING, TransportState.PAUSED_PLAYBACK) + ): + force_refresh = True + + self.async_schedule_update_ha_state(force_refresh) @property def available(self) -> bool: @@ -515,6 +539,15 @@ class DlnaDmrEntity(MediaPlayerEntity): if self._device.can_seek_rel_time: supported_features |= SUPPORT_SEEK + play_modes = self._device.valid_play_modes + if play_modes & {PlayMode.RANDOM, PlayMode.SHUFFLE}: + supported_features |= SUPPORT_SHUFFLE_SET + if play_modes & {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL}: + supported_features |= SUPPORT_REPEAT_SET + + if self._device.has_presets: + supported_features |= SUPPORT_SELECT_SOUND_MODE + return supported_features @property @@ -575,23 +608,44 @@ class DlnaDmrEntity(MediaPlayerEntity): ) -> None: """Play a piece of media.""" _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) - title = "Home Assistant" - assert self._device is not None + extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} + metadata: dict[str, Any] = extra.get("metadata") or {} + + title = extra.get("title") or metadata.get("title") or "Home Assistant" + thumb = extra.get("thumb") + if thumb: + metadata["album_art_uri"] = thumb + + # Translate metadata keys from HA names to DIDL-Lite names + for hass_key, didl_key in MEDIA_METADATA_DIDL.items(): + if hass_key in metadata: + metadata[didl_key] = metadata.pop(hass_key) + + # Create metadata specific to the given media type; different fields are + # available depending on what the upnp_class is. + upnp_class = MEDIA_UPNP_CLASS_MAP.get(media_type) + didl_metadata = await self._device.construct_play_media_metadata( + media_url=media_id, + media_title=title, + override_upnp_class=upnp_class, + meta_data=metadata, + ) # Stop current playing media if self._device.can_stop: await self.async_media_stop() # Queue media - await self._device.async_set_transport_uri(media_id, title) - await self._device.async_wait_for_can_play() + await self._device.async_set_transport_uri(media_id, title, didl_metadata) - # If already playing, no need to call Play - if self._device.transport_state == TransportState.PLAYING: + # If already playing, or don't want to autoplay, no need to call Play + autoplay = extra.get("autoplay", True) + if self._device.transport_state == TransportState.PLAYING or not autoplay: return # Play it + await self._device.async_wait_for_can_play() await self.async_media_play() @catch_request_errors @@ -606,6 +660,98 @@ class DlnaDmrEntity(MediaPlayerEntity): assert self._device is not None await self._device.async_next() + @property + def shuffle(self) -> bool | None: + """Boolean if shuffle is enabled.""" + if not self._device: + return None + + play_mode = self._device.play_mode + if not play_mode: + return None + + if play_mode == PlayMode.VENDOR_DEFINED: + return None + + return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM) + + @catch_request_errors + async def async_set_shuffle(self, shuffle: bool) -> None: + """Enable/disable shuffle mode.""" + assert self._device is not None + + repeat = self.repeat or REPEAT_MODE_OFF + potential_play_modes = SHUFFLE_PLAY_MODES[(shuffle, repeat)] + + valid_play_modes = self._device.valid_play_modes + + for mode in potential_play_modes: + if mode in valid_play_modes: + await self._device.async_set_play_mode(mode) + return + + _LOGGER.debug( + "Couldn't find a suitable mode for shuffle=%s, repeat=%s", shuffle, repeat + ) + + @property + def repeat(self) -> str | None: + """Return current repeat mode.""" + if not self._device: + return None + + play_mode = self._device.play_mode + if not play_mode: + return None + + if play_mode == PlayMode.VENDOR_DEFINED: + return None + + if play_mode == PlayMode.REPEAT_ONE: + return REPEAT_MODE_ONE + + if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM): + return REPEAT_MODE_ALL + + return REPEAT_MODE_OFF + + @catch_request_errors + async def async_set_repeat(self, repeat: str) -> None: + """Set repeat mode.""" + assert self._device is not None + + shuffle = self.shuffle or False + potential_play_modes = REPEAT_PLAY_MODES[(shuffle, repeat)] + + valid_play_modes = self._device.valid_play_modes + + for mode in potential_play_modes: + if mode in valid_play_modes: + await self._device.async_set_play_mode(mode) + return + + _LOGGER.debug( + "Couldn't find a suitable mode for shuffle=%s, repeat=%s", shuffle, repeat + ) + + @property + def sound_mode(self) -> str | None: + """Name of the current sound mode, not supported by DLNA.""" + return None + + @property + def sound_mode_list(self) -> list[str] | None: + """List of available sound modes.""" + if not self._device: + return None + return self._device.preset_names + + @catch_request_errors + async def async_select_sound_mode(self, sound_mode: str) -> None: + """Select sound mode.""" + assert self._device is not None + await self._device.async_select_preset(sound_mode) + @property def media_title(self) -> str | None: """Title of current playing media.""" @@ -705,12 +851,10 @@ class DlnaDmrEntity(MediaPlayerEntity): not self._device.media_season_number or self._device.media_season_number == "0" ) and self._device.media_episode_number: - try: + with contextlib.suppress(ValueError): episode = int(self._device.media_episode_number, 10) if episode > 100: return str(episode // 100) - except ValueError: - pass return self._device.media_season_number @property @@ -723,12 +867,10 @@ class DlnaDmrEntity(MediaPlayerEntity): not self._device.media_season_number or self._device.media_season_number == "0" ) and self._device.media_episode_number: - try: + with contextlib.suppress(ValueError): episode = int(self._device.media_episode_number, 10) if episode > 100: return str(episode % 100) - except ValueError: - pass return self._device.media_episode_number @property @@ -737,3 +879,10 @@ class DlnaDmrEntity(MediaPlayerEntity): if not self._device: return None return self._device.media_channel_name + + @property + def media_playlist(self) -> str | None: + """Title of Playlist currently playing.""" + if not self._device: + return None + return self._device.media_playlist_title diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index e12c23535fa..b9bbdbffc92 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -8,12 +8,13 @@ from types import MappingProxyType from typing import Any from unittest.mock import ANY, DEFAULT, Mock, patch +from async_upnp_client import UpnpService, UpnpStateVariable from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, UpnpResponseError, ) -from async_upnp_client.profiles.dlna import TransportState +from async_upnp_client.profiles.dlna import PlayMode, TransportState import pytest from homeassistant import const as ha_const @@ -67,6 +68,16 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) return entity_id +async def get_attrs(hass: HomeAssistant, entity_id: str) -> Mapping[str, Any]: + """Get updated device attributes.""" + await async_update_entity(hass, entity_id) + entity_state = hass.states.get(entity_id) + assert entity_state is not None + attrs = entity_state.attributes + assert attrs is not None + return attrs + + @pytest.fixture async def mock_entity_id( hass: HomeAssistant, @@ -335,9 +346,7 @@ async def test_setup_entry_with_options( async def test_event_subscribe_failure( - hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - dmr_device_mock: Mock, + hass: HomeAssistant, config_entry_mock: MockConfigEntry, dmr_device_mock: Mock ) -> None: """Test _device_connect aborts when async_subscribe_services fails.""" dmr_device_mock.async_subscribe_services.side_effect = UpnpError @@ -389,9 +398,7 @@ async def test_event_subscribe_rejected( async def test_available_device( - hass: HomeAssistant, - dmr_device_mock: Mock, - mock_entity_id: str, + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str ) -> None: """Test a DlnaDmrEntity with a connected DmrDevice.""" # Check hass device information is filled in @@ -429,19 +436,63 @@ async def test_available_device( assert entity_state is not None assert entity_state.state == ha_const.STATE_UNAVAILABLE - dmr_device_mock.profile_device.available = True - await async_update_entity(hass, mock_entity_id) +async def test_feature_flags( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test feature flags of a connected DlnaDmrEntity.""" + # Check supported feature flags, one at a time. + FEATURE_FLAGS: list[tuple[str, int]] = [ + ("has_volume_level", mp_const.SUPPORT_VOLUME_SET), + ("has_volume_mute", mp_const.SUPPORT_VOLUME_MUTE), + ("can_play", mp_const.SUPPORT_PLAY), + ("can_pause", mp_const.SUPPORT_PAUSE), + ("can_stop", mp_const.SUPPORT_STOP), + ("can_previous", mp_const.SUPPORT_PREVIOUS_TRACK), + ("can_next", mp_const.SUPPORT_NEXT_TRACK), + ("has_play_media", mp_const.SUPPORT_PLAY_MEDIA), + ("can_seek_rel_time", mp_const.SUPPORT_SEEK), + ("has_presets", mp_const.SUPPORT_SELECT_SOUND_MODE), + ] + + # Clear all feature properties + dmr_device_mock.valid_play_modes = set() + for feat_prop, _ in FEATURE_FLAGS: + setattr(dmr_device_mock, feat_prop, False) + attrs = await get_attrs(hass, mock_entity_id) + assert attrs[ha_const.ATTR_SUPPORTED_FEATURES] == 0 + + # Test the properties cumulatively + expected_features = 0 + for feat_prop, flag in FEATURE_FLAGS: + setattr(dmr_device_mock, feat_prop, True) + expected_features |= flag + attrs = await get_attrs(hass, mock_entity_id) + assert attrs[ha_const.ATTR_SUPPORTED_FEATURES] == expected_features + + # shuffle and repeat features depend on the available play modes + PLAY_MODE_FEATURE_FLAGS: list[tuple[PlayMode, int]] = [ + (PlayMode.NORMAL, 0), + (PlayMode.SHUFFLE, mp_const.SUPPORT_SHUFFLE_SET), + (PlayMode.REPEAT_ONE, mp_const.SUPPORT_REPEAT_SET), + (PlayMode.REPEAT_ALL, mp_const.SUPPORT_REPEAT_SET), + (PlayMode.RANDOM, mp_const.SUPPORT_SHUFFLE_SET), + (PlayMode.DIRECT_1, 0), + (PlayMode.INTRO, 0), + (PlayMode.VENDOR_DEFINED, 0), + ] + for play_modes, flag in PLAY_MODE_FEATURE_FLAGS: + dmr_device_mock.valid_play_modes = {play_modes} + attrs = await get_attrs(hass, mock_entity_id) + assert attrs[ha_const.ATTR_SUPPORTED_FEATURES] == expected_features | flag + + +async def test_attributes( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test attributes of a connected DlnaDmrEntity.""" # Check attributes come directly from the device - async def get_attrs() -> Mapping[str, Any]: - await async_update_entity(hass, mock_entity_id) - entity_state = hass.states.get(mock_entity_id) - assert entity_state is not None - attrs = entity_state.attributes - assert attrs is not None - return attrs - - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_VOLUME_LEVEL] is dmr_device_mock.volume_level assert attrs[mp_const.ATTR_MEDIA_VOLUME_MUTED] is dmr_device_mock.is_volume_muted assert attrs[mp_const.ATTR_MEDIA_DURATION] is dmr_device_mock.media_duration @@ -459,68 +510,65 @@ async def test_available_device( assert attrs[mp_const.ATTR_MEDIA_SEASON] is dmr_device_mock.media_season_number assert attrs[mp_const.ATTR_MEDIA_EPISODE] is dmr_device_mock.media_episode_number assert attrs[mp_const.ATTR_MEDIA_CHANNEL] is dmr_device_mock.media_channel_name + assert attrs[mp_const.ATTR_SOUND_MODE_LIST] is dmr_device_mock.preset_names + # Entity picture is cached, won't correspond to remote image assert isinstance(attrs[ha_const.ATTR_ENTITY_PICTURE], str) + # media_title depends on what is available assert attrs[mp_const.ATTR_MEDIA_TITLE] is dmr_device_mock.media_program_title dmr_device_mock.media_program_title = None - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_TITLE] is dmr_device_mock.media_title + # media_content_type is mapped from UPnP class to MediaPlayer type dmr_device_mock.media_class = "object.item.audioItem.musicTrack" - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_MUSIC dmr_device_mock.media_class = "object.item.videoItem.movie" - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_MOVIE dmr_device_mock.media_class = "object.item.videoItem.videoBroadcast" - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_TVSHOW + # media_season & media_episode have a special case dmr_device_mock.media_season_number = "0" dmr_device_mock.media_episode_number = "123" - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_SEASON] == "1" assert attrs[mp_const.ATTR_MEDIA_EPISODE] == "23" dmr_device_mock.media_season_number = "0" dmr_device_mock.media_episode_number = "S1E23" # Unexpected and not parsed - attrs = await get_attrs() + attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp_const.ATTR_MEDIA_SEASON] == "0" assert attrs[mp_const.ATTR_MEDIA_EPISODE] == "S1E23" - # Check supported feature flags, one at a time. - # tuple(async_upnp_client feature check property, HA feature flag) - FEATURE_FLAGS: list[tuple[str, int]] = [ - ("has_volume_level", mp_const.SUPPORT_VOLUME_SET), - ("has_volume_mute", mp_const.SUPPORT_VOLUME_MUTE), - ("can_play", mp_const.SUPPORT_PLAY), - ("can_pause", mp_const.SUPPORT_PAUSE), - ("can_stop", mp_const.SUPPORT_STOP), - ("can_previous", mp_const.SUPPORT_PREVIOUS_TRACK), - ("can_next", mp_const.SUPPORT_NEXT_TRACK), - ("has_play_media", mp_const.SUPPORT_PLAY_MEDIA), - ("can_seek_rel_time", mp_const.SUPPORT_SEEK), - ] - # Clear all feature properties - for feat_prop, _ in FEATURE_FLAGS: - setattr(dmr_device_mock, feat_prop, False) - await async_update_entity(hass, mock_entity_id) - entity_state = hass.states.get(mock_entity_id) - assert entity_state is not None - assert entity_state.attributes[ha_const.ATTR_SUPPORTED_FEATURES] == 0 - # Test the properties cumulatively - expected_features = 0 - for feat_prop, flag in FEATURE_FLAGS: - setattr(dmr_device_mock, feat_prop, True) - expected_features |= flag - await async_update_entity(hass, mock_entity_id) - entity_state = hass.states.get(mock_entity_id) - assert entity_state is not None - assert ( - entity_state.attributes[ha_const.ATTR_SUPPORTED_FEATURES] - == expected_features - ) + # shuffle and repeat is based on device's play mode + for play_mode, shuffle, repeat in [ + (PlayMode.NORMAL, False, mp_const.REPEAT_MODE_OFF), + (PlayMode.SHUFFLE, True, mp_const.REPEAT_MODE_OFF), + (PlayMode.REPEAT_ONE, False, mp_const.REPEAT_MODE_ONE), + (PlayMode.REPEAT_ALL, False, mp_const.REPEAT_MODE_ALL), + (PlayMode.RANDOM, True, mp_const.REPEAT_MODE_ALL), + (PlayMode.DIRECT_1, False, mp_const.REPEAT_MODE_OFF), + (PlayMode.INTRO, False, mp_const.REPEAT_MODE_OFF), + ]: + dmr_device_mock.play_mode = play_mode + attrs = await get_attrs(hass, mock_entity_id) + assert attrs[mp_const.ATTR_MEDIA_SHUFFLE] is shuffle + assert attrs[mp_const.ATTR_MEDIA_REPEAT] == repeat + for bad_play_mode in [None, PlayMode.VENDOR_DEFINED]: + dmr_device_mock.play_mode = bad_play_mode + attrs = await get_attrs(hass, mock_entity_id) + assert mp_const.ATTR_MEDIA_SHUFFLE not in attrs + assert mp_const.ATTR_MEDIA_REPEAT not in attrs + +async def test_services( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test service calls of a connected DlnaDmrEntity.""" # Check interface methods interact directly with the device await hass.services.async_call( MP_DOMAIN, @@ -578,15 +626,22 @@ async def test_available_device( blocking=True, ) dmr_device_mock.async_seek_rel_time.assert_awaited_once_with(timedelta(seconds=33)) + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_SELECT_SOUND_MODE, + {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_SOUND_MODE: "Default"}, + blocking=True, + ) + dmr_device_mock.async_select_preset.assert_awaited_once_with("Default") + +async def test_play_media_stopped( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media, starting from stopped and the device can stop.""" # play_media performs a few calls to the device for setup and play - # Start from stopped, and device can stop too dmr_device_mock.can_stop = True dmr_device_mock.transport_state = TransportState.STOPPED - dmr_device_mock.async_stop.reset_mock() - dmr_device_mock.async_set_transport_uri.reset_mock() - dmr_device_mock.async_wait_for_can_play.reset_mock() - dmr_device_mock.async_play.reset_mock() await hass.services.async_call( MP_DOMAIN, mp_const.SERVICE_PLAY_MEDIA, @@ -598,20 +653,27 @@ async def test_available_device( }, blocking=True, ) + + dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with( + media_url="http://192.88.99.20:8200/MediaItems/17621.mp3", + media_title="Home Assistant", + override_upnp_class="object.item.audioItem.musicTrack", + meta_data={}, + ) dmr_device_mock.async_stop.assert_awaited_once_with() dmr_device_mock.async_set_transport_uri.assert_awaited_once_with( - "http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant" + "http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY ) dmr_device_mock.async_wait_for_can_play.assert_awaited_once_with() dmr_device_mock.async_play.assert_awaited_once_with() - # play_media again, while the device is already playing and can't stop + +async def test_play_media_playing( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media, device is already playing and can't stop.""" dmr_device_mock.can_stop = False dmr_device_mock.transport_state = TransportState.PLAYING - dmr_device_mock.async_stop.reset_mock() - dmr_device_mock.async_set_transport_uri.reset_mock() - dmr_device_mock.async_wait_for_can_play.reset_mock() - dmr_device_mock.async_play.reset_mock() await hass.services.async_call( MP_DOMAIN, mp_const.SERVICE_PLAY_MEDIA, @@ -623,14 +685,232 @@ async def test_available_device( }, blocking=True, ) + + dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with( + media_url="http://192.88.99.20:8200/MediaItems/17621.mp3", + media_title="Home Assistant", + override_upnp_class="object.item.audioItem.musicTrack", + meta_data={}, + ) dmr_device_mock.async_stop.assert_not_awaited() dmr_device_mock.async_set_transport_uri.assert_awaited_once_with( - "http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant" + "http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY ) - dmr_device_mock.async_wait_for_can_play.assert_awaited_once_with() + dmr_device_mock.async_wait_for_can_play.assert_not_awaited() dmr_device_mock.async_play.assert_not_awaited() +async def test_play_media_no_autoplay( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media with autoplay=False.""" + # play_media performs a few calls to the device for setup and play + dmr_device_mock.can_stop = True + dmr_device_mock.transport_state = TransportState.STOPPED + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_ENQUEUE: False, + mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False}, + }, + blocking=True, + ) + + dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with( + media_url="http://192.88.99.20:8200/MediaItems/17621.mp3", + media_title="Home Assistant", + override_upnp_class="object.item.audioItem.musicTrack", + meta_data={}, + ) + dmr_device_mock.async_stop.assert_awaited_once_with() + dmr_device_mock.async_set_transport_uri.assert_awaited_once_with( + "http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY + ) + dmr_device_mock.async_wait_for_can_play.assert_not_awaited() + dmr_device_mock.async_play.assert_not_awaited() + + +async def test_play_media_metadata( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media constructs useful metadata from user params.""" + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_ENQUEUE: False, + mp_const.ATTR_MEDIA_EXTRA: { + "title": "Mock song", + "thumb": "http://192.88.99.20:8200/MediaItems/17621.jpg", + "metadata": {"artist": "Mock artist", "album": "Mock album"}, + }, + }, + blocking=True, + ) + + dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with( + media_url="http://192.88.99.20:8200/MediaItems/17621.mp3", + media_title="Mock song", + override_upnp_class="object.item.audioItem.musicTrack", + meta_data={ + "artist": "Mock artist", + "album": "Mock album", + "album_art_uri": "http://192.88.99.20:8200/MediaItems/17621.jpg", + }, + ) + + # Check again for a different media type + dmr_device_mock.construct_play_media_metadata.reset_mock() + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_TVSHOW, + mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/123.mkv", + mp_const.ATTR_MEDIA_ENQUEUE: False, + mp_const.ATTR_MEDIA_EXTRA: { + "title": "Mock show", + "metadata": {"season": 1, "episode": 12}, + }, + }, + blocking=True, + ) + + dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with( + media_url="http://192.88.99.20:8200/MediaItems/123.mkv", + media_title="Mock show", + override_upnp_class="object.item.videoItem.videoBroadcast", + meta_data={"episodeSeason": 1, "episodeNumber": 12}, + ) + + +async def test_shuffle_repeat_modes( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test setting repeat and shuffle modes.""" + # Test shuffle with all variations of existing play mode + dmr_device_mock.valid_play_modes = {mode.value for mode in PlayMode} + for init_mode, shuffle_set, expect_mode in [ + (PlayMode.NORMAL, False, PlayMode.NORMAL), + (PlayMode.SHUFFLE, False, PlayMode.NORMAL), + (PlayMode.REPEAT_ONE, False, PlayMode.REPEAT_ONE), + (PlayMode.REPEAT_ALL, False, PlayMode.REPEAT_ALL), + (PlayMode.RANDOM, False, PlayMode.REPEAT_ALL), + (PlayMode.NORMAL, True, PlayMode.SHUFFLE), + (PlayMode.SHUFFLE, True, PlayMode.SHUFFLE), + (PlayMode.REPEAT_ONE, True, PlayMode.RANDOM), + (PlayMode.REPEAT_ALL, True, PlayMode.RANDOM), + (PlayMode.RANDOM, True, PlayMode.RANDOM), + ]: + dmr_device_mock.play_mode = init_mode + await hass.services.async_call( + MP_DOMAIN, + ha_const.SERVICE_SHUFFLE_SET, + {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_SHUFFLE: shuffle_set}, + blocking=True, + ) + dmr_device_mock.async_set_play_mode.assert_awaited_with(expect_mode) + + # Test repeat with all variations of existing play mode + for init_mode, repeat_set, expect_mode in [ + (PlayMode.NORMAL, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), + (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_OFF, PlayMode.SHUFFLE), + (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), + (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), + (PlayMode.RANDOM, mp_const.REPEAT_MODE_OFF, PlayMode.SHUFFLE), + (PlayMode.NORMAL, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), + (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), + (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), + (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), + (PlayMode.RANDOM, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), + (PlayMode.NORMAL, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), + (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_ALL, PlayMode.RANDOM), + (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), + (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), + (PlayMode.RANDOM, mp_const.REPEAT_MODE_ALL, PlayMode.RANDOM), + ]: + dmr_device_mock.play_mode = init_mode + await hass.services.async_call( + MP_DOMAIN, + ha_const.SERVICE_REPEAT_SET, + {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_REPEAT: repeat_set}, + blocking=True, + ) + dmr_device_mock.async_set_play_mode.assert_awaited_with(expect_mode) + + # Test shuffle when the device doesn't support the desired play mode. + # Trying to go from RANDOM -> REPEAT_MODE_ALL, but nothing in the list is supported. + dmr_device_mock.async_set_play_mode.reset_mock() + dmr_device_mock.play_mode = PlayMode.RANDOM + dmr_device_mock.valid_play_modes = {PlayMode.SHUFFLE, PlayMode.RANDOM} + await hass.services.async_call( + MP_DOMAIN, + ha_const.SERVICE_SHUFFLE_SET, + {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_SHUFFLE: False}, + blocking=True, + ) + dmr_device_mock.async_set_play_mode.assert_not_awaited() + + # Test repeat when the device doesn't support the desired play mode. + # Trying to go from RANDOM -> SHUFFLE, but nothing in the list is supported. + dmr_device_mock.async_set_play_mode.reset_mock() + dmr_device_mock.play_mode = PlayMode.RANDOM + dmr_device_mock.valid_play_modes = {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL} + await hass.services.async_call( + MP_DOMAIN, + ha_const.SERVICE_REPEAT_SET, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_REPEAT: mp_const.REPEAT_MODE_OFF, + }, + blocking=True, + ) + dmr_device_mock.async_set_play_mode.assert_not_awaited() + + +async def test_playback_update_state( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test starting or pausing playback causes the state to be refreshed. + + This is necessary for responsive updates of the current track position and + total track time. + """ + on_event = dmr_device_mock.on_event + mock_service = Mock(UpnpService) + mock_service.service_id = "urn:upnp-org:serviceId:AVTransport" + mock_state_variable = Mock(UpnpStateVariable) + mock_state_variable.name = "TransportState" + + # Event update that device has started playing, device should get polled + mock_state_variable.value = TransportState.PLAYING + on_event(mock_service, [mock_state_variable]) + await hass.async_block_till_done() + dmr_device_mock.async_update.assert_awaited_once_with(do_ping=False) + + # Event update that device has paused playing, device should get polled + dmr_device_mock.async_update.reset_mock() + mock_state_variable.value = TransportState.PAUSED_PLAYBACK + on_event(mock_service, [mock_state_variable]) + await hass.async_block_till_done() + dmr_device_mock.async_update.assert_awaited_once_with(do_ping=False) + + # Different service shouldn't do anything + dmr_device_mock.async_update.reset_mock() + mock_service.service_id = "urn:upnp-org:serviceId:RenderingControl" + on_event(mock_service, [mock_state_variable]) + await hass.async_block_till_done() + dmr_device_mock.async_update.assert_not_awaited() + + async def test_unavailable_device( hass: HomeAssistant, domain_data_mock: Mock, @@ -691,6 +971,7 @@ async def test_unavailable_device( assert attrs[ha_const.ATTR_FRIENDLY_NAME] == MOCK_DEVICE_NAME assert attrs[ha_const.ATTR_SUPPORTED_FEATURES] == 0 + assert mp_const.ATTR_SOUND_MODE_LIST not in attrs # Check service calls do nothing SERVICES: list[tuple[str, dict]] = [ @@ -710,6 +991,9 @@ async def test_unavailable_device( mp_const.ATTR_MEDIA_ENQUEUE: False, }, ), + (mp_const.SERVICE_SELECT_SOUND_MODE, {mp_const.ATTR_SOUND_MODE: "Default"}), + (ha_const.SERVICE_SHUFFLE_SET, {mp_const.ATTR_MEDIA_SHUFFLE: True}), + (ha_const.SERVICE_REPEAT_SET, {mp_const.ATTR_MEDIA_REPEAT: "all"}), ] for service, data in SERVICES: await hass.services.async_call( @@ -1312,6 +1596,9 @@ async def test_disappearing_device( # media_image_url is normally hidden by entity_picture, but we want a direct check assert entity.media_image_url is None + # Check attributes that are normally pre-checked + assert entity.sound_mode_list is None + # Test service calls await entity.async_set_volume_level(0.1) await entity.async_mute_volume(True) @@ -1322,6 +1609,9 @@ async def test_disappearing_device( await entity.async_play_media("", "") await entity.async_media_previous_track() await entity.async_media_next_track() + await entity.async_set_shuffle(True) + await entity.async_set_repeat(mp_const.REPEAT_MODE_ALL) + await entity.async_select_sound_mode("Default") async def test_resubscribe_failure( @@ -1335,7 +1625,8 @@ async def test_resubscribe_failure( dmr_device_mock.async_update.reset_mock() on_event = dmr_device_mock.on_event - on_event(None, []) + mock_service = Mock(UpnpService) + on_event(mock_service, []) await hass.async_block_till_done() await async_update_entity(hass, mock_entity_id) From 37930aeeb63d0d12652e094ae15b283d0a8bb5b2 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:47:49 +0100 Subject: [PATCH 0062/1452] Aurora abb energy metering (#58454) Co-authored-by: J. Nick Koston --- .../aurora_abb_powerone/aurora_device.py | 2 +- .../components/aurora_abb_powerone/sensor.py | 67 ++++++++++++------- .../aurora_abb_powerone/test_sensor.py | 35 ++++------ 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index 0a7aab4a921..3913515a9b9 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -32,7 +32,7 @@ class AuroraDevice(Entity): def unique_id(self) -> str: """Return the unique id for this device.""" serial = self._data[ATTR_SERIAL_NUMBER] - return f"{serial}_{self.type}" + return f"{serial}_{self.entity_description.key}" @property def available(self) -> bool: diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 946f5645bdc..4f196c39630 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -1,6 +1,9 @@ """Support for Aurora ABB PowerOne Solar Photvoltaic (PV) inverter.""" +from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol @@ -8,19 +11,22 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_NAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.exceptions import InvalidStateError import homeassistant.helpers.config_validation as cv from .aurora_device import AuroraDevice @@ -28,6 +34,29 @@ from .const import DEFAULT_ADDRESS, DOMAIN _LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = [ + SensorEntityDescription( + key="instantaneouspower", + device_class=DEVICE_CLASS_POWER, + native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + name="Power Output", + ), + SensorEntityDescription( + key="temp", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + name="Temperature", + ), + SensorEntityDescription( + key="totalenergy", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + name="Total Energy", + ), +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -55,15 +84,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: """Set up aurora_abb_powerone sensor based on a config entry.""" entities = [] - sensortypes = [ - {"parameter": "instantaneouspower", "name": "Power Output"}, - {"parameter": "temperature", "name": "Temperature"}, - ] client = hass.data[DOMAIN][config_entry.unique_id] data = config_entry.data - for sens in sensortypes: - entities.append(AuroraSensor(client, data, sens["name"], sens["parameter"])) + for sens in SENSOR_TYPES: + entities.append(AuroraSensor(client, data, sens)) _LOGGER.debug("async_setup_entry adding %d entities", len(entities)) async_add_entities(entities, True) @@ -72,22 +97,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: class AuroraSensor(AuroraDevice, SensorEntity): """Representation of a Sensor on a Aurora ABB PowerOne Solar inverter.""" - _attr_state_class = STATE_CLASS_MEASUREMENT - - def __init__(self, client: AuroraSerialClient, data, name, typename): + def __init__( + self, + client: AuroraSerialClient, + data: Mapping[str, Any], + entity_description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" super().__init__(client, data) - if typename == "instantaneouspower": - self.type = typename - self._attr_native_unit_of_measurement = POWER_WATT - self._attr_device_class = DEVICE_CLASS_POWER - elif typename == "temperature": - self.type = typename - self._attr_native_unit_of_measurement = TEMP_CELSIUS - self._attr_device_class = DEVICE_CLASS_TEMPERATURE - else: - raise InvalidStateError(f"Unrecognised typename '{typename}'") - self._attr_name = f"{name}" + self.entity_description = entity_description self.availableprev = True def update(self): @@ -98,13 +116,16 @@ class AuroraSensor(AuroraDevice, SensorEntity): try: self.availableprev = self._attr_available self.client.connect() - if self.type == "instantaneouspower": + if self.entity_description.key == "instantaneouspower": # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) self._attr_native_value = round(power_watts, 1) - elif self.type == "temperature": + elif self.entity_description.key == "temp": temperature_c = self.client.measure(21) self._attr_native_value = round(temperature_c, 1) + elif self.entity_description.key == "totalenergy": + energy_wh = self.client.cumulated_energy(5) + self._attr_native_value = round(energy_wh / 1000, 2) self._attr_available = True except AuroraError as error: diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 26486c6a116..ae9360498c7 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import patch from aurorapy.client import AuroraError -import pytest from homeassistant.components.aurora_abb_powerone.const import ( ATTR_DEVICE_NAME, @@ -13,10 +12,8 @@ from homeassistant.components.aurora_abb_powerone.const import ( DEFAULT_INTEGRATION_TITLE, DOMAIN, ) -from homeassistant.components.aurora_abb_powerone.sensor import AuroraSensor from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_ADDRESS, CONF_PORT -from homeassistant.exceptions import InvalidStateError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -39,6 +36,7 @@ def _simulated_returns(index, global_measure=None): returns = { 3: 45.678, # power 21: 9.876, # temperature + 5: 12345, # energy } return returns[index] @@ -66,7 +64,12 @@ async def test_setup_platform_valid_config(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, - ), assert_setup_component(1, "sensor"): + ), patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), assert_setup_component( + 1, "sensor" + ): assert await async_setup_component(hass, "sensor", TEST_CONFIG) await hass.async_block_till_done() power = hass.states.get("sensor.power_output") @@ -91,6 +94,9 @@ async def test_sensors(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) @@ -104,24 +110,9 @@ async def test_sensors(hass): assert temperature assert temperature.state == "9.9" - -async def test_sensor_invalid_type(hass): - """Test invalid sensor type during setup.""" - entities = [] - mock_entry = _mock_config_entry() - - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=_simulated_returns, - ): - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - - client = hass.data[DOMAIN][mock_entry.unique_id] - data = mock_entry.data - with pytest.raises(InvalidStateError): - entities.append(AuroraSensor(client, data, "WrongSensor", "wrongparameter")) + energy = hass.states.get("sensor.total_energy") + assert energy + assert energy.state == "12.35" async def test_sensor_dark(hass): From 107bd11a31b645467fc64094e9a6c7f8c7874d51 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 23:58:33 +0200 Subject: [PATCH 0063/1452] Use DeviceInfo in wemo (#58638) --- homeassistant/components/wemo/light.py | 17 +++++++++-------- homeassistant/components/wemo/wemo_device.py | 15 ++++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 980675bd4ff..c46a4e78440 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -17,6 +17,7 @@ from homeassistant.components.light import ( from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN @@ -99,15 +100,15 @@ class WemoLight(WemoEntity, LightEntity): return self.light.uniqueID @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "name": self.name, - "connections": {(CONNECTION_ZIGBEE, self._unique_id)}, - "identifiers": {(WEMO_DOMAIN, self._unique_id)}, - "model": self._model_name, - "manufacturer": "Belkin", - } + return DeviceInfo( + connections={(CONNECTION_ZIGBEE, self._unique_id)}, + identifiers={(WEMO_DOMAIN, self._unique_id)}, + manufacturer="Belkin", + model=self._model_name, + name=self.name, + ) @property def brightness(self): diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 1690d30e082..a4f20eb55f5 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT @@ -120,13 +121,13 @@ class DeviceCoordinator(DataUpdateCoordinator): raise UpdateFailed("WeMo update failed") from err -def _device_info(wemo: WeMoDevice): - return { - "name": wemo.name, - "identifiers": {(DOMAIN, wemo.serialnumber)}, - "model": wemo.model_name, - "manufacturer": "Belkin", - } +def _device_info(wemo: WeMoDevice) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, wemo.serialnumber)}, + manufacturer="Belkin", + model=wemo.model_name, + name=wemo.name, + ) async def async_register_device( From 95f7b0c026d5045fa881cee4cd3f1cf6fd036b32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Oct 2021 23:58:53 +0200 Subject: [PATCH 0064/1452] Use DeviceInfo in waze-travel-time (#58637) --- homeassistant/components/waze_travel_time/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 81ee48ebd2f..8612f3ee54b 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import Config, CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType @@ -163,11 +164,11 @@ class WazeTravelTime(SensorEntity): """Representation of a Waze travel time sensor.""" _attr_native_unit_of_measurement = TIME_MINUTES - _attr_device_info = { - "name": "Waze", - "identifiers": {(DOMAIN, DOMAIN)}, - "entry_type": "service", - } + _attr_device_info = DeviceInfo( + entry_type="service", + name="Waze", + identifiers={(DOMAIN, DOMAIN)}, + ) def __init__(self, unique_id, name, origin, destination, waze_data): """Initialize the Waze travel time sensor.""" From 8925f5cc4582787b3db747f5abf06152f5b03596 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 00:19:25 +0200 Subject: [PATCH 0065/1452] Use DeviceInfo in wiffi (#58639) --- homeassistant/components/wiffi/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 5f87141e423..05b86c209a6 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import utcnow @@ -141,16 +141,14 @@ class WiffiEntity(Entity): def __init__(self, device, metric, options): """Initialize the base elements of a wiffi entity.""" self._id = generate_unique_id(device, metric) - self._device_info = { - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, device.mac_address) - }, - "identifiers": {(DOMAIN, device.mac_address)}, - "manufacturer": "stall.biz", - "name": f"{device.moduletype} {device.mac_address}", - "model": device.moduletype, - "sw_version": device.sw_version, - } + self._device_info = DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, device.mac_address)}, + identifiers={(DOMAIN, device.mac_address)}, + manufacturer="stall.biz", + model=device.moduletype, + name=f"{device.moduletype} {device.mac_address}", + sw_version=device.sw_version, + ) self._name = metric.description self._expiration_date = None self._value = None From 42a2aed8afe012a6ccad98d9c919a5b027e66a94 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 00:27:22 +0200 Subject: [PATCH 0066/1452] Fix missing temperature level on Tuya Heater (qn) devices (#58643) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/select.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index db43ec7a8eb..a9f7afb0ec5 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -210,6 +210,7 @@ class DPCode(str, Enum): LED_TYPE_1 = "led_type_1" LED_TYPE_2 = "led_type_2" LED_TYPE_3 = "led_type_3" + LEVEL = "level" LIGHT = "light" # Light LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index f56d2929a84..6df5b4e84dd 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -75,6 +75,15 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Heater + # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm + "qn": ( + SelectEntityDescription( + key=DPCode.LEVEL, + name="Temperature Level", + icon="mdi:thermometer-lines", + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 638bd743a5fe2319a917e6a935b10540042a9433 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 00:37:20 +0200 Subject: [PATCH 0067/1452] Use DeviceInfo in xbox (#58640) --- homeassistant/components/xbox/base_sensor.py | 17 +++++++++-------- homeassistant/components/xbox/media_player.py | 16 ++++++++-------- homeassistant/components/xbox/remote.py | 16 ++++++++-------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index c463b31d3c5..8ef0928f903 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from yarl import URL +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PresenceData, XboxUpdateCoordinator @@ -66,12 +67,12 @@ class XboxBaseSensorEntity(CoordinatorEntity): return self.attribute == "online" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(DOMAIN, "xbox_live")}, - "name": "Xbox Live", - "manufacturer": "Microsoft", - "model": "Xbox Live", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, "xbox_live")}, + manufacturer="Microsoft", + model="Xbox Live", + name="Xbox Live", + ) diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 17390f81fad..cdeb016d604 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -29,6 +29,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ConsoleData, XboxUpdateCoordinator @@ -213,21 +214,20 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): await self.client.smartglass.launch_app(self._console.id, media_id) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" # Turns "XboxOneX" into "Xbox One X" for display matches = re.finditer( ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", self._console.console_type, ) - model = " ".join([m.group(0) for m in matches]) - return { - "identifiers": {(DOMAIN, self._console.id)}, - "name": self._console.name, - "manufacturer": "Microsoft", - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, self._console.id)}, + manufacturer="Microsoft", + model=" ".join([m.group(0) for m in matches]), + name=self._console.name, + ) def _find_media_image(images: list[Image]) -> Image | None: diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 31e6220172a..04f25c5f632 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -20,6 +20,7 @@ from homeassistant.components.remote import ( DEFAULT_DELAY_SECS, RemoteEntity, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ConsoleData, XboxUpdateCoordinator @@ -98,18 +99,17 @@ class XboxRemote(CoordinatorEntity, RemoteEntity): await asyncio.sleep(delay) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" # Turns "XboxOneX" into "Xbox One X" for display matches = re.finditer( ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", self._console.console_type, ) - model = " ".join([m.group(0) for m in matches]) - return { - "identifiers": {(DOMAIN, self._console.id)}, - "name": self._console.name, - "manufacturer": "Microsoft", - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, self._console.id)}, + manufacturer="Microsoft", + model=" ".join([m.group(0) for m in matches]), + name=self._console.name, + ) From 335fdf96ba1cfafb523d7e7a36d34fb0f25aa71b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 00:37:55 +0200 Subject: [PATCH 0068/1452] Use DeviceInfo in xiaomi-miio (#58642) --- .../xiaomi_miio/alarm_control_panel.py | 9 ++--- .../components/xiaomi_miio/device.py | 35 ++++++++++--------- homeassistant/components/xiaomi_miio/light.py | 9 ++--- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 26421770771..1fccbcf8056 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_ALARM_ARMING, STATE_ALARM_DISARMED, ) +from homeassistant.helpers.entity import DeviceInfo from .const import CONF_GATEWAY, DOMAIN @@ -65,11 +66,11 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): return self._gateway_device_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info of the gateway.""" - return { - "identifiers": {(DOMAIN, self._gateway_device_id)}, - } + return DeviceInfo( + identifiers={(DOMAIN, self._gateway_device_id)}, + ) @property def name(self): diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index be9c1151aa5..8203b021ef9 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -7,8 +7,9 @@ import logging from construct.core import ChecksumError from miio import Device, DeviceException +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_MAC, CONF_MODEL, DOMAIN, AuthException, SetupException @@ -85,17 +86,17 @@ class XiaomiMiioEntity(Entity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "manufacturer": "Xiaomi", - "name": self._name, - "model": self._model, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer="Xiaomi", + model=self._model, + name=self._name, + ) if self._mac is not None: - device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} + device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} return device_info @@ -125,17 +126,17 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "manufacturer": "Xiaomi", - "name": self._device_name, - "model": self._model, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer="Xiaomi", + model=self._model, + name=self._device_name, + ) if self._mac is not None: - device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} + device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} return device_info diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 04597eadf81..03722380a69 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -26,6 +26,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_TOKEN import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import color, dt from .const import ( @@ -944,11 +945,11 @@ class XiaomiGatewayLight(LightEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info of the gateway.""" - return { - "identifiers": {(DOMAIN, self._gateway_device_id)}, - } + return DeviceInfo( + identifiers={(DOMAIN, self._gateway_device_id)}, + ) @property def name(self): From 16a3a9170e92a10408694c4daca365cb33a8444d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 00:38:27 +0200 Subject: [PATCH 0069/1452] Use DeviceInfo in yale-smart-alarm (#58644) --- .../yale_smart_alarm/alarm_control_panel.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index ae5596ee2e1..4011e7dfbdc 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -12,15 +12,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_HOME, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -96,12 +88,12 @@ class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" - return { - ATTR_NAME: str(self.name), - ATTR_MANUFACTURER: MANUFACTURER, - ATTR_MODEL: MODEL, - ATTR_IDENTIFIERS: {(DOMAIN, self._identifier)}, - } + return DeviceInfo( + identifiers={(DOMAIN, self._identifier)}, + manufacturer=MANUFACTURER, + model=MODEL, + name=str(self.name), + ) @property def state(self): From 991c41532a7e2f07bb130092b397584b055c1490 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 00:46:01 +0200 Subject: [PATCH 0070/1452] Use DeviceInfo in youless (#58645) --- homeassistant/components/youless/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 16ed918914d..355d4f83127 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -71,12 +72,12 @@ class YoulessBaseSensor(CoordinatorEntity, SensorEntity): self._sensor_id = sensor_id self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}" - self._attr_device_info = { - "identifiers": {(DOMAIN, f"{device}_{device_group}")}, - "name": friendly_name, - "manufacturer": "YouLess", - "model": self.coordinator.data.model, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{device}_{device_group}")}, + manufacturer="YouLess", + model=self.coordinator.data.model, + name=friendly_name, + ) @property def get_sensor(self) -> YoulessSensor | None: From 147bbb30a628a78e63212bf56c8f03069728c896 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 01:17:40 +0200 Subject: [PATCH 0071/1452] Use DeviceInfo in zerproc (#58647) --- homeassistant/components/zerproc/light.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 3f0136f9b0d..bc9b3cae410 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util @@ -117,13 +118,13 @@ class ZerprocLight(LightEntity): return self._light.address @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for this light.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Zerproc", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Zerproc", + name=self.name, + ) @property def icon(self) -> str | None: From 34a6ce6f2a1f86f4a4f7d94d5fe29ab12884a3d2 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Thu, 28 Oct 2021 19:58:59 -0400 Subject: [PATCH 0072/1452] Bump greeclimate to 0.12.3 (#58635) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index accd02dbe79..62d5bec6bb8 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.2"], + "requirements": ["greeclimate==0.12.3"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 79515f9abec..085403bf9e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.2 +greeclimate==0.12.3 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 091a8cebbbc..edaaa08dc27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -458,7 +458,7 @@ google-nest-sdm==0.3.8 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.2 +greeclimate==0.12.3 # homeassistant.components.growatt_server growattServer==1.1.0 From e10bd39827d8cd98111c031451fb40069556f7f5 Mon Sep 17 00:00:00 2001 From: schreyack Date: Thu, 28 Oct 2021 16:59:50 -0700 Subject: [PATCH 0073/1452] Add a Preset mode for Honeywell permanent hold (#58060) Co-authored-by: J. Nick Koston --- homeassistant/components/honeywell/climate.py | 34 +++++++++++++++++-- .../components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 7c40a0bb684..d2766515595 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -57,6 +57,8 @@ ATTR_FAN_ACTION = "fan_action" ATTR_PERMANENT_HOLD = "permanent_hold" +PRESET_HOLD = "Hold" + PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_REGION), PLATFORM_SCHEMA.extend( @@ -161,7 +163,7 @@ class HoneywellUSThermostat(ClimateEntity): self._attr_temperature_unit = ( TEMP_CELSIUS if device.temperature_unit == "C" else TEMP_FAHRENHEIT ) - self._attr_preset_modes = [PRESET_NONE, PRESET_AWAY] + self._attr_preset_modes = [PRESET_NONE, PRESET_AWAY, PRESET_HOLD] self._attr_is_aux_heat = device.system_mode == "emheat" # not all honeywell HVACs support all modes @@ -268,7 +270,12 @@ class HoneywellUSThermostat(ClimateEntity): @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" - return PRESET_AWAY if self._away else None + if self._away: + return PRESET_AWAY + if self._is_permanent_hold(): + return PRESET_HOLD + + return None @property def fan_mode(self) -> str | None: @@ -353,8 +360,26 @@ class HoneywellUSThermostat(ClimateEntity): "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") ) + def _turn_hold_mode_on(self) -> None: + """Turn permanent hold on.""" + try: + # Get current mode + mode = self._device.system_mode + except somecomfort.SomeComfortError: + _LOGGER.error("Can not get system mode") + return + # Check that we got a valid mode back + if mode in HW_MODE_TO_HVAC_MODE: + try: + # Set permanent hold + setattr(self._device, f"hold_{mode}", True) + except somecomfort.SomeComfortError: + _LOGGER.error("Couldn't set permanent hold") + else: + _LOGGER.error("Invalid system mode returned: %s", mode) + def _turn_away_mode_off(self) -> None: - """Turn away off.""" + """Turn away/hold off.""" self._away = False try: # Disabling all hold modes @@ -367,6 +392,9 @@ class HoneywellUSThermostat(ClimateEntity): """Set new preset mode.""" if preset_mode == PRESET_AWAY: self._turn_away_mode_on() + elif preset_mode == PRESET_HOLD: + self._away = False + self._turn_hold_mode_on() else: self._turn_away_mode_off() diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index a308a704c74..9bf4932a953 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["somecomfort==0.7.0"], + "requirements": ["somecomfort==0.8.0"], "codeowners": ["@rdfurman"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 085403bf9e4..df931fc82a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2193,7 +2193,7 @@ solaredge==0.0.2 solax==0.2.8 # homeassistant.components.honeywell -somecomfort==0.7.0 +somecomfort==0.8.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edaaa08dc27..81ab8d0a475 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ soco==0.24.0 solaredge==0.0.2 # homeassistant.components.honeywell -somecomfort==0.7.0 +somecomfort==0.8.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 From c8cbd0070dbd236e3f5463daed5980c32fcb95e6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Oct 2021 02:00:12 +0200 Subject: [PATCH 0074/1452] Use DeviceInfo in zwave-js (#58649) --- homeassistant/components/zwave_js/entity.py | 8 ++++---- homeassistant/components/zwave_js/sensor.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index aa915cd5822..cf15f32932b 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -10,7 +10,7 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -54,9 +54,9 @@ class ZWaveBaseEntity(Entity): ) self._attr_assumed_state = self.info.assumed_state # device is precreated in main handler - self._attr_device_info = { - "identifiers": {get_device_id(self.client, self.info.node)}, - } + self._attr_device_info = DeviceInfo( + identifiers={get_device_id(self.client, self.info.node)}, + ) @callback def on_value_update(self) -> None: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 715affe351e..a3d06a21f89 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -47,6 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -476,9 +477,9 @@ class ZWaveNodeStatusSensor(SensorEntity): f"{self.client.driver.controller.home_id}.{node.node_id}.node_status" ) # device is precreated in main handler - self._attr_device_info = { - "identifiers": {get_device_id(self.client, self.node)}, - } + self._attr_device_info = DeviceInfo( + identifiers={get_device_id(self.client, self.node)}, + ) self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: From 4d5705c0fe927b2a2be4c32fa0b236609d8b98f3 Mon Sep 17 00:00:00 2001 From: Eddy G Date: Thu, 28 Oct 2021 17:00:31 -0700 Subject: [PATCH 0075/1452] Add 'delta_values' option to utility_meter (#54964) --- .../components/utility_meter/__init__.py | 2 + .../components/utility_meter/const.py | 1 + .../components/utility_meter/sensor.py | 16 +++++- tests/components/utility_meter/test_sensor.py | 57 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index c91bbcb7e57..bdb88d05240 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -17,6 +17,7 @@ from .const import ( ATTR_TARIFF, CONF_CRON_PATTERN, CONF_METER, + CONF_METER_DELTA_VALUES, CONF_METER_NET_CONSUMPTION, CONF_METER_OFFSET, CONF_METER_TYPE, @@ -84,6 +85,7 @@ METER_CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_METER_OFFSET, default=DEFAULT_OFFSET): vol.All( cv.time_period, cv.positive_timedelta, max_28_days ), + vol.Optional(CONF_METER_DELTA_VALUES, default=False): cv.boolean, vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_TARIFFS, default=[]): vol.All( cv.ensure_list, [cv.string] diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 3e127e4a643..097496e231d 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -28,6 +28,7 @@ CONF_METER = "meter" CONF_SOURCE_SENSOR = "source" CONF_METER_TYPE = "cycle" CONF_METER_OFFSET = "offset" +CONF_METER_DELTA_VALUES = "delta_values" CONF_METER_NET_CONSUMPTION = "net_consumption" CONF_PAUSED = "paused" CONF_TARIFFS = "tariffs" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ec553cce58a..64cde15aa4f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -38,6 +38,7 @@ from .const import ( BIMONTHLY, CONF_CRON_PATTERN, CONF_METER, + CONF_METER_DELTA_VALUES, CONF_METER_NET_CONSUMPTION, CONF_METER_OFFSET, CONF_METER_TYPE, @@ -100,6 +101,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf_meter_source = hass.data[DATA_UTILITY][meter][CONF_SOURCE_SENSOR] conf_meter_type = hass.data[DATA_UTILITY][meter].get(CONF_METER_TYPE) conf_meter_offset = hass.data[DATA_UTILITY][meter][CONF_METER_OFFSET] + conf_meter_delta_values = hass.data[DATA_UTILITY][meter][ + CONF_METER_DELTA_VALUES + ] conf_meter_net_consumption = hass.data[DATA_UTILITY][meter][ CONF_METER_NET_CONSUMPTION ] @@ -113,6 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf.get(CONF_NAME), conf_meter_type, conf_meter_offset, + conf_meter_delta_values, conf_meter_net_consumption, conf.get(CONF_TARIFF), conf_meter_tariff_entity, @@ -143,6 +148,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): name, meter_type, meter_offset, + delta_values, net_consumption, tariff=None, tariff_entity=None, @@ -171,6 +177,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): _LOGGER.debug("CRON pattern: %s", self._cron_pattern) else: self._cron_pattern = cron_pattern + self._sensor_delta_values = delta_values self._sensor_net_consumption = net_consumption self._tariff = tariff self._tariff_entity = tariff_entity @@ -206,12 +213,15 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: - diff = Decimal(new_state.state) - Decimal(old_state.state) + if self._sensor_delta_values: + adjustment = Decimal(new_state.state) + else: + adjustment = Decimal(new_state.state) - Decimal(old_state.state) - if (not self._sensor_net_consumption) and diff < 0: + if (not self._sensor_net_consumption) and adjustment < 0: # Source sensor just rolled over for unknown reasons, return - self._state += diff + self._state += adjustment except ValueError as err: _LOGGER.warning("While processing state changes: %s", err) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index a41ddcfa9fc..8d9b819f610 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -414,6 +414,63 @@ async def test_non_net_consumption(hass): assert state.state == "0" +async def test_delta_values(hass): + """Test utility meter "delta_values" mode.""" + config = { + "utility_meter": { + "energy_bill": {"source": "sensor.energy", "delta_values": True} + } + } + + now = dt_util.utcnow() + with alter_time(now): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]["energy_bill"]["source"] + + async_fire_time_changed(hass, now) + hass.states.async_set( + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state.attributes.get("status") == PAUSED + + now += timedelta(seconds=30) + with alter_time(now): + async_fire_time_changed(hass, now) + hass.states.async_set( + entity_id, + 3, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + force_update=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state.attributes.get("status") == COLLECTING + + now += timedelta(seconds=30) + with alter_time(now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + hass.states.async_set( + entity_id, + 6, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + force_update=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state is not None + + assert state.state == "9" + + def gen_config(cycle, offset=None): """Generate configuration.""" config = { From 1b7253611919215f4e55d5626eccd8d55544ab41 Mon Sep 17 00:00:00 2001 From: John Parchem Date: Thu, 28 Oct 2021 19:03:27 -0500 Subject: [PATCH 0076/1452] Add support for Levoit Core 400S air purifier to VeSync integration (#57126) --- homeassistant/components/vesync/fan.py | 2 ++ homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index c32ac6d2a25..ce7f833c264 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -19,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", "Core200S": "fan", + "Core400S": "fan", } FAN_MODE_AUTO = "auto" @@ -27,6 +28,7 @@ FAN_MODE_SLEEP = "sleep" PRESET_MODES = { "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], "Core200S": [FAN_MODE_SLEEP], + "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], } SPEED_RANGE = (1, 3) # off is not included diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 70c46d0f02e..cceb0157286 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==1.4.0"], + "requirements": ["pyvesync==1.4.1"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index df931fc82a2..230d901957b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1989,7 +1989,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==1.4.0 +pyvesync==1.4.1 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81ab8d0a475..045c5bff5ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ pyuptimerobot==21.9.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==1.4.0 +pyvesync==1.4.1 # homeassistant.components.vizio pyvizio==0.1.57 From 7d408e32887a260b7c92f1d8c23c8ac757bdaff1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 29 Oct 2021 00:30:22 +0000 Subject: [PATCH 0077/1452] [ci skip] Translation update --- .../aurora_abb_powerone/translations/it.json | 23 +++++++++++++++ .../binary_sensor/translations/hu.json | 2 +- .../binary_sensor/translations/it.json | 21 ++++++++++++++ .../binary_sensor/translations/zh-Hant.json | 13 +++++++++ .../devolo_home_network/translations/en.json | 4 +-- .../components/dlna_dmr/translations/it.json | 19 +++++++++--- .../components/dlna_dmr/translations/no.json | 4 +-- .../components/netatmo/translations/it.json | 5 ++++ .../netatmo/translations/zh-Hant.json | 5 ++++ .../components/octoprint/translations/it.json | 29 +++++++++++++++++++ .../components/sense/translations/ca.json | 3 +- .../components/sense/translations/de.json | 3 +- .../components/sense/translations/et.json | 3 +- .../components/sense/translations/it.json | 3 +- .../components/sense/translations/ru.json | 3 +- .../sense/translations/zh-Hant.json | 3 +- .../tuya/translations/select.it.json | 27 +++++++++++++++++ .../tuya/translations/sensor.it.json | 15 ++++++++++ .../components/venstar/translations/it.json | 23 +++++++++++++++ .../components/watttime/translations/it.json | 10 +++++++ .../components/yeelight/translations/it.json | 2 +- .../components/yeelight/translations/no.json | 2 +- .../yeelight/translations/zh-Hant.json | 2 +- 23 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/aurora_abb_powerone/translations/it.json create mode 100644 homeassistant/components/octoprint/translations/it.json create mode 100644 homeassistant/components/tuya/translations/sensor.it.json create mode 100644 homeassistant/components/venstar/translations/it.json diff --git a/homeassistant/components/aurora_abb_powerone/translations/it.json b/homeassistant/components/aurora_abb_powerone/translations/it.json new file mode 100644 index 00000000000..a16c655d282 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_serial_ports": "Nessuna porta COM trovata. Serve un dispositivo RS485 valido per comunicare." + }, + "error": { + "cannot_connect": "Impossibile connettersi, controllare la porta seriale, l'indirizzo, la connessione elettrica e che l'inverter sia acceso (alla luce del giorno)", + "cannot_open_serial_port": "Impossibile aprire la porta seriale, controllare e riprovare", + "invalid_serial_port": "La porta seriale non \u00e8 un dispositivo valido o non pu\u00f2 essere aperta", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "address": "Indirizzo dell'inverter", + "port": "Porta adattatore RS485 o USB-RS485" + }, + "description": "L'inverter deve essere collegato tramite un adattatore RS485, selezionare la porta seriale e l'indirizzo dell'inverter come configurato sul pannello LCD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 016b0d71072..876cdb8b2e4 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -102,7 +102,7 @@ } }, "device_class": { - "cold": "hideg", + "cold": "h\u0171t\u00e9s", "gas": "g\u00e1z", "heat": "f\u0171t\u00e9s", "moisture": "nedvess\u00e9g", diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index ef16af64af7..f0de143b244 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} \u00e8 collegato", "is_not_powered": "{entity_name} non \u00e8 alimentato", "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_running": "{entity_name} non \u00e8 in funzionamento", "is_not_tampered": "{entity_name} non rileva manomissioni", "is_not_unsafe": "{entity_name} \u00e8 sicuro", "is_occupied": "{entity_name} \u00e8 occupato", @@ -41,6 +42,7 @@ "is_powered": "{entity_name} \u00e8 alimentato", "is_present": "{entity_name} \u00e8 presente", "is_problem": "{entity_name} sta rilevando un problema", + "is_running": "{entity_name} \u00e8 in funzionamento", "is_smoke": "{entity_name} sta rilevando il fumo", "is_sound": "{entity_name} sta rilevando il suono", "is_tampered": "{entity_name} rileva manomissioni", @@ -81,6 +83,7 @@ "not_plugged_in": "{entity_name} \u00e8 scollegato", "not_powered": "{entity_name} non \u00e8 alimentato", "not_present": "{entity_name} non \u00e8 presente", + "not_running": "{entity_name} non \u00e8 pi\u00f9 in funzione", "not_unsafe": "{entity_name} \u00e8 diventato sicuro", "occupied": "{entity_name} \u00e8 diventato occupato", "opened": "{entity_name} \u00e8 aperto", @@ -88,6 +91,7 @@ "powered": "{entity_name} \u00e8 alimentato", "present": "{entity_name} \u00e8 presente", "problem": "{entity_name} ha iniziato a rilevare un problema", + "running": "{entity_name} ha iniziato a funzionare", "smoke": "{entity_name} ha iniziato la rilevazione di fumo", "sound": "{entity_name} ha iniziato il rilevamento del suono", "turned_off": "{entity_name} disattivato", @@ -97,6 +101,19 @@ "vibration": "{entity_name} iniziato a rilevare le vibrazioni" } }, + "device_class": { + "cold": "freddo", + "gas": "gas", + "heat": "caldo", + "moisture": "umidit\u00e0", + "motion": "movimento", + "occupancy": "occupazione", + "power": "potenza", + "problem": "problema", + "smoke": "fumo", + "sound": "suono", + "vibration": "vibrazione" + }, "state": { "_": { "off": "Spento", @@ -174,6 +191,10 @@ "off": "OK", "on": "Problema" }, + "running": { + "off": "Non in esecuzione", + "on": "In esecuzione" + }, "safety": { "off": "Sicuro", "on": "Non Sicuro" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index 5f27ce7319a..d9705225361 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -101,6 +101,19 @@ "vibration": "{entity_name}\u5df2\u5075\u6e2c\u5230\u9707\u52d5" } }, + "device_class": { + "cold": "\u51b7", + "gas": "\u6c23\u9ad4", + "heat": "\u71b1", + "moisture": "\u6fd5\u6c23", + "motion": "\u52d5\u4f5c", + "occupancy": "\u4f54\u7a7a", + "power": "\u96fb\u529b", + "problem": "\u7570\u5e38", + "smoke": "\u7159\u9727", + "sound": "\u8072\u97f3", + "vibration": "\u9707\u52d5" + }, "state": { "_": { "off": "\u95dc\u9589", diff --git a/homeassistant/components/devolo_home_network/translations/en.json b/homeassistant/components/devolo_home_network/translations/en.json index 52e51d953c1..39c0b6d331f 100644 --- a/homeassistant/components/devolo_home_network/translations/en.json +++ b/homeassistant/components/devolo_home_network/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "home_control": "The devolo Home Control Central Unit does not work with this integration." }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "flow_title": "{product} ({name})", diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json index 0ab40e3c804..545d3cadbcb 100644 --- a/homeassistant/components/dlna_dmr/translations/it.json +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -2,15 +2,18 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "alternative_integration": "Il dispositivo \u00e8 meglio supportato da un'altra integrazione", + "cannot_connect": "Impossibile connettersi", "could_not_connect": "Impossibile connettersi al dispositivo DLNA", "discovery_error": "Impossibile individuare un dispositivo DLNA corrispondente", "incomplete_config": "Nella configurazione manca una variabile richiesta", "non_unique_id": "Pi\u00f9 dispositivi trovati con lo stesso ID univoco", - "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer" + "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer supportato" }, "error": { + "cannot_connect": "Impossibile connettersi", "could_not_connect": "Impossibile connettersi al dispositivo DLNA", - "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer" + "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer supportato" }, "flow_title": "{name}", "step": { @@ -20,12 +23,20 @@ "import_turn_on": { "description": "Accendi il dispositivo e fai clic su Invia per continuare la migrazione" }, - "user": { + "manual": { "data": { "url": "URL" }, "description": "URL di un file XML di descrizione del dispositivo", - "title": "DLNA Digital Media Renderer" + "title": "Connessione manuale del dispositivo DLNA DMR" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "Scegli un dispositivo da configurare o lascia vuoto per inserire un URL", + "title": "Rilevati dispositivi DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index 3b0f5854aca..a1ce1fdce32 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -8,12 +8,12 @@ "discovery_error": "Kunne ikke finne en matchende DLNA -enhet", "incomplete_config": "Konfigurasjonen mangler en n\u00f8dvendig variabel", "non_unique_id": "Flere enheter ble funnet med samme unike ID", - "not_dmr": "Enheten er ikke en Digital Media Renderer" + "not_dmr": "Enheten er ikke en st\u00f8ttet Digital Media Renderer" }, "error": { "cannot_connect": "Tilkobling mislyktes", "could_not_connect": "Kunne ikke koble til DLNA -enhet", - "not_dmr": "Enheten er ikke en Digital Media Renderer" + "not_dmr": "Enheten er ikke en st\u00f8ttet Digital Media Renderer" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index 152f7d47597..3f9e7df3ad6 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione Netatmo deve riautenticare il tuo account", + "title": "Autenticare nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index c89f91d1d91..f8d181be5d3 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Netatmo \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } }, diff --git a/homeassistant/components/octoprint/translations/it.json b/homeassistant/components/octoprint/translations/it.json new file mode 100644 index 00000000000..084307b6323 --- /dev/null +++ b/homeassistant/components/octoprint/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "auth_failed": "Impossibile recuperare la chiave API dell'applicazione", + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "Stampante OctoPrint: {host}", + "progress": { + "get_api_key": "Apri l'interfaccia utente di OctoPrint e fai clic su \"Consenti\" nella richiesta di accesso per \"Home Assistant\"." + }, + "step": { + "user": { + "data": { + "host": "Host", + "path": "Percorso dell'applicazione", + "port": "Numero porta", + "ssl": "Utilizzare SSL", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/ca.json b/homeassistant/components/sense/translations/ca.json index 096b4419dae..aff80de710d 100644 --- a/homeassistant/components/sense/translations/ca.json +++ b/homeassistant/components/sense/translations/ca.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Correu electr\u00f2nic", - "password": "Contrasenya" + "password": "Contrasenya", + "timeout": "Temps d'espera" }, "title": "Connexi\u00f3 amb Sense Energy Monitor" } diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index df36684c8b4..d0290abdf98 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-Mail", - "password": "Passwort" + "password": "Passwort", + "timeout": "Zeit\u00fcberschreitung" }, "title": "Stelle eine Verbindung zu deinem Sense Energy Monitor her" } diff --git a/homeassistant/components/sense/translations/et.json b/homeassistant/components/sense/translations/et.json index aec4f3655a5..8438be5c677 100644 --- a/homeassistant/components/sense/translations/et.json +++ b/homeassistant/components/sense/translations/et.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-post", - "password": "Salas\u00f5na" + "password": "Salas\u00f5na", + "timeout": "Ajal\u00f5pp" }, "title": "\u00dchendu oma Sense Energy Monitor'iga" } diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json index 277e2e1539b..2ab80941a6a 100644 --- a/homeassistant/components/sense/translations/it.json +++ b/homeassistant/components/sense/translations/it.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-mail", - "password": "Password" + "password": "Password", + "timeout": "Tempo scaduto" }, "title": "Connettiti al tuo Sense Energy Monitor" } diff --git a/homeassistant/components/sense/translations/ru.json b/homeassistant/components/sense/translations/ru.json index 0bb299e2208..c113c06a021 100644 --- a/homeassistant/components/sense/translations/ru.json +++ b/homeassistant/components/sense/translations/ru.json @@ -12,7 +12,8 @@ "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" + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442" }, "title": "Sense Energy Monitor" } diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index c97983c0b03..5ca9a9f847d 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", - "password": "\u5bc6\u78bc" + "password": "\u5bc6\u78bc", + "timeout": "\u903e\u6642" }, "title": "\u9023\u7dda\u81f3 Sense \u80fd\u6e90\u76e3\u63a7" } diff --git a/homeassistant/components/tuya/translations/select.it.json b/homeassistant/components/tuya/translations/select.it.json index a7bed12090c..74410c0a593 100644 --- a/homeassistant/components/tuya/translations/select.it.json +++ b/homeassistant/components/tuya/translations/select.it.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Disabilitato", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatico", + "1": "Spento", + "2": "Acceso" + }, + "tuya__decibel_sensitivity": { + "0": "Bassa sensibilit\u00e0", + "1": "Alta sensibilit\u00e0" + }, + "tuya__ipc_work_mode": { + "0": "Modalit\u00e0 a basso consumo", + "1": "Modalit\u00e0 di lavoro continua" + }, "tuya__led_type": { "halogen": "Alogena", "incandescent": "Incandescenza", @@ -10,6 +28,15 @@ "pos": "Indica la posizione dell'interruttore", "relay": "Indica lo stato di accensione/spegnimento dell'interruttore" }, + "tuya__motion_sensitivity": { + "0": "Bassa sensibilit\u00e0", + "1": "Sensibilit\u00e0 media", + "2": "Alta sensibilit\u00e0" + }, + "tuya__record_mode": { + "1": "Registra solo gli eventi", + "2": "Registrazione continua" + }, "tuya__relay_status": { "last": "Ricorda l'ultimo stato", "memory": "Ricorda l'ultimo stato", diff --git a/homeassistant/components/tuya/translations/sensor.it.json b/homeassistant/components/tuya/translations/sensor.it.json new file mode 100644 index 00000000000..a7b7bb272dd --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.it.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Temperatura di ebollizione", + "cooling": "Raffreddamento", + "heating": "Riscaldamento", + "heating_temp": "Temperatura di riscaldamento", + "reserve_1": "Riserva 1", + "reserve_2": "Riserva 2", + "reserve_3": "Riserva 3", + "standby": "Pausa", + "warm": "Conservazione del calore" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/it.json b/homeassistant/components/venstar/translations/it.json new file mode 100644 index 00000000000..66b7fac78bd --- /dev/null +++ b/homeassistant/components/venstar/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "pin": "Codice PIN", + "ssl": "Utilizza un certificato SSL", + "username": "Nome utente" + }, + "title": "Collegati al termostato Venstar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/it.json b/homeassistant/components/watttime/translations/it.json index 4be720042f1..ecca75e5b5d 100644 --- a/homeassistant/components/watttime/translations/it.json +++ b/homeassistant/components/watttime/translations/it.json @@ -38,5 +38,15 @@ "description": "Inserisci il tuo nome utente e password:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra la posizione monitorata sulla mappa" + }, + "title": "Configura WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index 4036b6d6338..ce34523bb61 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Modello (opzionale)", + "model": "Modello", "nightlight_switch": "Usa l'interruttore luce notturna", "save_on_change": "Salva stato su modifica", "transition": "Tempo di transizione (ms)", diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json index 6814ec518cb..ea4436d7769 100644 --- a/homeassistant/components/yeelight/translations/no.json +++ b/homeassistant/components/yeelight/translations/no.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Modell (valgfritt)", + "model": "Modell", "nightlight_switch": "Bruk nattlysbryter", "save_on_change": "Lagre status ved endring", "transition": "Overgangstid (ms)", diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index c0c83c213b0..785584a6f2a 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "\u578b\u865f\uff08\u9078\u9805\uff09", + "model": "\u578b\u865f", "nightlight_switch": "\u4f7f\u7528\u591c\u71c8\u958b\u95dc", "save_on_change": "\u65bc\u8b8a\u66f4\u6642\u5132\u5b58\u72c0\u614b", "transition": "\u8f49\u63db\u6642\u9593\uff08\u6beb\u79d2\uff09", From 6d30105c9fec91d5bbcce15a17bec652520a07e6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 29 Oct 2021 17:04:00 +1300 Subject: [PATCH 0078/1452] Add configuration_url to ESPHome (#58565) --- homeassistant/components/esphome/__init__.py | 4 ++++ homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index f985394c8e9..8965e5b9960 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -328,9 +328,13 @@ def _async_setup_device_registry( sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" + configuration_url = None + if device_info.webserver_port > 0: + configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, + configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, manufacturer="espressif", diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 4b54a4d7883..247c78abb92 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.1.0"], + "requirements": ["aioesphomeapi==10.2.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 230d901957b..e102cea4f0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.1.0 +aioesphomeapi==10.2.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 045c5bff5ce..c6eed5d97a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.1.0 +aioesphomeapi==10.2.0 # homeassistant.components.flo aioflo==0.4.1 From 9c5a79c641cc4bca216f359cb32e2238a0d2fcc0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 28 Oct 2021 21:07:29 -0700 Subject: [PATCH 0079/1452] Add an image placeholder for Nest WebRTC cameras (#58250) --- homeassistant/components/nest/camera_sdm.py | 44 ++++++++++++++++++++- tests/components/nest/test_camera_sdm.py | 14 ++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 99234c3de8a..abebc8db3ef 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import Callable import datetime +import io import logging from typing import Any +from PIL import Image, ImageDraw, ImageFilter from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, @@ -38,6 +40,15 @@ _LOGGER = logging.getLogger(__name__) # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) +# The Google Home app dispays a placeholder image that appears as a faint +# light source (dim, blurred sphere) giving the user an indication the camera +# is available, not just a blank screen. These constants define a blurred +# ellipse at the top left of the thumbnail. +PLACEHOLDER_ELLIPSE_BLUR = 0.1 +PLACEHOLDER_ELLIPSE_XY = [-0.4, 0.3, 0.3, 0.4] +PLACEHOLDER_OVERLAY_COLOR = "#ffffff" +PLACEHOLDER_ELLIPSE_OPACITY = 255 + async def async_setup_sdm_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -62,6 +73,30 @@ async def async_setup_sdm_entry( async_add_entities(entities) +def placeholder_image(width: int | None = None, height: int | None = None) -> Image: + """Return a camera image preview for cameras without live thumbnails.""" + if not width or not height: + return Image.new("RGB", (1, 1)) + # Draw a dark scene with a fake light source + blank = Image.new("RGB", (width, height)) + overlay = Image.new("RGB", blank.size, color=PLACEHOLDER_OVERLAY_COLOR) + ellipse = Image.new("L", blank.size, color=0) + draw = ImageDraw.Draw(ellipse) + draw.ellipse( + ( + width * PLACEHOLDER_ELLIPSE_XY[0], + height * PLACEHOLDER_ELLIPSE_XY[1], + width * PLACEHOLDER_ELLIPSE_XY[2], + height * PLACEHOLDER_ELLIPSE_XY[3], + ), + fill=PLACEHOLDER_ELLIPSE_OPACITY, + ) + mask = ellipse.filter( + ImageFilter.GaussianBlur(radius=width * PLACEHOLDER_ELLIPSE_BLUR) + ) + return Image.composite(overlay, blank, mask) + + class NestCamera(Camera): """Devices that support cameras.""" @@ -212,7 +247,14 @@ class NestCamera(Camera): # Fetch still image from the live stream stream_url = await self.stream_source() if not stream_url: - return None + if self.frontend_stream_type != STREAM_TYPE_WEB_RTC: + return None + # Nest Web RTC cams only have image previews for events, and not + # for "now" by design to save batter, and need a placeholder. + image = placeholder_image(width=width, height=height) + with io.BytesIO() as content: + image.save(content, format="JPEG", optimize=True) + return content.getvalue() return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) async def _async_active_event_image(self) -> bytes | None: diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index b7637bf3e2e..1ac1b4ca6f9 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -135,7 +135,7 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -async def async_get_image(hass): +async def async_get_image(hass, width=None, height=None): """Get image from the camera, a wrapper around camera.async_get_image.""" # Note: this patches ImageFrame to simulate decoding an image from a live # stream, however the test may not use it. Tests assert on the image @@ -145,7 +145,9 @@ async def async_get_image(hass): autopatch=True, return_value=IMAGE_BYTES_FROM_STREAM, ): - return await camera.async_get_image(hass, "camera.my_camera") + return await camera.async_get_image( + hass, "camera.my_camera", width=width, height=height + ) async def test_no_devices(hass): @@ -721,9 +723,11 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): assert msg["success"] assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" - # Nest WebRTC cameras do not support a still image - with pytest.raises(HomeAssistantError): - await async_get_image(hass) + # Nest WebRTC cameras return a placeholder + content = await async_get_image(hass) + assert content.content_type == "image/jpeg" + content = await async_get_image(hass, width=1024, height=768) + assert content.content_type == "image/jpeg" async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): From c6157d55208017a03cc209e950e51776ac146be8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 06:08:59 +0200 Subject: [PATCH 0080/1452] Migrate Tuya unique IDs for switches & lights (#58631) --- homeassistant/components/tuya/__init__.py | 85 ++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index bcc5c9dc79c..4f34d3c31bf 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -14,9 +14,11 @@ from tuya_iot import ( TuyaOpenMQ, ) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( @@ -33,6 +35,7 @@ from .const import ( PLATFORMS, TUYA_DISCOVERY_NEW, TUYA_HA_SIGNAL_UPDATE_ENTITY, + DPCode, ) _LOGGER = logging.getLogger(__name__) @@ -115,6 +118,9 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(home_manager.update_device_cache) await cleanup_device_registry(hass, device_manager) + # Migrate old unique_ids to the new format + async_migrate_entities_unique_ids(hass, entry, device_manager) + # Register known device IDs device_registry = dr.async_get(hass) for device in device_manager.device_map.values(): @@ -143,6 +149,83 @@ async def cleanup_device_registry( break +@callback +def async_migrate_entities_unique_ids( + hass: HomeAssistant, config_entry: ConfigEntry, device_manager: TuyaDeviceManager +) -> None: + """Migrate unique_ids in the entity registry to the new format.""" + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + light_entries = { + entry.unique_id: entry + for entry in registry_entries + if entry.domain == LIGHT_DOMAIN + } + switch_entries = { + entry.unique_id: entry + for entry in registry_entries + if entry.domain == SWITCH_DOMAIN + } + + for device in device_manager.device_map.values(): + # Old lights where in `tuya.{device_id}` format, now the DPCode is added. + # + # If the device is a previously supported light category and still has + # the old format for the unique ID, migrate it to the new format. + # + # Previously only devices providing the SWITCH_LED DPCode were supported, + # thus this can be added to those existing IDs. + # + # `tuya.{device_id}` -> `tuya.{device_id}{SWITCH_LED}` + if ( + device.category in ("dc", "dd", "dj", "fs", "fwl", "jsq", "xdd", "xxj") + and (entry := light_entries.get(f"tuya.{device.id}")) + and f"tuya.{device.id}{DPCode.SWITCH_LED}" not in light_entries + ): + entity_registry.async_update_entity( + entry.entity_id, new_unique_id=f"tuya.{device.id}{DPCode.SWITCH_LED}" + ) + + # Old switches has different formats for the unique ID, but is mappable. + # + # If the device is a previously supported switch category and still has + # the old format for the unique ID, migrate it to the new format. + # + # `tuya.{device_id}` -> `tuya.{device_id}{SWITCH}` + # `tuya.{device_id}_1` -> `tuya.{device_id}{SWITCH_1}` + # ... + # `tuya.{device_id}_6` -> `tuya.{device_id}{SWITCH_6}` + # `tuya.{device_id}_usb1` -> `tuya.{device_id}{SWITCH_USB1}` + # ... + # `tuya.{device_id}_usb6` -> `tuya.{device_id}{SWITCH_USB6}` + # + # In all other cases, the unique ID is not changed. + if device.category in ("bh", "cwysj", "cz", "dlq", "kg", "kj", "pc", "xxj"): + for postfix, dpcode in ( + ("", DPCode.SWITCH), + ("_1", DPCode.SWITCH_1), + ("_2", DPCode.SWITCH_2), + ("_3", DPCode.SWITCH_3), + ("_4", DPCode.SWITCH_4), + ("_5", DPCode.SWITCH_5), + ("_6", DPCode.SWITCH_6), + ("_usb1", DPCode.SWITCH_USB1), + ("_usb2", DPCode.SWITCH_USB2), + ("_usb3", DPCode.SWITCH_USB3), + ("_usb4", DPCode.SWITCH_USB4), + ("_usb5", DPCode.SWITCH_USB5), + ("_usb6", DPCode.SWITCH_USB6), + ): + if ( + entry := switch_entries.get(f"tuya.{device.id}{postfix}") + ) and f"tuya.{device.id}{dpcode}" not in switch_entries: + entity_registry.async_update_entity( + entry.entity_id, new_unique_id=f"tuya.{device.id}{dpcode}" + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From 7516db3600af10c04954b19ffae836c80ac2208c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 23:22:17 -0500 Subject: [PATCH 0081/1452] Avoid circular import in network integration (#58655) --- homeassistant/components/network/__init__.py | 63 ++---------------- homeassistant/components/network/websocket.py | 66 +++++++++++++++++++ 2 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/network/websocket.py diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 024075ba2c1..e8a2c4c80fd 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -4,22 +4,12 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_interface 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, callback from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import util -from .const import ( - ATTR_ADAPTERS, - ATTR_CONFIGURED_ADAPTERS, - DOMAIN, - IPV4_BROADCAST_ADDR, - NETWORK_CONFIG_SCHEMA, -) +from .const import DOMAIN, IPV4_BROADCAST_ADDR from .models import Adapter from .network import Network @@ -107,50 +97,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Adapters: %s", network.adapters) - websocket_api.async_register_command(hass, websocket_network_adapters) - websocket_api.async_register_command(hass, websocket_network_adapters_configure) + # Avoid circular issue: http->network->websocket_api->http + from .websocket import ( # pylint: disable=import-outside-toplevel + async_register_websocket_commands, + ) + + async_register_websocket_commands(hass) return True - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required("type"): "network"}) -@websocket_api.async_response -async def websocket_network_adapters( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, -) -> None: - """Return network preferences.""" - network: Network = hass.data[DOMAIN] - connection.send_result( - msg["id"], - { - ATTR_ADAPTERS: network.adapters, - ATTR_CONFIGURED_ADAPTERS: network.configured_adapters, - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - { - vol.Required("type"): "network/configure", - vol.Required("config", default={}): NETWORK_CONFIG_SCHEMA, - } -) -@websocket_api.async_response -async def websocket_network_adapters_configure( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, -) -> None: - """Update network config.""" - network: Network = hass.data[DOMAIN] - - await network.async_reconfig(msg["config"]) - - connection.send_result( - msg["id"], - {ATTR_CONFIGURED_ADAPTERS: network.configured_adapters}, - ) diff --git a/homeassistant/components/network/websocket.py b/homeassistant/components/network/websocket.py new file mode 100644 index 00000000000..77e01375b75 --- /dev/null +++ b/homeassistant/components/network/websocket.py @@ -0,0 +1,66 @@ +"""The Network Configuration integration websocket commands.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback + +from .const import ( + ATTR_ADAPTERS, + ATTR_CONFIGURED_ADAPTERS, + DOMAIN, + NETWORK_CONFIG_SCHEMA, +) +from .network import Network + + +@callback +def async_register_websocket_commands(hass: HomeAssistant) -> None: + """Register network websocket commands.""" + websocket_api.async_register_command(hass, websocket_network_adapters) + websocket_api.async_register_command(hass, websocket_network_adapters_configure) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "network"}) +@websocket_api.async_response +async def websocket_network_adapters( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, +) -> None: + """Return network preferences.""" + network: Network = hass.data[DOMAIN] + connection.send_result( + msg["id"], + { + ATTR_ADAPTERS: network.adapters, + ATTR_CONFIGURED_ADAPTERS: network.configured_adapters, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "network/configure", + vol.Required("config", default={}): NETWORK_CONFIG_SCHEMA, + } +) +@websocket_api.async_response +async def websocket_network_adapters_configure( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, +) -> None: + """Update network config.""" + network: Network = hass.data[DOMAIN] + + await network.async_reconfig(msg["config"]) + + connection.send_result( + msg["id"], + {ATTR_CONFIGURED_ADAPTERS: network.configured_adapters}, + ) From 3a76d92e0fd47e2f9642ffc72c6df491181e5e07 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 29 Oct 2021 06:28:02 +0200 Subject: [PATCH 0082/1452] Add zwave_js binary sensor descriptions (#58641) --- .../components/zwave_js/binary_sensor.py | 385 +++++++++--------- .../components/zwave_js/test_binary_sensor.py | 10 +- 2 files changed, 199 insertions(+), 196 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 4007064109d..a5883e9bcbf 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -1,8 +1,8 @@ """Representation of Z-Wave binary sensors.""" from __future__ import annotations +from dataclasses import dataclass import logging -from typing import TypedDict from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SOUND, DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -38,186 +39,197 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) -NOTIFICATION_SMOKE_ALARM = 1 -NOTIFICATION_CARBON_MONOOXIDE = 2 -NOTIFICATION_CARBON_DIOXIDE = 3 -NOTIFICATION_HEAT = 4 -NOTIFICATION_WATER = 5 -NOTIFICATION_ACCESS_CONTROL = 6 -NOTIFICATION_HOME_SECURITY = 7 -NOTIFICATION_POWER_MANAGEMENT = 8 -NOTIFICATION_SYSTEM = 9 -NOTIFICATION_EMERGENCY = 10 -NOTIFICATION_CLOCK = 11 -NOTIFICATION_APPLIANCE = 12 -NOTIFICATION_HOME_HEALTH = 13 -NOTIFICATION_SIREN = 14 -NOTIFICATION_WATER_VALVE = 15 -NOTIFICATION_WEATHER = 16 -NOTIFICATION_IRRIGATION = 17 -NOTIFICATION_GAS = 18 +NOTIFICATION_SMOKE_ALARM = "1" +NOTIFICATION_CARBON_MONOOXIDE = "2" +NOTIFICATION_CARBON_DIOXIDE = "3" +NOTIFICATION_HEAT = "4" +NOTIFICATION_WATER = "5" +NOTIFICATION_ACCESS_CONTROL = "6" +NOTIFICATION_HOME_SECURITY = "7" +NOTIFICATION_POWER_MANAGEMENT = "8" +NOTIFICATION_SYSTEM = "9" +NOTIFICATION_EMERGENCY = "10" +NOTIFICATION_CLOCK = "11" +NOTIFICATION_APPLIANCE = "12" +NOTIFICATION_HOME_HEALTH = "13" +NOTIFICATION_SIREN = "14" +NOTIFICATION_WATER_VALVE = "15" +NOTIFICATION_WEATHER = "16" +NOTIFICATION_IRRIGATION = "17" +NOTIFICATION_GAS = "18" -class NotificationSensorMapping(TypedDict, total=False): - """Represent a notification sensor mapping dict type.""" +@dataclass +class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription): + """Represent a Z-Wave JS binary sensor entity description.""" - type: int # required - states: list[str] - device_class: str - enabled: bool + states: tuple[str, ...] | None = None + + +@dataclass +class PropertyZWaveJSMixin: + """Represent the mixin for property sensor descriptions.""" + + on_states: tuple[str, ...] + + +@dataclass +class PropertyZWaveJSEntityDescription( + BinarySensorEntityDescription, PropertyZWaveJSMixin +): + """Represent the entity description for property name sensors.""" # Mappings for Notification sensors # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json -NOTIFICATION_SENSOR_MAPPINGS: list[NotificationSensorMapping] = [ - { +NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = ( + NotificationZWaveJSEntityDescription( # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected - "type": NOTIFICATION_SMOKE_ALARM, - "states": ["1", "2"], - "device_class": DEVICE_CLASS_SMOKE, - }, - { + key=NOTIFICATION_SMOKE_ALARM, + states=("1", "2"), + device_class=DEVICE_CLASS_SMOKE, + ), + NotificationZWaveJSEntityDescription( # NotificationType 1: Smoke Alarm - All other State Id's - "type": NOTIFICATION_SMOKE_ALARM, - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_SMOKE_ALARM, + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 - "type": NOTIFICATION_CARBON_MONOOXIDE, - "states": ["1", "2"], - "device_class": DEVICE_CLASS_GAS, - }, - { + key=NOTIFICATION_CARBON_MONOOXIDE, + states=("1", "2"), + device_class=DEVICE_CLASS_GAS, + ), + NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - All other State Id's - "type": NOTIFICATION_CARBON_MONOOXIDE, - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_CARBON_MONOOXIDE, + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 3: Carbon Dioxide - State Id's 1 and 2 - "type": NOTIFICATION_CARBON_DIOXIDE, - "states": ["1", "2"], - "device_class": DEVICE_CLASS_GAS, - }, - { + key=NOTIFICATION_CARBON_DIOXIDE, + states=("1", "2"), + device_class=DEVICE_CLASS_GAS, + ), + NotificationZWaveJSEntityDescription( # NotificationType 3: Carbon Dioxide - All other State Id's - "type": NOTIFICATION_CARBON_DIOXIDE, - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_CARBON_DIOXIDE, + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat) - "type": NOTIFICATION_HEAT, - "states": ["1", "2", "5", "6"], - "device_class": DEVICE_CLASS_HEAT, - }, - { + key=NOTIFICATION_HEAT, + states=("1", "2", "5", "6"), + device_class=DEVICE_CLASS_HEAT, + ), + NotificationZWaveJSEntityDescription( # NotificationType 4: Heat - All other State Id's - "type": NOTIFICATION_HEAT, - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_HEAT, + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 5: Water - State Id's 1, 2, 3, 4 - "type": NOTIFICATION_WATER, - "states": ["1", "2", "3", "4"], - "device_class": DEVICE_CLASS_MOISTURE, - }, - { + key=NOTIFICATION_WATER, + states=("1", "2", "3", "4"), + device_class=DEVICE_CLASS_MOISTURE, + ), + NotificationZWaveJSEntityDescription( # NotificationType 5: Water - All other State Id's - "type": NOTIFICATION_WATER, - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_WATER, + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock) - "type": NOTIFICATION_ACCESS_CONTROL, - "states": ["1", "2", "3", "4"], - "device_class": DEVICE_CLASS_LOCK, - }, - { + key=NOTIFICATION_ACCESS_CONTROL, + states=("1", "2", "3", "4"), + device_class=DEVICE_CLASS_LOCK, + ), + NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id 16 (door/window open) - "type": NOTIFICATION_ACCESS_CONTROL, - "states": ["22"], - "device_class": DEVICE_CLASS_DOOR, - }, - { + key=NOTIFICATION_ACCESS_CONTROL, + states=("22",), + device_class=DEVICE_CLASS_DOOR, + ), + NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id 17 (door/window closed) - "type": NOTIFICATION_ACCESS_CONTROL, - "states": ["23"], - "enabled": False, - }, - { + key=NOTIFICATION_ACCESS_CONTROL, + states=("23",), + entity_registry_enabled_default=False, + ), + NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) - "type": NOTIFICATION_HOME_SECURITY, - "states": ["1", "2"], - "device_class": DEVICE_CLASS_SAFETY, - }, - { + key=NOTIFICATION_HOME_SECURITY, + states=("1", "2"), + device_class=DEVICE_CLASS_SAFETY, + ), + NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) - "type": NOTIFICATION_HOME_SECURITY, - "states": ["3", "4", "9"], - "device_class": DEVICE_CLASS_SAFETY, - }, - { + key=NOTIFICATION_HOME_SECURITY, + states=("3", "4", "9"), + device_class=DEVICE_CLASS_SAFETY, + ), + NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) - "type": NOTIFICATION_HOME_SECURITY, - "states": ["5", "6"], - "device_class": DEVICE_CLASS_SAFETY, - }, - { + key=NOTIFICATION_HOME_SECURITY, + states=("5", "6"), + device_class=DEVICE_CLASS_SAFETY, + ), + NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 7, 8 (motion) - "type": NOTIFICATION_HOME_SECURITY, - "states": ["7", "8"], - "device_class": DEVICE_CLASS_MOTION, - }, - { + key=NOTIFICATION_HOME_SECURITY, + states=("7", "8"), + device_class=DEVICE_CLASS_MOTION, + ), + NotificationZWaveJSEntityDescription( # NotificationType 9: System - State Id's 1, 2, 6, 7 - "type": NOTIFICATION_SYSTEM, - "states": ["1", "2", "6", "7"], - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_SYSTEM, + states=("1", "2", "6", "7"), + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 10: Emergency - State Id's 1, 2, 3 - "type": NOTIFICATION_EMERGENCY, - "states": ["1", "2", "3"], - "device_class": DEVICE_CLASS_PROBLEM, - }, - { + key=NOTIFICATION_EMERGENCY, + states=("1", "2", "3"), + device_class=DEVICE_CLASS_PROBLEM, + ), + NotificationZWaveJSEntityDescription( # NotificationType 14: Siren - "type": NOTIFICATION_SIREN, - "states": ["1"], - "device_class": DEVICE_CLASS_SOUND, - }, - { + key=NOTIFICATION_SIREN, + states=("1",), + device_class=DEVICE_CLASS_SOUND, + ), + NotificationZWaveJSEntityDescription( # NotificationType 18: Gas - "type": NOTIFICATION_GAS, - "states": ["1", "2", "3", "4"], - "device_class": DEVICE_CLASS_GAS, - }, - { + key=NOTIFICATION_GAS, + states=("1", "2", "3", "4"), + device_class=DEVICE_CLASS_GAS, + ), + NotificationZWaveJSEntityDescription( # NotificationType 18: Gas - "type": NOTIFICATION_GAS, - "states": ["6"], - "device_class": DEVICE_CLASS_PROBLEM, - }, -] - - -class PropertySensorMapping(TypedDict, total=False): - """Represent a property sensor mapping dict type.""" - - property_name: str # required - on_states: list[str] # required - device_class: str - enabled: bool + key=NOTIFICATION_GAS, + states=("6",), + device_class=DEVICE_CLASS_PROBLEM, + ), +) # Mappings for property sensors -PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [ - { - "property_name": DOOR_STATUS_PROPERTY, - "on_states": ["open"], - "device_class": DEVICE_CLASS_DOOR, - "enabled": True, - }, -] +PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { + DOOR_STATUS_PROPERTY: PropertyZWaveJSEntityDescription( + key=DOOR_STATUS_PROPERTY, + on_states=("open",), + device_class=DEVICE_CLASS_DOOR, + ), +} + + +# Mappings for boolean sensors +BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { + CommandClass.BATTERY: BinarySensorEntityDescription( + key=str(CommandClass.BATTERY), + device_class=DEVICE_CLASS_BATTERY, + ), +} async def async_setup_entry( @@ -242,8 +254,14 @@ async def async_setup_entry( entities.append( ZWaveNotificationBinarySensor(config_entry, client, info, state_key) ) - elif info.platform_hint == "property": - entities.append(ZWavePropertyBinarySensor(config_entry, client, info)) + elif info.platform_hint == "property" and ( + description := PROPERTY_SENSOR_MAPPINGS.get( + info.primary_value.property_name + ) + ): + entities.append( + ZWavePropertyBinarySensor(config_entry, client, info, description) + ) else: # boolean sensor entities.append(ZWaveBooleanBinarySensor(config_entry, client, info)) @@ -273,11 +291,10 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): # Entity class attributes self._attr_name = self.generate_name(include_value_name=True) - self._attr_device_class = ( - DEVICE_CLASS_BATTERY - if self.info.primary_value.command_class == CommandClass.BATTERY - else None - ) + if description := BOOLEAN_SENSOR_MAPPINGS.get( + self.info.primary_value.command_class + ): + self.entity_description = description @property def is_on(self) -> bool | None: @@ -301,7 +318,8 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): super().__init__(config_entry, client, info) self.state_key = state_key # check if we have a custom mapping for this value - self._mapping_info = self._get_sensor_mapping() + if description := self._get_sensor_description(): + self.entity_description = description # Entity class attributes self._attr_name = self.generate_name( @@ -309,11 +327,7 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): alternate_value_name=self.info.primary_value.property_name, additional_info=[self.info.primary_value.metadata.states[self.state_key]], ) - self._attr_device_class = self._mapping_info.get("device_class") self._attr_unique_id = f"{self._attr_unique_id}.{self.state_key}" - self._attr_entity_registry_enabled_default = ( - True if not self._mapping_info else self._mapping_info.get("enabled", True) - ) @property def is_on(self) -> bool | None: @@ -323,56 +337,39 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return int(self.info.primary_value.value) == int(self.state_key) @callback - def _get_sensor_mapping(self) -> NotificationSensorMapping: + def _get_sensor_description(self) -> NotificationZWaveJSEntityDescription | None: """Try to get a device specific mapping for this sensor.""" - for mapping in NOTIFICATION_SENSOR_MAPPINGS: + for description in NOTIFICATION_SENSOR_MAPPINGS: if ( - mapping["type"] - != self.info.primary_value.metadata.cc_specific[ + int(description.key) + == self.info.primary_value.metadata.cc_specific[ CC_SPECIFIC_NOTIFICATION_TYPE ] - ): - continue - if not mapping.get("states") or self.state_key in mapping["states"]: - # match found - return mapping - return {} + ) and (not description.states or self.state_key in description.states): + return description + return None class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Representation of a Z-Wave binary_sensor from a property.""" + entity_description: PropertyZWaveJSEntityDescription + def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + description: PropertyZWaveJSEntityDescription, ) -> None: """Initialize a ZWavePropertyBinarySensor entity.""" super().__init__(config_entry, client, info) - # check if we have a custom mapping for this value - self._mapping_info = self._get_sensor_mapping() - - # Entity class attributes + self.entity_description = description self._attr_name = self.generate_name(include_value_name=True) - self._attr_device_class = self._mapping_info.get("device_class") - # We hide some more advanced sensors by default to not overwhelm users - # unless explicitly stated in a mapping, assume deisabled by default - self._attr_entity_registry_enabled_default = self._mapping_info.get( - "enabled", False - ) @property def is_on(self) -> bool | None: """Return if the sensor is on or off.""" if self.info.primary_value.value is None: return None - return self.info.primary_value.value in self._mapping_info["on_states"] - - @callback - def _get_sensor_mapping(self) -> PropertySensorMapping: - """Try to get a device specific mapping for this sensor.""" - mapping_info = PropertySensorMapping() - for mapping in PROPERTY_SENSOR_MAPPINGS: - if mapping["property_name"] == self.info.primary_value.property_name: - mapping_info = mapping.copy() - break - - return mapping_info + return self.info.primary_value.value in self.entity_description.on_states diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 421c808bc0b..1cb91547a0a 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,7 +1,10 @@ """Test the Z-Wave JS binary sensor platform.""" from zwave_js_server.event import Event -from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOTION, +) from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er @@ -93,8 +96,9 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration): node = lock_august_pro state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) - assert state is not None + assert state assert state.state == STATE_OFF + assert state.attributes["device_class"] == DEVICE_CLASS_DOOR # open door event = Event( @@ -116,6 +120,7 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration): ) node.receive_event(event) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state assert state.state == STATE_ON # close door @@ -138,4 +143,5 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration): ) node.receive_event(event) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state assert state.state == STATE_OFF From d1474d8e920e41f2de9f71e5c4413742f9c4a264 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Fri, 29 Oct 2021 03:09:06 -0700 Subject: [PATCH 0083/1452] Update greeneye_monitor sensor state when first connected to a monitor (#58587) --- homeassistant/components/greeneye_monitor/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 7fbfa717229..5904b8652da 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -136,6 +136,7 @@ class GEMSensor(SensorEntity): self._sensor = self._get_sensor(monitor) self._sensor.add_listener(self.async_write_ha_state) + self.async_write_ha_state() return True From b3e7eeb02017b19115fd5b365c04f4be54fea1da Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 13:21:57 +0200 Subject: [PATCH 0084/1452] Fix spelling of OctoPrint (#58686) --- homeassistant/components/octoprint/__init__.py | 2 +- .../components/octoprint/binary_sensor.py | 6 +++--- homeassistant/components/octoprint/sensor.py | 6 +++--- tests/components/octoprint/__init__.py | 4 ++-- tests/components/octoprint/test_binary_sensor.py | 8 ++++---- tests/components/octoprint/test_sensor.py | 14 +++++++------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 7ee6a3169da..eee1ccd2814 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -47,7 +47,7 @@ def ensure_valid_path(value): PLATFORMS = ["binary_sensor", "sensor"] -DEFAULT_NAME = "Octoprint" +DEFAULT_NAME = "OctoPrint" CONF_NUMBER_OF_TOOLS = "number_of_tools" CONF_BED = "bed" diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index fe18af4f808..1adb04d3417 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -53,7 +53,7 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) self._device_id = device_id - self._attr_name = f"Octoprint {sensor_type}" + self._attr_name = f"OctoPrint {sensor_type}" self._attr_unique_id = f"{sensor_type}-{device_id}" @property @@ -61,8 +61,8 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Device info.""" return { "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "Octoprint", - "name": "Octoprint", + "manufacturer": "OctoPrint", + "name": "OctoPrint", } @property diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 3feff099297..5a9614c69b4 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -76,7 +76,7 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) self._device_id = device_id - self._attr_name = f"Octoprint {sensor_type}" + self._attr_name = f"OctoPrint {sensor_type}" self._attr_unique_id = f"{sensor_type}-{device_id}" @property @@ -84,8 +84,8 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Device info.""" return { "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "Octoprint", - "name": "Octoprint", + "manufacturer": "OctoPrint", + "name": "OctoPrint", } diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 4af4a2ea131..5176d2209b1 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -67,12 +67,12 @@ async def init_integration( data={ "host": "1.1.1.1", "api_key": "test-key", - "name": "Octoprint", + "name": "OctoPrint", "port": 81, "ssl": True, "path": "/", }, - title="Octoprint", + title="OctoPrint", ) config_entry.add_to_hass(hass) diff --git a/tests/components/octoprint/test_binary_sensor.py b/tests/components/octoprint/test_binary_sensor.py index 139ed0dc139..55e240eb282 100644 --- a/tests/components/octoprint/test_binary_sensor.py +++ b/tests/components/octoprint/test_binary_sensor.py @@ -21,14 +21,14 @@ async def test_sensors(hass): state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None assert state.state == STATE_ON - assert state.name == "Octoprint Printing" + assert state.name == "OctoPrint Printing" entry = entity_registry.async_get("binary_sensor.octoprint_printing") assert entry.unique_id == "Printing-uuid" state = hass.states.get("binary_sensor.octoprint_printing_error") assert state is not None assert state.state == STATE_OFF - assert state.name == "Octoprint Printing Error" + assert state.name == "OctoPrint Printing Error" entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") assert entry.unique_id == "Printing Error-uuid" @@ -42,13 +42,13 @@ async def test_sensors_printer_offline(hass): state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None assert state.state == STATE_UNAVAILABLE - assert state.name == "Octoprint Printing" + assert state.name == "OctoPrint Printing" entry = entity_registry.async_get("binary_sensor.octoprint_printing") assert entry.unique_id == "Printing-uuid" state = hass.states.get("binary_sensor.octoprint_printing_error") assert state is not None assert state.state == STATE_UNAVAILABLE - assert state.name == "Octoprint Printing Error" + assert state.name == "OctoPrint Printing Error" entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") assert entry.unique_id == "Printing Error-uuid" diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index 3954e9ffbca..c3a02c1bab5 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -29,48 +29,48 @@ async def test_sensors(hass): state = hass.states.get("sensor.octoprint_job_percentage") assert state is not None assert state.state == "50" - assert state.name == "Octoprint Job Percentage" + assert state.name == "OctoPrint Job Percentage" entry = entity_registry.async_get("sensor.octoprint_job_percentage") assert entry.unique_id == "Job Percentage-uuid" state = hass.states.get("sensor.octoprint_current_state") assert state is not None assert state.state == "Operational" - assert state.name == "Octoprint Current State" + assert state.name == "OctoPrint Current State" entry = entity_registry.async_get("sensor.octoprint_current_state") assert entry.unique_id == "Current State-uuid" state = hass.states.get("sensor.octoprint_actual_tool1_temp") assert state is not None assert state.state == "18.83" - assert state.name == "Octoprint actual tool1 temp" + assert state.name == "OctoPrint actual tool1 temp" entry = entity_registry.async_get("sensor.octoprint_actual_tool1_temp") assert entry.unique_id == "actual tool1 temp-uuid" state = hass.states.get("sensor.octoprint_target_tool1_temp") assert state is not None assert state.state == "37.83" - assert state.name == "Octoprint target tool1 temp" + assert state.name == "OctoPrint target tool1 temp" entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") assert entry.unique_id == "target tool1 temp-uuid" state = hass.states.get("sensor.octoprint_target_tool1_temp") assert state is not None assert state.state == "37.83" - assert state.name == "Octoprint target tool1 temp" + assert state.name == "OctoPrint target tool1 temp" entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") assert entry.unique_id == "target tool1 temp-uuid" state = hass.states.get("sensor.octoprint_start_time") assert state is not None assert state.state == "2020-02-20T09:00:00" - assert state.name == "Octoprint Start Time" + assert state.name == "OctoPrint Start Time" entry = entity_registry.async_get("sensor.octoprint_start_time") assert entry.unique_id == "Start Time-uuid" state = hass.states.get("sensor.octoprint_estimated_finish_time") assert state is not None assert state.state == "2020-02-20T10:50:00" - assert state.name == "Octoprint Estimated Finish Time" + assert state.name == "OctoPrint Estimated Finish Time" entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") assert entry.unique_id == "Estimated Finish Time-uuid" From a0d0e325e0e5500b29f860a7427b7707eaf277d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 14:07:25 +0200 Subject: [PATCH 0085/1452] Fix OctoPrint config flow schema (#58688) --- homeassistant/components/octoprint/config_flow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 9674806d49f..ea2013f29b9 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -25,11 +25,11 @@ _LOGGER = logging.getLogger(__name__) def _schema_with_defaults(username="", host="", port=80, path="/", ssl=False): return vol.Schema( { - vol.Required(CONF_USERNAME, default=username): cv.string, - vol.Required(CONF_HOST, default=host): cv.string, - vol.Optional(CONF_PORT, default=port): cv.port, - vol.Optional(CONF_PATH, default=path): cv.string, - vol.Optional(CONF_SSL, default=ssl): cv.boolean, + vol.Required(CONF_USERNAME, default=username): str, + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): cv.port, + vol.Required(CONF_PATH, default=path): str, + vol.Required(CONF_SSL, default=ssl): bool, }, extra=vol.ALLOW_EXTRA, ) From 659a0d9a95050402e36920c7e2d27d3c64bdbc73 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 29 Oct 2021 06:47:15 -0600 Subject: [PATCH 0086/1452] Add select platform to Litter-Robot integration (#58323) --- .../components/litterrobot/__init__.py | 2 +- .../components/litterrobot/select.py | 53 +++++++++++++++ tests/components/litterrobot/test_select.py | 66 +++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/litterrobot/select.py create mode 100644 tests/components/litterrobot/test_select.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 04a98a5bf60..17bef9a23a8 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -9,7 +9,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = ["sensor", "switch", "vacuum"] +PLATFORMS = ["select", "sensor", "switch", "vacuum"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py new file mode 100644 index 00000000000..6dfb44e97f6 --- /dev/null +++ b/homeassistant/components/litterrobot/select.py @@ -0,0 +1,53 @@ +"""Support for Litter-Robot selects.""" +from __future__ import annotations + +from pylitterbot.robot import VALID_WAIT_TIMES + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import LitterRobotControlEntity +from .hub import LitterRobotHub + +TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES = "Clean Cycle Wait Time Minutes" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Litter-Robot selects using config entry.""" + hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] + + entities = [ + LitterRobotSelect( + robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub + ) + for robot in hub.account.robots + ] + + async_add_entities(entities) + + +class LitterRobotSelect(LitterRobotControlEntity, SelectEntity): + """Litter-Robot Select.""" + + _attr_icon = "mdi:timer-outline" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return str(self.robot.clean_cycle_wait_time_minutes) + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return [str(minute) for minute in VALID_WAIT_TIMES] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.perform_action_and_refresh(self.robot.set_wait_time, int(option)) diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py new file mode 100644 index 00000000000..c0c436764af --- /dev/null +++ b/tests/components/litterrobot/test_select.py @@ -0,0 +1,66 @@ +"""Test the Litter-Robot select entity.""" +from datetime import timedelta + +from pylitterbot.robot import VALID_WAIT_TIMES +import pytest + +from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .conftest import setup_integration + +from tests.common import async_fire_time_changed + +SELECT_ENTITY_ID = "select.test_clean_cycle_wait_time_minutes" + + +async def test_wait_time_select(hass: HomeAssistant, mock_account): + """Tests the wait time select entity.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + + select = hass.states.get(SELECT_ENTITY_ID) + assert select + + data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} + + count = 0 + for wait_time in VALID_WAIT_TIMES: + count += 1 + data[ATTR_OPTION] = wait_time + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SELECT_OPTION, + data, + blocking=True, + ) + + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) + async_fire_time_changed(hass, future) + assert mock_account.robots[0].set_wait_time.call_count == count + + +async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account): + """Tests the wait time select entity with invalid value.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + + select = hass.states.get(SELECT_ENTITY_ID) + assert select + + data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "10"} + + with pytest.raises(ValueError): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SELECT_OPTION, + data, + blocking=True, + ) + assert not mock_account.robots[0].set_wait_time.called From 39867c9b83d1804f97b39702520bb00a314fbfb5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:48:11 +0200 Subject: [PATCH 0087/1452] Convert RGBW and RGBWW colors in light turn_on calls (#58680) --- homeassistant/components/light/__init__.py | 36 ++++++ tests/components/light/test_init.py | 135 +++++++++++++++++++++ tests/components/tasmota/test_light.py | 16 +-- 3 files changed, 179 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a09a2fcd58e..8b1d8fe1ec6 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -401,6 +401,14 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None: params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + elif (rgbw_color := params.pop(ATTR_RGBW_COLOR, None)) is not None: + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: + rgb_color = color_util.color_rgbww_to_rgb( + *rgbww_color, light.min_mireds, light.max_mireds + ) + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif ATTR_HS_COLOR in params and COLOR_MODE_HS not in supported_color_modes: hs_color = params.pop(ATTR_HS_COLOR) if COLOR_MODE_RGB in supported_color_modes: @@ -441,6 +449,34 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) + elif ATTR_RGBW_COLOR in params and COLOR_MODE_RGBW not in supported_color_modes: + rgbw_color = params.pop(ATTR_RGBW_COLOR) + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + if COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = rgb_color + elif COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, light.min_mireds, light.max_mireds + ) + elif COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif ( + ATTR_RGBWW_COLOR in params and COLOR_MODE_RGBWW not in supported_color_modes + ): + rgbww_color = params.pop(ATTR_RGBWW_COLOR) + rgb_color = color_util.color_rgbww_to_rgb( + *rgbww_color, light.min_mireds, light.max_mireds + ) + if COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = rgb_color + elif COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) + elif COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) # If both white and brightness are specified, override white if ( diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index f3bd4583676..2a61a3bbf4d 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1588,6 +1588,141 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati # The midpoint the the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbw_color": (128, 0, 0, 64), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.592, 0.308)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + _, data = entity6.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by adding green + blue + assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbw_color": (255, 255, 255, 255), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.323, 0.329)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + _, data = entity6.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by adding green + blue + assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbww_color": (128, 0, 0, 64, 32), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.639, 0.312)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbww_color": (255, 255, 255, 255, 255), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.396, 0.359)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity5.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by decreasing green + blue + assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} + async def test_light_service_call_color_temp_emulation( hass, enable_custom_integrations diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index ec3d67ef8f2..f85cf0d3c5b 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -885,21 +885,21 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", 0, False, ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", 0, False, ) @@ -992,21 +992,21 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", 0, False, ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", 0, False, ) From 72d7817dbf10f7c378558780d226c2655410ce70 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:51:14 +0200 Subject: [PATCH 0088/1452] Update light turn_on schema to coerce colors to tuple before asserting sequence type (#58670) * Make color_name_to_rgb return a tuple * Tweak * Tweak * Update test * Tweak test --- homeassistant/components/group/light.py | 5 + homeassistant/components/light/__init__.py | 10 +- tests/components/group/test_light.py | 189 ++++++++++++++++++++- tests/components/light/test_init.py | 78 +++++++++ 4 files changed, 273 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index a3a02ee6b9c..4a14bc5dcf3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import Counter import itertools +import logging from typing import Any, Set, cast import voluptuous as vol @@ -66,6 +67,8 @@ SUPPORT_GROUP_LIGHT = ( SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -152,6 +155,8 @@ class LightGroup(GroupEntity, light.LightEntity): } data[ATTR_ENTITY_ID] = self._entity_ids + _LOGGER.debug("Forwarded turn_on command: %s", data) + await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8b1d8fe1ec6..c5ae88eaaa0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -202,25 +202,25 @@ LIGHT_TURN_ON_SCHEMA = { ), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) ), vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 4) ), vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 5) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index e769bf33f8a..e1a45d6fe53 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -3,12 +3,15 @@ from os import path import unittest.mock from unittest.mock import MagicMock, patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -28,6 +31,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, + COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE, @@ -261,6 +265,77 @@ async def test_color_hs(hass, enable_custom_integrations): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 +async def test_color_rgb(hass, enable_custom_integrations): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGB} + entity0.color_mode = COLOR_MODE_RGB + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGB} + entity1.color_mode = COLOR_MODE_RGB + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (0, 64, 128) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (127, 96, 96) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (255, 128, 64) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + async def test_color_rgbw(hass, enable_custom_integrations): """Test rgbw color reporting.""" platform = getattr(hass.components, "test.light") @@ -1039,14 +1114,40 @@ async def test_supported_features(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 -async def test_service_calls(hass): +@pytest.mark.parametrize("supported_color_modes", [COLOR_MODE_HS, COLOR_MODE_RGB]) +async def test_service_calls(hass, enable_custom_integrations, supported_color_modes): """Test service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("bed_light", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("ceiling_lights", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("kitchen_lights", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {supported_color_modes} + entity0.color_mode = supported_color_modes + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {supported_color_modes} + entity1.color_mode = supported_color_modes + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {supported_color_modes} + entity2.color_mode = supported_color_modes + entity2.brightness = 255 + entity2.rgb_color = (255, 128, 64) + await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, "entities": [ @@ -1062,14 +1163,16 @@ async def test_service_calls(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON + group_state = hass.states.get("light.light_group") + assert group_state.state == STATE_ON + assert group_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [supported_color_modes] + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "light.light_group"}, blocking=True, ) - assert hass.states.get("light.bed_light").state == STATE_OFF assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF @@ -1096,6 +1199,84 @@ async def test_service_calls(hass): assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_RGB_COLOR: (42, 255, 255), + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + state = hass.states.get("light.kitchen_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_COLOR_NAME: "red", + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.kitchen_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + +async def test_service_call_effect(hass): + """Test service calls.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.light_group").state == STATE_ON + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 2a61a3bbf4d..d51a5b64861 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component +import homeassistant.util.color as color_util from tests.common import async_mock_service @@ -1724,6 +1725,83 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} +async def test_light_service_call_color_conversion_named_tuple( + hass, enable_custom_integrations +): + """Test a named tuple (RGBColor) is handled correctly.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGB} + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_XY} + + entity3 = platform.ENTITIES[3] + entity3.supported_color_modes = { + light.COLOR_MODE_HS, + light.COLOR_MODE_RGB, + light.COLOR_MODE_XY, + } + + entity4 = platform.ENTITIES[4] + entity4.supported_features = light.SUPPORT_COLOR + + entity5 = platform.ENTITIES[5] + entity5.supported_color_modes = {light.COLOR_MODE_RGBW} + + entity6 = platform.ENTITIES[6] + entity6.supported_color_modes = {light.COLOR_MODE_RGBWW} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 25, + "rgb_color": color_util.RGBColor(128, 0, 0), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 64, "xy_color": (0.701, 0.299)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} + + async def test_light_service_call_color_temp_emulation( hass, enable_custom_integrations ): From f3bd13d179471e7d7d862d05364f98e8bbb9376b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:59:16 +0200 Subject: [PATCH 0089/1452] Fix regression in MQTT discovery (#58684) * Fix regression in MQTT discovery * Update test --- homeassistant/components/mqtt/discovery.py | 5 +- tests/components/mqtt/test_discovery.py | 70 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a237ea7aea1..ffc0c1de435 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,6 +10,7 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -141,7 +142,9 @@ async def async_start( # noqa: C901 if value[-1] == TOPIC_BASE and key.endswith("topic"): payload[key] = f"{value[:-1]}{base}" if payload.get(CONF_AVAILABILITY): - for availability_conf in payload[CONF_AVAILABILITY]: + for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + if not isinstance(availability_conf, dict): + continue if topic := availability_conf.get(CONF_TOPIC): if topic[0] == TOPIC_BASE: availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 8d2106c90fa..60c3961477b 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -504,6 +504,76 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE +async def test_discovery_expansion_2(hass, mqtt_mock, caplog): + """Test expansion of abbreviated discovery payload.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability": {' + ' "topic":"~/avail_item1",' + ' "payload_available": "available",' + ' "payload_not_available": "not_available"' + " }," + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", "available") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_expansion_3(hass, mqtt_mock, caplog): + """Test expansion of broken discovery payload.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability": "incorrect",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + assert hass.states.get("switch.DiscoveryExpansionTest1") is None + # Make sure the malformed availability data does not trip up discovery by asserting + # there are schema valdiation errors in the log + assert ( + "voluptuous.error.MultipleInvalid: expected a dictionary @ data['availability'][0]" + in caplog.text + ) + + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE", From 520a36aa51b9a04f0d4ddd9c2fe081d6b67e21b0 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Fri, 29 Oct 2021 09:59:32 -0400 Subject: [PATCH 0090/1452] Bump pyhik to 0.3.0 (#58659) --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 9676870ecc4..a8f89401148 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -2,7 +2,7 @@ "domain": "hikvision", "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", - "requirements": ["pyhik==0.2.8"], + "requirements": ["pyhik==0.3.0"], "codeowners": ["@mezz64"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index e102cea4f0b..2339da67302 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1520,7 +1520,7 @@ pyhaversion==21.10.0 pyheos==0.7.2 # homeassistant.components.hikvision -pyhik==0.2.8 +pyhik==0.3.0 # homeassistant.components.hive pyhiveapi==0.4.2 From 4b64b92dba8bca1d6ae8f6738a8ac5658a874911 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 16:34:27 +0200 Subject: [PATCH 0091/1452] Fix OctoPrint SSDP URL parsing and discovered values (#58698) --- homeassistant/components/octoprint/config_flow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index ea2013f29b9..acc1449bd96 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,9 +1,9 @@ """Config flow for OctoPrint integration.""" import logging -from urllib.parse import urlsplit from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol +from yarl import URL from homeassistant import config_entries, data_entry_flow, exceptions from homeassistant.const import ( @@ -162,14 +162,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() - url = urlsplit(discovery_info["presentationURL"]) + url = URL(discovery_info["presentationURL"]) self.context["title_placeholders"] = { - CONF_HOST: url.hostname, + CONF_HOST: url.host, } self.discovery_schema = _schema_with_defaults( - host=url.hostname, + host=url.host, + path=url.path, port=url.port, + ssl=url.scheme == "https", ) return await self.async_step_user() From d182bae1125f97917f00db4d16828d4752451114 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Oct 2021 12:29:02 -0500 Subject: [PATCH 0092/1452] Avoid doorbird device probe during discovery for known devices (#58701) --- homeassistant/components/doorbird/config_flow.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 4c62fab17ef..01fcc2b2c22 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -99,13 +99,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") - if not await async_verify_supported_device(self.hass, host): - return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._async_abort_entries_match({CONF_HOST: host}) + + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") + chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] if friendly_hostname.endswith(chop_ending): From a4a5a2e7827b316f16db7b6aa062a9069746d200 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Oct 2021 13:43:39 -0600 Subject: [PATCH 0093/1452] Bump aioambient to 2021.10.1 (#58708) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 116a52f58c5..857ce6de585 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==2021.10.0"], + "requirements": ["aioambient==2021.10.1"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 2339da67302..628cd65bf34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.0 +aioambient==2021.10.1 # homeassistant.components.asuswrt aioasuswrt==1.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6eed5d97a6..71bb64ce1c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.0 +aioambient==2021.10.1 # homeassistant.components.asuswrt aioasuswrt==1.3.4 From 6e7fe13d51b7a00791fa054b69eb0f60bd3cca48 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 29 Oct 2021 14:43:59 -0500 Subject: [PATCH 0094/1452] Disable polling Sonos switches by default (#58705) --- homeassistant/components/sonos/switch.py | 1 + tests/components/sonos/test_switch.py | 38 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 9af6c1eebec..830f3b09481 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -136,6 +136,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): self._attr_icon = FEATURE_ICONS.get(feature_type) if feature_type in POLL_REQUIRED: + self._attr_entity_registry_enabled_default = False self._attr_should_poll = True async def _async_poll(self) -> None: diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 906695bdbaf..c2f997dbcb6 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -1,7 +1,10 @@ """Tests for the Sonos Alarm switch platform.""" from copy import copy +from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER from homeassistant.components.sonos.switch import ( ATTR_DURATION, ATTR_ID, @@ -10,9 +13,15 @@ from homeassistant.components.sonos.switch import ( ATTR_RECURRENCE, ATTR_VOLUME, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_TIME, STATE_ON from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .conftest import SonosMockEvent + +from tests.common import async_fire_time_changed async def setup_platform(hass, config_entry, config): @@ -67,7 +76,36 @@ async def test_switch_attributes(hass, config_entry, config, soco): crossfade_state = hass.states.get(crossfade.entity_id) assert crossfade_state.state == STATE_ON + # Ensure switches are disabled status_light = entity_registry.entities["switch.sonos_zone_a_status_light"] + assert hass.states.get(status_light.entity_id) is None + + touch_controls = entity_registry.entities["switch.sonos_zone_a_touch_controls"] + assert hass.states.get(touch_controls.entity_id) is None + + # Enable disabled switches + for entity in (status_light, touch_controls): + entity_registry.async_update_entity( + entity_id=entity.entity_id, disabled_by=None + ) + await hass.async_block_till_done() + + # Fire event to cancel poll timer and avoid triggering errors during time jump + service = soco.contentDirectory + empty_event = SonosMockEvent(soco, service, {}) + subscription = service.subscribe.return_value + subscription.callback(event=empty_event) + await hass.async_block_till_done() + + # Mock shutdown calls during config entry reload + with patch.object(hass.data[DATA_SONOS_DISCOVERY_MANAGER], "async_shutdown") as m: + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + assert m.called + status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From a2102deb6476df9c459e8ee16039a548ddc472a1 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 29 Oct 2021 13:24:30 -0700 Subject: [PATCH 0095/1452] Support reverse proxying of motionEye streams (#53440) --- homeassistant/components/motioneye/camera.py | 19 +++++- .../components/motioneye/config_flow.py | 16 +++++ homeassistant/components/motioneye/const.py | 1 + .../components/motioneye/strings.json | 5 +- tests/components/motioneye/test_camera.py | 59 +++++++++++++++++++ .../components/motioneye/test_config_flow.py | 37 ++++++++++++ 6 files changed, 133 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 3e8df99e0aa..f21769b24f2 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -5,7 +5,8 @@ from types import MappingProxyType from typing import Any import aiohttp -from motioneye_client.client import MotionEyeClient +from jinja2 import Template +from motioneye_client.client import MotionEyeClient, MotionEyeClientURLParseError from motioneye_client.const import ( DEFAULT_SURVEILLANCE_USERNAME, KEY_MOTION_DETECTION, @@ -41,6 +42,7 @@ from . import ( from .const import ( CONF_CLIENT, CONF_COORDINATOR, + CONF_STREAM_URL_TEMPLATE, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, DOMAIN, @@ -129,11 +131,24 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): ): auth = camera[KEY_STREAMING_AUTH_MODE] + streaming_template = self._options.get(CONF_STREAM_URL_TEMPLATE, "").strip() + streaming_url = None + + if streaming_template: + # Note: Can't use homeassistant.helpers.template as it requires hass + # which is not available during entity construction. + streaming_url = Template(streaming_template).render(**camera) + else: + try: + streaming_url = self._client.get_camera_stream_url(camera) + except MotionEyeClientURLParseError: + pass + return { CONF_NAME: camera[KEY_NAME], CONF_USERNAME: self._surveillance_username if auth is not None else None, CONF_PASSWORD: self._surveillance_password if auth is not None else None, - CONF_MJPEG_URL: self._client.get_camera_stream_url(camera) or "", + CONF_MJPEG_URL: streaming_url or "", CONF_STILL_IMAGE_URL: self._client.get_camera_snapshot_url(camera), CONF_AUTHENTICATION: auth, } diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index a767cf7ecad..54b4a9f0b09 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -26,6 +26,7 @@ from . import create_motioneye_client from .const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, + CONF_STREAM_URL_TEMPLATE, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, CONF_WEBHOOK_SET, @@ -218,4 +219,19 @@ class MotionEyeOptionsFlow(OptionsFlow): ): bool, } + if self.show_advanced_options: + # The input URL is not validated as being a URL, to allow for the possibility + # the template input won't be a valid URL until after it's rendered. + schema.update( + { + vol.Required( + CONF_STREAM_URL_TEMPLATE, + default=self._config_entry.options.get( + CONF_STREAM_URL_TEMPLATE, + "", + ), + ): str + } + ) + return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index 41fb2c18d63..f9f25a3b7ee 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -32,6 +32,7 @@ CONF_CLIENT: Final = "client" CONF_COORDINATOR: Final = "coordinator" CONF_ADMIN_PASSWORD: Final = "admin_password" CONF_ADMIN_USERNAME: Final = "admin_username" +CONF_STREAM_URL_TEMPLATE: Final = "stream_url_template" CONF_SURVEILLANCE_USERNAME: Final = "surveillance_username" CONF_SURVEILLANCE_PASSWORD: Final = "surveillance_password" CONF_WEBHOOK_SET: Final = "webhook_set" diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index 9763e1caf34..0f17699e652 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -31,9 +31,10 @@ "init": { "data": { "webhook_set": "Configure motionEye webhooks to report events to Home Assistant", - "webhook_set_overwrite": "Overwrite unrecognized webhooks" + "webhook_set_overwrite": "Overwrite unrecognized webhooks", + "stream_url_template": "Stream URL template" } } } } -} +} \ No newline at end of file diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index b2264e78556..c14290afdde 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -8,6 +8,7 @@ from aiohttp.web_exceptions import HTTPBadGateway from motioneye_client.client import ( MotionEyeClientError, MotionEyeClientInvalidAuthError, + MotionEyeClientURLParseError, ) from motioneye_client.const import ( KEY_CAMERAS, @@ -20,6 +21,7 @@ import pytest from homeassistant.components.camera import async_get_image, async_get_mjpeg_stream from homeassistant.components.motioneye import get_motioneye_device_identifier from homeassistant.components.motioneye.const import ( + CONF_STREAM_URL_TEMPLATE, CONF_SURVEILLANCE_USERNAME, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -320,3 +322,60 @@ async def test_device_info(hass: HomeAssistant) -> None: for entry in er.async_entries_for_device(entity_registry, device.id) ] assert TEST_CAMERA_ENTITY_ID in entities_from_device + + +async def test_camera_option_stream_url_template( + aiohttp_server: Any, hass: HomeAssistant +) -> None: + """Verify camera with a stream URL template option.""" + client = create_mock_motioneye_client() + + stream_handler = Mock(return_value="") + app = web.Application() + app.add_routes([web.get(f"/{TEST_CAMERA_NAME}/{TEST_CAMERA_ID}", stream_handler)]) + stream_server = await aiohttp_server(app) + + client = create_mock_motioneye_client() + + config_entry = create_mock_motioneye_config_entry( + hass, + data={ + CONF_URL: f"http://localhost:{stream_server.port}", + # The port won't be used as the client is a mock. + CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, + }, + options={ + CONF_STREAM_URL_TEMPLATE: ( + f"http://localhost:{stream_server.port}/" "{{ name }}/{{ id }}" + ) + }, + ) + + await setup_mock_motioneye_config_entry( + hass, config_entry=config_entry, client=client + ) + await hass.async_block_till_done() + + # It won't actually get a stream from the dummy handler, so just catch + # the expected exception, then verify the right handler was called. + with pytest.raises(HTTPBadGateway): + await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID) + assert stream_handler.called + assert not client.get_camera_stream_url.called + + +async def test_get_stream_from_camera_with_broken_host( + aiohttp_server: Any, hass: HomeAssistant +) -> None: + """Test getting a stream with a broken URL (no host).""" + + client = create_mock_motioneye_client() + config_entry = create_mock_motioneye_config_entry(hass, data={CONF_URL: "http://"}) + client.get_camera_stream_url = Mock(side_effect=MotionEyeClientURLParseError) + + await setup_mock_motioneye_config_entry( + hass, config_entry=config_entry, client=client + ) + await hass.async_block_till_done() + with pytest.raises(HTTPBadGateway): + await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 591bbaa4c7d..878795a5a70 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, + CONF_STREAM_URL_TEMPLATE, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, CONF_WEBHOOK_SET, @@ -460,3 +461,39 @@ async def test_options(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] + assert CONF_STREAM_URL_TEMPLATE not in result["data"] + + +async def test_advanced_options(hass: HomeAssistant) -> None: + """Check an options flow with advanced options.""" + + config_entry = create_mock_motioneye_config_entry(hass) + + mock_client = create_mock_motioneye_client() + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ) as mock_setup, patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_WEBHOOK_SET: True, + CONF_WEBHOOK_SET_OVERWRITE: True, + CONF_STREAM_URL_TEMPLATE: "http://moo", + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_WEBHOOK_SET] + assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] + assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 From 944a7c09c4e6067ebd7ec15c4ba359e7eef83c54 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 29 Oct 2021 14:14:26 -0700 Subject: [PATCH 0096/1452] Add motionEye sensor platform (#53415) --- .../components/motioneye/__init__.py | 8 +- homeassistant/components/motioneye/const.py | 1 + homeassistant/components/motioneye/sensor.py | 94 +++++++++++++ tests/components/motioneye/__init__.py | 25 +++- tests/components/motioneye/test_camera.py | 4 +- tests/components/motioneye/test_sensor.py | 132 ++++++++++++++++++ 6 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/motioneye/sensor.py create mode 100644 tests/components/motioneye/test_sensor.py diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 56c95115cb9..90abe39f075 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -31,6 +31,7 @@ from motioneye_client.const import ( ) from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.webhook import ( async_generate_id, @@ -82,7 +83,7 @@ from .const import ( ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [CAMERA_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] def create_motioneye_client( @@ -478,3 +479,8 @@ class MotionEyeEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo(identifiers={self._device_identifier}) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._camera is not None and super().available diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index f9f25a3b7ee..4e30cfb8514 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -84,6 +84,7 @@ MOTIONEYE_MANUFACTURER: Final = "motionEye" SIGNAL_CAMERA_ADD: Final = f"{DOMAIN}_camera_add_signal." "{}" SIGNAL_CAMERA_REMOVE: Final = f"{DOMAIN}_camera_remove_signal." "{}" +TYPE_MOTIONEYE_ACTION_SENSOR = f"{DOMAIN}_action_sensor" TYPE_MOTIONEYE_MJPEG_CAMERA: Final = "motioneye_mjpeg_camera" TYPE_MOTIONEYE_SWITCH_BASE: Final = f"{DOMAIN}_switch" diff --git a/homeassistant/components/motioneye/sensor.py b/homeassistant/components/motioneye/sensor.py new file mode 100644 index 00000000000..c8b7679149c --- /dev/null +++ b/homeassistant/components/motioneye/sensor.py @@ -0,0 +1,94 @@ +"""Sensor platform for motionEye.""" +from __future__ import annotations + +import logging +from types import MappingProxyType +from typing import Any + +from motioneye_client.client import MotionEyeClient +from motioneye_client.const import KEY_ACTIONS, KEY_NAME + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MotionEyeEntity, get_camera_from_cameras, listen_for_new_cameras +from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_ACTION_SENSOR + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up motionEye from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + + @callback + def camera_add(camera: dict[str, Any]) -> None: + """Add a new motionEye camera.""" + async_add_entities( + [ + MotionEyeActionSensor( + entry.entry_id, + camera, + entry_data[CONF_CLIENT], + entry_data[CONF_COORDINATOR], + entry.options, + ) + ] + ) + + listen_for_new_cameras(hass, entry, camera_add) + + +class MotionEyeActionSensor(MotionEyeEntity, SensorEntity): + """motionEye action sensor camera.""" + + def __init__( + self, + config_entry_id: str, + camera: dict[str, Any], + client: MotionEyeClient, + coordinator: DataUpdateCoordinator, + options: MappingProxyType[str, str], + ) -> None: + """Initialize an action sensor.""" + super().__init__( + config_entry_id, + TYPE_MOTIONEYE_ACTION_SENSOR, + camera, + client, + coordinator, + options, + SensorEntityDescription( + key=TYPE_MOTIONEYE_ACTION_SENSOR, entity_registry_enabled_default=False + ), + ) + + @property + def name(self) -> str: + """Return the name of the sensor.""" + camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else "" + return f"{camera_prepend}Actions" + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return len(self._camera.get(KEY_ACTIONS, [])) if self._camera else 0 + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Add actions as attribute.""" + if actions := (self._camera.get(KEY_ACTIONS) if self._camera else None): + return {KEY_ACTIONS: actions} + return None + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + super()._handle_coordinator_update() diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index dcc030e7e5b..c695313c084 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -6,11 +6,13 @@ from unittest.mock import AsyncMock, Mock, patch from motioneye_client.const import DEFAULT_PORT +from homeassistant.components.motioneye import get_motioneye_entity_unique_id from homeassistant.components.motioneye.const import DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -23,7 +25,7 @@ TEST_CAMERA_DEVICE_IDENTIFIER = (DOMAIN, f"{TEST_CONFIG_ENTRY_ID}_{TEST_CAMERA_I TEST_CAMERA = { "show_frame_changes": False, "framerate": 25, - "actions": [], + "actions": ["one", "two", "three"], "preserve_movies": 0, "auto_threshold_tuning": True, "recording_mode": "motion-triggered", @@ -133,6 +135,7 @@ TEST_CAMERA = { } TEST_CAMERAS = {"cameras": [TEST_CAMERA]} TEST_SURVEILLANCE_USERNAME = "surveillance_username" +TEST_SENSOR_ACTION_ENTITY_ID = "sensor.test_camera_actions" TEST_SWITCH_ENTITY_ID_BASE = "switch.test_camera" TEST_SWITCH_MOTION_DETECTION_ENTITY_ID = ( f"{TEST_SWITCH_ENTITY_ID_BASE}_motion_detection" @@ -189,3 +192,23 @@ async def setup_mock_motioneye_config_entry( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry + + +def register_test_entity( + hass: HomeAssistant, platform: str, camera_id: int, type_name: str, entity_id: str +) -> None: + """Register a test entity.""" + + unique_id = get_motioneye_entity_unique_id( + TEST_CONFIG_ENTRY_ID, camera_id, type_name + ) + entity_id = entity_id.split(".")[1] + + entity_registry = er.async_get(hass) + entity_registry.async_get_or_create( + platform, + DOMAIN, + unique_id, + suggested_object_id=entity_id, + disabled_by=None, + ) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index c14290afdde..6a669adc65f 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -222,12 +222,12 @@ async def test_get_still_image_from_camera( server = await aiohttp_server(app) client = create_mock_motioneye_client() client.get_camera_snapshot_url = Mock( - return_value=f"http://localhost:{server.port}/foo" + return_value=f"http://127.0.0.1:{server.port}/foo" ) config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{server.port}", + CONF_URL: f"http://127.0.0.1:{server.port}", CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, ) diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py new file mode 100644 index 00000000000..474b8690308 --- /dev/null +++ b/tests/components/motioneye/test_sensor.py @@ -0,0 +1,132 @@ +"""Tests for the motionEye switch platform.""" +import copy +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from motioneye_client.const import KEY_ACTIONS + +from homeassistant.components.motioneye import get_motioneye_device_identifier +from homeassistant.components.motioneye.const import ( + DEFAULT_SCAN_INTERVAL, + TYPE_MOTIONEYE_ACTION_SENSOR, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +import homeassistant.util.dt as dt_util + +from . import ( + TEST_CAMERA, + TEST_CAMERA_ID, + TEST_SENSOR_ACTION_ENTITY_ID, + create_mock_motioneye_client, + register_test_entity, + setup_mock_motioneye_config_entry, +) + +from tests.common import async_fire_time_changed + + +async def test_sensor_actions(hass: HomeAssistant) -> None: + """Test the actions sensor.""" + register_test_entity( + hass, + SENSOR_DOMAIN, + TEST_CAMERA_ID, + TYPE_MOTIONEYE_ACTION_SENSOR, + TEST_SENSOR_ACTION_ENTITY_ID, + ) + + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "3" + assert entity_state.attributes.get(KEY_ACTIONS) == ["one", "two", "three"] + + updated_camera = copy.deepcopy(TEST_CAMERA) + updated_camera[KEY_ACTIONS] = ["one"] + + # When the next refresh is called return the updated values. + client.async_get_cameras = AsyncMock(return_value={"cameras": [updated_camera]}) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "1" + assert entity_state.attributes.get(KEY_ACTIONS) == ["one"] + + del updated_camera[KEY_ACTIONS] + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "0" + assert entity_state.attributes.get(KEY_ACTIONS) is None + + +async def test_sensor_device_info(hass: HomeAssistant) -> None: + """Verify device information includes expected details.""" + + # Enable the action sensor (it is disabled by default). + register_test_entity( + hass, + SENSOR_DOMAIN, + TEST_CAMERA_ID, + TYPE_MOTIONEYE_ACTION_SENSOR, + TEST_SENSOR_ACTION_ENTITY_ID, + ) + + config_entry = await setup_mock_motioneye_config_entry(hass) + + device_identifer = get_motioneye_device_identifier( + config_entry.entry_id, TEST_CAMERA_ID + ) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({device_identifer}) + assert device + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + assert TEST_SENSOR_ACTION_ENTITY_ID in entities_from_device + + +async def test_sensor_actions_can_be_enabled(hass: HomeAssistant) -> None: + """Verify the action sensor can be enabled.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get(TEST_SENSOR_ACTION_ENTITY_ID) + assert entry + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION + entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) + assert not entity_state + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ): + updated_entry = entity_registry.async_update_entity( + TEST_SENSOR_ACTION_ENTITY_ID, disabled_by=None + ) + assert not updated_entry.disabled + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) + assert entity_state From b1b782419ba2e853be7162ef5d5b7a53969be11a Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Fri, 29 Oct 2021 23:36:47 +0200 Subject: [PATCH 0097/1452] Add REST sensor/binary_sensor/switch templated headers & params (#54426) --- homeassistant/components/rest/__init__.py | 3 ++ homeassistant/components/rest/data.py | 8 +++-- homeassistant/components/rest/schema.py | 4 +-- homeassistant/components/rest/switch.py | 21 +++++++++---- homeassistant/components/rest/utils.py | 27 ++++++++++++++++ tests/components/rest/test_binary_sensor.py | 34 ++++++++++++++++++++ tests/components/rest/test_sensor.py | 34 ++++++++++++++++++++ tests/components/rest/test_switch.py | 35 +++++++++++++++++++-- 8 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/rest/utils.py diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 8186db1c3c2..f2cffebdfcb 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -37,6 +37,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData from .schema import CONFIG_SCHEMA # noqa: F401 +from .utils import inject_hass_in_templates_list _LOGGER = logging.getLogger(__name__) @@ -161,6 +162,8 @@ def create_rest_data_from_config(hass, config): resource_template.hass = hass resource = resource_template.async_render(parse_result=False) + inject_hass_in_templates_list(hass, [headers, params]) + if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: auth = httpx.DigestAuth(username, password) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 8b03bcfb128..513f2393127 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,6 +3,7 @@ import logging import httpx +from homeassistant.components.rest.utils import render_templates from homeassistant.helpers.httpx_client import get_async_client DEFAULT_TIMEOUT = 10 @@ -51,13 +52,16 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) + rendered_headers = render_templates(self._headers) + rendered_params = render_templates(self._params) + _LOGGER.debug("Updating from %s", self._resource) try: response = await self._async_client.request( self._method, self._resource, - headers=self._headers, - params=self._params, + headers=rendered_headers, + params=rendered_params, auth=self._auth, data=self._request_data, timeout=self._timeout, diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index a4b87051c4b..c5b6949bd39 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -54,8 +54,8 @@ RESOURCE_SCHEMA = { vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.template}), vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index e6b16de40aa..83bd5ae27ae 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -27,6 +27,8 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from .utils import inject_hass_in_templates_list, render_templates + _LOGGER = logging.getLogger(__name__) CONF_BODY_OFF = "body_off" CONF_BODY_ON = "body_on" @@ -46,8 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_STATE_RESOURCE): cv.url, - vol.Optional(CONF_HEADERS): {cv.string: cv.string}, - vol.Optional(CONF_PARAMS): {cv.string: cv.string}, + vol.Optional(CONF_HEADERS): {cv.string: cv.template}, + vol.Optional(CONF_PARAMS): {cv.string: cv.template}, vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, vol.Optional(CONF_IS_ON_TEMPLATE): cv.template, @@ -90,6 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= body_on.hass = hass if body_off is not None: body_off.hass = hass + inject_hass_in_templates_list(hass, [headers, params]) timeout = config.get(CONF_TIMEOUT) try: @@ -204,13 +207,16 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) + rendered_headers = render_templates(self._headers) + rendered_params = render_templates(self._params) + with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, "utf-8"), - headers=self._headers, - params=self._params, + headers=rendered_headers, + params=rendered_params, ) return req @@ -227,12 +233,15 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) + rendered_headers = render_templates(self._headers) + rendered_params = render_templates(self._params) + with async_timeout.timeout(self._timeout): req = await websession.get( self._state_resource, auth=self._auth, - headers=self._headers, - params=self._params, + headers=rendered_headers, + params=rendered_params, ) text = await req.text() diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py new file mode 100644 index 00000000000..24c58d294e1 --- /dev/null +++ b/homeassistant/components/rest/utils.py @@ -0,0 +1,27 @@ +"""Reusable utilities for the Rest component.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.template import Template + + +def inject_hass_in_templates_list( + hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None] +): + """Inject hass in a list of dict of templates.""" + for tpl_dict in tpl_dict_list: + if tpl_dict is not None: + for tpl in tpl_dict.values(): + tpl.hass = hass + + +def render_templates(tpl_dict: dict[str, Template] | None): + """Render a dict of templates.""" + if tpl_dict is None: + return None + + rendered_items = {} + for item_name, template_header in tpl_dict.items(): + if (value := template_header.async_render()) is not None: + rendered_items[item_name] = value + return rendered_items diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 8160a5976a7..a0cd7d5108c 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -179,6 +179,40 @@ async def test_setup_get(hass): assert state.attributes[ATTR_DEVICE_CLASS] == binary_sensor.DEVICE_CLASS_PLUG +@respx.mock +async def test_setup_get_template_headers_params(hass): + """Test setup with valid configuration.""" + respx.get("http://localhost").respond(status_code=200, json={}) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "name": "foo", + "verify_ssl": "true", + "timeout": 30, + "headers": { + "Accept": CONTENT_TYPE_JSON, + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + }, + "params": { + "start": 0, + "end": "{{ 3 + 2 }}", + }, + } + }, + ) + await async_setup_component(hass, "homeassistant", {}) + + assert respx.calls.last.request.headers["Accept"] == CONTENT_TYPE_JSON + assert respx.calls.last.request.headers["User-Agent"] == "Mozilla/5.0" + assert respx.calls.last.request.url.query == b"start=0&end=5" + + @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 4ff8ca12dad..a576acb2fe3 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -217,6 +217,40 @@ async def test_setup_get(hass): assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT +@respx.mock +async def test_setup_get_templated_headers_params(hass): + """Test setup with valid configuration.""" + respx.get("http://localhost").respond(status_code=200, json={}) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "name": "foo", + "verify_ssl": "true", + "timeout": 30, + "headers": { + "Accept": CONTENT_TYPE_JSON, + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + }, + "params": { + "start": 0, + "end": "{{ 3 + 2 }}", + }, + } + }, + ) + await async_setup_component(hass, "homeassistant", {}) + + assert respx.calls.last.request.headers["Accept"] == CONTENT_TYPE_JSON + assert respx.calls.last.request.headers["User-Agent"] == "Mozilla/5.0" + assert respx.calls.last.request.url.query == b"start=0&end=5" + + @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 4370386dcff..1b724052b1e 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -27,7 +27,6 @@ DEVICE_CLASS = DEVICE_CLASS_SWITCH METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE -HEADERS = {"Content-type": CONTENT_TYPE_JSON} AUTH = None PARAMS = None @@ -151,19 +150,51 @@ async def test_setup_with_state_resource(hass, aioclient_mock): assert_setup_component(1, SWITCH_DOMAIN) +async def test_setup_with_templated_headers_params(hass, aioclient_mock): + """Test setup with valid configuration.""" + aioclient_mock.get("http://localhost", status=HTTPStatus.OK) + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: "http://localhost", + CONF_HEADERS: { + "Accept": CONTENT_TYPE_JSON, + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + }, + CONF_PARAMS: { + "start": 0, + "end": "{{ 3 + 2 }}", + }, + } + }, + ) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON + assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" + assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" + assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" + assert_setup_component(1, SWITCH_DOMAIN) + + """Tests for REST switch platform.""" def _setup_test_switch(hass): body_on = Template("on", hass) body_off = Template("off", hass) + headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} switch = rest.RestSwitch( NAME, DEVICE_CLASS, RESOURCE, STATE_RESOURCE, METHOD, - HEADERS, + headers, PARAMS, AUTH, body_on, From fa7b72a4ed7b65a9cbd1b153512583f67ab06960 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 30 Oct 2021 00:04:57 +0200 Subject: [PATCH 0098/1452] reload service: remove entities before disconnection (#58712) --- homeassistant/components/knx/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 54d4b4b7237..57c88b84cc7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -239,15 +239,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # First check for config file. If for some reason it is no longer there # or knx is no longer mentioned, stop the reload. config = await async_integration_yaml_config(hass, DOMAIN) - if not config or DOMAIN not in config: return - await knx_module.xknx.stop() - await asyncio.gather( *(platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)) ) + await knx_module.xknx.stop() await async_setup(hass, config) From 9fafa5707493807232c4711ffbdc61ee7cbe9933 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 30 Oct 2021 00:11:34 +0000 Subject: [PATCH 0099/1452] [ci skip] Translation update --- .../devolo_home_network/translations/ca.json | 25 +++++++++++++++++++ .../devolo_home_network/translations/de.json | 25 +++++++++++++++++++ .../devolo_home_network/translations/et.json | 25 +++++++++++++++++++ .../devolo_home_network/translations/hu.json | 25 +++++++++++++++++++ .../devolo_home_network/translations/nl.json | 25 +++++++++++++++++++ .../devolo_home_network/translations/ru.json | 25 +++++++++++++++++++ .../components/dlna_dmr/translations/nl.json | 8 +++--- .../components/motioneye/translations/ca.json | 1 + .../components/motioneye/translations/de.json | 1 + .../components/motioneye/translations/en.json | 1 + .../components/sense/translations/hu.json | 3 ++- .../components/sense/translations/nl.json | 3 ++- .../tuya/translations/sensor.nl.json | 4 ++- .../components/venstar/translations/nl.json | 10 ++++++++ .../components/yeelight/translations/nl.json | 2 +- 15 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/translations/ca.json create mode 100644 homeassistant/components/devolo_home_network/translations/de.json create mode 100644 homeassistant/components/devolo_home_network/translations/et.json create mode 100644 homeassistant/components/devolo_home_network/translations/hu.json create mode 100644 homeassistant/components/devolo_home_network/translations/nl.json create mode 100644 homeassistant/components/devolo_home_network/translations/ru.json diff --git a/homeassistant/components/devolo_home_network/translations/ca.json b/homeassistant/components/devolo_home_network/translations/ca.json new file mode 100644 index 00000000000..c175a1a1246 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "home_control": "La unitat central de control dom\u00e8stic de devolo no funciona amb aquesta integraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + }, + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "zeroconf_confirm": { + "description": "Vols afegir a Home Assistant el dispositiu de xarxa dom\u00e8stica devolo amb nom d'amfitri\u00f3 `{host_name}`?", + "title": "Dispositiu de xarxa dom\u00e8stica devolo descobert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/de.json b/homeassistant/components/devolo_home_network/translations/de.json new file mode 100644 index 00000000000..005d8fb8fdc --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "home_control": "Die devolo Home Control-Zentrale funktioniert nicht mit dieser Integration." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse" + }, + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest das devolo-Heimnetzwerkger\u00e4t mit dem Hostnamen `{host_name}` zum Home Assistant hinzuf\u00fcgen?", + "title": "Gefundenes devolo Heimnetzwerkger\u00e4t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/et.json b/homeassistant/components/devolo_home_network/translations/et.json new file mode 100644 index 00000000000..dff9df53c72 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "home_control": "Devolo Home Controli kesk\u00fcksus ei t\u00f6\u00f6ta selle sidumisega." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{product} ( {name} )", + "step": { + "user": { + "data": { + "ip_address": "IP aadress" + }, + "description": "Kas alutada seadistamist?" + }, + "zeroconf_confirm": { + "description": "Kas soovitd lisada devolo koduv\u00f5rgu seadme hostinimega `{host_name}` Home Assistanti?", + "title": "Avastati devolo koduv\u00f5rgu seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/hu.json b/homeassistant/components/devolo_home_network/translations/hu.json new file mode 100644 index 00000000000..dfae08312df --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "home_control": "A devolo Home Control k\u00f6zponti egys\u00e9g nem m\u0171k\u00f6dik ezzel az integr\u00e1ci\u00f3val." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm" + }, + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "zeroconf_confirm": { + "description": "Szeretn\u00e9 hozz\u00e1adni a `{host_name}`nev\u0171 a devolo otthoni h\u00e1l\u00f3zati eszk\u00f6zt Home Assistanthoz?", + "title": "Felfedezett devolo otthoni h\u00e1l\u00f3zati eszk\u00f6z" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/nl.json b/homeassistant/components/devolo_home_network/translations/nl.json new file mode 100644 index 00000000000..e8730f44b5e --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "home_control": "De devolo Home Control Centrale Unit werkt niet met deze integratie." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP-adres" + }, + "description": "Wilt u beginnen met instellen?" + }, + "zeroconf_confirm": { + "description": "Wilt u het devolo-thuisnetwerkapparaat met de hostnaam ` {host_name} ` aan Home Assistant toevoegen?", + "title": "Ontdekt devolo thuisnetwerk apparaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/ru.json b/homeassistant/components/devolo_home_network/translations/ru.json new file mode 100644 index 00000000000..4cc909b8816 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/ru.json @@ -0,0 +1,25 @@ +{ + "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.", + "home_control": "\u0426\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0431\u043b\u043e\u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f devolo Home Control \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e devolo `{host_name}` \u0432 Home Assistant?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e devolo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index ff14a957526..5331f3340dd 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -8,12 +8,12 @@ "discovery_error": "Kan geen overeenkomend DLNA-apparaat vinden", "incomplete_config": "Configuratie mist een vereiste variabele", "non_unique_id": "Meerdere apparaten gevonden met hetzelfde unieke ID", - "not_dmr": "Apparaat is geen Digital Media Renderer" + "not_dmr": "Apparaat is een niet-ondersteund Digital Media Renderer" }, "error": { "cannot_connect": "Kan geen verbinding maken", "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", - "not_dmr": "Apparaat is geen Digital Media Renderer" + "not_dmr": "Apparaat is een niet-ondersteund Digital Media Renderer" }, "flow_title": "{name}", "step": { @@ -35,8 +35,8 @@ "host": "Host", "url": "URL" }, - "description": "URL naar een XML-bestand met apparaatbeschrijvingen", - "title": "DLNA Digital Media Renderer" + "description": "Kies een apparaat om te configureren of laat leeg om een URL in te voeren", + "title": "Ontdekt DLNA Digital Media Renderer" } } }, diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json index 85477627f38..5139cd22229 100644 --- a/homeassistant/components/motioneye/translations/ca.json +++ b/homeassistant/components/motioneye/translations/ca.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Plantilla URL de flux de reproducci\u00f3", "webhook_set": "Configura els webhooks de motionEye per enviar esdeveniments a Home Assistant", "webhook_set_overwrite": "Sobreescriu els webhooks no reconeguts" } diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index d565329b7cd..e4d72b07398 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Stream-URL-Vorlage", "webhook_set": "MotionEye-Webhooks konfigurieren, um Ereignisse an Home Assistant zu melden", "webhook_set_overwrite": "\u00dcberschreiben von nicht bekannten Webhooks" } diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json index 6c24b7850d4..2b12c978f54 100644 --- a/homeassistant/components/motioneye/translations/en.json +++ b/homeassistant/components/motioneye/translations/en.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Stream URL template", "webhook_set": "Configure motionEye webhooks to report events to Home Assistant", "webhook_set_overwrite": "Overwrite unrecognized webhooks" } diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json index acd67b9e6f9..9defa2971bb 100644 --- a/homeassistant/components/sense/translations/hu.json +++ b/homeassistant/components/sense/translations/hu.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-mail", - "password": "Jelsz\u00f3" + "password": "Jelsz\u00f3", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s" }, "title": "Csatlakoztassa a Sense Energy Monitort" } diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index df64e83da16..59e0e3ade8a 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-mail", - "password": "Wachtwoord" + "password": "Wachtwoord", + "timeout": "Timeout" }, "title": "Maak verbinding met uw Sense Energy Monitor" } diff --git a/homeassistant/components/tuya/translations/sensor.nl.json b/homeassistant/components/tuya/translations/sensor.nl.json index 286befbede0..68092c434a3 100644 --- a/homeassistant/components/tuya/translations/sensor.nl.json +++ b/homeassistant/components/tuya/translations/sensor.nl.json @@ -7,7 +7,9 @@ "heating_temp": "Verwarmingstemperatuur", "reserve_1": "Reserve 1", "reserve_2": "Reserve 2", - "reserve_3": "Reserve 3" + "reserve_3": "Reserve 3", + "standby": "Stand-by", + "warm": "Warmtebehoud" } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/nl.json b/homeassistant/components/venstar/translations/nl.json index d2c837d2f91..3c8a61faf20 100644 --- a/homeassistant/components/venstar/translations/nl.json +++ b/homeassistant/components/venstar/translations/nl.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, "step": { "user": { "data": { + "host": "Host", + "password": "Wachtwoord", + "pin": "PIN-code", "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam" }, diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 08a971f0225..a83ef72695c 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (optioneel)", + "model": "Model", "nightlight_switch": "Gebruik Nachtlichtschakelaar", "save_on_change": "Bewaar status bij wijziging", "transition": "Overgangstijd (ms)", From e97133613adcb9b3f060d9b338368f915e27b495 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 02:52:23 +0200 Subject: [PATCH 0100/1452] Fix round - wallbox (#58689) * Fix wallbox round * Add test case --- homeassistant/components/wallbox/__init__.py | 2 +- tests/components/wallbox/__init__.py | 2 +- tests/components/wallbox/const.py | 1 + tests/components/wallbox/test_sensor.py | 5 +++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index e5c8b7719a3..410a3115f9f 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -75,7 +75,7 @@ class WallboxCoordinator(DataUpdateCoordinator): filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} for key, value in filtered_data.items(): - if sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]: + if (sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]) is not None: try: filtered_data[key] = round(value, sensor_round) except TypeError: diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 4a403d0afc8..f8031bd86a4 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -31,7 +31,7 @@ test_response = json.loads( json.dumps( { CONF_CHARGING_POWER_KEY: 0, - CONF_MAX_AVAILABLE_POWER_KEY: 25, + CONF_MAX_AVAILABLE_POWER_KEY: 25.2, CONF_CHARGING_SPEED_KEY: 0, CONF_ADDED_RANGE_KEY: "xx", CONF_ADDED_ENERGY_KEY: "44.697", diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index 3aa2dde38f0..9777602f6c9 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -8,3 +8,4 @@ CONF_STATUS = "status" CONF_MOCK_NUMBER_ENTITY_ID = "number.mock_title_max_charging_current" CONF_MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.mock_title_charging_speed" CONF_MOCK_SENSOR_CHARGING_POWER_ID = "sensor.mock_title_charging_power" +CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER = "sensor.mock_title_max_available_power" diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 41dcd0e6ee0..2551eed6a2e 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -5,6 +5,7 @@ from tests.components.wallbox import entry, setup_integration from tests.components.wallbox.const import ( CONF_MOCK_SENSOR_CHARGING_POWER_ID, CONF_MOCK_SENSOR_CHARGING_SPEED_ID, + CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER, ) @@ -21,4 +22,8 @@ async def test_wallbox_sensor_class(hass): assert state.attributes[CONF_ICON] == "mdi:speedometer" assert state.name == "Mock Title Charging Speed" + # Test round with precision '0' works + state = hass.states.get(CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER) + assert state.state == "25.0" + await hass.config_entries.async_unload(entry.entry_id) From 061b1abd1ba0c2f2a5572fc006d2edff571d0cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Oct 2021 19:57:01 -0500 Subject: [PATCH 0101/1452] Improve handling of invalid serial numbers in HomeKit Controller (#58723) Fixes #58719 --- .../components/homekit_controller/__init__.py | 6 +++--- .../components/homekit_controller/connection.py | 12 +++++++++++- .../specific_devices/test_ryse_smart_bridge.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index bab989ba9bc..f91355906dc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -19,7 +19,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, Entity from .config_flow import normalize_hkid -from .connection import HKDevice +from .connection import HKDevice, valid_serial_number from .const import ( CONTROLLER, DOMAIN, @@ -141,7 +141,7 @@ class HomeKitEntity(Entity): """Return the ID of this device.""" info = self.accessory_info serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if serial: + if valid_serial_number(serial): return f"homekit-{serial}-{self._iid}" # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" @@ -161,7 +161,7 @@ class HomeKitEntity(Entity): """Return the device info.""" info = self.accessory_info accessory_serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if accessory_serial: + if valid_serial_number(accessory_serial): # Some accessories do not have a serial number identifier = (DOMAIN, IDENTIFIER_SERIAL_NUMBER, accessory_serial) else: diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index cf8381a9a28..8523fec7b8f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -36,6 +36,16 @@ MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 _LOGGER = logging.getLogger(__name__) +def valid_serial_number(serial): + """Return if the serial number appears to be valid.""" + if not serial: + return False + try: + return float("".join(serial.rsplit(".", 1))) > 1 + except ValueError: + return True + + def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" result = {} @@ -211,7 +221,7 @@ class HKDevice: serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if serial_number: + if valid_serial_number(serial_number): identifiers = {(DOMAIN, IDENTIFIER_SERIAL_NUMBER, serial_number)} else: # Some accessories do not have a serial number diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 8430919297c..ad5180658ad 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -19,7 +19,7 @@ async def test_ryse_smart_bridge_setup(hass): # Check that the cover.master_bath_south is correctly found and set up cover_id = "cover.master_bath_south" cover = entity_registry.async_get(cover_id) - assert cover.unique_id == "homekit-1.0.0-48" + assert cover.unique_id == "homekit-00:00:00:00:00:00-2-48" cover_helper = Helper( hass, From 687c40a622cc93a5bf2d8b9f5fd94dde34ca1c5c Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Fri, 29 Oct 2021 18:54:40 -0700 Subject: [PATCH 0102/1452] Enable strict typing for greeneye_monitor (#58571) * Enable strict typing for greeneye_monitor * Fix pylint --- .strict-typing | 1 + .../components/greeneye_monitor/__init__.py | 11 +- .../components/greeneye_monitor/sensor.py | 134 ++++++++++++------ mypy.ini | 11 ++ 4 files changed, 110 insertions(+), 47 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8977c9f68c8..9dd026a0b67 100644 --- a/.strict-typing +++ b/.strict-typing @@ -52,6 +52,7 @@ homeassistant.components.fritz.* homeassistant.components.geo_location.* homeassistant.components.gios.* homeassistant.components.goalzero.* +homeassistant.components.greeneye_monitor.* homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index cc7b8955756..bb564655ecb 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,5 +1,8 @@ """Support for monitoring a GreenEye Monitor energy monitor.""" +from __future__ import annotations + import logging +from typing import Any from greeneye import Monitors import voluptuous as vol @@ -15,8 +18,10 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -117,7 +122,7 @@ COMPONENT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the GreenEye Monitor component.""" monitors = Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors @@ -125,7 +130,7 @@ async def async_setup(hass, config): server_config = config[DOMAIN] server = await monitors.start_server(server_config[CONF_PORT]) - async def close_server(*args): + async def close_server(*args: list[Any]) -> None: """Close the monitoring server.""" await server.close() @@ -189,7 +194,7 @@ async def async_setup(hass, config): return False hass.async_create_task( - async_load_platform(hass, "sensor", DOMAIN, all_sensors, config) + async_load_platform(hass, "sensor", DOMAIN, {CONF_SENSORS: all_sensors}, config) ) return True diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 5904b8652da..63f069e02a9 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,8 +1,16 @@ """Support for the sensors in a GreenEye Monitor.""" +from __future__ import annotations + +from typing import Any, Generic, TypeVar + +import greeneye +from greeneye import Monitors + from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_NAME, CONF_SENSOR_TYPE, + CONF_SENSORS, CONF_TEMPERATURE_UNIT, DEVICE_CLASS_TEMPERATURE, ELECTRIC_POTENTIAL_VOLT, @@ -11,6 +19,9 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType from . import ( CONF_COUNTED_QUANTITY, @@ -37,13 +48,15 @@ TEMPERATURE_ICON = "mdi:thermometer" VOLTAGE_ICON = "mdi:current-ac" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: Config, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType, +) -> None: """Set up a single GEM temperature sensor.""" - if not discovery_info: - return - - entities = [] - for sensor in discovery_info: + entities: list[GEMSensor] = [] + for sensor in discovery_info[CONF_SENSORS]: sensor_type = sensor[CONF_SENSOR_TYPE] if sensor_type == SENSOR_TYPE_CURRENT: entities.append( @@ -86,42 +99,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class GEMSensor(SensorEntity): +T = TypeVar( + "T", + greeneye.monitor.Channel, + greeneye.monitor.Monitor, + greeneye.monitor.PulseCounter, + greeneye.monitor.TemperatureSensor, +) + + +class GEMSensor(Generic[T], SensorEntity): """Base class for GreenEye Monitor sensors.""" _attr_should_poll = False - def __init__(self, monitor_serial_number, name, sensor_type, number): + def __init__( + self, monitor_serial_number: int, name: str, sensor_type: str, number: int + ) -> None: """Construct the entity.""" self._monitor_serial_number = monitor_serial_number self._name = name - self._sensor = None + self._sensor: T | None = None self._sensor_type = sensor_type self._number = number @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID for this sensor.""" return f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}" @property - def name(self): + def name(self) -> str: """Return the name of the channel.""" return self._name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Wait for and connect to the sensor.""" monitors = self.hass.data[DATA_GREENEYE_MONITOR] if not self._try_connect_to_monitor(monitors): monitors.add_listener(self._on_new_monitor) - def _on_new_monitor(self, *args): + def _on_new_monitor(self, *args: list[Any]) -> None: monitors = self.hass.data[DATA_GREENEYE_MONITOR] if self._try_connect_to_monitor(monitors): monitors.remove_listener(self._on_new_monitor) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove listener from the sensor.""" if self._sensor: self._sensor.remove_listener(self.async_write_ha_state) @@ -129,7 +153,7 @@ class GEMSensor(SensorEntity): monitors = self.hass.data[DATA_GREENEYE_MONITOR] monitors.remove_listener(self._on_new_monitor) - def _try_connect_to_monitor(self, monitors): + def _try_connect_to_monitor(self, monitors: Monitors) -> bool: monitor = monitors.monitors.get(self._monitor_serial_number) if not monitor: return False @@ -140,34 +164,39 @@ class GEMSensor(SensorEntity): return True - def _get_sensor(self, monitor): + def _get_sensor(self, monitor: greeneye.monitor.Monitor) -> T: raise NotImplementedError() -class CurrentSensor(GEMSensor): +class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): """Entity showing power usage on one channel of the monitor.""" _attr_icon = CURRENT_SENSOR_ICON _attr_native_unit_of_measurement = UNIT_WATTS - def __init__(self, monitor_serial_number, number, name, net_metering): + def __init__( + self, monitor_serial_number: int, number: int, name: str, net_metering: bool + ) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "current", number) self._net_metering = net_metering - def _get_sensor(self, monitor): + def _get_sensor( + self, monitor: greeneye.monitor.Monitor + ) -> greeneye.monitor.Channel: return monitor.channels[self._number - 1] @property - def native_value(self): + def native_value(self) -> float | None: """Return the current number of watts being used by the channel.""" if not self._sensor: return None + assert isinstance(self._sensor.watts, float) return self._sensor.watts @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return total wattseconds in the state dictionary.""" if not self._sensor: return None @@ -180,43 +209,47 @@ class CurrentSensor(GEMSensor): return {DATA_WATT_SECONDS: watt_seconds} -class PulseCounter(GEMSensor): +class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): """Entity showing rate of change in one pulse counter of the monitor.""" _attr_icon = COUNTER_ICON def __init__( self, - monitor_serial_number, - number, - name, - counted_quantity, - time_unit, - counted_quantity_per_pulse, - ): + monitor_serial_number: int, + number: int, + name: str, + counted_quantity: str, + time_unit: str, + counted_quantity_per_pulse: float, + ) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "pulse", number) self._counted_quantity = counted_quantity self._counted_quantity_per_pulse = counted_quantity_per_pulse self._time_unit = time_unit - def _get_sensor(self, monitor): + def _get_sensor( + self, monitor: greeneye.monitor.Monitor + ) -> greeneye.monitor.PulseCounter: return monitor.pulse_counters[self._number - 1] @property - def native_value(self): + def native_value(self) -> float | None: """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 - return ( + result = ( self._sensor.pulses_per_second * self._counted_quantity_per_pulse * self._seconds_per_time_unit ) + assert isinstance(result, float) + return result @property - def _seconds_per_time_unit(self): + def _seconds_per_time_unit(self) -> int: """Return the number of seconds in the given display time unit.""" if self._time_unit == TIME_SECONDS: return 1 @@ -225,13 +258,18 @@ class PulseCounter(GEMSensor): if self._time_unit == TIME_HOURS: return 3600 + # Config schema should have ensured it is one of the above values + raise Exception( + f"Invalid value for time unit: {self._time_unit}. Expected one of {TIME_SECONDS}, {TIME_MINUTES}, or {TIME_HOURS}" + ) + @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit of measurement for this pulse counter.""" return f"{self._counted_quantity}/{self._time_unit}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return total pulses in the data dictionary.""" if not self._sensor: return None @@ -239,52 +277,60 @@ class PulseCounter(GEMSensor): return {DATA_PULSES: self._sensor.pulses} -class TemperatureSensor(GEMSensor): +class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): """Entity showing temperature from one temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_icon = TEMPERATURE_ICON - def __init__(self, monitor_serial_number, number, name, unit): + def __init__( + self, monitor_serial_number: int, number: int, name: str, unit: str + ) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "temp", number) self._unit = unit - def _get_sensor(self, monitor): + def _get_sensor( + self, monitor: greeneye.monitor.Monitor + ) -> greeneye.monitor.TemperatureSensor: return monitor.temperature_sensors[self._number - 1] @property - def native_value(self): + def native_value(self) -> float | None: """Return the current temperature being reported by this sensor.""" if not self._sensor: return None + assert isinstance(self._sensor.temperature, float) return self._sensor.temperature @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit of measurement for this sensor (user specified).""" return self._unit -class VoltageSensor(GEMSensor): +class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): """Entity showing voltage.""" _attr_icon = VOLTAGE_ICON _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT - def __init__(self, monitor_serial_number, number, name): + def __init__(self, monitor_serial_number: int, number: int, name: str) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "volts", number) - def _get_sensor(self, monitor): + def _get_sensor( + self, monitor: greeneye.monitor.Monitor + ) -> greeneye.monitor.Monitor: """Wire the updates to the monitor itself, since there is no voltage element in the API.""" return monitor @property - def native_value(self): + def native_value(self) -> float | None: """Return the current voltage being reported by this sensor.""" if not self._sensor: return None + assert isinstance(self._sensor.voltage, float) return self._sensor.voltage diff --git a/mypy.ini b/mypy.ini index 4192e1a10ea..ca9dc983674 100644 --- a/mypy.ini +++ b/mypy.ini @@ -583,6 +583,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.greeneye_monitor.*] +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.group.*] check_untyped_defs = true disallow_incomplete_defs = true From c469358e5faa823eeb17b7c4f26f73ba545e4376 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Oct 2021 23:17:15 -0600 Subject: [PATCH 0103/1452] Fix bug with volumes in SimpliSafe set_system_properties service (#58721) Co-authored-by: Paulus Schoutsen --- homeassistant/components/simplisafe/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index ddaf70cc071..8fc4c919ed8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -118,7 +118,12 @@ PLATFORMS = ( "sensor", ) -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +VOLUME_MAP = { + "high": VOLUME_HIGH, + "low": VOLUME_LOW, + "medium": VOLUME_MEDIUM, + "off": VOLUME_OFF, +} SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -137,8 +142,8 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( lambda value: value.total_seconds(), vol.Range(min=30, max=480), ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), - vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), + vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), + vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( cv.time_period, lambda value: value.total_seconds(), @@ -157,7 +162,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( ), vol.Optional(ATTR_LIGHT): cv.boolean, vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.Coerce(int), vol.In(VOLUMES) + vol.In(VOLUME_MAP), VOLUME_MAP.get ), } ) From f4c823f33864a4f92f4ebfa14336de7b962e9f2e Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 30 Oct 2021 11:15:38 +0200 Subject: [PATCH 0104/1452] Fix lcn in place update of config entry data (#58729) * Fix in place update of config entry data * Deep copy of device configs * Fix review comments --- homeassistant/components/lcn/helpers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 2834cc1940e..657657ea1d0 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from copy import deepcopy from itertools import chain import re from typing import Tuple, Type, Union, cast @@ -336,8 +337,9 @@ async def async_update_config_entry( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Fill missing values in config_entry with infos from LCN bus.""" + device_configs = deepcopy(config_entry.data[CONF_DEVICES]) coros = [] - for device_config in config_entry.data[CONF_DEVICES]: + for device_config in device_configs: device_connection = get_device_connection( hass, device_config[CONF_ADDRESS], config_entry ) @@ -345,8 +347,10 @@ async def async_update_config_entry( await asyncio.gather(*coros) + new_data = {**config_entry.data, CONF_DEVICES: device_configs} + # schedule config_entry for save - hass.config_entries.async_update_entry(config_entry) + hass.config_entries.async_update_entry(config_entry, data=new_data) def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]: From a90c8ab5584ce50e8dfae67790cb56f3d1d1ea86 Mon Sep 17 00:00:00 2001 From: Anders Liljekvist Date: Sat, 30 Oct 2021 12:14:19 +0200 Subject: [PATCH 0105/1452] Add myself as codeowner of bluesound (#58733) --- CODEOWNERS | 1 + homeassistant/components/bluesound/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 680424fbdea..e411e2a84cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blebox/* @bbx-a @bbx-jp homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core +homeassistant/components/bluesound/* @thrawnarn homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bond/* @bdraco @prystupa @joshs85 diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 648ff2a1809..bfefff36601 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -3,6 +3,6 @@ "name": "Bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": ["xmltodict==0.12.0"], - "codeowners": [], + "codeowners": ["@thrawnarn"], "iot_class": "local_polling" } From 65b965e84bb4ec3a1dc4baded94458d1b3ab3af7 Mon Sep 17 00:00:00 2001 From: Anders Liljekvist Date: Sat, 30 Oct 2021 14:57:45 +0200 Subject: [PATCH 0106/1452] Fix bluesound player internally used id (#58732) --- .../components/bluesound/media_player.py | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index ddc67bed6ab..6c90a511a05 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -106,8 +106,6 @@ SERVICE_TO_METHOD = { def _add_player(hass, async_add_entities, host, port=None, name=None): """Add Bluesound players.""" - if host in [x.host for x in hass.data[DATA_BLUESOUND]]: - return @callback def _init_player(event=None): @@ -127,6 +125,11 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): @callback def _add_player_cb(): """Add player after first sync fetch.""" + if player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: + _LOGGER.warning("Player already added %s", player.id) + return + + hass.data[DATA_BLUESOUND].append(player) async_add_entities([player]) _LOGGER.info("Added device with name: %s", player.name) @@ -138,7 +141,6 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) player = BluesoundPlayer(hass, host, port, name, _add_player_cb) - hass.data[DATA_BLUESOUND].append(player) if hass.is_running: _init_player() @@ -208,6 +210,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. self._name = name + self._id = None self._icon = None self._capture_items = [] self._services_items = [] @@ -225,6 +228,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._bluesound_device_name = None self._init_callback = init_callback + if self.port is None: self.port = DEFAULT_PORT @@ -251,6 +255,8 @@ class BluesoundPlayer(MediaPlayerEntity): if not self._name: self._name = self._sync_status.get("@name", self.host) + if not self._id: + self._id = self._sync_status.get("@id", None) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) if not self._icon: @@ -259,17 +265,19 @@ class BluesoundPlayer(MediaPlayerEntity): if (master := self._sync_status.get("master")) is not None: self._is_master = False master_host = master.get("#text") + master_port = master.get("@port", "11000") + master_id = f"{master_host}:{master_port}" master_device = [ device for device in self._hass.data[DATA_BLUESOUND] - if device.host == master_host + if device.id == master_id ] - if master_device and master_host != self.host: + if master_device and master_id != self.id: self._master = master_device[0] else: self._master = None - _LOGGER.error("Master not found %s", master_host) + _LOGGER.error("Master not found %s", master_id) else: if self._master is not None: self._master = None @@ -287,14 +295,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:%s is offline, retrying later", self.name, self.port) 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:%s", self.name, self.port) except Exception: - _LOGGER.exception("Unexpected error in %s", self._name) + _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) raise def start_polling(self): @@ -314,12 +322,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.force_update_sync_status(self._init_callback, True) except (asyncio.TimeoutError, ClientError): - _LOGGER.info("Node %s is offline, retrying later", self.host) + _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION ) except Exception: - _LOGGER.exception("Unexpected when initiating error in %s", self.host) + _LOGGER.exception( + "Unexpected when initiating error in %s:%s", self.host, self.port + ) raise async def async_update(self): @@ -366,9 +376,9 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, aiohttp.ClientError): if raise_timeout: - _LOGGER.info("Timeout: %s", self.host) + _LOGGER.info("Timeout: %s:%s", self.host, self.port) raise - _LOGGER.debug("Failed communicating: %s", self.host) + _LOGGER.debug("Failed communicating: %s:%s", self.host, self.port) return None return data @@ -403,7 +413,7 @@ class BluesoundPlayer(MediaPlayerEntity): group_name = self._status.get("groupName") if group_name != self._group_name: - _LOGGER.debug("Group name change detected on device: %s", self.host) + _LOGGER.debug("Group name change detected on device: %s", self.id) self._group_name = group_name # rebuild ordered list of entity_ids that are in the group, master is first @@ -659,6 +669,11 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute + @property + def id(self): + """Get id of device.""" + return self._id + @property def name(self): """Return the name of the device.""" @@ -831,8 +846,8 @@ class BluesoundPlayer(MediaPlayerEntity): if master_device: _LOGGER.debug( "Trying to join player: %s to master: %s", - self.host, - master_device[0].host, + self.id, + master_device[0].id, ) await master_device[0].async_add_slave(self) @@ -877,7 +892,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._master is None: return - _LOGGER.debug("Trying to unjoin player: %s", self.host) + _LOGGER.debug("Trying to unjoin player: %s", self.id) await self._master.async_remove_slave(self) async def async_add_slave(self, slave_device): @@ -896,7 +911,7 @@ class BluesoundPlayer(MediaPlayerEntity): """Increase sleep time on player.""" sleep_time = await self.send_bluesound_command("/Sleep") if sleep_time is None: - _LOGGER.error("Error while increasing sleep time on player: %s", self.host) + _LOGGER.error("Error while increasing sleep time on player: %s", self.id) return 0 return int(sleep_time.get("sleep", "0")) From 855e0fc2eb34633903e784e6f98dec8c95b65367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 30 Oct 2021 15:43:05 +0200 Subject: [PATCH 0107/1452] Update Mill library, add support for generation 3 heaters. #58536 (#58738) --- homeassistant/components/mill/climate.py | 2 +- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 3cc8d58abda..ad50bf437bb 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -95,7 +95,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): model=f"generation {1 if heater.is_gen1 else 2}", name=self.name, ) - if heater.is_gen1: + if heater.is_gen1 or heater.is_gen3: self._attr_hvac_modes = [HVAC_MODE_HEAT] else: self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 342b54d2483..7347cf16daa 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.7.3"], + "requirements": ["millheater==0.8.0"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 628cd65bf34..bfe226744e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1008,7 +1008,7 @@ micloud==0.4 miflora==0.7.0 # homeassistant.components.mill -millheater==0.7.3 +millheater==0.8.0 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71bb64ce1c4..8741dc8d232 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -603,7 +603,7 @@ mficlient==0.3.0 micloud==0.4 # homeassistant.components.mill -millheater==0.7.3 +millheater==0.8.0 # homeassistant.components.minio minio==4.0.9 From bbbbcfbb93c3b6dd8cf667be3a283c069ffa5522 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 30 Oct 2021 06:48:01 -0700 Subject: [PATCH 0108/1452] Add motionEye services (#53411) --- homeassistant/components/motioneye/camera.py | 93 ++++++++++ homeassistant/components/motioneye/const.py | 5 + .../components/motioneye/services.yaml | 110 ++++++++++++ tests/components/motioneye/test_camera.py | 168 +++++++++++++++++- 4 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/motioneye/services.yaml diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index f21769b24f2..428e2f31c81 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -9,10 +9,20 @@ from jinja2 import Template from motioneye_client.client import MotionEyeClient, MotionEyeClientURLParseError from motioneye_client.const import ( DEFAULT_SURVEILLANCE_USERNAME, + KEY_ACTION_SNAPSHOT, KEY_MOTION_DETECTION, KEY_NAME, KEY_STREAMING_AUTH_MODE, + KEY_TEXT_OVERLAY_CAMERA_NAME, + KEY_TEXT_OVERLAY_CUSTOM_TEXT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT, + KEY_TEXT_OVERLAY_DISABLED, + KEY_TEXT_OVERLAY_LEFT, + KEY_TEXT_OVERLAY_RIGHT, + KEY_TEXT_OVERLAY_TIMESTAMP, ) +import voluptuous as vol from homeassistant.components.mjpeg.camera import ( CONF_MJPEG_URL, @@ -30,6 +40,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -40,6 +51,7 @@ from . import ( listen_for_new_cameras, ) from .const import ( + CONF_ACTION, CONF_CLIENT, CONF_COORDINATOR, CONF_STREAM_URL_TEMPLATE, @@ -47,11 +59,40 @@ from .const import ( CONF_SURVEILLANCE_USERNAME, DOMAIN, MOTIONEYE_MANUFACTURER, + SERVICE_ACTION, + SERVICE_SET_TEXT_OVERLAY, + SERVICE_SNAPSHOT, TYPE_MOTIONEYE_MJPEG_CAMERA, ) PLATFORMS = ["camera"] +SCHEMA_TEXT_OVERLAY = vol.In( + [ + KEY_TEXT_OVERLAY_DISABLED, + KEY_TEXT_OVERLAY_TIMESTAMP, + KEY_TEXT_OVERLAY_CUSTOM_TEXT, + KEY_TEXT_OVERLAY_CAMERA_NAME, + ] +) +SCHEMA_SERVICE_SET_TEXT = vol.Schema( + vol.All( + { + vol.Optional(KEY_TEXT_OVERLAY_LEFT): SCHEMA_TEXT_OVERLAY, + vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT): cv.string, + vol.Optional(KEY_TEXT_OVERLAY_RIGHT): SCHEMA_TEXT_OVERLAY, + vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT): cv.string, + }, + cv.has_at_least_one_key( + KEY_TEXT_OVERLAY_LEFT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT, + KEY_TEXT_OVERLAY_RIGHT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT, + ), + ), + extra=vol.ALLOW_EXTRA, +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -80,6 +121,23 @@ async def async_setup_entry( listen_for_new_cameras(hass, entry, camera_add) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_SET_TEXT_OVERLAY, + SCHEMA_SERVICE_SET_TEXT, + "async_set_text_overlay", + ) + platform.async_register_entity_service( + SERVICE_ACTION, + {vol.Required(CONF_ACTION): cv.string}, + "async_request_action", + ) + platform.async_register_entity_service( + SERVICE_SNAPSHOT, + {}, + "async_request_snapshot", + ) + class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): """motionEye mjpeg camera.""" @@ -201,3 +259,38 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return self._motion_detection_enabled + + async def async_set_text_overlay( + self, + left_text: str = None, + right_text: str = None, + custom_left_text: str = None, + custom_right_text: str = None, + ) -> None: + """Set text overlay for a camera.""" + # Fetch the very latest camera config to reduce the risk of updating with a + # stale configuration. + camera = await self._client.async_get_camera(self._camera_id) + if not camera: + return + if left_text is not None: + camera[KEY_TEXT_OVERLAY_LEFT] = left_text + if right_text is not None: + camera[KEY_TEXT_OVERLAY_RIGHT] = right_text + if custom_left_text is not None: + camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT] = custom_left_text.encode( + "unicode_escape" + ).decode("UTF-8") + if custom_right_text is not None: + camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT] = custom_right_text.encode( + "unicode_escape" + ).decode("UTF-8") + await self._client.async_set_camera(self._camera_id, camera) + + async def async_request_action(self, action: str) -> None: + """Call a motionEye action on a camera.""" + await self._client.async_action(self._camera_id, action) + + async def async_request_snapshot(self) -> None: + """Request a motionEye snapshot be saved.""" + await self.async_request_action(KEY_ACTION_SNAPSHOT) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index 4e30cfb8514..1dbb78f1e03 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -28,6 +28,7 @@ DOMAIN: Final = "motioneye" ATTR_EVENT_TYPE: Final = "event_type" ATTR_WEBHOOK_ID: Final = "webhook_id" +CONF_ACTION: Final = "action" CONF_CLIENT: Final = "client" CONF_COORDINATOR: Final = "coordinator" CONF_ADMIN_PASSWORD: Final = "admin_password" @@ -81,6 +82,10 @@ EVENT_FILE_STORED_KEYS: Final = [ MOTIONEYE_MANUFACTURER: Final = "motionEye" +SERVICE_SET_TEXT_OVERLAY: Final = "set_text_overlay" +SERVICE_ACTION: Final = "action" +SERVICE_SNAPSHOT: Final = "snapshot" + SIGNAL_CAMERA_ADD: Final = f"{DOMAIN}_camera_add_signal." "{}" SIGNAL_CAMERA_REMOVE: Final = f"{DOMAIN}_camera_remove_signal." "{}" diff --git a/homeassistant/components/motioneye/services.yaml b/homeassistant/components/motioneye/services.yaml new file mode 100644 index 00000000000..2970124c000 --- /dev/null +++ b/homeassistant/components/motioneye/services.yaml @@ -0,0 +1,110 @@ +set_text_overlay: + name: Set Text Overlay + description: Sets the text overlay for a camera. + target: + device: + integration: motioneye + entity: + integration: motioneye + fields: + left_text: + name: Left Text Overlay + description: Text to display on the left + required: false + advanced: false + example: "timestamp" + default: "" + selector: + select: + options: + - "disabled" + - "camera-name" + - "timestamp" + - "custom-text" + custom_left_text: + name: Left Custom Text + description: Custom text to display on the left + required: false + advanced: false + example: "Hello on the left!" + default: "" + selector: + text: + multiline: true + right_text: + name: Right Text Overlay + description: Text to display on the right + required: false + advanced: false + example: "timestamp" + default: "" + selector: + select: + options: + - "disabled" + - "camera-name" + - "timestamp" + - "custom-text" + custom_right_text: + name: Right Custom Text + description: Custom text to display on the right + required: false + advanced: false + example: "Hello on the right!" + default: "" + selector: + text: + multiline: true + +action: + name: Action + description: Trigger a motionEye action + target: + device: + integration: motioneye + entity: + integration: motioneye + fields: + action: + name: Action + description: Action to trigger + required: true + advanced: false + example: "snapshot" + default: "" + selector: + select: + options: + - "snapshot" + - "record_start" + - "record_stop" + - "lock" + - "unlock" + - "light_on" + - "light_off" + - "alarm_on" + - "alarm_off" + - "up" + - "right" + - "down" + - "left" + - "zoom_in" + - "zoom_out" + - "preset1" + - "preset2" + - "preset3" + - "preset4" + - "preset5" + - "preset6" + - "preset7" + - "preset8" + - "preset9" + +snapshot: + name: Snapshot + description: Trigger a motionEye still snapshot + target: + device: + integration: motioneye + entity: + integration: motioneye diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 6a669adc65f..b3d19237165 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -1,7 +1,7 @@ """Test the motionEye camera.""" import copy from typing import Any, cast -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, call from aiohttp import web from aiohttp.web_exceptions import HTTPBadGateway @@ -14,20 +14,31 @@ from motioneye_client.const import ( KEY_CAMERAS, KEY_MOTION_DETECTION, KEY_NAME, + KEY_TEXT_OVERLAY_CUSTOM_TEXT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT, + KEY_TEXT_OVERLAY_LEFT, + KEY_TEXT_OVERLAY_RIGHT, + KEY_TEXT_OVERLAY_TIMESTAMP, KEY_VIDEO_STREAMING, ) import pytest +import voluptuous as vol from homeassistant.components.camera import async_get_image, async_get_mjpeg_stream from homeassistant.components.motioneye import get_motioneye_device_identifier from homeassistant.components.motioneye.const import ( + CONF_ACTION, CONF_STREAM_URL_TEMPLATE, CONF_SURVEILLANCE_USERNAME, DEFAULT_SCAN_INTERVAL, DOMAIN, MOTIONEYE_MANUFACTURER, + SERVICE_ACTION, + SERVICE_SET_TEXT_OVERLAY, + SERVICE_SNAPSHOT, ) -from homeassistant.const import CONF_URL +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -35,6 +46,7 @@ from homeassistant.helpers.device_registry import async_get_registry import homeassistant.util.dt as dt_util from . import ( + TEST_CAMERA, TEST_CAMERA_DEVICE_IDENTIFIER, TEST_CAMERA_ENTITY_ID, TEST_CAMERA_ID, @@ -379,3 +391,155 @@ async def test_get_stream_from_camera_with_broken_host( await hass.async_block_till_done() with pytest.raises(HTTPBadGateway): await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID) + + +async def test_set_text_overlay_bad_extra_key(hass: HomeAssistant) -> None: + """Test text overlay with incorrect input data.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, "extra_key": "foo"} + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + + +async def test_set_text_overlay_bad_entity_identifier(hass: HomeAssistant) -> None: + """Test text overlay with bad entity identifier.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = { + ATTR_ENTITY_ID: "some random string", + KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP, + } + + client.reset_mock() + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + assert not client.async_set_camera.called + + +async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None: + """Test text overlay with incorrect input data.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, {}) + await hass.async_block_till_done() + + +async def test_set_text_overlay_bad_no_left_or_right(hass: HomeAssistant) -> None: + """Test text overlay with incorrect input data.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID} + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + + +async def test_set_text_overlay_good(hass: HomeAssistant) -> None: + """Test a working text overlay.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + custom_left_text = "one\ntwo\nthree" + custom_right_text = "four\nfive\nsix" + data = { + ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, + KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_CUSTOM_TEXT, + KEY_TEXT_OVERLAY_RIGHT: KEY_TEXT_OVERLAY_CUSTOM_TEXT, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT: custom_left_text, + KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT: custom_right_text, + } + client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA)) + + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + assert client.async_get_camera.called + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_TEXT_OVERLAY_LEFT] = KEY_TEXT_OVERLAY_CUSTOM_TEXT + expected_camera[KEY_TEXT_OVERLAY_RIGHT] = KEY_TEXT_OVERLAY_CUSTOM_TEXT + expected_camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT] = "one\\ntwo\\nthree" + expected_camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT] = "four\\nfive\\nsix" + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + +async def test_set_text_overlay_good_entity_id(hass: HomeAssistant) -> None: + """Test a working text overlay with entity_id.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = { + ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, + KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP, + } + client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA)) + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + assert client.async_get_camera.called + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_TEXT_OVERLAY_LEFT] = KEY_TEXT_OVERLAY_TIMESTAMP + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + +async def test_set_text_overlay_bad_device(hass: HomeAssistant) -> None: + """Test a working text overlay.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = { + ATTR_DEVICE_ID: "not a device", + KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP, + } + client.reset_mock() + client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA)) + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + assert not client.async_get_camera.called + assert not client.async_set_camera.called + + +async def test_set_text_overlay_no_such_camera(hass: HomeAssistant) -> None: + """Test a working text overlay.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = { + ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, + KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP, + } + client.reset_mock() + client.async_get_camera = AsyncMock(return_value={}) + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() + assert not client.async_set_camera.called + + +async def test_request_action(hass: HomeAssistant) -> None: + """Test requesting an action.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = { + ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, + CONF_ACTION: "foo", + } + await hass.services.async_call(DOMAIN, SERVICE_ACTION, data) + await hass.async_block_till_done() + assert client.async_action.call_args == call(TEST_CAMERA_ID, data[CONF_ACTION]) + + +async def test_request_snapshot(hass: HomeAssistant) -> None: + """Test requesting a snapshot.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID} + + await hass.services.async_call(DOMAIN, SERVICE_SNAPSHOT, data) + await hass.async_block_till_done() + assert client.async_action.call_args == call(TEST_CAMERA_ID, "snapshot") From aacc009cbbd32f263af66f4c089d267ff9ceaa4c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 30 Oct 2021 08:13:03 -0600 Subject: [PATCH 0109/1452] Get Litter-Robot to 100% code coverage and minor code cleanup (#58704) --- homeassistant/components/litterrobot/select.py | 16 ++++++++-------- homeassistant/components/litterrobot/sensor.py | 2 +- homeassistant/components/litterrobot/switch.py | 2 +- homeassistant/components/litterrobot/vacuum.py | 15 +++++++-------- tests/components/litterrobot/conftest.py | 6 ++++++ tests/components/litterrobot/test_vacuum.py | 12 ++++++++++++ 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 6dfb44e97f6..ceb20b52d40 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -23,14 +23,14 @@ async def async_setup_entry( """Set up Litter-Robot selects using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] - entities = [ - LitterRobotSelect( - robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub - ) - for robot in hub.account.robots - ] - - async_add_entities(entities) + async_add_entities( + [ + LitterRobotSelect( + robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub + ) + for robot in hub.account.robots + ] + ) class LitterRobotSelect(LitterRobotControlEntity, SelectEntity): diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index cbcb75c0b23..1fab6983249 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -98,4 +98,4 @@ async def async_setup_entry( ) ) - async_add_entities(entities, True) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 3c4e4bb9937..25385ecf650 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -76,4 +76,4 @@ async def async_setup_entry( for switch_class, switch_type in ROBOT_SWITCHES: entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) - async_add_entities(entities, True) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index e40a971f43f..f1717bf0209 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATUS, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - StateVacuumEntity, + VacuumEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF @@ -47,13 +47,12 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [] - for robot in hub.account.robots: - entities.append( + async_add_entities( + [ LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) - ) - - async_add_entities(entities, True) + for robot in hub.account.robots + ] + ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -76,7 +75,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): +class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" @property diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index c408ed28819..c5355a218af 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -59,6 +59,12 @@ def mock_account_with_no_robots() -> MagicMock: return create_mock_account(skip_robots=True) +@pytest.fixture +def mock_account_with_sleeping_robot() -> MagicMock: + """Mock a Litter-Robot account with a sleeping robot.""" + return create_mock_account({"sleepModeActive": "102:00:00"}) + + @pytest.fixture def mock_account_with_error() -> MagicMock: """Mock a Litter-Robot account with error.""" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 7db0ca5dde4..aa0d38583e2 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -12,6 +12,7 @@ from homeassistant.components.litterrobot.vacuum import ( SERVICE_SET_WAIT_TIME, ) from homeassistant.components.vacuum import ( + ATTR_STATUS, DOMAIN as PLATFORM_DOMAIN, SERVICE_START, SERVICE_TURN_OFF, @@ -46,6 +47,17 @@ async def test_vacuum(hass: HomeAssistant, mock_account): assert vacuum.attributes["is_sleeping"] is False +async def test_vacuum_status_when_sleeping( + hass: HomeAssistant, mock_account_with_sleeping_robot +): + """Tests the vacuum status when sleeping.""" + await setup_integration(hass, mock_account_with_sleeping_robot, PLATFORM_DOMAIN) + + vacuum = hass.states.get(VACUUM_ENTITY_ID) + assert vacuum + assert vacuum.attributes.get(ATTR_STATUS) == "Ready (Sleeping)" + + async def test_no_robots(hass: HomeAssistant, mock_account_with_no_robots): """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) From f7dea3aa1d09a92243852c6781c7969ac9dae02e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 30 Oct 2021 08:27:48 -0600 Subject: [PATCH 0110/1452] Add Ridwell integration (#57590) --- .coveragerc | 2 + .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/ridwell/__init__.py | 84 +++++++++++ .../components/ridwell/config_flow.py | 132 +++++++++++++++++ homeassistant/components/ridwell/const.py | 9 ++ .../components/ridwell/manifest.json | 13 ++ homeassistant/components/ridwell/sensor.py | 93 ++++++++++++ homeassistant/components/ridwell/strings.json | 28 ++++ .../components/ridwell/translations/en.json | 28 ++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ridwell/__init__.py | 1 + tests/components/ridwell/test_config_flow.py | 138 ++++++++++++++++++ 16 files changed, 548 insertions(+) create mode 100644 homeassistant/components/ridwell/__init__.py create mode 100644 homeassistant/components/ridwell/config_flow.py create mode 100644 homeassistant/components/ridwell/const.py create mode 100644 homeassistant/components/ridwell/manifest.json create mode 100644 homeassistant/components/ridwell/sensor.py create mode 100644 homeassistant/components/ridwell/strings.json create mode 100644 homeassistant/components/ridwell/translations/en.json create mode 100644 tests/components/ridwell/__init__.py create mode 100644 tests/components/ridwell/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index bd19ab31b52..4f161e7b5e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -871,6 +871,8 @@ omit = homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py + homeassistant/components/ridwell/__init__.py + homeassistant/components/ridwell/sensor.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py homeassistant/components/rocketchat/notify.py diff --git a/.strict-typing b/.strict-typing index 9dd026a0b67..059ef850ab7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -101,6 +101,7 @@ homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.remote.* homeassistant.components.renault.* +homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.rpi_power.* homeassistant.components.samsungtv.* diff --git a/CODEOWNERS b/CODEOWNERS index e411e2a84cf..3534b73676d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -431,6 +431,7 @@ homeassistant/components/renault/* @epenet homeassistant/components/repetier/* @MTrab homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 +homeassistant/components/ridwell/* @bachya homeassistant/components/ring/* @balloob homeassistant/components/risco/* @OnFreund homeassistant/components/rituals_perfume_genie/* @milanmeu diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py new file mode 100644 index 00000000000..419c74456aa --- /dev/null +++ b/homeassistant/components/ridwell/__init__.py @@ -0,0 +1,84 @@ +"""The Ridwell integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta + +from aioridwell import async_get_client +from aioridwell.client import RidwellAccount, RidwellPickupEvent +from aioridwell.errors import InvalidCredentialsError, RidwellError + +from homeassistant.config_entries import ConfigEntry +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 .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, LOGGER + +DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) + +PLATFORMS: list[str] = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ridwell from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} + + session = aiohttp_client.async_get_clientsession(hass) + + try: + client = await async_get_client( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session + ) + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err + except RidwellError as err: + raise ConfigEntryNotReady(err) from err + + accounts = await client.async_get_accounts() + + async def async_update_data() -> dict[str, RidwellPickupEvent]: + """Get the latest pickup events.""" + data = {} + + async def async_get_pickups(account: RidwellAccount) -> None: + """Get the latest pickups for an account.""" + data[account.account_id] = await account.async_get_next_pickup_event() + + tasks = [async_get_pickups(account) for account in accounts.values()] + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, InvalidCredentialsError): + raise ConfigEntryAuthFailed("Invalid username/password") from result + if isinstance(result, RidwellError): + raise UpdateFailed(result) from result + + return data + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=entry.title, + update_interval=DEFAULT_UPDATE_INTERVAL, + update_method=async_update_data, + ) + + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] = accounts + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = 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 diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py new file mode 100644 index 00000000000..80c07e5bf99 --- /dev/null +++ b/homeassistant/components/ridwell/config_flow.py @@ -0,0 +1,132 @@ +"""Config flow for Ridwell integration.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from aioridwell import async_get_client +from aioridwell.errors import InvalidCredentialsError, RidwellError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, LOGGER + +STEP_REAUTH_CONFIRM_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + } +) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for WattTime.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize.""" + self._password: str | None = None + self._reauthing: bool = False + self._username: str | None = None + + @callback + def _async_show_errors( + self, errors: dict, error_step_id: str, error_schema: vol.Schema + ) -> FlowResult: + """Show an error on the correct form.""" + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors=errors, + description_placeholders={CONF_USERNAME: self._username}, + ) + + async def _async_validate( + self, error_step_id: str, error_schema: vol.Schema + ) -> FlowResult: + """Validate input credentials and proceed accordingly.""" + session = aiohttp_client.async_get_clientsession(self.hass) + + if TYPE_CHECKING: + assert self._password + assert self._username + + try: + await async_get_client(self._username, self._password, session=session) + except InvalidCredentialsError: + return self._async_show_errors( + {"base": "invalid_auth"}, error_step_id, error_schema + ) + except RidwellError as err: + LOGGER.error("Unknown Ridwell error: %s", err) + return self._async_show_errors( + {"base": "unknown"}, error_step_id, error_schema + ) + + if self._reauthing: + if existing_entry := await self.async_set_unique_id(self._username): + self.hass.config_entries.async_update_entry( + existing_entry, + data={**existing_entry.data, CONF_PASSWORD: self._password}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self._username, + data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, + ) + + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauthing = True + self._username = config[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_CONFIRM_DATA_SCHEMA, + description_placeholders={CONF_USERNAME: self._username}, + ) + + self._password = user_input[CONF_PASSWORD] + + return await self._async_validate( + "reauth_confirm", STEP_REAUTH_CONFIRM_DATA_SCHEMA + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if not user_input: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + return await self._async_validate("user", STEP_USER_DATA_SCHEMA) diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py new file mode 100644 index 00000000000..8d280bf2cc0 --- /dev/null +++ b/homeassistant/components/ridwell/const.py @@ -0,0 +1,9 @@ +"""Constants for the Ridwell integration.""" +import logging + +DOMAIN = "ridwell" + +LOGGER = logging.getLogger(__package__) + +DATA_ACCOUNT = "account" +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json new file mode 100644 index 00000000000..8c9b2e71304 --- /dev/null +++ b/homeassistant/components/ridwell/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "ridwell", + "name": "Ridwell", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ridwell", + "requirements": [ + "aioridwell==0.2.0" + ], + "codeowners": [ + "@bachya" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py new file mode 100644 index 00000000000..1938bd960e7 --- /dev/null +++ b/homeassistant/components/ridwell/sensor.py @@ -0,0 +1,93 @@ +"""Support for Ridwell sensors.""" +from __future__ import annotations + +from collections.abc import Mapping +from datetime import date, datetime +from typing import Any + +from aioridwell.client import RidwellAccount + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +from homeassistant.util.dt import as_utc + +from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN + +ATTR_CATEGORY = "category" +ATTR_PICKUP_STATE = "pickup_state" +ATTR_PICKUP_TYPES = "pickup_types" +ATTR_QUANTITY = "quantity" + +DEFAULT_ATTRIBUTION = "Pickup data provided by Ridwell" + + +@callback +def async_get_utc_midnight(target_date: date) -> datetime: + """Get UTC midnight for a given date.""" + return as_utc(datetime.combine(target_date, datetime.min.time())) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up WattTime sensors based on a config entry.""" + accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + async_add_entities( + [RidwellSensor(coordinator, account) for account in accounts.values()] + ) + + +class RidwellSensor(CoordinatorEntity, SensorEntity): + """Define a Ridwell pickup sensor.""" + + _attr_device_class = DEVICE_CLASS_TIMESTAMP + + def __init__( + self, coordinator: DataUpdateCoordinator, account: RidwellAccount + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._account = account + self._attr_name = f"Ridwell Pickup ({account.address['street1']})" + self._attr_unique_id = account.account_id + + @property + def extra_state_attributes(self) -> Mapping[str, Any]: + """Return entity specific state attributes.""" + event = self.coordinator.data[self._account.account_id] + + attrs: dict[str, Any] = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + ATTR_PICKUP_TYPES: {}, + ATTR_PICKUP_STATE: event.state, + } + + for pickup in event.pickups: + if pickup.name not in attrs[ATTR_PICKUP_TYPES]: + attrs[ATTR_PICKUP_TYPES][pickup.name] = { + ATTR_CATEGORY: pickup.category, + ATTR_QUANTITY: pickup.quantity, + } + else: + # Ridwell's API will return distinct objects, even if they have the + # same name (e.g. two pickups of Latex Paint will show up as two + # objects) – so, we sum the quantities: + attrs[ATTR_PICKUP_TYPES][pickup.name]["quantity"] += pickup.quantity + + return attrs + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + event = self.coordinator.data[self._account.account_id] + return async_get_utc_midnight(event.pickup_date).isoformat() diff --git a/homeassistant/components/ridwell/strings.json b/homeassistant/components/ridwell/strings.json new file mode 100644 index 00000000000..2c9d3708419 --- /dev/null +++ b/homeassistant/components/ridwell/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please re-enter the password for {username}:", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, + "user": { + "description": "Input your username and password:", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/ridwell/translations/en.json b/homeassistant/components/ridwell/translations/en.json new file mode 100644 index 00000000000..43315a4e45a --- /dev/null +++ b/homeassistant/components/ridwell/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}:", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Input your username and password:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b45c06b10b5..bff1305504f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -238,6 +238,7 @@ FLOWS = [ "recollect_waste", "renault", "rfxtrx", + "ridwell", "ring", "risco", "rituals_perfume_genie", diff --git a/mypy.ini b/mypy.ini index ca9dc983674..f52ee13b689 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1122,6 +1122,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ridwell.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index bfe226744e0..acc4fa3dc65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -242,6 +242,9 @@ aiopylgtv==0.4.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 +# homeassistant.components.ridwell +aioridwell==0.2.0 + # homeassistant.components.shelly aioshelly==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8741dc8d232..ba8e40248a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,9 @@ aiopylgtv==0.4.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 +# homeassistant.components.ridwell +aioridwell==0.2.0 + # homeassistant.components.shelly aioshelly==1.0.2 diff --git a/tests/components/ridwell/__init__.py b/tests/components/ridwell/__init__.py new file mode 100644 index 00000000000..7393c0b0364 --- /dev/null +++ b/tests/components/ridwell/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ridwell integration.""" diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py new file mode 100644 index 00000000000..62aae3baab5 --- /dev/null +++ b/tests/components/ridwell/test_config_flow.py @@ -0,0 +1,138 @@ +"""Test the Ridwell config flow.""" +from unittest.mock import AsyncMock, patch + +from aioridwell.errors import InvalidCredentialsError, RidwellError +import pytest + +from homeassistant import config_entries +from homeassistant.components.ridwell.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="client") +def client_fixture(): + """Define a fixture for an aioridwell client.""" + return AsyncMock(return_value=None) + + +@pytest.fixture(name="client_login") +def client_login_fixture(client): + """Define a fixture for patching the aioridwell coroutine to get a client.""" + with patch( + "homeassistant.components.ridwell.config_flow.async_get_client" + ) as mock_client: + mock_client.side_effect = client + yield mock_client + + +async def test_duplicate_error(hass: HomeAssistant): + """Test that errors are shown when duplicate entries are added.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="user@email.com", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_show_form_user(hass: HomeAssistant) -> None: + """Test showing the form to input credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] is None + + +async def test_step_reauth(hass: HomeAssistant, client_login) -> None: + """Test a full reauth flow.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="user@email.com", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.ridwell.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_step_user(hass: HomeAssistant, client_login) -> None: + """Test that the full user step succeeds.""" + with patch( + "homeassistant.components.ridwell.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + +@pytest.mark.parametrize( + "client", + [AsyncMock(side_effect=InvalidCredentialsError)], +) +async def test_step_user_invalid_credentials(hass: HomeAssistant, client_login) -> None: + """Test that invalid credentials are handled correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "client", + [AsyncMock(side_effect=RidwellError)], +) +async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> None: + """Test that an unknown Ridwell error is handled correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From a48ddcadd4b4d04110b8a7edbb11b08d6b5f052f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 16:29:07 +0200 Subject: [PATCH 0111/1452] Use assignment expressions 31 (#58715) --- homeassistant/components/enocean/sensor.py | 3 +-- homeassistant/components/envisalink/__init__.py | 3 +-- homeassistant/components/esphome/entry_data.py | 3 +-- homeassistant/components/fastdotcom/sensor.py | 3 +-- homeassistant/components/generic_hygrostat/humidifier.py | 3 +-- homeassistant/components/harmony/remote.py | 3 +-- homeassistant/components/integration/sensor.py | 3 +-- homeassistant/components/iperf3/sensor.py | 3 +-- homeassistant/components/isy994/light.py | 3 +-- homeassistant/components/limitlessled/light.py | 3 +-- homeassistant/components/lovelace/dashboard.py | 4 +--- homeassistant/components/lovelace/resources.py | 4 +--- homeassistant/components/manual/alarm_control_panel.py | 3 +-- homeassistant/components/owntracks/device_tracker.py | 4 +--- homeassistant/components/person/__init__.py | 3 +-- homeassistant/components/pilight/base_class.py | 3 +-- homeassistant/components/rest/__init__.py | 3 +-- homeassistant/components/rpi_gpio_pwm/light.py | 6 ++---- homeassistant/components/smartthings/smartapp.py | 3 +-- homeassistant/components/speedtestdotnet/sensor.py | 3 +-- homeassistant/components/switchbot/switch.py | 3 +-- homeassistant/components/template/switch.py | 3 +-- homeassistant/components/traccar/device_tracker.py | 3 +-- homeassistant/components/utility_meter/sensor.py | 3 +-- homeassistant/components/websocket_api/http.py | 3 +-- homeassistant/helpers/storage.py | 4 +--- 26 files changed, 27 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index ca0f5e95109..9c4c9df73e9 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -139,8 +139,7 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): if self._attr_native_value is not None: return - state = await self.async_get_last_state() - if state is not None: + if (state := await self.async_get_last_state()) is not None: self._attr_native_value = state.state def value_changed(self, packet): diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 75d4bff3dd1..d5a8b39e8f9 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -198,8 +198,7 @@ async def async_setup(hass, config): _LOGGER.info("Start envisalink") controller.start() - result = await sync_connect - if not result: + if not await sync_connect: return False # Load sub-components for Envisalink diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 51fc18ee37e..847997731d4 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -137,8 +137,7 @@ class RuntimeEntryData: async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserService]]: """Load the retained data from store and return de-serialized data.""" - restored = await self.store.async_load() - if restored is None: + if (restored := await self.store.async_load()) is None: return [], [] restored = cast("dict[str, Any]", restored) self._storage_contents = restored.copy() diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 2a82cee7cea..8363981b526 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -49,8 +49,7 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): ) ) - state = await self.async_get_last_state() - if not state: + if not (state := await self.async_get_last_state()): return self._attr_native_value = state.state diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 726b6e654e7..383674a7f75 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -171,8 +171,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) - old_state = await self.async_get_last_state() - if old_state is not None: + if (old_state := await self.async_get_last_state()) is not None: if old_state.attributes.get(ATTR_MODE) == MODE_AWAY: self._is_away = True self._saved_target_humidity = self._target_humidity diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 3431eff7994..91022cf8d42 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -144,8 +144,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): # Restore the last activity so we know # how what to turn on if nothing # is specified - last_state = await self.async_get_last_state() - if not last_state: + if not (last_state := await self.async_get_last_state()): return if ATTR_LAST_ACTIVITY not in last_state.attributes: return diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index a2fd77fb4e1..22e80e7879e 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -124,8 +124,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): 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() - if state: + if state := await self.async_get_last_state(): try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index dfc4abba707..4e102b69782 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -60,8 +60,7 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): ) ) - state = await self.async_get_last_state() - if not state: + if not (state := await self.async_get_last_state()): return self._attr_native_value = state.state diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 509fd259830..3d208d18fa9 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -117,8 +117,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): await super().async_added_to_hass() self._last_brightness = self.brightness or 255 - last_state = await self.async_get_last_state() - if not last_state: + if not (last_state := await self.async_get_last_state()): return if ( diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index ac307f68d08..41b8f446541 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -231,8 +231,7 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): async def async_added_to_hass(self): """Handle entity about to be added to hass event.""" await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: + if last_state := await self.async_get_last_state(): self._is_on = last_state.state == STATE_ON self._brightness = last_state.attributes.get("brightness") self._temperature = last_state.attributes.get("color_temp") diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 3c9fb03d863..c02a65cc425 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -235,9 +235,7 @@ class DashboardsCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" - data = await self.store.async_load() - - if data is None: + if (data := await self.store.async_load()) is None: return cast(Optional[dict], data) updated = False diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 2a098361962..22297c54d6c 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -70,9 +70,7 @@ class ResourceStorageCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" - data = await self.store.async_load() - - if data is not None: + if (data := await self.store.async_load()) is not None: return cast(Optional[dict], data) # Import it from config. diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index d8b1ed088e3..39fc032b4b7 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -427,8 +427,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): if ( state.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING) and hasattr(state, "attributes") diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 7ba9346013f..ca6f4f4a343 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -130,9 +130,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): if self._data: return - state = await self.async_get_last_state() - - if state is None: + if (state := await self.async_get_last_state()) is None: return attr = state.attributes diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 4b7d6a54b1f..23b9bafcc47 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -420,8 +420,7 @@ class Person(RestoreEntity): async def async_added_to_hass(self): """Register device trackers.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._parse_source_state(state) if self.hass.is_running: diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py index 95eb987875b..97ebaef0080 100644 --- a/homeassistant/components/pilight/base_class.py +++ b/homeassistant/components/pilight/base_class.py @@ -86,8 +86,7 @@ class PilightBaseDevice(RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._is_on = state.state == STATE_ON self._brightness = state.attributes.get("brightness") diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index f2cffebdfcb..ba101624673 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -52,8 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def reload_service_handler(service): """Remove all user-defined groups and load new ones from config.""" - conf = await component.async_prepare_reload() - if conf is None: + if (conf := await component.async_prepare_reload()) is None: return await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) _async_setup_shared_data(hass) diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index 0673047c6a7..efd3f03c9c0 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -117,8 +117,7 @@ class PwmSimpleLed(LightEntity, RestoreEntity): async def async_added_to_hass(self): """Handle entity about to be added to hass event.""" await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: + if last_state := await self.async_get_last_state(): self._is_on = last_state.state == STATE_ON self._brightness = last_state.attributes.get( "brightness", DEFAULT_BRIGHTNESS @@ -193,8 +192,7 @@ class PwmRgbLed(PwmSimpleLed): async def async_added_to_hass(self): """Handle entity about to be added to hass event.""" await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: + if last_state := await self.async_get_last_state(): self._color = last_state.attributes.get("hs_color", DEFAULT_COLOR) @property diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 2086d564753..8feb5f512d6 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -211,8 +211,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): # Get/create config to store a unique id for this hass instance. store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - config = await store.async_load() - if not config: + if not (config := await store.async_load()): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 06f180a570f..e6d2138bfc3 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -100,6 +100,5 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._state = state.state diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 22e4bb33f1a..a2064eb16d6 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -117,8 +117,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if not last_state: + if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON self._last_run_success = last_state.attributes["last_run_success"] diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 0e083df13f4..a654e59eaa2 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -148,8 +148,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): # restore state after startup await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._state = state.state == STATE_ON # no need to listen for events diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 16cd9ba94e5..d800123e3fa 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -402,8 +402,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity): if self._latitude is not None or self._longitude is not None: return - state = await self.async_get_last_state() - if state is None: + if (state := await self.async_get_last_state()) is None: self._latitude = None self._longitude = None self._accuracy = None diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 64cde15aa4f..69770ec2445 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -297,8 +297,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): async_dispatcher_connect(self.hass, SIGNAL_RESET_METER, self.async_reset_meter) - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): try: self._state = Decimal(state.state) except InvalidOperation: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 8d75d50e59a..22fa9816f4e 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -73,8 +73,7 @@ class WebSocketHandler: # Exceptions if Socket disconnected or cancelled by connection handler with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): while not self.wsock.closed: - message = await self._to_write.get() - if message is None: + if (message := await self._to_write.get()) is None: break self._logger.debug("Sending %s", message) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 116c9186149..4d69d3be070 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -37,10 +37,8 @@ async def async_migrator( async def old_conf_migrate_func(old_data) """ - store_data = await store.async_load() - # If we already have store data we have already migrated in the past. - if store_data is not None: + if (store_data := await store.async_load()) is not None: return store_data def load_old_config(): From 887d04be60ff78eaf6e6802837e1fb998fd4b579 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 16:30:13 +0200 Subject: [PATCH 0112/1452] Use assignment expressions 32 (#58716) --- homeassistant/components/counter/__init__.py | 17 +++++++++-------- .../components/elkm1/alarm_control_panel.py | 3 +-- .../components/fireservicerota/sensor.py | 3 +-- homeassistant/components/hassio/__init__.py | 4 +--- homeassistant/components/homekit/aidmanager.py | 3 +-- .../components/input_datetime/__init__.py | 3 +-- .../components/modbus/base_platform.py | 3 +-- .../components/modbus/binary_sensor.py | 3 +-- homeassistant/components/modbus/cover.py | 3 +-- homeassistant/components/modbus/sensor.py | 3 +-- homeassistant/components/mqtt/number.py | 6 ++---- homeassistant/components/mqtt/select.py | 6 ++---- homeassistant/components/mqtt/switch.py | 6 ++---- .../components/pvpc_hourly_pricing/sensor.py | 3 +-- .../components/smart_meter_texas/sensor.py | 3 +-- homeassistant/components/zha/entity.py | 3 +-- 16 files changed, 27 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 75b2b4902cb..d035d658206 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -240,14 +240,15 @@ class Counter(RestoreEntity): await super().async_added_to_hass() # __init__ will set self._state to self._initial, only override # if needed. - if self._config[CONF_RESTORE]: - state = await self.async_get_last_state() - if state is not None: - self._state = self.compute_next_state(int(state.state)) - self._config[CONF_INITIAL] = state.attributes.get(ATTR_INITIAL) - self._config[CONF_MAXIMUM] = state.attributes.get(ATTR_MAXIMUM) - self._config[CONF_MINIMUM] = state.attributes.get(ATTR_MINIMUM) - self._config[CONF_STEP] = state.attributes.get(ATTR_STEP) + if ( + self._config[CONF_RESTORE] + and (state := await self.async_get_last_state()) is not None + ): + self._state = self.compute_next_state(int(state.state)) + self._config[CONF_INITIAL] = state.attributes.get(ATTR_INITIAL) + self._config[CONF_MAXIMUM] = state.attributes.get(ATTR_MAXIMUM) + self._config[CONF_MINIMUM] = state.attributes.get(ATTR_MINIMUM) + self._config[CONF_STEP] = state.attributes.get(ATTR_STEP) @callback def async_decrement(self) -> None: diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 5b3a20b3448..04881b9f085 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -117,8 +117,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): self._element.add_callback(self._watch_area) # We do not get changed_by back from resync. - last_state = await self.async_get_last_state() - if not last_state: + if not (last_state := await self.async_get_last_state()): return if ATTR_CHANGED_BY_KEYPAD in last_state.attributes: diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index a87b1609ec9..f2e415daec9 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -102,8 +102,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): """Run when about to be added to hass.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._state = state.state self._state_attributes = state.attributes if "id" in self._state_attributes: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6ad1c67f1f3..ef273b388ca 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -421,9 +421,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: _LOGGER.warning("Not connected with the supervisor / system too busy!") store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - data = await store.async_load() - - if data is None: + if (data := await store.async_load()) is None: data = {} refresh_token = None diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 0f5e29426a8..c7ddc29a788 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -82,8 +82,7 @@ class AccessoryAidStorage: aidstore = get_aid_storage_filename_for_entry_id(self._entry) self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) - raw_storage = await self.store.async_load() - if not raw_storage: + if not (raw_storage := await self.store.async_load()): # There is no data about aid allocations yet return diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index de767d69ba8..d1a89fca67b 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -270,8 +270,7 @@ class InputDatetime(RestoreEntity): default_value = py_datetime.datetime.today().strftime("%Y-%m-%d 00:00:00") # Priority 2: Old state - old_state = await self.async_get_last_state() - if old_state is None: + if (old_state := await self.async_get_last_state()) is None: self._current_datetime = dt_util.parse_datetime(default_value) return diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index c962f298586..682b3430807 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -254,8 +254,7 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._attr_is_on = state.state == STATE_ON async def async_turn(self, command: int) -> None: diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 171486639f4..07756b0f207 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -41,8 +41,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._attr_is_on = state.state == STATE_ON async def async_update(self, now: datetime | None = None) -> None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 152239e88c7..1a9a7a82e9c 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -100,8 +100,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): convert = { STATE_CLOSED: self._state_closed, STATE_CLOSING: self._state_closing, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6911098dd94..e4249594940 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -53,8 +53,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._attr_native_value = state.state async def async_update(self, now: datetime | None = None) -> None: diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e3ff03b2fbe..f39ca870505 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -190,10 +190,8 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): }, ) - if self._optimistic: - last_state = await self.async_get_last_state() - if last_state: - self._current_number = last_state.state + if self._optimistic and (last_state := await self.async_get_last_state()): + self._current_number = last_state.state @property def min_value(self) -> float: diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 6ef0dbc3776..3857184a330 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -157,10 +157,8 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): }, ) - if self._optimistic: - last_state = await self.async_get_last_state() - if last_state: - self._attr_current_option = last_state.state + if self._optimistic and (last_state := await self.async_get_last_state()): + self._attr_current_option = last_state.state async def async_select_option(self, option: str) -> None: """Update the current value.""" diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 13a241f7ab1..d3252525b76 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -145,10 +145,8 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): }, ) - if self._optimistic: - last_state = await self.async_get_last_state() - if last_state: - self._state = last_state.state == STATE_ON + if self._optimistic and (last_state := await self.async_get_last_state()): + self._state = last_state.state == STATE_ON @property def is_on(self): diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 9cc5603e35b..1000d23b5bf 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -66,8 +66,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): self._pvpc_data.state = state.state # Update 'state' value in hour changes diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index ed5c84f0bce..e897048660f 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -92,7 +92,6 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): if self.coordinator.last_update_success: return - last_state = await self.async_get_last_state() - if last_state: + if last_state := await self.async_get_last_state(): self._state = last_state.state self._available = True diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 0ba75a99306..80697c704bf 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -206,8 +206,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): if not self.zha_device.is_mains_powered: # mains powered devices will get real time state - last_state = await self.async_get_last_state() - if last_state: + if last_state := await self.async_get_last_state(): self.async_restore_last_state(last_state) self.async_accept_signal( From b1d49b3b663a65b4ae1b149b4e558bba0e64c46e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 16:31:43 +0200 Subject: [PATCH 0113/1452] Use assignment expressions 29 (#58713) --- .../components/acer_projector/switch.py | 3 +-- homeassistant/components/blueprint/importer.py | 7 ++----- .../components/conversation/default_agent.py | 4 +--- homeassistant/components/history/__init__.py | 16 +++++----------- .../components/input_datetime/__init__.py | 15 +++++---------- homeassistant/components/lcn/helpers.py | 3 +-- homeassistant/components/litterrobot/entity.py | 4 +--- homeassistant/components/minio/minio_helper.py | 3 +-- homeassistant/components/mqtt/discovery.py | 3 +-- homeassistant/components/openuv/sensor.py | 3 +-- homeassistant/components/script/__init__.py | 12 +++--------- homeassistant/components/tts/__init__.py | 6 ++---- .../components/xiaomi_miio/config_flow.py | 3 +-- homeassistant/helpers/entity_component.py | 4 +--- homeassistant/helpers/intent.py | 3 +-- 15 files changed, 27 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 747c7c98d73..f3e4b3b03e5 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -111,8 +111,7 @@ class AcerSwitch(SwitchEntity): """Write msg, obtain answer and format output.""" # answers are formatted as ***\answer\r*** awns = self._write_read(msg) - match = re.search(r"\r(.+)\r", awns) - if match: + if match := re.search(r"\r(.+)\r", awns): return match.group(1) return STATE_UNKNOWN diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 99dffb114e1..de39741d8ed 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -59,9 +59,7 @@ def _get_github_import_url(url: str) -> str: if url.startswith("https://raw.githubusercontent.com/"): return url - match = GITHUB_FILE_PATTERN.match(url) - - if match is None: + if (match := GITHUB_FILE_PATTERN.match(url)) is None: raise UnsupportedUrl("Not a GitHub file url") repo, path = match.groups() @@ -74,8 +72,7 @@ def _get_community_post_import_url(url: str) -> str: Async friendly. """ - match = COMMUNITY_TOPIC_PATTERN.match(url) - if match is None: + if (match := COMMUNITY_TOPIC_PATTERN.match(url)) is None: raise UnsupportedUrl("Not a topic url") _topic, post = match.groups() diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d957eb8e0b2..405a4f818f4 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -118,9 +118,7 @@ class DefaultAgent(AbstractConversationAgent): for intent_type, matchers in intents.items(): for matcher in matchers: - match = matcher.match(text) - - if not match: + if not (match := matcher.match(text)): continue return await intent.async_handle( diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 2ac9a77c025..960b9749220 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -130,16 +130,14 @@ async def ws_get_statistics_during_period( start_time_str = msg["start_time"] end_time_str = msg.get("end_time") - start_time = dt_util.parse_datetime(start_time_str) - if start_time: + if start_time := dt_util.parse_datetime(start_time_str): start_time = dt_util.as_utc(start_time) else: connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") return if end_time_str: - end_time = dt_util.parse_datetime(end_time_str) - if end_time: + if end_time := dt_util.parse_datetime(end_time_str): end_time = dt_util.as_utc(end_time) else: connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") @@ -194,11 +192,8 @@ class HistoryPeriodView(HomeAssistantView): ) -> web.Response: """Return history over a period of time.""" datetime_ = None - if datetime: - datetime_ = dt_util.parse_datetime(datetime) - - if datetime_ is None: - return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) + if datetime and (datetime_ := dt_util.parse_datetime(datetime)) is None: + return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) now = dt_util.utcnow() @@ -212,8 +207,7 @@ class HistoryPeriodView(HomeAssistantView): return self.json([]) if end_time_str := request.query.get("end_time"): - end_time = dt_util.parse_datetime(end_time_str) - if end_time: + if end_time := dt_util.parse_datetime(end_time_str): end_time = dt_util.as_utc(end_time) else: return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index d1a89fca67b..c25680b7180 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -87,19 +87,16 @@ def valid_initial(conf): return conf if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: - parsed_value = dt_util.parse_datetime(initial) - if parsed_value is not None: + if dt_util.parse_datetime(initial) is not None: return conf raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime") if conf[CONF_HAS_DATE]: - parsed_value = dt_util.parse_date(initial) - if parsed_value is not None: + if dt_util.parse_date(initial) is not None: return conf raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date") - parsed_value = dt_util.parse_time(initial) - if parsed_value is not None: + if dt_util.parse_time(initial) is not None: return conf raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time") @@ -282,15 +279,13 @@ class InputDatetime(RestoreEntity): current_datetime = date_time elif self.has_date: - date = dt_util.parse_date(old_state.state) - if date is None: + if (date := dt_util.parse_date(old_state.state)) is None: current_datetime = dt_util.parse_datetime(default_value) else: current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) else: - time = dt_util.parse_time(old_state.state) - if time is None: + if (time := dt_util.parse_time(old_state.state)) is None: current_datetime = dt_util.parse_datetime(default_value) else: current_datetime = py_datetime.datetime.combine( diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 657657ea1d0..feb27e337e8 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -385,8 +385,7 @@ def is_address(value: str) -> tuple[AddressType, str]: myhome.0.g11 myhome.s0.g11 """ - matcher = PATTERN_ADDRESS.match(value) - if matcher: + if matcher := PATTERN_ADDRESS.match(value): is_group = matcher.group("type") == "g" addr = (int(matcher.group("seg_id")), int(matcher.group("id")), is_group) conn_id = matcher.group("conn_id") diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index fbcd129411a..5a6590f9360 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -99,9 +99,7 @@ class LitterRobotControlEntity(LitterRobotEntity): @staticmethod def parse_time_at_default_timezone(time_str: str) -> time | None: """Parse a time string and add default timezone.""" - parsed_time = dt_util.parse_time(time_str) - - if parsed_time is None: + if (parsed_time := dt_util.parse_time(time_str)) is None: return None return ( diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index b7fb3157c71..4f10da10998 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -22,8 +22,7 @@ def normalize_metadata(metadata: dict) -> dict: """Normalize object metadata by stripping the prefix.""" new_metadata = {} for meta_key, meta_value in metadata.items(): - match = _METADATA_RE.match(meta_key) - if not match: + if not (match := _METADATA_RE.match(meta_key)): continue new_metadata[match.group(1).lower()] = meta_value diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index ffc0c1de435..d8ab9193cc4 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -97,9 +97,8 @@ async def async_start( # noqa: C901 payload = msg.payload topic = msg.topic topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) - match = TOPIC_MATCHER.match(topic_trimmed) - if not match: + if not (match := TOPIC_MATCHER.match(topic_trimmed)): if topic_trimmed.endswith("config"): _LOGGER.warning( "Received message on illegal discovery topic '%s'", topic diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index f1d0ba9e0b1..bcfac6e3684 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -157,8 +157,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_native_value = UV_LEVEL_LOW elif self.entity_description.key == TYPE_MAX_UV_INDEX: self._attr_native_value = data["uv_max"] - uv_max_time = parse_datetime(data["uv_max_time"]) - if uv_max_time: + if uv_max_time := parse_datetime(data["uv_max_time"]): self._attr_extra_state_attributes.update( {ATTR_MAX_UV_TIME: as_local(uv_max_time)} ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index b07cc2325b1..14160268073 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -96,9 +96,7 @@ def entities_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - script_entity = component.get_entity(entity_id) - - if script_entity is None: + if (script_entity := component.get_entity(entity_id)) is None: return [] return list(script_entity.script.referenced_entities) @@ -127,9 +125,7 @@ def devices_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - script_entity = component.get_entity(entity_id) - - if script_entity is None: + if (script_entity := component.get_entity(entity_id)) is None: return [] return list(script_entity.script.referenced_devices) @@ -158,9 +154,7 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - script_entity = component.get_entity(entity_id) - - if script_entity is None: + if (script_entity := component.get_entity(entity_id)) is None: return [] return list(script_entity.script.referenced_areas) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 00e1dae6a8c..e2fddf3afe5 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -462,8 +462,7 @@ class SpeechManager: This method is a coroutine. """ - record = _RE_VOICE_FILE.match(filename.lower()) - if not record: + if not (record := _RE_VOICE_FILE.match(filename.lower())): raise HomeAssistantError("Wrong tts file format!") key = KEY_PATTERN.format( @@ -571,8 +570,7 @@ def _get_cache_files(cache_dir): folder_data = os.listdir(cache_dir) for file_data in folder_data: - record = _RE_VOICE_FILE.match(file_data) - if record: + if record := _RE_VOICE_FILE.match(file_data): key = KEY_PATTERN.format( record.group(1), record.group(2), record.group(3), record.group(4) ) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 96a06f6a33d..744d80494c8 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -161,8 +161,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.mac = discovery_info.get("properties", {}).get("mac") if self.mac is None: poch = discovery_info.get("properties", {}).get("poch", "") - result = search(r"mac=\w+", poch) - if result is not None: + if (result := search(r"mac=\w+", poch)) is not None: self.mac = result.group(0).split("=")[1] if not name or not self.host or not self.mac: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index d65f485166b..fb1abcbf7e9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -49,9 +49,7 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: ) return - entity_obj = entity_comp.get_entity(entity_id) - - if entity_obj is None: + if (entity_obj := entity_comp.get_entity(entity_id)) is None: logging.getLogger(__name__).warning( "Forced update failed. Entity %s not found.", entity_id ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index a153d994471..d3494c3f41b 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -168,8 +168,7 @@ def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> T | N pattern = ".*?".join(name) regex = re.compile(pattern, re.IGNORECASE) for idx, item in enumerate(items): - match = regex.search(key(item)) - if match: + if match := regex.search(key(item)): # Add key length so we prefer shorter keys with the same group and start. # Add index so we pick first match in case same group, start, and key length. matches.append( From 7063c05127d40b3707d683b71e5ac5bd9bd86e84 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 16:32:41 +0200 Subject: [PATCH 0114/1452] Use assignment expressions 33 (#58717) --- homeassistant/components/alexa/config.py | 3 +-- homeassistant/components/august/lock.py | 3 +-- homeassistant/components/automation/__init__.py | 6 ++---- homeassistant/components/camera/prefs.py | 4 +--- homeassistant/components/cloud/prefs.py | 4 +--- homeassistant/components/derivative/sensor.py | 3 +-- homeassistant/components/device_tracker/legacy.py | 3 +-- homeassistant/components/generic_thermostat/climate.py | 3 +-- homeassistant/components/geofency/device_tracker.py | 4 +--- homeassistant/components/google_assistant/helpers.py | 3 +-- homeassistant/components/gpslogger/device_tracker.py | 3 +-- homeassistant/components/group/__init__.py | 3 +-- homeassistant/components/homekit_controller/storage.py | 3 +-- homeassistant/components/mobile_app/__init__.py | 3 +-- homeassistant/components/mobile_app/device_tracker.py | 4 +--- homeassistant/components/mobile_app/entity.py | 3 +-- homeassistant/components/onboarding/__init__.py | 4 +--- homeassistant/components/rflink/__init__.py | 4 +--- homeassistant/components/rflink/cover.py | 4 +--- homeassistant/components/script/__init__.py | 3 +-- homeassistant/components/unifi/__init__.py | 4 +--- 21 files changed, 22 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index cc5c604dc8c..739ce6be6a3 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -64,8 +64,7 @@ class AbstractConfig(ABC): async def async_disable_proactive_mode(self): """Disable proactive mode.""" - unsub_func = await self._unsub_proactive_report - if unsub_func: + if unsub_func := await self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 5f4fe85bc71..665b0036557 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -119,8 +119,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if not last_state: + if not (last_state := await self.async_get_last_state()): return if ATTR_CHANGED_BY in last_state.attributes: diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 92fbd0e8b04..08764b35ea3 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -262,8 +262,7 @@ async def async_setup(hass, config): async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" - conf = await component.async_prepare_reload() - if conf is None: + if (conf := await component.async_prepare_reload()) is None: return async_get_blueprints(hass).async_reset_cache() await _async_process_config(hass, conf, component) @@ -392,8 +391,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) self.action_script.update_logger(self._logger) - state = await self.async_get_last_state() - if state: + if state := await self.async_get_last_state(): enable_automation = state.state == STATE_ON last_triggered = state.attributes.get("last_triggered") if last_triggered is not None: diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 36f3d60d0db..53a149ff7d8 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -40,9 +40,7 @@ class CameraPreferences: async def async_initialize(self) -> None: """Finish initializing the preferences.""" - prefs = await self._store.async_load() - - if prefs is None: + if (prefs := await self._store.async_load()) is None: prefs = {} self._prefs = prefs diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a4c81bcc64f..816ebe26d24 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -54,9 +54,7 @@ class CloudPreferences: async def async_initialize(self): """Finish initializing the preferences.""" - prefs = await self._store.async_load() - - if prefs is None: + if (prefs := await self._store.async_load()) is None: prefs = self._empty_config("") self._prefs = prefs diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 45f5db57a90..f6de217ff2b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -122,8 +122,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): 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() - if state is not None: + if (state := await self.async_get_last_state()) is not None: try: self._state = Decimal(state.state) except SyntaxError as err: diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 94638c031a3..d81743c530a 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -788,8 +788,7 @@ class Device(RestoreEntity): async def async_added_to_hass(self) -> None: """Add an entity.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if not state: + if not (state := await self.async_get_last_state()): return self._state = state.state self.last_update_home = state.state == STATE_HOME diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 4d52240535f..2c27d371c5e 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -240,8 +240,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state - old_state = await self.async_get_last_state() - if old_state is not None: + if (old_state := await self.async_get_last_state()) is not None: # If we have no initial temperature, restore if self._target_temp is None: # If we have a previously saved temperature diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index b2d26dcb2a5..2904d1a1b1d 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -106,9 +106,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): if self._attributes: return - state = await self.async_get_last_state() - - if state is None: + if (state := await self.async_get_last_state()) is None: self._gps = (None, None) return diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 14667dbb303..76933cecd91 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -346,8 +346,7 @@ class GoogleConfigStore: async def async_load(self): """Store current configuration to disk.""" - data = await self._store.async_load() - if data: + if data := await self._store.async_load(): self._data = data diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 8b0965cc434..18b5b7fa585 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -132,8 +132,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): if self._location is not None: return - state = await self.async_get_last_state() - if state is None: + if (state := await self.async_get_last_state()) is None: self._location = (None, None) self._accuracy = None self._attributes = { diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 523b45a94f7..e3816d52d60 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -224,8 +224,7 @@ async def async_setup(hass, config): """Remove all user-defined groups and load new ones from config.""" auto = list(filter(lambda e: not e.user_defined, component.entities)) - conf = await component.async_prepare_reload() - if conf is None: + if (conf := await component.async_prepare_reload()) is None: return await _async_process_config(hass, conf, component) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ffc5bdc2381..4d512fbbc5d 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -34,8 +34,7 @@ class EntityMapStorage: async def async_initialize(self): """Get the pairing cache data.""" - raw_storage = await self.store.async_load() - if not raw_storage: + if not (raw_storage := await self.store.async_load()): # There is no cached data about HomeKit devices yet return diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 47e21d53515..a83931bab23 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -37,8 +37,7 @@ PLATFORMS = "sensor", "binary_sensor", "device_tracker" 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() - if app_config is None: + if (app_config := await store.async_load()) is None: app_config = { DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index f4b5866eacd..28f5e13c4fd 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -119,9 +119,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): if self._data is not None: return - state = await self.async_get_last_state() - - if state is None: + if (state := await self.async_get_last_state()) is None: self._data = {} return diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 0cdec984f55..03b6d95c2a2 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -44,9 +44,8 @@ class MobileAppEntity(RestoreEntity): self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update ) ) - state = await self.async_get_last_state() - if state is None: + if (state := await self.async_get_last_state()) is None: return self.async_restore_last_state(state) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index e383e4e32c4..c2ab9a495bf 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -50,9 +50,7 @@ def async_is_user_onboarded(hass): async def async_setup(hass, config): """Set up the onboarding component.""" store = OnboadingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True) - data = await store.async_load() - - if data is None: + if (data := await store.async_load()) is None: data = {"done": []} if STEP_USER not in data["done"]: diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 9cff8377c35..e0ce94cb723 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -580,9 +580,7 @@ class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): async def async_added_to_hass(self): """Restore RFLink device state (ON/OFF).""" await super().async_added_to_hass() - - old_state = await self.async_get_last_state() - if old_state is not None: + if (old_state := await self.async_get_last_state()) is not None: self._state = old_state.state == STATE_ON def _handle_event(self, event): diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 2e6837d21ea..b98fb0fe4a9 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -117,9 +117,7 @@ class RflinkCover(RflinkCommand, CoverEntity, RestoreEntity): async def async_added_to_hass(self): """Restore RFLink cover state (OPEN/CLOSE).""" await super().async_added_to_hass() - - old_state = await self.async_get_last_state() - if old_state is not None: + if (old_state := await self.async_get_last_state()) is not None: self._state = old_state.state == STATE_OPEN def _handle_event(self, event): diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 14160268073..a8ffb271336 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -172,8 +172,7 @@ async def async_setup(hass, config): async def reload_service(service): """Call a service to reload scripts.""" - conf = await component.async_prepare_reload() - if conf is None: + if (conf := await component.async_prepare_reload()) is None: return await _async_process_config(hass, conf, component) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index b935a7d01da..eb026643515 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -106,9 +106,7 @@ class UnifiWirelessClients: async def async_load(self): """Load data from file.""" - data = await self._store.async_load() - - if data is not None: + if (data := await self._store.async_load()) is not None: self.data = data @callback From 84618fa831e1ea80ab376613b43879c950c62c89 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 16:33:42 +0200 Subject: [PATCH 0115/1452] Use assignment expressions 30 (#58714) --- homeassistant/components/aruba/device_tracker.py | 3 +-- homeassistant/components/automation/__init__.py | 12 +++--------- homeassistant/components/bond/light.py | 3 +-- homeassistant/components/camera/__init__.py | 8 ++------ homeassistant/components/dlna_dmr/config_flow.py | 3 +-- homeassistant/components/dlna_dmr/media_player.py | 9 +++------ homeassistant/components/hangouts/hangouts_bot.py | 4 +--- .../components/homekit_controller/config_flow.py | 3 +-- homeassistant/components/lcn/helpers.py | 4 +--- homeassistant/components/logbook/__init__.py | 7 ++----- homeassistant/components/media_player/__init__.py | 6 ++---- homeassistant/components/media_source/models.py | 4 +--- homeassistant/components/melcloud/__init__.py | 3 +-- homeassistant/components/octoprint/binary_sensor.py | 3 +-- homeassistant/components/octoprint/sensor.py | 3 +-- homeassistant/components/pandora/media_player.py | 9 +++------ homeassistant/components/recorder/statistics.py | 3 +-- homeassistant/components/rflink/light.py | 3 +-- homeassistant/components/system_log/__init__.py | 3 +-- homeassistant/components/tado/climate.py | 3 +-- homeassistant/components/thomson/device_tracker.py | 3 +-- homeassistant/components/todoist/calendar.py | 3 +-- .../components/yamaha_musiccast/media_player.py | 4 +--- homeassistant/components/zeroconf/__init__.py | 4 +--- 24 files changed, 33 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index 49074dba3be..e9799411197 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -125,8 +125,7 @@ class ArubaDeviceScanner(DeviceScanner): devices = {} for device in devices_result: - match = _DEVICES_REGEX.search(device.decode("utf-8")) - if match: + if match := _DEVICES_REGEX.search(device.decode("utf-8")): devices[match.group("ip")] = { "ip": match.group("ip"), "mac": match.group("mac").upper(), diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 08764b35ea3..699fdc8745b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -156,9 +156,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - automation_entity = component.get_entity(entity_id) - - if automation_entity is None: + if (automation_entity := component.get_entity(entity_id)) is None: return [] return list(automation_entity.referenced_entities) @@ -187,9 +185,7 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - automation_entity = component.get_entity(entity_id) - - if automation_entity is None: + if (automation_entity := component.get_entity(entity_id)) is None: return [] return list(automation_entity.referenced_devices) @@ -218,9 +214,7 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: component = hass.data[DOMAIN] - automation_entity = component.get_entity(entity_id) - - if automation_entity is None: + if (automation_entity := component.get_entity(entity_id)) is None: return [] return list(automation_entity.referenced_areas) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index dd4699ad006..255f848c167 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -180,8 +180,7 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness: + if brightness := kwargs.get(ATTR_BRIGHTNESS): await self._hub.bond.action( self._device.device_id, Action.set_brightness(round((brightness * 100) / 255)), diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5a3d730e7d3..4aa443c2ed6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -275,9 +275,7 @@ def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: if (component := hass.data.get(DOMAIN)) is None: raise HomeAssistantError("Camera integration not set up") - camera = component.get_entity(entity_id) - - if camera is None: + if (camera := component.get_entity(entity_id)) is None: raise HomeAssistantError("Camera not found") if not camera.is_on: @@ -596,9 +594,7 @@ class CameraView(HomeAssistantView): async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse: """Start a GET request.""" - camera = self.component.get_entity(entity_id) - - if camera is None: + if (camera := self.component.get_entity(entity_id)) is None: raise web.HTTPNotFound() camera = cast(Camera, camera) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 454a28c9f7d..2ac74fb4cbd 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -81,8 +81,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("async_step_user: user_input: %s", user_input) if user_input is not None: - host = user_input.get(CONF_HOST) - if not host: + if not (host := user_input.get(CONF_HOST)): # No device chosen, user might want to directly enter an URL return await self.async_step_manual() # User has chosen a device, ask for confirmation diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 2b699735108..68a41e1fa62 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -613,8 +613,7 @@ class DlnaDmrEntity(MediaPlayerEntity): metadata: dict[str, Any] = extra.get("metadata") or {} title = extra.get("title") or metadata.get("title") or "Home Assistant" - thumb = extra.get("thumb") - if thumb: + if thumb := extra.get("thumb"): metadata["album_art_uri"] = thumb # Translate metadata keys from HA names to DIDL-Lite names @@ -666,8 +665,7 @@ class DlnaDmrEntity(MediaPlayerEntity): if not self._device: return None - play_mode = self._device.play_mode - if not play_mode: + if not (play_mode := self._device.play_mode): return None if play_mode == PlayMode.VENDOR_DEFINED: @@ -700,8 +698,7 @@ class DlnaDmrEntity(MediaPlayerEntity): if not self._device: return None - play_mode = self._device.play_mode - if not play_mode: + if not (play_mode := self._device.play_mode): return None if play_mode == PlayMode.VENDOR_DEFINED: diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 16872079be3..5c0625411ae 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -182,9 +182,7 @@ class HangoutsBot: """Detect a matching intent.""" for intent_type, data in intents.items(): for matcher in data.get(CONF_MATCHERS, []): - match = matcher.match(text) - - if not match: + if not (match := matcher.match(text)): continue if intent_type == INTENT_HELP: return await self.hass.helpers.intent.async_handle( diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index cc4addfae4f..02b4c396783 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -75,8 +75,7 @@ def ensure_pin_format(pin, allow_insecure_setup_codes=None): If incorrect code is entered, an exception is raised. """ - match = PIN_FORMAT.search(pin.strip()) - if not match: + if not (match := PIN_FORMAT.search(pin.strip())): raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") pin_without_dashes = "".join(match.groups()) if not allow_insecure_setup_codes and pin_without_dashes in INSECURE_CODES: diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index feb27e337e8..b879c2d3f72 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -304,10 +304,8 @@ async def async_update_device_config( device_connection: DeviceConnectionType, device_config: ConfigType ) -> None: """Fill missing values in device_config with infos from LCN bus.""" - is_group = device_config[CONF_ADDRESS][2] - # fetch serial info if device is module - if not is_group: # is module + if not (is_group := device_config[CONF_ADDRESS][2]): # is module await device_connection.serial_known if device_config[CONF_HARDWARE_SERIAL] == -1: device_config[CONF_HARDWARE_SERIAL] = device_connection.hardware_serial diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index a758f850b93..89e70f346ed 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -193,9 +193,7 @@ class LogbookView(HomeAssistantView): async def get(self, request, datetime=None): """Retrieve logbook entries.""" if datetime: - datetime = dt_util.parse_datetime(datetime) - - if datetime is None: + if (datetime := dt_util.parse_datetime(datetime)) is None: return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) else: datetime = dt_util.start_of_local_day() @@ -219,8 +217,7 @@ class LogbookView(HomeAssistantView): end_day = start_day + timedelta(days=period) else: start_day = datetime - end_day = dt_util.parse_datetime(end_time) - if end_day is None: + if (end_day := dt_util.parse_datetime(end_time)) is None: return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) hass = request.app["hass"] diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5f15270563a..41ebd93f3b5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1026,8 +1026,7 @@ class MediaPlayerImageView(HomeAssistantView): media_content_id: str | None = None, ) -> web.Response: """Start a get request.""" - player = self.component.get_entity(entity_id) - if player is None: + if (player := self.component.get_entity(entity_id)) is None: status = ( HTTPStatus.NOT_FOUND if request[KEY_AUTHENTICATED] @@ -1071,9 +1070,8 @@ async def websocket_handle_thumbnail(hass, connection, msg): Async friendly. """ component = hass.data[DOMAIN] - player = component.get_entity(msg["entity_id"]) - if player is None: + if (player := component.get_entity(msg["entity_id"])) is None: connection.send_message( websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") ) diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 32f0070176f..b48ee784c23 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -93,9 +93,7 @@ class MediaSourceItem: @classmethod def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem: """Create an item from a uri.""" - match = URI_SCHEME_REGEX.match(uri) - - if not match: + if not (match := URI_SCHEME_REGEX.match(uri)): raise ValueError("Invalid media source URI") domain = match.group("domain") diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 3ab3f603dbd..518d902a7e1 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -130,8 +130,7 @@ class MelCloudDevice: def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" model = None - unit_infos = self.device.units - if unit_infos is not None: + if (unit_infos := self.device.units) is not None: model = ", ".join([x["model"] for x in unit_infos if x["model"]]) return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 1adb04d3417..221fc453e07 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -68,8 +68,7 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): @property def is_on(self): """Return true if binary sensor is on.""" - printer = self.coordinator.data["printer"] - if not printer: + if not (printer := self.coordinator.data["printer"]): return None return bool(self._get_flag_state(printer)) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 5a9614c69b4..8682f246aa3 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -130,8 +130,7 @@ class OctoPrintJobPercentageSensor(OctoPrintSensorBase): if not job: return None - state = job.progress.completion - if not state: + if not (state := job.progress.completion): return 0 return round(state, 2) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 33ea72f94ff..902ccebb191 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -274,8 +274,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): def _update_current_station(self, response): """Update current station.""" - station_match = re.search(STATION_PATTERN, response) - if station_match: + if station_match := re.search(STATION_PATTERN, response): self._station = station_match.group(1) _LOGGER.debug("Got station as: %s", self._station) else: @@ -283,8 +282,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): def _update_current_song(self, response): """Update info about current song.""" - song_match = re.search(CURRENT_SONG_PATTERN, response) - if song_match: + if song_match := re.search(CURRENT_SONG_PATTERN, response): ( self._media_title, self._media_artist, @@ -343,8 +341,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): _LOGGER.debug("Getting stations: %s", station_lines) self._stations = [] for line in station_lines.split("\r\n"): - match = re.search(r"\d+\).....(.+)", line) - if match: + if match := re.search(r"\d+\).....(.+)", line): station = match.group(1).strip() _LOGGER.debug("Found station %s", station) self._stations.append(station) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index edac688bdd1..e5fe84cc874 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -620,8 +620,7 @@ def list_statistic_ids( platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type) for statistic_id, info in platform_statistic_ids.items(): - unit = info["unit_of_measurement"] - if unit is not None: + if (unit := info["unit_of_measurement"]) is not None: # Display unit according to user settings unit = _configured_unit(unit, units) platform_statistic_ids[statistic_id]["unit_of_measurement"] = unit diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 5a0d6766179..a4017275dea 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -254,8 +254,7 @@ class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity): """Adjust state if Rflink picks up a remote command for this device.""" self.cancel_queued_send_commands() - command = event["command"] - if command == "on": + if event["command"] == "on": # if the state is unknown or false, it gets set as true # if the state is true, it gets set as false self._state = self._state in [None, False] diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 8a88eef7bfc..fa8df5af870 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -81,8 +81,7 @@ def _figure_out_source(record, call_stack, hass): for pathname in reversed(stack): # Try to match with a file within Home Assistant - match = re.match(paths_re, pathname[0]) - if match: + if match := re.match(paths_re, pathname[0]): return [match.group(1), pathname[1]] # Ok, we don't know what this is return (record.pathname, record.lineno) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index aa9852f643f..5c0c4debf80 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -398,8 +398,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if self._current_tado_hvac_mode not in ( diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 4922117b64c..98302ad396a 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -110,8 +110,7 @@ class ThomsonDeviceScanner(DeviceScanner): devices = {} for device in devices_result: - match = _DEVICES_REGEX.search(device.decode("utf-8")) - if match: + if match := _DEVICES_REGEX.search(device.decode("utf-8")): devices[match.group("ip")] = { "ip": match.group("ip"), "mac": match.group("mac").upper(), diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 51f4e859a1f..7097446ba4f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -233,8 +233,7 @@ def _parse_due_date(data: dict, gmt_string) -> datetime | None: # Add time information to date only strings. if len(data["date"]) == 10: return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC) - nowtime = dt.parse_datetime(data["date"]) - if not nowtime: + if not (nowtime := dt.parse_datetime(data["date"])): return None if nowtime.tzinfo is None: data["date"] += gmt_string diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 502a0b0c3f1..b1d0bdcd2e9 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -342,9 +342,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): parts = media_id.split(":") if parts[0] == "list": - index = parts[3] - - if index == "-1": + if (index := parts[3]) == "-1": index = "0" await self.coordinator.musiccast.play_list_media(index, self._zone_id) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 8b845f303cd..4062200972d 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -469,9 +469,7 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: if isinstance(value, bytes): properties[key] = value.decode("utf-8") - addresses = service.addresses - - if not addresses: + if not (addresses := service.addresses): return None if (host := _first_non_link_local_or_v6_address(addresses)) is None: return None From 18ebdbed3e4d619b3cef0e7c32ad4c797485edb8 Mon Sep 17 00:00:00 2001 From: Bastien Gautier Date: Sat, 30 Oct 2021 16:43:22 +0200 Subject: [PATCH 0116/1452] Add AUCTION coin (#58709) --- homeassistant/components/coinbase/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 65c2636cd82..a01db46b095 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -36,6 +36,7 @@ WALLETS = { "AOA": "AOA", "ARS": "ARS", "ATOM": "ATOM", + "AUCTION": "AUCTION", "AUD": "AUD", "AWG": "AWG", "AZN": "AZN", @@ -284,6 +285,7 @@ RATES = { "AOA": "AOA", "ARS": "ARS", "ATOM": "ATOM", + "AUCTION": "AUCTION", "AUD": "AUD", "AWG": "AWG", "AZN": "AZN", From 56f746fd39ccce633cd151199121b4407f27052a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 30 Oct 2021 16:50:24 +0200 Subject: [PATCH 0117/1452] Coerce to tuple before asserting the sequence (#58672) --- homeassistant/components/flux_led/light.py | 2 +- homeassistant/components/lifx/light.py | 6 +++--- homeassistant/components/opencv/image_processing.py | 2 +- homeassistant/components/yeelight/light.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 6f1db96a7aa..f1fa4ed7dbb 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -165,7 +165,7 @@ CUSTOM_EFFECT_DICT: Final = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=16), - [vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple))], + [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], ), vol.Optional(CONF_SPEED_PCT, default=50): vol.All( vol.Range(min=0, max=100), vol.Coerce(int) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 106c66c8900..998b99ef88f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -119,19 +119,19 @@ LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( vol.Coerce(int), vol.Range(min=1) diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index bf63ec0bfff..9228ab26ec5 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_NEIGHBORS, DEFAULT_NEIGHBORS ): cv.positive_int, vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE): vol.Schema( - vol.All(vol.ExactSequence([int, int]), vol.Coerce(tuple)) + vol.All(vol.Coerce(tuple), vol.ExactSequence([int, int])) ), } ), diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index d70845ae86d..a6b51046fc6 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -180,20 +180,20 @@ SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA SERVICE_SCHEMA_SET_COLOR_SCENE = { vol.Required(ATTR_RGB_COLOR): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } SERVICE_SCHEMA_SET_HSV_SCENE = { vol.Required(ATTR_HS_COLOR): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } From 5224050df8ab94f845a96df91b4b660cc8a0402c Mon Sep 17 00:00:00 2001 From: Kapernicus Date: Sat, 30 Oct 2021 11:11:37 -0500 Subject: [PATCH 0118/1452] Bump nad_receiver to version 0.3.0 (#58751) --- homeassistant/components/nad/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 59d82acddf2..12c1f84aa37 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -2,7 +2,7 @@ "domain": "nad", "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", - "requirements": ["nad_receiver==0.2.0"], + "requirements": ["nad_receiver==0.3.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index acc4fa3dc65..ccf2d417be1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,7 +1041,7 @@ mychevy==2.1.1 mycroftapi==2.0 # homeassistant.components.nad -nad_receiver==0.2.0 +nad_receiver==0.3.0 # homeassistant.components.keenetic_ndms2 ndms2_client==0.1.1 From 3374005b336284730209c571240039683cf662bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Oct 2021 12:18:39 -0500 Subject: [PATCH 0119/1452] Bump zeroconf 0.36.11 (#58755) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 9870258027b..3f4dfb4929e 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.36.9"], + "requirements": ["zeroconf==0.36.11"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c4f1a31f5c6..5ff0536e395 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.9 +zeroconf==0.36.11 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index ccf2d417be1..49d5c11d096 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.9 +zeroconf==0.36.11 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba8e40248a7..aa23feddc00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1436,7 +1436,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.9 +zeroconf==0.36.11 # homeassistant.components.zha zha-quirks==0.0.63 From 972dbac1f74079b0dab962dfbab24d96ca6d1c8a Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Sat, 30 Oct 2021 21:19:18 +0100 Subject: [PATCH 0120/1452] Add __init__ for growatt sensor types (#58749) --- homeassistant/components/growatt_server/sensor_types/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 homeassistant/components/growatt_server/sensor_types/__init__.py diff --git a/homeassistant/components/growatt_server/sensor_types/__init__.py b/homeassistant/components/growatt_server/sensor_types/__init__.py new file mode 100644 index 00000000000..3f5be3be7f5 --- /dev/null +++ b/homeassistant/components/growatt_server/sensor_types/__init__.py @@ -0,0 +1 @@ +"""Sensor types for supported Growatt systems.""" From 6c426fea9e5718efc43d7bcd0ea8335d47fec222 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 30 Oct 2021 13:44:28 -0700 Subject: [PATCH 0121/1452] Serve nest placeholder image from disk rather than generate on the fly (#58663) * Serve placeholder image from disk rather than generate on the flay The placeholder image was generated from hoome assistant, saved, flipped, and crushed a bit. The image is 640x480 and the integration does not support any on the fly resizing. * Cache Nest WebRTC placeholder image on camera Cache Nest WebRTC placeholder image rather than reading from disk every time. --- homeassistant/components/nest/camera_sdm.py | 48 ++++-------------- homeassistant/components/nest/placeholder.png | Bin 0 -> 2689 bytes 2 files changed, 9 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/nest/placeholder.png diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index abebc8db3ef..71798eb40c3 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,11 +3,10 @@ from __future__ import annotations from collections.abc import Callable import datetime -import io import logging +from pathlib import Path from typing import Any -from PIL import Image, ImageDraw, ImageFilter from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, @@ -37,18 +36,11 @@ from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) +PLACEHOLDER = Path(__file__).parent / "placeholder.png" + # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) -# The Google Home app dispays a placeholder image that appears as a faint -# light source (dim, blurred sphere) giving the user an indication the camera -# is available, not just a blank screen. These constants define a blurred -# ellipse at the top left of the thumbnail. -PLACEHOLDER_ELLIPSE_BLUR = 0.1 -PLACEHOLDER_ELLIPSE_XY = [-0.4, 0.3, 0.3, 0.4] -PLACEHOLDER_OVERLAY_COLOR = "#ffffff" -PLACEHOLDER_ELLIPSE_OPACITY = 255 - async def async_setup_sdm_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -73,30 +65,6 @@ async def async_setup_sdm_entry( async_add_entities(entities) -def placeholder_image(width: int | None = None, height: int | None = None) -> Image: - """Return a camera image preview for cameras without live thumbnails.""" - if not width or not height: - return Image.new("RGB", (1, 1)) - # Draw a dark scene with a fake light source - blank = Image.new("RGB", (width, height)) - overlay = Image.new("RGB", blank.size, color=PLACEHOLDER_OVERLAY_COLOR) - ellipse = Image.new("L", blank.size, color=0) - draw = ImageDraw.Draw(ellipse) - draw.ellipse( - ( - width * PLACEHOLDER_ELLIPSE_XY[0], - height * PLACEHOLDER_ELLIPSE_XY[1], - width * PLACEHOLDER_ELLIPSE_XY[2], - height * PLACEHOLDER_ELLIPSE_XY[3], - ), - fill=PLACEHOLDER_ELLIPSE_OPACITY, - ) - mask = ellipse.filter( - ImageFilter.GaussianBlur(radius=width * PLACEHOLDER_ELLIPSE_BLUR) - ) - return Image.composite(overlay, blank, mask) - - class NestCamera(Camera): """Devices that support cameras.""" @@ -112,6 +80,7 @@ class NestCamera(Camera): self._event_image_bytes: bytes | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self._placeholder_image: bytes | None = None @property def should_poll(self) -> bool: @@ -251,10 +220,11 @@ class NestCamera(Camera): return None # Nest Web RTC cams only have image previews for events, and not # for "now" by design to save batter, and need a placeholder. - image = placeholder_image(width=width, height=height) - with io.BytesIO() as content: - image.save(content, format="JPEG", optimize=True) - return content.getvalue() + if not self._placeholder_image: + self._placeholder_image = await self.hass.async_add_executor_job( + PLACEHOLDER.read_bytes + ) + return self._placeholder_image return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) async def _async_active_event_image(self) -> bytes | None: diff --git a/homeassistant/components/nest/placeholder.png b/homeassistant/components/nest/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..5ccc755abfd3ccda6c43b08cfc8352717a3208c2 GIT binary patch literal 2689 zcmb_e`&(027QOe9kn|!Xm5WePO?W5?Qo+_w9#wKdC{e0FR8T=k(SdeEKoCt!NG`8} zh(N7~PI&}GrD%Cn1!RyUl(!G0BLa#fLWPk42_ldXl1!-o!kqov-s|kM&)MHzXPpb& z=VuCA!vFx7?q>P00RX}P0F)684uIU8?gjubYqyWr0WLWGDm8Y*!z}Z4e4RqyJy3Co zf*-Q@ugetuOe+JN&_NB8b>zJ1-~K_mZ60rvp)z1Vg5&HzEobKZ1#H6ScSK zMg0(I4NUYU%5FB5mHgmW{^_;1@}&JSB~uJqC1-_xK(f-xRU}n%=-$d7llc9j*^N>c zIt{VG5T}JFDI$-73y~|8t@^gHC=mofWFG1>xuWCh6JN4jgha1xIahxuUiGuO%}1pbzveW)b2>Xx+G8J`Y~J%`=sG#X%oug%a~`u5J;p>5PK%-Ak8e5k9H*6|Q* zjBl|irQE(n^DL#;jFgx>`lS8^EqQANJLM2HPRcpPF=kk(J*PnOSJ zzVw1SdNy&-284`|nU~2Jb4)GuxEH%el0#-0&=5J4$gpC^Q<>!pJdwx3_WTgKHwu2U zkxLXue|%waJ)_CGD9y%DB8;~D!_2Lc!;&y(^egqF`iC=DZtuE%xHy5rm)cp7y}%_~ zUF1t=_jPtvX?;xBs+{YbwM|xxE6@#cSS`kOS==K;av_FWdXOqndwqUzmeS*}qbpIg zknqQkY&PnDG|?X%{Koju`^l4MHzr+k2FjY?sI>{FcH)W-JTz3emX2>M$9^t2FSof; z>wrHd!tIf8kK23=y3XLNhl@5x{#D3(5aIVFJNfe^bi&3&hJFXS-%`tjXArO)A^PY;4AcyGW;PXK@5(Rq*UJGC#&5W5#9FO;9>XcY>-j*GzCdB}k-sT>qM9#oXr3uY@6A$N!M4`DYmsb8DwZ<5)TgJ^-+ z^$D10u_m+5QMFYU5x!KE@?hGemCc;csFE1Z2@{%Jdja9Q5>9H#$K9jVi}rNkRcW5@ zR>H9JbVe=km%d}NkJ-a72Y5#IF48iR%0eaRchl3b zj#|lGskFWJVQz1kQ;qq6hveCfQ=$yo_D1AsZhZ7h!OanKh#aI!el^!TV`+1O?ZOh; z;66Su7V z%oB!=c>!&?f$&$hn{?p=iiTHHXCFXmI5yheF1@w<>N3RNyqcURw$^bf`ETV%M3tORVh&t7#vd-He^P9nw%c3?UqCH+}DGTm)}e} zLlwKt#CF+k7&wG1HP8&vz&fR*n!aQU`Eteg zElRCf&iJ@K%;BEhQvN(c7J$OWI2vws`T-Cmt{M>~qA1`E7-$0`*KHt=k0oQ#U4B!3 zbNBt1dWv$#pH>Zmr7dg|mV}In*LF%`R(yaoC=G%T1OK#~+d9{mz}&Asb5BupU#DKy zge_6hp^DR2yB31s&NL?j$9v$YTl zT-pUNWc*8Fo_IZ?8}*yhjRdLV&NbwGxfuf7R z+lC8-Cp2@Q7w{M?6!XyI(zphytUu~q;4}Hk5e4bjl}Cef+9&+8C1cKV@MPrE*Z>ul zGClU^i7u}VAsItmiU)7DP~(ko$_&y_mnUC?FMzh&VB@rW6S0<_dR_LQwxZhoVzAGn z&iT2Y4#n&HP9aUhUOT$_8k!>pEU{0d3b}ZxH}& zSKd0mu;Epj{Y)VEG#VPbu%|RDxyhD#GWjOAZ zwDHRX=21oAa&%~f!@^CSRG&TmQdBb=`mI@Z$uFgj-gQxnMJ@!N^<#}S=DR1gIr7FqFHeD4cK^;AqGUPZ zynNjB5on^qO2(^YU!uTu&d3pfK?AoDGS)UJj4BrYoipw$vV}$UZlgL)yMxqZ#W5yf z^*b;9!>wDJ3-E~7G99gi7*YVWeM;tDA!WBHBKE!^1S^KK2O5(8(^dR252br^V(CCx zs-smwnimKrqDnAGp$Vy_IBCUuylf*H&+0w*Qq2Q1P#}_7&A|+lwg}Q`D$J@UMy^gx z(%W0hXFQfDi-Zhh4G|@2k(7fjN&t_;<5p4MI_I6AUA~_e5OSLmrf>Ni|2OL3DQ}B4 zfhf!ZL73<%T*?mfFg)o!(qM9*pOaeiEccgH0fGY637WF~cq79F*r?%TQg*9g-GWD3 zFZJs#6H(oqmM?_74Gbcz=GrfJ9b8oaMtEFlEr#-V{`9s(TGC+^FEirMNY)3~fHVGB zTg_1wo<=tq3Q7p={G5f9oss>m{Cc{ub18ULBmwQL+s*q#FT&G-7PD2LavL5uX2kRM z{JW;eqJS!cBxz46$E4PQC`QU|a`U8r2>DrT2IdkGvJev*)JpqrITp;0R7lsj)2#qB mC_k&e;!^T!U#yD2|KB3sag_F5=9bJ) literal 0 HcmV?d00001 From f87f72bb8ee45e411209b5b81dea9b7b2f780287 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sat, 30 Oct 2021 15:23:47 -0700 Subject: [PATCH 0122/1452] Switch to update coordinator, and bump venstarcolortouch to 0.15 (#58601) --- homeassistant/components/venstar/__init__.py | 98 +++++++++++++------ homeassistant/components/venstar/climate.py | 68 ++++++++++--- .../components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 129 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 27d3e77754a..5f2aabe6738 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1,9 +1,11 @@ """The venstar component.""" import asyncio +from datetime import timedelta from requests import RequestException from venstarcolortouch import VenstarColorTouch +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -11,8 +13,9 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import update_coordinator +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT @@ -37,11 +40,13 @@ async def async_setup_entry(hass, config): proto=protocol, ) - try: - await hass.async_add_executor_job(client.update_info) - except (OSError, RequestException) as ex: - raise ConfigEntryNotReady(f"Unable to connect to the thermostat: {ex}") from ex - hass.data.setdefault(DOMAIN, {})[config.entry_id] = client + venstar_data_coordinator = VenstarDataUpdateCoordinator( + hass, + venstar_connection=client, + ) + await venstar_data_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[config.entry_id] = venstar_data_coordinator hass.config_entries.async_setup_platforms(config, PLATFORMS) return True @@ -55,35 +60,74 @@ async def async_unload_entry(hass, config): return unload_ok -class VenstarEntity(Entity): - """Get the latest data and update.""" +class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): + """Class to manage fetching Venstar data.""" - def __init__(self, config, client): - """Initialize the data object.""" - self._config = config - self._client = client + def __init__( + self, + hass: HomeAssistant, + *, + venstar_connection: VenstarColorTouch, + ) -> None: + """Initialize global Venstar data updater.""" + self.client = venstar_connection - async def async_update(self): + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self) -> None: """Update the state.""" try: - info_success = await self.hass.async_add_executor_job( - self._client.update_info - ) + await self.hass.async_add_executor_job(self.client.update_info) except (OSError, RequestException) as ex: - _LOGGER.error("Exception during info update: %s", ex) + raise update_coordinator.UpdateFailed( + f"Exception during Venstar info update: {ex}" + ) from ex # older venstars sometimes cannot handle rapid sequential connections await asyncio.sleep(3) try: - sensor_success = await self.hass.async_add_executor_job( - self._client.update_sensors - ) + await self.hass.async_add_executor_job(self.client.update_sensors) except (OSError, RequestException) as ex: - _LOGGER.error("Exception during sensor update: %s", ex) + raise update_coordinator.UpdateFailed( + f"Exception during Venstar sensor update: {ex}" + ) from ex + return None - if not info_success or not sensor_success: - _LOGGER.error("Failed to update data") + +class VenstarEntity(CoordinatorEntity): + """Representation of a Venstar entity.""" + + def __init__( + self, + venstar_data_coordinator: VenstarDataUpdateCoordinator, + config: ConfigEntry, + ) -> None: + """Initialize the data object.""" + super().__init__(venstar_data_coordinator) + self._config = config + self._update_attr() + self.coordinator = venstar_data_coordinator + + @property + def _client(self): + """Return the venstar client.""" + return self.coordinator.client + + @callback + def _update_attr(self) -> None: + """Update the state and attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() @property def name(self): @@ -102,8 +146,6 @@ class VenstarEntity(Entity): "identifiers": {(DOMAIN, self._config.entry_id)}, "name": self._client.name, "manufacturer": "Venstar", - # pylint: disable=protected-access - "model": f"{self._client.model}-{self._client._type}", - # pylint: disable=protected-access - "sw_version": self._client._api_ver, + "model": f"{self._client.model}-{self._client.get_type()}", + "sw_version": self._client.get_api_ver(), } diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index d86a5953169..c53cc9685e2 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,4 +1,6 @@ """Support for Venstar WiFi Thermostats.""" +from functools import partial + import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -24,7 +26,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -38,9 +40,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VenstarEntity +from . import VenstarDataUpdateCoordinator, VenstarEntity from .const import ( _LOGGER, ATTR_FAN_STATE, @@ -68,10 +72,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Venstar thermostat.""" - client = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([VenstarThermostat(config_entry, client)], True) + venstar_data_coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + VenstarThermostat( + venstar_data_coordinator, + config_entry, + ) + ], + ) async def async_setup_platform(hass, config, add_entities, discovery_info=None): @@ -95,9 +110,13 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): class VenstarThermostat(VenstarEntity, ClimateEntity): """Representation of a Venstar thermostat.""" - def __init__(self, config, client): + def __init__( + self, + venstar_data_coordinator: VenstarDataUpdateCoordinator, + config: ConfigEntry, + ) -> None: """Initialize the thermostat.""" - super().__init__(config, client) + super().__init__(venstar_data_coordinator, config) self._mode_map = { HVAC_MODE_HEAT: self._client.MODE_HEAT, HVAC_MODE_COOL: self._client.MODE_COOL, @@ -257,7 +276,12 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _LOGGER.error("Failed to change the operation mode") return success - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): + """Set a new target temperature.""" + await self.hass.async_add_executor_job(partial(self._set_temperature, **kwargs)) + self.async_write_ha_state() + + def _set_temperature(self, **kwargs): """Set a new target temperature.""" set_temp = True operation_mode = kwargs.get(ATTR_HVAC_MODE) @@ -295,7 +319,12 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): if not success: _LOGGER.error("Failed to change the temperature") - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set a new target fan mode.""" + await self.hass.async_add_executor_job(self._set_fan_mode, fan_mode) + self.async_write_ha_state() + + def _set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode == STATE_ON: success = self._client.set_fan(self._client.FAN_ON) @@ -305,18 +334,33 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): if not success: _LOGGER.error("Failed to change the fan mode") - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set a new target operation mode.""" + await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode) + self.async_write_ha_state() + + def _set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" self._set_operation_mode(hvac_mode) - def set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: + """Set a new target humidity.""" + await self.hass.async_add_executor_job(self._set_humidity, humidity) + self.async_write_ha_state() + + def _set_humidity(self, humidity): """Set new target humidity.""" success = self._client.set_hum_setpoint(humidity) if not success: _LOGGER.error("Failed to change the target humidity level") - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the hold mode.""" + await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode) + self.async_write_ha_state() + + def _set_preset_mode(self, preset_mode): """Set the hold mode.""" if preset_mode == PRESET_AWAY: success = self._client.set_away(self._client.AWAY_AWAY) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 943790b532e..6fef7bf5d57 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ - "venstarcolortouch==0.14" + "venstarcolortouch==0.15" ], "codeowners": ["@garbled1"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 49d5c11d096..01cbde009aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,7 +2369,7 @@ vallox-websocket-api==2.8.1 velbus-aio==2021.10.7 # homeassistant.components.venstar -venstarcolortouch==0.14 +venstarcolortouch==0.15 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa23feddc00..44e333122ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ uvcclient==0.11.0 velbus-aio==2021.10.7 # homeassistant.components.venstar -venstarcolortouch==0.14 +venstarcolortouch==0.15 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From d6e49bc5bcb02952102fc046e6e7f6fc0c85f45d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 00:27:12 +0200 Subject: [PATCH 0123/1452] Fix litterrobot vacuum base class (#58765) --- homeassistant/components/litterrobot/vacuum.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index f1717bf0209..81f9e631beb 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATUS, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumEntity, + StateVacuumEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF @@ -75,7 +75,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): +class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" @property @@ -148,4 +148,5 @@ class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): "power_status": self.robot.power_status, "status_code": self.robot.status_code, "last_seen": self.robot.last_seen, + "status": self.status, } From 733280e1690c6c075d50d4168ea5109678bd8022 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 00:33:07 +0200 Subject: [PATCH 0124/1452] Use EntityDescription - wallbox (#58690) --- homeassistant/components/wallbox/__init__.py | 7 +- homeassistant/components/wallbox/const.py | 176 +++++++++---------- homeassistant/components/wallbox/number.py | 11 +- homeassistant/components/wallbox/sensor.py | 32 ++-- 4 files changed, 108 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 410a3115f9f..a1361984606 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -16,10 +16,9 @@ from .const import ( CONF_CONNECTIONS, CONF_DATA_KEY, CONF_MAX_CHARGING_CURRENT_KEY, - CONF_ROUND, - CONF_SENSOR_TYPES, CONF_STATION, DOMAIN, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -72,10 +71,10 @@ class WallboxCoordinator(DataUpdateCoordinator): CONF_MAX_CHARGING_CURRENT_KEY ] - filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} + filtered_data = {k: data[k] for k in SENSOR_TYPES if k in data} for key, value in filtered_data.items(): - if (sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]) is not None: + if (sensor_round := SENSOR_TYPES[key].precision) is not None: try: filtered_data[key] = round(value, sensor_round) except TypeError: diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 62c9b2f6efd..26af7f4c499 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -1,9 +1,10 @@ """Constants for the Wallbox integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import ( - CONF_DEVICE_CLASS, - CONF_ICON, - CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -33,91 +34,86 @@ CONF_STATE_OF_CHARGE_KEY = "state_of_charge" CONF_STATUS_DESCRIPTION_KEY = "status_description" CONF_CONNECTIONS = "connections" -CONF_ROUND = "round" -CONF_SENSOR_TYPES = { - CONF_CHARGING_POWER_KEY: { - CONF_ICON: None, - CONF_NAME: "Charging Power", - CONF_ROUND: 2, - CONF_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - }, - CONF_MAX_AVAILABLE_POWER_KEY: { - CONF_ICON: None, - CONF_NAME: "Max Available Power", - CONF_ROUND: 0, - CONF_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - CONF_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - }, - CONF_CHARGING_SPEED_KEY: { - CONF_ICON: "mdi:speedometer", - CONF_NAME: "Charging Speed", - CONF_ROUND: 0, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_ADDED_RANGE_KEY: { - CONF_ICON: "mdi:map-marker-distance", - CONF_NAME: "Added Range", - CONF_ROUND: 0, - CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, - CONF_DEVICE_CLASS: None, - }, - CONF_ADDED_ENERGY_KEY: { - CONF_ICON: None, - CONF_NAME: "Added Energy", - CONF_ROUND: 2, - CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - }, - CONF_CHARGING_TIME_KEY: { - CONF_ICON: "mdi:timer", - CONF_NAME: "Charging Time", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_COST_KEY: { - CONF_ICON: "mdi:ev-station", - CONF_NAME: "Cost", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_STATE_OF_CHARGE_KEY: { - CONF_ICON: None, - CONF_NAME: "State of Charge", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, - CONF_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - }, - CONF_CURRENT_MODE_KEY: { - CONF_ICON: "mdi:ev-station", - CONF_NAME: "Current Mode", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_DEPOT_PRICE_KEY: { - CONF_ICON: "mdi:ev-station", - CONF_NAME: "Depot Price", - CONF_ROUND: 2, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_STATUS_DESCRIPTION_KEY: { - CONF_ICON: "mdi:ev-station", - CONF_NAME: "Status Description", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: None, - CONF_DEVICE_CLASS: None, - }, - CONF_MAX_CHARGING_CURRENT_KEY: { - CONF_ICON: None, - CONF_NAME: "Max. Charging Current", - CONF_ROUND: None, - CONF_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - CONF_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - }, + +@dataclass +class WallboxSensorEntityDescription(SensorEntityDescription): + """Describes Wallbox sensor entity.""" + + precision: int | None = None + + +SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { + CONF_CHARGING_POWER_KEY: WallboxSensorEntityDescription( + key=CONF_CHARGING_POWER_KEY, + name="Charging Power", + precision=2, + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + CONF_MAX_AVAILABLE_POWER_KEY: WallboxSensorEntityDescription( + key=CONF_MAX_AVAILABLE_POWER_KEY, + name="Max Available Power", + precision=0, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( + key=CONF_CHARGING_SPEED_KEY, + icon="mdi:speedometer", + name="Charging Speed", + precision=0, + ), + CONF_ADDED_RANGE_KEY: WallboxSensorEntityDescription( + key=CONF_ADDED_RANGE_KEY, + icon="mdi:map-marker-distance", + name="Added Range", + precision=0, + native_unit_of_measurement=LENGTH_KILOMETERS, + ), + CONF_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( + key=CONF_ADDED_ENERGY_KEY, + name="Added Energy", + precision=2, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + CONF_CHARGING_TIME_KEY: WallboxSensorEntityDescription( + key=CONF_CHARGING_TIME_KEY, + icon="mdi:timer", + name="Charging Time", + ), + CONF_COST_KEY: WallboxSensorEntityDescription( + key=CONF_COST_KEY, + icon="mdi:ev-station", + name="Cost", + ), + CONF_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( + key=CONF_STATE_OF_CHARGE_KEY, + name="State of Charge", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + CONF_CURRENT_MODE_KEY: WallboxSensorEntityDescription( + key=CONF_CURRENT_MODE_KEY, + icon="mdi:ev-station", + name="Current Mode", + ), + CONF_DEPOT_PRICE_KEY: WallboxSensorEntityDescription( + key=CONF_DEPOT_PRICE_KEY, + icon="mdi:ev-station", + name="Depot Price", + precision=2, + ), + CONF_STATUS_DESCRIPTION_KEY: WallboxSensorEntityDescription( + key=CONF_STATUS_DESCRIPTION_KEY, + icon="mdi:ev-station", + name="Status Description", + ), + CONF_MAX_CHARGING_CURRENT_KEY: WallboxSensorEntityDescription( + key=CONF_MAX_CHARGING_CURRENT_KEY, + name="Max. Charging Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), } diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index d99d6511822..01e453593f3 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -1,7 +1,5 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" - from homeassistant.components.number import NumberEntity -from homeassistant.const import CONF_DEVICE_CLASS from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import InvalidAuth @@ -9,9 +7,8 @@ from .const import ( CONF_CONNECTIONS, CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, - CONF_NAME, - CONF_SENSOR_TYPES, DOMAIN, + SENSOR_TYPES, ) @@ -35,11 +32,11 @@ class WallboxNumber(CoordinatorEntity, NumberEntity): def __init__(self, coordinator, config): """Initialize a Wallbox sensor.""" super().__init__(coordinator) - _properties = CONF_SENSOR_TYPES[CONF_MAX_CHARGING_CURRENT_KEY] + sensor_description = SENSOR_TYPES[CONF_MAX_CHARGING_CURRENT_KEY] self._coordinator = coordinator - self._attr_name = f"{config.title} {_properties[CONF_NAME]}" + self._attr_name = f"{config.title} {sensor_description.name}" self._attr_min_value = 6 - self._attr_device_class = _properties[CONF_DEVICE_CLASS] + self._attr_device_class = sensor_description.device_class @property def max_value(self): diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 37450a5ea79..3b87d3b29c3 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -1,16 +1,12 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" - from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_DEVICE_CLASS from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_CONNECTIONS, - CONF_ICON, - CONF_NAME, - CONF_SENSOR_TYPES, - CONF_UNIT_OF_MEASUREMENT, DOMAIN, + SENSOR_TYPES, + WallboxSensorEntityDescription, ) CONF_STATION = "station" @@ -22,26 +18,28 @@ async def async_setup_entry(hass, config, async_add_entities): coordinator = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] async_add_entities( - WallboxSensor(coordinator, idx, ent, config) - for idx, ent in enumerate(coordinator.data) + [ + WallboxSensor(coordinator, config, description) + for ent in coordinator.data + if (description := SENSOR_TYPES[ent]) + ] ) class WallboxSensor(CoordinatorEntity, SensorEntity): """Representation of the Wallbox portal.""" - def __init__(self, coordinator, idx, ent, config): + entity_description: WallboxSensorEntityDescription + + def __init__( + self, coordinator, config, description: WallboxSensorEntityDescription + ): """Initialize a Wallbox sensor.""" super().__init__(coordinator) - self._attr_name = f"{config.title} {CONF_SENSOR_TYPES[ent][CONF_NAME]}" - self._attr_icon = CONF_SENSOR_TYPES[ent][CONF_ICON] - self._attr_native_unit_of_measurement = CONF_SENSOR_TYPES[ent][ - CONF_UNIT_OF_MEASUREMENT - ] - self._attr_device_class = CONF_SENSOR_TYPES[ent][CONF_DEVICE_CLASS] - self._ent = ent + self.entity_description = description + self._attr_name = f"{config.title} {description.name}" @property def native_value(self): """Return the state of the sensor.""" - return self.coordinator.data[self._ent] + return self.coordinator.data[self.entity_description.key] From 8da010cacde1f68deb816bbe1a8923911444a265 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 31 Oct 2021 00:12:41 +0000 Subject: [PATCH 0125/1452] [ci skip] Translation update --- .../components/motioneye/translations/et.json | 1 + .../components/motioneye/translations/hu.json | 1 + .../components/motioneye/translations/ru.json | 1 + .../components/ridwell/translations/ca.json | 28 +++++++++++++++++++ .../components/ridwell/translations/de.json | 28 +++++++++++++++++++ .../components/ridwell/translations/et.json | 28 +++++++++++++++++++ .../components/ridwell/translations/ru.json | 28 +++++++++++++++++++ 7 files changed, 115 insertions(+) create mode 100644 homeassistant/components/ridwell/translations/ca.json create mode 100644 homeassistant/components/ridwell/translations/de.json create mode 100644 homeassistant/components/ridwell/translations/et.json create mode 100644 homeassistant/components/ridwell/translations/ru.json diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json index b3e3919123c..89fe7a60d5d 100644 --- a/homeassistant/components/motioneye/translations/et.json +++ b/homeassistant/components/motioneye/translations/et.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Voo URL-i mall", "webhook_set": "Seadista motionEye veebihaagid, et teatada s\u00fcndmustest Home Assistanti'le", "webhook_set_overwrite": "Kirjuta tundmatud veebihaagid \u00fcle" } diff --git a/homeassistant/components/motioneye/translations/hu.json b/homeassistant/components/motioneye/translations/hu.json index 0acc46509a4..60afb07d52b 100644 --- a/homeassistant/components/motioneye/translations/hu.json +++ b/homeassistant/components/motioneye/translations/hu.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Stream URL-sablon", "webhook_set": "\u00c1ll\u00edtsa be a motionEye webhookokat az esem\u00e9nyek jelent\u00e9s\u00e9nek Home Assistant sz\u00e1m\u00e1ra", "webhook_set_overwrite": "Fel\u00fcl\u00edrja a fel nem ismert webhookokat" } diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json index fbda6e7abdc..f21feba5f9a 100644 --- a/homeassistant/components/motioneye/translations/ru.json +++ b/homeassistant/components/motioneye/translations/ru.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "\u0428\u0430\u0431\u043b\u043e\u043d URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0442\u043e\u043a\u0430", "webhook_set": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Webhook motionEye \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0445 \u0432 Home Assistant", "webhook_set_overwrite": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0435 Webhook" } diff --git a/homeassistant/components/ridwell/translations/ca.json b/homeassistant/components/ridwell/translations/ca.json new file mode 100644 index 00000000000..6e84fe58325 --- /dev/null +++ b/homeassistant/components/ridwell/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Torna a introduir la contrasenya de {username}:", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix el nom d'usuari i la contrasenya:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/de.json b/homeassistant/components/ridwell/translations/de.json new file mode 100644 index 00000000000..6849ba28022 --- /dev/null +++ b/homeassistant/components/ridwell/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein:", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Gib deinen Benutzernamen und dein Passwort ein:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/et.json b/homeassistant/components/ridwell/translations/et.json new file mode 100644 index 00000000000..ee9abfe9d17 --- /dev/null +++ b/homeassistant/components/ridwell/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta uuesti {username} salas\u00f5na:", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma kasutajanimi ja salas\u00f5na:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/ru.json b/homeassistant/components/ridwell/translations/ru.json new file mode 100644 index 00000000000..db59743f1fe --- /dev/null +++ b/homeassistant/components/ridwell/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", + "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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ No newline at end of file From 26b951194c857580b80fd034a6f45e023f3c1fbe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Oct 2021 08:00:31 +0100 Subject: [PATCH 0126/1452] Add configuration_url to OctoPrint (#58753) * Add configuration_url to Octoprint * fix device_info() return Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/octoprint/__init__.py | 30 +++++++++++++-- .../components/octoprint/binary_sensor.py | 31 +++++++-------- homeassistant/components/octoprint/sensor.py | 38 ++++++++++--------- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index eee1ccd2814..706f54ac708 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,9 +1,11 @@ """Support for monitoring OctoPrint 3D printers.""" from datetime import timedelta import logging +from typing import cast from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline import voluptuous as vol +from yarl import URL from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -20,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify import homeassistant.util.dt as dt_util @@ -160,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): client.set_api_key(entry.data[CONF_API_KEY]) - coordinator = OctoprintDataUpdateCoordinator(hass, client, entry.entry_id, 30) + coordinator = OctoprintDataUpdateCoordinator(hass, client, entry, 30) await coordinator.async_config_entry_first_refresh() @@ -184,20 +187,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Octoprint data.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, octoprint: OctoprintClient, - config_entry_id: str, + config_entry: ConfigEntry, interval: int, ) -> None: """Initialize.""" super().__init__( hass, _LOGGER, - name=f"octoprint-{config_entry_id}", + name=f"octoprint-{config_entry.entry_id}", update_interval=timedelta(seconds=interval), ) + self.config_entry = config_entry self._octoprint = octoprint self._printer_offline = False self.data = {"printer": None, "job": None, "last_read_time": None} @@ -225,3 +231,21 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): self._printer_offline = False return {"job": job, "printer": printer, "last_read_time": dt_util.utcnow()} + + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + unique_id = cast(str, self.config_entry.unique_id) + configuration_url = URL.build( + scheme=self.config_entry.data[CONF_SSL] and "https" or "http", + host=self.config_entry.data[CONF_HOST], + port=self.config_entry.data[CONF_PORT], + path=self.config_entry.data[CONF_PATH], + ) + + return DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="OctoPrint", + name="OctoPrint", + configuration_url=str(configuration_url), + ) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 221fc453e07..e1db7a95136 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from abc import abstractmethod -import logging from pyoctoprintapi import OctoprintPrinterInfo @@ -10,14 +9,10 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN as COMPONENT_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import OctoprintDataUpdateCoordinator +from .const import DOMAIN async def async_setup_entry( @@ -26,7 +21,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available OctoPrint binary sensors.""" - coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id ]["coordinator"] device_id = config_entry.unique_id @@ -44,9 +39,11 @@ async def async_setup_entry( class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Representation an OctoPrint binary sensor.""" + coordinator: OctoprintDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, sensor_type: str, device_id: str, ) -> None: @@ -59,11 +56,7 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "OctoPrint", - "name": "OctoPrint", - } + return self.coordinator.device_info @property def is_on(self): @@ -86,7 +79,9 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): class OctoPrintPrintingBinarySensor(OctoPrintBinarySensorBase): """Representation an OctoPrint binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Printing", device_id) @@ -97,7 +92,9 @@ class OctoPrintPrintingBinarySensor(OctoPrintBinarySensorBase): class OctoPrintPrintingErrorBinarySensor(OctoPrintBinarySensorBase): """Representation an OctoPrint binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Printing Error", device_id) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 8682f246aa3..70151d86022 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -16,12 +16,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN as COMPONENT_DOMAIN +from . import OctoprintDataUpdateCoordinator +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,7 +30,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available OctoPrint binary sensors.""" - coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id ]["coordinator"] device_id = config_entry.unique_id @@ -67,9 +65,11 @@ async def async_setup_entry( class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Representation of an OctoPrint sensor.""" + coordinator: OctoprintDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, sensor_type: str, device_id: str, ) -> None: @@ -82,11 +82,7 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "OctoPrint", - "name": "OctoPrint", - } + return self.coordinator.device_info class OctoPrintStatusSensor(OctoPrintSensorBase): @@ -94,7 +90,9 @@ class OctoPrintStatusSensor(OctoPrintSensorBase): _attr_icon = "mdi:printer-3d" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Current State", device_id) @@ -119,7 +117,9 @@ class OctoPrintJobPercentageSensor(OctoPrintSensorBase): _attr_native_unit_of_measurement = PERCENTAGE _attr_icon = "mdi:file-percent" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Job Percentage", device_id) @@ -141,7 +141,9 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): _attr_device_class = DEVICE_CLASS_TIMESTAMP - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Estimated Finish Time", device_id) @@ -162,7 +164,9 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): _attr_device_class = DEVICE_CLASS_TIMESTAMP - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Start Time", device_id) @@ -188,7 +192,7 @@ class OctoPrintTemperatureSensor(OctoPrintSensorBase): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, tool: str, temp_type: str, device_id: str, From f561533d2cdf801963efae973ab146092374bcb2 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Sun, 31 Oct 2021 01:30:11 -0700 Subject: [PATCH 0127/1452] Fix some typing issues in greeneye_monitor (#58788) * Improve typing * Import cast --- .../components/greeneye_monitor/__init__.py | 5 ++--- .../components/greeneye_monitor/sensor.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index bb564655ecb..3417d0c08dc 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from greeneye import Monitors import voluptuous as vol @@ -18,7 +17,7 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType @@ -130,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: server_config = config[DOMAIN] server = await monitors.start_server(server_config[CONF_PORT]) - async def close_server(*args: list[Any]) -> None: + async def close_server(event: Event) -> None: """Close the monitoring server.""" await server.close() diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 63f069e02a9..e14c24efbe6 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,7 +1,7 @@ """Support for the sensors in a GreenEye Monitor.""" from __future__ import annotations -from typing import Any, Generic, TypeVar +from typing import Any, Generic, Optional, TypeVar, cast import greeneye from greeneye import Monitors @@ -140,7 +140,7 @@ class GEMSensor(Generic[T], SensorEntity): if not self._try_connect_to_monitor(monitors): monitors.add_listener(self._on_new_monitor) - def _on_new_monitor(self, *args: list[Any]) -> None: + def _on_new_monitor(self, monitor: greeneye.monitor.Monitor) -> None: monitors = self.hass.data[DATA_GREENEYE_MONITOR] if self._try_connect_to_monitor(monitors): monitors.remove_listener(self._on_new_monitor) @@ -192,8 +192,7 @@ class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): if not self._sensor: return None - assert isinstance(self._sensor.watts, float) - return self._sensor.watts + return cast(Optional[float], self._sensor.watts) @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -245,8 +244,7 @@ class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): * self._counted_quantity_per_pulse * self._seconds_per_time_unit ) - assert isinstance(result, float) - return result + return cast(float, result) @property def _seconds_per_time_unit(self) -> int: @@ -301,8 +299,7 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): if not self._sensor: return None - assert isinstance(self._sensor.temperature, float) - return self._sensor.temperature + return cast(Optional[float], self._sensor.temperature) @property def native_unit_of_measurement(self) -> str: @@ -332,5 +329,4 @@ class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): if not self._sensor: return None - assert isinstance(self._sensor.voltage, float) - return self._sensor.voltage + return cast(Optional[float], self._sensor.voltage) From 7047205fb800aa5de6124746547111494dee9cc8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 31 Oct 2021 11:47:25 +0100 Subject: [PATCH 0128/1452] Correct fjaraskupan post merge review comments (#58796) --- homeassistant/components/fjaraskupan/__init__.py | 2 +- homeassistant/components/fjaraskupan/fan.py | 3 ++- homeassistant/components/fjaraskupan/number.py | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index f5cedad243d..babcdc6649a 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DISPATCH_DETECTION, DOMAIN -PLATFORMS = ["binary_sensor", "fan", "light", "sensor", "number"] +PLATFORMS = ["binary_sensor", "fan", "light", "number", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 7cb7c7cd18e..bbc04a9607c 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -50,7 +51,7 @@ PRESET_TO_COMMAND = { } -class UnsupportedPreset(Exception): +class UnsupportedPreset(HomeAssistantError): """The preset is unsupported.""" diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index d5862bf2e7f..66f719abd6f 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -22,7 +22,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up sensors dynamically through discovery.""" + """Set up number entities dynamically through discovery.""" def _constructor(device_state: DeviceState) -> list[Entity]: return [ @@ -40,7 +40,6 @@ class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): _attr_max_value: float = 59 _attr_min_value: float = 0 _attr_step: float = 1 - _attr_entity_registry_enabled_default = True _attr_entity_category = ENTITY_CATEGORY_CONFIG _attr_unit_of_measurement = TIME_MINUTES @@ -50,7 +49,7 @@ class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): device: Device, device_info: DeviceInfo, ) -> None: - """Init sensor.""" + """Init number entities.""" super().__init__(coordinator) self._device = device self._attr_unique_id = f"{device.address}-periodic-venting" From 9bc0e8e8ab1e45c82e4e0b7bacb45d08bc57feba Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 31 Oct 2021 07:48:35 -0300 Subject: [PATCH 0129/1452] Improve code quality of the Broadlink light platform (#58790) --- homeassistant/components/broadlink/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 4fc8f4c2120..be4b08f5cee 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -28,9 +28,10 @@ BROADLINK_COLOR_MODE_SCENES = 2 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink light.""" device = hass.data[DOMAIN].devices[config_entry.entry_id] + lights = [] if device.api.type == "LB1": - lights = [BroadlinkLight(device)] + lights.append(BroadlinkLight(device)) async_add_entities(lights) @@ -129,8 +130,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): ) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to set state: %s", err) - return False + return self._update_state(state) self.async_write_ha_state() - return True From 1e92e35bfff99e4c65448d1b30572539b41b3ced Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 31 Oct 2021 07:48:52 -0300 Subject: [PATCH 0130/1452] Improve code quality of the Broadlink switch platform (#58794) --- homeassistant/components/broadlink/switch.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 5ed1e424f53..a06e67defba 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -1,6 +1,5 @@ """Support for Broadlink switches.""" from abc import ABC, abstractmethod -from functools import partial import logging from broadlink.exceptions import BroadlinkException @@ -108,25 +107,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink switch.""" device = hass.data[DOMAIN].devices[config_entry.entry_id] + switches = [] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {}) user_defined_switches = platform_data.get(device.api.mac, {}) - switches = [ + switches.extend( BroadlinkRMSwitch(device, config) for config in user_defined_switches - ] + ) elif device.api.type == "SP1": - switches = [BroadlinkSP1Switch(device)] + switches.append(BroadlinkSP1Switch(device)) elif device.api.type in {"SP2", "SP2S", "SP3", "SP3S", "SP4", "SP4B"}: - switches = [BroadlinkSP2Switch(device)] + switches.append(BroadlinkSP2Switch(device)) elif device.api.type == "BG1": - switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)] + switches.extend(BroadlinkBG1Slot(device, slot) for slot in range(1, 3)) elif device.api.type == "MP1": - switches = [BroadlinkMP1Slot(device, slot) for slot in range(1, 5)] + switches.extend(BroadlinkMP1Slot(device, slot) for slot in range(1, 5)) async_add_entities(switches) @@ -273,9 +273,9 @@ class BroadlinkBG1Slot(BroadlinkSwitch): async def _async_send_packet(self, packet): """Send a packet to the device.""" - set_state = partial(self._device.api.set_state, **{f"pwr{self._slot}": packet}) + state = {f"pwr{self._slot}": packet} try: - await self._device.async_request(set_state) + await self._device.async_request(self._device.api.set_state, **state) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False From 968e582468ed62ad2cee8b50f4e02d9d3400e91a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 31 Oct 2021 12:58:17 +0100 Subject: [PATCH 0131/1452] Remove `last_knx_update` extra_state_attribute from KNX BinarySensor and Sensor (#58786) --- homeassistant/components/knx/binary_sensor.py | 6 +----- homeassistant/components/knx/const.py | 1 - homeassistant/components/knx/sensor.py | 6 +----- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 5ed0e55765d..b4e3354fc37 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -19,9 +19,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt -from .const import ATTR_COUNTER, ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN +from .const import ATTR_COUNTER, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -92,7 +91,4 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): attr[ATTR_COUNTER] = self._device.counter if self._device.last_telegram is not None: attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) - attr[ATTR_LAST_KNX_UPDATE] = str( - dt.as_utc(self._device.last_telegram.timestamp) - ) return attr diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 421297da9d6..7eae5f1c19f 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -37,7 +37,6 @@ CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" ATTR_COUNTER: Final = "counter" -ATTR_LAST_KNX_UPDATE: Final = "last_knx_update" ATTR_SOURCE: Final = "source" diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index fb64f65968b..84f535fdc8f 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -15,9 +15,8 @@ from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.util import dt -from .const import ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN +from .const import ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity from .schema import SensorSchema @@ -82,7 +81,4 @@ class KNXSensor(KnxEntity, SensorEntity): if self._device.last_telegram is not None: attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) - attr[ATTR_LAST_KNX_UPDATE] = str( - dt.as_utc(self._device.last_telegram.timestamp) - ) return attr From 81845bb0b5fc3af93540e2adb168bdb57cad3f55 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Oct 2021 13:32:49 +0100 Subject: [PATCH 0132/1452] Fix channel.send in Discord (#58756) --- homeassistant/components/discord/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 10ad1e8e018..16f30fbf051 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -94,9 +94,9 @@ class DiscordNotificationService(BaseNotificationService): for channelid in kwargs[ATTR_TARGET]: channelid = int(channelid) try: - channel = discord_bot.fetch_channel( + channel = await discord_bot.fetch_channel( channelid - ) or discord_bot.fetch_user(channelid) + ) or await discord_bot.fetch_user(channelid) except discord.NotFound: _LOGGER.warning("Channel not found for ID: %s", channelid) continue From 89ae88519d901b87ef5466d3ce7277aceee84d41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Oct 2021 09:19:37 -0500 Subject: [PATCH 0133/1452] Add additional test coverage for RYSE smartbridges with HK (#58746) --- .../test_ryse_smart_bridge.py | 68 ++ .../ryse_smart_bridge_four_shades.json | 1066 +++++++++++++++++ 2 files changed, 1134 insertions(+) create mode 100644 tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index ad5180658ad..e10e0ccd62a 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -71,3 +71,71 @@ async def test_ryse_smart_bridge_setup(hass): assert device.name == "RYSE SmartShade" assert device.model == "RYSE Shade" assert device.sw_version == "" + + +async def test_ryse_smart_bridge_four_shades_setup(hass): + """Test that a Ryse smart bridge with four shades can be correctly setup in HA.""" + accessories = await setup_accessories_from_file( + hass, "ryse_smart_bridge_four_shades.json" + ) + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + + cover_id = "cover.lr_left" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-2-48" + + cover_id = "cover.lr_right" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-3-48" + + cover_id = "cover.br_left" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-4-48" + + cover_id = "cover.rzss" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-5-48" + + sensor_id = "sensor.lr_left_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-2-64" + + sensor_id = "sensor.lr_right_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-3-64" + + sensor_id = "sensor.br_left_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-4-64" + + sensor_id = "sensor.rzss_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-5-64" + + cover_helper = Helper( + hass, + cover_id, + pairing, + accessories[0], + config_entry, + ) + + cover_state = await cover_helper.poll_and_get_state() + assert cover_state.attributes["friendly_name"] == "RZSS" + assert cover_state.state == "open" + + device_registry = dr.async_get(hass) + + device = device_registry.async_get(cover.device_id) + assert device.manufacturer == "RYSE Inc." + assert device.name == "RZSS" + assert device.model == "RYSE Shade" + assert device.sw_version == "3.0.8" + + bridge = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "RYSE Inc." + assert bridge.name == "RYSE SmartBridge" + assert bridge.model == "RYSE SmartBridge" + assert bridge.sw_version == "1.3.0" diff --git a/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json b/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json new file mode 100644 index 00000000000..b2e7aabd95d --- /dev/null +++ b/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json @@ -0,0 +1,1066 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.3.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "4.1;3fac0fb4", + "perms": [ + "pr", + "hd" + ], + "ev": false + }, + { + "iid": 10, + "type": "220", + "format": "data", + "value": "Yhl9CmseEb8=", + "perms": [ + "pr", + "hd" + ], + "ev": false, + "maxDataLen": 8 + } + ] + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Left", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 89, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Right", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 4, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "BR Left", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 5, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RZSS", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + } +] From e38f3e447c953a4d21e5e411e3e79338404f88a6 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Sun, 31 Oct 2021 15:36:37 +0100 Subject: [PATCH 0134/1452] Add ROCKROBO_S4 to xiaomi_miio vaccum models (#58682) --- homeassistant/components/xiaomi_miio/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index a1f2414b713..6630def38ef 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -197,9 +197,11 @@ MODELS_LIGHT = ( ) # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme +ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" MODELS_VACUUM = [ ROCKROBO_V1, + ROCKROBO_S4, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From f94bbf351db26a354d67bd4e0f5e1eb718c53021 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 31 Oct 2021 15:38:01 +0100 Subject: [PATCH 0135/1452] Set Netatmo max default temperature (#58718) --- homeassistant/components/netatmo/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 145735f4c95..db324cd1722 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -232,6 +232,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if self._model == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) + self._attr_max_temp = DEFAULT_MAX_TEMP self._attr_unique_id = f"{self._id}-{self._model}" async def async_added_to_hass(self) -> None: @@ -446,7 +447,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, temp + self._id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) ) self.async_write_ha_state() From faecc90b389dd47ee36dcf1c94d9d0b644b5c4c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Oct 2021 10:11:07 -0500 Subject: [PATCH 0136/1452] Workaround brightness transition delay from off in older yeelight models (#58774) --- homeassistant/components/yeelight/__init__.py | 14 +++++++++++++ homeassistant/components/yeelight/light.py | 6 +++++- tests/components/yeelight/test_light.py | 20 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e57979a7ea2..1408cb56709 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -35,6 +35,20 @@ _LOGGER = logging.getLogger(__name__) STATE_CHANGE_TIME = 0.40 # seconds POWER_STATE_CHANGE_TIME = 1 # seconds +# +# These models do not transition correctly when turning on, and +# yeelight is no longer updating the firmware on older devices +# +# https://github.com/home-assistant/core/issues/58315 +# +# The problem can be worked around by always setting the brightness +# even when the bulb is reporting the brightness is already at the +# desired level. +# +MODELS_WITH_DELAYED_ON_TRANSITION = { + "color", # YLDP02YL +} + DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a6b51046fc6..3d84a30f44e 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -63,6 +63,7 @@ from . import ( DATA_DEVICE, DATA_UPDATED, DOMAIN, + MODELS_WITH_DELAYED_ON_TRANSITION, POWER_STATE_CHANGE_TIME, YEELIGHT_FLOW_TRANSITION_SCHEMA, YeelightEntity, @@ -614,7 +615,10 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb brightness.""" if not brightness: return - if math.floor(self.brightness) == math.floor(brightness): + if ( + math.floor(self.brightness) == math.floor(brightness) + and self._bulb.model not in MODELS_WITH_DELAYED_ON_TRANSITION + ): _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 diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index a4e9e2d9746..4377efe129f 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -641,6 +641,25 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.last_properties["flowing"] = "0" + mocked_bulb.model = "color" # color model needs a workaround (see MODELS_WITH_DELAYED_ON_TRANSITION) + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"], + }, + 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 == [ + call(pytest.approx(50.1, 0.1), duration=350, light_type=ANY) + ] + mocked_bulb.async_set_brightness.reset_mock() + + mocked_bulb.model = "colora" # colora does not need a workaround await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -683,6 +702,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["flowing"] = "1" + await hass.services.async_call( "light", SERVICE_TURN_ON, From ce27fb87c662867631dd90f08f38a3fee2ea40d5 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 1 Nov 2021 02:11:20 +1100 Subject: [PATCH 0137/1452] dlna_dmr: less eager discovery (#58780) --- homeassistant/components/dlna_dmr/config_flow.py | 16 ++++++++++++++++ homeassistant/components/dlna_dmr/manifest.json | 12 ------------ homeassistant/generated/ssdp.py | 12 ------------ tests/components/dlna_dmr/test_config_flow.py | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 2ac74fb4cbd..bfee37722b8 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -470,4 +470,20 @@ def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: return True + # Special cases for devices with other discovery methods (e.g. mDNS), or + # that advertise multiple unrelated (sent in separate discovery packets) + # UPnP devices. + manufacturer = discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() + model = discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() + + if manufacturer.startswith("xbmc") or model == "kodi": + # kodi + return True + if manufacturer.startswith("samsung") and "tv" in model: + # samsungtv + return True + if manufacturer.startswith("lg") and "tv" in model: + # webostv + return True + return False diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 2c87260834f..962b2e167be 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -17,18 +17,6 @@ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" } ], "codeowners": ["@StevenLooman", "@chishm"], diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 925ba5b82fe..9434bc11f61 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -95,18 +95,6 @@ SSDP = { { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" } ], "fritz": [ diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 5a2327ecce9..e2d82d5b559 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -631,6 +631,22 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "alternative_integration" + for manufacturer, model in [ + ("XBMC Foundation", "Kodi"), + ("Samsung", "Smart TV"), + ("LG Electronics.", "LG TV"), + ]: + discovery = dict(MOCK_DISCOVERY) + discovery[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer + discovery[ssdp.ATTR_UPNP_MODEL_NAME] = model + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: """Test a config flow started by unignoring a device.""" From 13386fc41b502aeb992c9c6a72874c9fc6b30364 Mon Sep 17 00:00:00 2001 From: purcell-lab <79175134+purcell-lab@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:11:48 +1100 Subject: [PATCH 0138/1452] Fix solaredge energy sensor names (#58773) --- homeassistant/components/solaredge/const.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index d97c191a36b..6e353ffe339 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -147,45 +147,45 @@ SENSOR_TYPES = [ icon="mdi:car-battery", ), SolarEdgeSensorEntityDescription( - key="purchased_power", + key="purchased_energy", json_key="Purchased", - name="Imported Power", + name="Imported Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="production_power", + key="production_energy", json_key="Production", - name="Production Power", + name="Production Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="consumption_power", + key="consumption_energy", json_key="Consumption", - name="Consumption Power", + name="Consumption Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="selfconsumption_power", + key="selfconsumption_energy", json_key="SelfConsumption", - name="SelfConsumption Power", + name="SelfConsumption Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="feedin_power", + key="feedin_energy", json_key="FeedIn", - name="Exported Power", + name="Exported Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, From 2ae86124c782ebf09c8a2faeec619651b38356d5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Oct 2021 16:13:26 +0100 Subject: [PATCH 0139/1452] Add zeroconf ATTR constants (#58671) Co-authored-by: epenet --- homeassistant/components/hue/config_flow.py | 10 +++-- homeassistant/components/zeroconf/__init__.py | 38 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 72938ebfe0a..7149b4d9442 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -11,11 +11,12 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import DiscoveryInfoType from .bridge import authenticate_bridge from .const import ( @@ -207,14 +208,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = bridge return await self.async_step_link() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle a discovered Hue bridge. This flow is triggered by the Zeroconf component. It will check if the host is already configured and delegate to the import step if not. """ bridge = self._async_get_bridge( - discovery_info["host"], discovery_info["properties"]["bridgeid"] + discovery_info[zeroconf.ATTR_HOST], + discovery_info[zeroconf.ATTR_PROPERTIES]["bridgeid"], ) await self.async_set_unique_id(bridge.id) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 4062200972d..cd48b572577 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -8,7 +8,7 @@ from ipaddress import IPv4Address, IPv6Address, ip_address import logging import socket import sys -from typing import Any, TypedDict, cast +from typing import Any, Final, TypedDict, cast import voluptuous as vol from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange @@ -61,6 +61,12 @@ MAX_PROPERTY_VALUE_LEN = 230 # Dns label max length MAX_NAME_LEN = 63 +# Attributes for HaServiceInfo +ATTR_HOST: Final = "host" +ATTR_NAME: Final = "name" +ATTR_PROPERTIES: Final = "properties" + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -349,7 +355,7 @@ class ZeroconfDiscovery: # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - props = info["properties"] + props = info[ATTR_PROPERTIES] if domain := async_get_homekit_discovery_domain(self.homekit_models, props): discovery_flow.async_create_flow( self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT}, info @@ -371,18 +377,18 @@ class ZeroconfDiscovery: # likely bad homekit data return - if "name" in info: - lowercase_name: str | None = info["name"].lower() + if ATTR_NAME in info: + lowercase_name: str | None = info[ATTR_NAME].lower() else: lowercase_name = None - if "macaddress" in info["properties"]: - uppercase_mac: str | None = info["properties"]["macaddress"].upper() + if "macaddress" in info[ATTR_PROPERTIES]: + uppercase_mac: str | None = info[ATTR_PROPERTIES]["macaddress"].upper() else: uppercase_mac = None - if "manufacturer" in info["properties"]: - lowercase_manufacturer: str | None = info["properties"][ + if "manufacturer" in info[ATTR_PROPERTIES]: + lowercase_manufacturer: str | None = info[ATTR_PROPERTIES][ "manufacturer" ].lower() else: @@ -474,14 +480,14 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: if (host := _first_non_link_local_or_v6_address(addresses)) is None: return None - return { - "host": str(host), - "port": service.port, - "hostname": service.server, - "type": service.type, - "name": service.name, - "properties": properties, - } + return HaServiceInfo( + host=str(host), + port=service.port, + hostname=service.server, + type=service.type, + name=service.name, + properties=properties, + ) def _first_non_link_local_or_v6_address(addresses: list[bytes]) -> str | None: From 4f83a251facdad81e1057933ac7a5b8d7c5d7338 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 1 Nov 2021 02:16:50 +1100 Subject: [PATCH 0140/1452] Bump async-upnp-client to 0.22.11 (#58803) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 962b2e167be..0f2135094c9 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.10"], + "requirements": ["async-upnp-client==0.22.11"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 2017dd9e75c..21f1af73823 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.10"], + "requirements": ["async-upnp-client==0.22.11"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 05464c54914..6d329788477 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.10"], + "requirements": ["async-upnp-client==0.22.11"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 0b7718074a7..117ec5ea620 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.10"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.11"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5ff0536e395..36ab9ad8c38 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.10 +async-upnp-client==0.22.11 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 01cbde009aa..a8b25240fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.10 +async-upnp-client==0.22.11 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44e333122ed..b99582c82d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.10 +async-upnp-client==0.22.11 # homeassistant.components.aurora auroranoaa==0.0.2 From 3c5799e394eb168d962b18eead9f08e2787d266d Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 31 Oct 2021 16:17:35 +0100 Subject: [PATCH 0141/1452] =?UTF-8?q?Cleanup=20old=20config=20entry=20migr?= =?UTF-8?q?ation=20from=20M=C3=A9t=C3=A9o-France=20(#58809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/meteo_france/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 27203aab298..2f47aee9c02 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -60,23 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Meteo-France account from a config entry.""" hass.data.setdefault(DOMAIN, {}) - latitude = entry.data.get(CONF_LATITUDE) - client = MeteoFranceClient() - # Migrate from previous config - if not latitude: - places = await hass.async_add_executor_job( - client.search_places, entry.data[CONF_CITY] - ) - hass.config_entries.async_update_entry( - entry, - title=f"{places[0]}", - data={ - CONF_LATITUDE: places[0].latitude, - CONF_LONGITUDE: places[0].longitude, - }, - ) - latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] From ab7d8db481242dae8824de2b9985d712369bb7d4 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 31 Oct 2021 08:59:31 -0700 Subject: [PATCH 0142/1452] Add motionEye media browser (#53436) --- .../components/motioneye/__init__.py | 101 ++++ homeassistant/components/motioneye/const.py | 3 + .../components/motioneye/manifest.json | 1 + .../components/motioneye/media_source.py | 349 +++++++++++++ .../components/motioneye/test_media_source.py | 482 ++++++++++++++++++ tests/components/motioneye/test_web_hooks.py | 156 +++++- 6 files changed, 1090 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/motioneye/media_source.py create mode 100644 tests/components/motioneye/test_media_source.py diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 90abe39f075..37a15931920 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -3,9 +3,11 @@ from __future__ import annotations import asyncio from collections.abc import Callable +import contextlib from http import HTTPStatus import json import logging +import os from types import MappingProxyType from typing import Any from urllib.parse import urlencode, urljoin @@ -15,13 +17,17 @@ from motioneye_client.client import ( MotionEyeClient, MotionEyeClientError, MotionEyeClientInvalidAuthError, + MotionEyeClientPathError, ) from motioneye_client.const import ( KEY_CAMERAS, KEY_HTTP_METHOD_POST_JSON, KEY_ID, KEY_NAME, + KEY_ROOT_DIRECTORY, KEY_WEB_HOOK_CONVERSION_SPECIFIERS, + KEY_WEB_HOOK_CS_FILE_PATH, + KEY_WEB_HOOK_CS_FILE_TYPE, KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, KEY_WEB_HOOK_NOTIFICATIONS_URL, @@ -31,6 +37,7 @@ from motioneye_client.const import ( ) from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.media_source.const import URI_SCHEME from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.webhook import ( @@ -74,6 +81,8 @@ from .const import ( DOMAIN, EVENT_FILE_STORED, EVENT_FILE_STORED_KEYS, + EVENT_FILE_URL, + EVENT_MEDIA_CONTENT_ID, EVENT_MOTION_DETECTED, EVENT_MOTION_DETECTED_KEYS, MOTIONEYE_MANUFACTURER, @@ -101,6 +110,20 @@ def get_motioneye_device_identifier( return (DOMAIN, f"{config_entry_id}_{camera_id}") +def split_motioneye_device_identifier( + identifier: tuple[str, str] +) -> tuple[str, str, int] | None: + """Get the identifiers for a motionEye device.""" + if len(identifier) != 2 or identifier[0] != DOMAIN or "_" not in identifier[1]: + return None + config_id, camera_id_str = identifier[1].split("_", 1) + try: + camera_id = int(camera_id_str) + except ValueError: + return None + return (DOMAIN, config_id, camera_id) + + def get_motioneye_entity_unique_id( config_entry_id: str, camera_id: int, entity_type: str ) -> str: @@ -428,6 +451,21 @@ async def handle_webhook( status=HTTPStatus.BAD_REQUEST, ) + if KEY_WEB_HOOK_CS_FILE_PATH in data and KEY_WEB_HOOK_CS_FILE_TYPE in data: + try: + event_file_type = int(data[KEY_WEB_HOOK_CS_FILE_TYPE]) + except ValueError: + pass + else: + data.update( + _get_media_event_data( + hass, + device, + data[KEY_WEB_HOOK_CS_FILE_PATH], + event_file_type, + ) + ) + hass.bus.async_fire( f"{DOMAIN}.{event_type}", { @@ -440,6 +478,69 @@ async def handle_webhook( return None +def _get_media_event_data( + hass: HomeAssistant, + device: dr.DeviceEntry, + event_file_path: str, + event_file_type: int, +) -> dict[str, str]: + config_entry_id = next(iter(device.config_entries), None) + if not config_entry_id or config_entry_id not in hass.data[DOMAIN]: + return {} + + config_entry_data = hass.data[DOMAIN][config_entry_id] + client = config_entry_data[CONF_CLIENT] + coordinator = config_entry_data[CONF_COORDINATOR] + + for identifier in device.identifiers: + data = split_motioneye_device_identifier(identifier) + if data is not None: + camera_id = data[2] + camera = get_camera_from_cameras(camera_id, coordinator.data) + break + else: + return {} + + root_directory = camera.get(KEY_ROOT_DIRECTORY) if camera else None + if root_directory is None: + return {} + + kind = "images" if client.is_file_type_image(event_file_type) else "movies" + + # The file_path in the event is the full local filesystem path to the + # media. To convert that to the media path that motionEye will + # understand, we need to strip the root directory from the path. + if os.path.commonprefix([root_directory, event_file_path]) != root_directory: + return {} + + file_path = "/" + os.path.relpath(event_file_path, root_directory) + output = { + EVENT_MEDIA_CONTENT_ID: ( + f"{URI_SCHEME}{DOMAIN}/{config_entry_id}#{device.id}#{kind}#{file_path}" + ), + } + url = get_media_url( + client, + camera_id, + file_path, + kind == "images", + ) + if url: + output[EVENT_FILE_URL] = url + return output + + +def get_media_url( + client: MotionEyeClient, camera_id: int, path: str, image: bool +) -> str | None: + """Get the URL for a motionEye media item.""" + with contextlib.suppress(MotionEyeClientPathError): + if image: + return client.get_image_url(camera_id, path) + return client.get_movie_url(camera_id, path) + return None + + class MotionEyeEntity(CoordinatorEntity): """Base class for motionEye entities.""" diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index 1dbb78f1e03..37e751236da 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -80,6 +80,9 @@ EVENT_FILE_STORED_KEYS: Final = [ KEY_WEB_HOOK_CS_MOTION_VERSION, ] +EVENT_FILE_URL: Final = "file_url" +EVENT_MEDIA_CONTENT_ID: Final = "media_content_id" + MOTIONEYE_MANUFACTURER: Final = "motionEye" SERVICE_SET_TEXT_OVERLAY: Final = "set_text_overlay" diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 9be95c21162..ae6d3108f96 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -5,6 +5,7 @@ "config_flow": true, "dependencies": [ "http", + "media_source", "webhook" ], "requirements": [ diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py new file mode 100644 index 00000000000..4cc3dd9f2f7 --- /dev/null +++ b/homeassistant/components/motioneye/media_source.py @@ -0,0 +1,349 @@ +"""motionEye Media Source Implementation.""" +from __future__ import annotations + +import logging +from pathlib import PurePath +from typing import Optional, Tuple, cast + +from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_source.error import MediaSourceError, Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr + +from . import get_media_url, split_motioneye_device_identifier +from .const import CONF_CLIENT, DOMAIN + +MIME_TYPE_MAP = { + "movies": "video/mp4", + "images": "image/jpeg", +} + +MEDIA_CLASS_MAP = { + "movies": MEDIA_CLASS_VIDEO, + "images": MEDIA_CLASS_IMAGE, +} + +_LOGGER = logging.getLogger(__name__) + + +# Hierarchy: +# +# url (e.g. http://my-motioneye-1, http://my-motioneye-2) +# -> Camera (e.g. "Office", "Kitchen") +# -> kind (e.g. Images, Movies) +# -> path hierarchy as configured on motionEye + + +async def async_get_media_source(hass: HomeAssistant) -> MotionEyeMediaSource: + """Set up motionEye media source.""" + return MotionEyeMediaSource(hass) + + +class MotionEyeMediaSource(MediaSource): + """Provide motionEye stills and videos as media sources.""" + + name: str = "motionEye Media" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize MotionEyeMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + config_id, device_id, kind, path = self._parse_identifier(item.identifier) + + if not config_id or not device_id or not kind or not path: + raise Unresolvable( + f"Incomplete media identifier specified: {item.identifier}" + ) + + config = self._get_config_or_raise(config_id) + device = self._get_device_or_raise(device_id) + self._verify_kind_or_raise(kind) + + url = get_media_url( + self.hass.data[DOMAIN][config.entry_id][CONF_CLIENT], + self._get_camera_id_or_raise(config, device), + self._get_path_or_raise(path), + kind == "images", + ) + if not url: + raise Unresolvable(f"Could not resolve media item: {item.identifier}") + + return PlayMedia(url, MIME_TYPE_MAP[kind]) + + @callback + @classmethod + def _parse_identifier( + cls, identifier: str + ) -> tuple[str | None, str | None, str | None, str | None]: + base = [None] * 4 + data = identifier.split("#", 3) + return cast( + Tuple[Optional[str], Optional[str], Optional[str], Optional[str]], + tuple(data + base)[:4], # type: ignore[operator] + ) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if item.identifier: + config_id, device_id, kind, path = self._parse_identifier(item.identifier) + config = device = None + if config_id: + config = self._get_config_or_raise(config_id) + if device_id: + device = self._get_device_or_raise(device_id) + if kind: + self._verify_kind_or_raise(kind) + path = self._get_path_or_raise(path) + + if config and device and kind: + return await self._build_media_path(config, device, kind, path) + if config and device: + return self._build_media_kinds(config, device) + if config: + return self._build_media_devices(config) + return self._build_media_configs() + + def _get_config_or_raise(self, config_id: str) -> ConfigEntry: + """Get a config entry from a URL.""" + entry = self.hass.config_entries.async_get_entry(config_id) + if not entry: + raise MediaSourceError(f"Unable to find config entry with id: {config_id}") + return entry + + def _get_device_or_raise(self, device_id: str) -> dr.DeviceEntry: + """Get a config entry from a URL.""" + device_registry = dr.async_get(self.hass) + device = device_registry.async_get(device_id) + if not device: + raise MediaSourceError(f"Unable to find device with id: {device_id}") + return device + + @classmethod + def _verify_kind_or_raise(cls, kind: str) -> None: + """Verify kind is an expected value.""" + if kind in MEDIA_CLASS_MAP: + return + raise MediaSourceError(f"Unknown media type: {kind}") + + @classmethod + def _get_path_or_raise(cls, path: str | None) -> str: + """Verify path is a valid motionEye path.""" + if not path: + return "/" + if PurePath(path).root == "/": + return path + raise MediaSourceError( + f"motionEye media path must start with '/', received: {path}" + ) + + @classmethod + def _get_camera_id_or_raise( + cls, config: ConfigEntry, device: dr.DeviceEntry + ) -> int: + """Get a config entry from a URL.""" + for identifier in device.identifiers: + data = split_motioneye_device_identifier(identifier) + if data is not None: + return data[2] + raise MediaSourceError(f"Could not find camera id for device id: {device.id}") + + @classmethod + def _build_media_config(cls, config: ConfigEntry) -> BrowseMediaSource: + return BrowseMediaSource( + domain=DOMAIN, + identifier=config.entry_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=config.title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + def _build_media_configs(self) -> BrowseMediaSource: + """Build the media sources for config entries.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title="motionEye Media", + can_play=False, + can_expand=True, + children=[ + self._build_media_config(entry) + for entry in self.hass.config_entries.async_entries(DOMAIN) + ], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + @classmethod + def _build_media_device( + cls, + config: ConfigEntry, + device: dr.DeviceEntry, + full_title: bool = True, + ) -> BrowseMediaSource: + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{config.entry_id}#{device.id}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=f"{config.title} {device.name}" if full_title else device.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + def _build_media_devices(self, config: ConfigEntry) -> BrowseMediaSource: + """Build the media sources for device entries.""" + device_registry = dr.async_get(self.hass) + devices = dr.async_entries_for_config_entry(device_registry, config.entry_id) + + base = self._build_media_config(config) + base.children = [ + self._build_media_device(config, device, full_title=False) + for device in devices + ] + return base + + @classmethod + def _build_media_kind( + cls, + config: ConfigEntry, + device: dr.DeviceEntry, + kind: str, + full_title: bool = True, + ) -> BrowseMediaSource: + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{config.entry_id}#{device.id}#{kind}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_CLASS_DIRECTORY, + title=( + f"{config.title} {device.name} {kind.title()}" + if full_title + else kind.title() + ), + can_play=False, + can_expand=True, + children_media_class=( + MEDIA_CLASS_MOVIE if kind == "movies" else MEDIA_CLASS_IMAGE + ), + ) + + def _build_media_kinds( + self, config: ConfigEntry, device: dr.DeviceEntry + ) -> BrowseMediaSource: + base = self._build_media_device(config, device) + base.children = [ + self._build_media_kind(config, device, kind, full_title=False) + for kind in MEDIA_CLASS_MAP + ] + return base + + async def _build_media_path( + self, + config: ConfigEntry, + device: dr.DeviceEntry, + kind: str, + path: str, + ) -> BrowseMediaSource: + """Build the media sources for media kinds.""" + base = self._build_media_kind(config, device, kind) + + parsed_path = PurePath(path) + if path != "/": + base.title += " " + str(PurePath(*parsed_path.parts[1:])) + + base.children = [] + + client = self.hass.data[DOMAIN][config.entry_id][CONF_CLIENT] + camera_id = self._get_camera_id_or_raise(config, device) + + if kind == "movies": + resp = await client.async_get_movies(camera_id) + else: + resp = await client.async_get_images(camera_id) + + sub_dirs: set[str] = set() + parts = parsed_path.parts + for media in resp.get(KEY_MEDIA_LIST, []): + if ( + KEY_PATH not in media + or KEY_MIME_TYPE not in media + or media[KEY_MIME_TYPE] not in MIME_TYPE_MAP.values() + ): + continue + + # Example path: '/2021-04-21/21-13-10.mp4' + parts_media = PurePath(media[KEY_PATH]).parts + + if parts_media[: len(parts)] == parts and len(parts_media) > len(parts): + full_child_path = str(PurePath(*parts_media[: len(parts) + 1])) + display_child_path = parts_media[len(parts)] + + # Child is a media file. + if len(parts) + 1 == len(parts_media): + if kind == "movies": + thumbnail_url = client.get_movie_url( + camera_id, full_child_path, preview=True + ) + else: + thumbnail_url = client.get_image_url( + camera_id, full_child_path, preview=True + ) + + base.children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{config.entry_id}#{device.id}#{kind}#{full_child_path}", + media_class=MEDIA_CLASS_MAP[kind], + media_content_type=media[KEY_MIME_TYPE], + title=display_child_path, + can_play=(kind == "movies"), + can_expand=False, + thumbnail=thumbnail_url, + ) + ) + + # Child is a subdirectory. + elif len(parts) + 1 < len(parts_media): + if full_child_path not in sub_dirs: + sub_dirs.add(full_child_path) + base.children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=( + f"{config.entry_id}#{device.id}" + f"#{kind}#{full_child_path}" + ), + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_CLASS_DIRECTORY, + title=display_child_path, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + ) + return base diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py new file mode 100644 index 00000000000..65c700ab2ee --- /dev/null +++ b/tests/components/motioneye/test_media_source.py @@ -0,0 +1,482 @@ +"""Test Local Media Source.""" +import logging +from unittest.mock import AsyncMock, Mock, call + +from motioneye_client.client import MotionEyeClientPathError +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_source import const +from homeassistant.components.media_source.error import MediaSourceError, Unresolvable +from homeassistant.components.media_source.models import PlayMedia +from homeassistant.components.motioneye.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import ( + TEST_CAMERA_DEVICE_IDENTIFIER, + TEST_CAMERA_ID, + TEST_CONFIG_ENTRY_ID, + create_mock_motioneye_client, + setup_mock_motioneye_config_entry, +) + +TEST_MOVIES = { + "mediaList": [ + { + "mimeType": "video/mp4", + "sizeStr": "4.7 MB", + "momentStrShort": "25 Apr, 00:26", + "timestamp": 1619335614.0353653, + "momentStr": "25 April 2021, 00:26", + "path": "/2021-04-25/00-26-22.mp4", + }, + { + "mimeType": "video/mp4", + "sizeStr": "9.2 MB", + "momentStrShort": "25 Apr, 00:37", + "timestamp": 1619336268.0683491, + "momentStr": "25 April 2021, 00:37", + "path": "/2021-04-25/00-36-49.mp4", + }, + { + "mimeType": "video/mp4", + "sizeStr": "28.3 MB", + "momentStrShort": "25 Apr, 00:03", + "timestamp": 1619334211.0403328, + "momentStr": "25 April 2021, 00:03", + "path": "/2021-04-25/00-02-27.mp4", + }, + ] +} + +TEST_IMAGES = { + "mediaList": [ + { + "mimeType": "image/jpeg", + "sizeStr": "216.5 kB", + "momentStrShort": "12 Apr, 20:13", + "timestamp": 1618283619.6541321, + "momentStr": "12 April 2021, 20:13", + "path": "/2021-04-12/20-13-39.jpg", + } + ], +} + + +_LOGGER = logging.getLogger(__name__) + + +async def test_async_browse_media_success(hass: HomeAssistant) -> None: + """Test successful browse media.""" + + client = create_mock_motioneye_client() + config = await setup_mock_motioneye_config_entry(hass, client=client) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + media = await media_source.async_browse_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}", + ) + + assert media.as_dict() == { + "title": "motionEye Media", + "media_class": "directory", + "media_content_type": "", + "media_content_id": "media-source://motioneye", + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + "children": [ + { + "title": "http://test:8766", + "media_class": "directory", + "media_content_type": "", + "media_content_id": ( + "media-source://motioneye/74565ad414754616000674c87bdc876c" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + } + ], + } + + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}" + ) + + assert media.as_dict() == { + "title": "http://test:8766", + "media_class": "directory", + "media_content_type": "", + "media_content_id": ( + "media-source://motioneye/74565ad414754616000674c87bdc876c" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + "children": [ + { + "title": "Test Camera", + "media_class": "directory", + "media_content_type": "", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + } + ], + } + + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}" + ) + assert media.as_dict() == { + "title": "http://test:8766 Test Camera", + "media_class": "directory", + "media_content_type": "", + "media_content_id": ( + f"media-source://motioneye/74565ad414754616000674c87bdc876c#{device.id}" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + "children": [ + { + "title": "Movies", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "movie", + "thumbnail": None, + }, + { + "title": "Images", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#images" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "image", + "thumbnail": None, + }, + ], + } + + client.async_get_movies = AsyncMock(return_value=TEST_MOVIES) + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies" + ) + + assert media.as_dict() == { + "title": "http://test:8766 Test Camera Movies", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "movie", + "thumbnail": None, + "children": [ + { + "title": "2021-04-25", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies#/2021-04-25" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "directory", + "thumbnail": None, + } + ], + } + + client.get_movie_url = Mock(return_value="http://movie") + media = await media_source.async_browse_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", + ) + assert media.as_dict() == { + "title": "http://test:8766 Test Camera Movies 2021-04-25", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "movie", + "thumbnail": None, + "children": [ + { + "title": "00-26-22.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies#" + "/2021-04-25/00-26-22.mp4" + ), + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": "http://movie", + }, + { + "title": "00-36-49.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies#" + "/2021-04-25/00-36-49.mp4" + ), + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": "http://movie", + }, + { + "title": "00-02-27.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies#" + "/2021-04-25/00-02-27.mp4" + ), + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": "http://movie", + }, + ], + } + + +async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: + """Test successful browse media of images.""" + + client = create_mock_motioneye_client() + config = await setup_mock_motioneye_config_entry(hass, client=client) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + client.async_get_images = AsyncMock(return_value=TEST_IMAGES) + client.get_image_url = Mock(return_value="http://image") + + media = await media_source.async_browse_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#images#/2021-04-12", + ) + assert media.as_dict() == { + "title": "http://test:8766 Test Camera Images 2021-04-12", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#images" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "image", + "thumbnail": None, + "children": [ + { + "title": "20-13-39.jpg", + "media_class": "image", + "media_content_type": "image/jpeg", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#images#" + "/2021-04-12/20-13-39.jpg" + ), + "can_play": False, + "can_expand": False, + "children_media_class": None, + "thumbnail": "http://image", + } + ], + } + + +async def test_async_resolve_media_success(hass: HomeAssistant) -> None: + """Test successful resolve media.""" + + client = create_mock_motioneye_client() + + config = await setup_mock_motioneye_config_entry(hass, client=client) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + # Test successful resolve for a movie. + client.get_movie_url = Mock(return_value="http://movie-url") + media = await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" + ), + ) + assert media == PlayMedia(url="http://movie-url", mime_type="video/mp4") + assert client.get_movie_url.call_args == call(TEST_CAMERA_ID, "/foo.mp4") + + # Test successful resolve for an image. + client.get_image_url = Mock(return_value="http://image-url") + media = await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/foo.jpg" + ), + ) + assert media == PlayMedia(url="http://image-url", mime_type="image/jpeg") + assert client.get_image_url.call_args == call(TEST_CAMERA_ID, "/foo.jpg") + + +async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: + """Test failed resolve media calls.""" + + client = create_mock_motioneye_client() + + config = await setup_mock_motioneye_config_entry(hass, client=client) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + broken_device_1 = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={(DOMAIN, config.entry_id)}, + ) + broken_device_2 = device_registry.async_get_or_create( + config_entry_id=config.entry_id, + identifiers={(DOMAIN, f"{config.entry_id}_NOTINT")}, + ) + client.get_movie_url = Mock(return_value="http://url") + + # URI doesn't contain necessary components. + with pytest.raises(Unresolvable): + await media_source.async_resolve_media(hass, f"{const.URI_SCHEME}{DOMAIN}/foo") + + # Config entry doesn't exist. + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4" + ) + + # Device doesn't exist. + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4" + ) + + # Device identifiers are incorrect (no camera id) + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_1.id}#images#4" + ), + ) + + # Device identifiers are incorrect (non integer camera id) + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_2.id}#images#4" + ), + ) + + # Kind is incorrect. + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#{device.id}#games#moo", + ) + + # Playback URL raises exception. + client.get_movie_url = Mock(side_effect=MotionEyeClientPathError) + with pytest.raises(Unresolvable): + await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" + ), + ) + + # Media path does not start with '/' + client.get_movie_url = Mock(side_effect=MotionEyeClientPathError) + with pytest.raises(MediaSourceError): + await media_source.async_resolve_media( + hass, + ( + f"{const.URI_SCHEME}{DOMAIN}" + f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#foo.mp4" + ), + ) + + # Media missing path. + broken_movies = {"mediaList": [{}, {"path": "something", "mimeType": "NOT_A_MIME"}]} + client.async_get_movies = AsyncMock(return_value=broken_movies) + media = await media_source.async_browse_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", + ) + assert media.as_dict() == { + "title": "http://test:8766 Test Camera Movies 2021-04-25", + "media_class": "directory", + "media_content_type": "directory", + "media_content_id": ( + f"media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies" + ), + "can_play": False, + "can_expand": True, + "children_media_class": "movie", + "thumbnail": None, + "children": [], + } diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index cf4fe46c73a..6a51ea871c7 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -2,11 +2,12 @@ import copy from http import HTTPStatus from typing import Any -from unittest.mock import AsyncMock, call, patch +from unittest.mock import AsyncMock, Mock, call, patch from motioneye_client.const import ( KEY_CAMERAS, KEY_HTTP_METHOD_POST_JSON, + KEY_ROOT_DIRECTORY, KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, KEY_WEB_HOOK_NOTIFICATIONS_URL, @@ -18,6 +19,7 @@ from motioneye_client.const import ( from homeassistant.components.motioneye.const import ( ATTR_EVENT_TYPE, CONF_WEBHOOK_SET_OVERWRITE, + DEFAULT_SCAN_INTERVAL, DOMAIN, EVENT_FILE_STORED, EVENT_MOTION_DETECTED, @@ -28,6 +30,7 @@ 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 +import homeassistant.util.dt as dt_util from . import ( TEST_CAMERA, @@ -36,13 +39,14 @@ from . import ( TEST_CAMERA_ID, TEST_CAMERA_NAME, TEST_CAMERAS, + TEST_CONFIG_ENTRY_ID, TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry, setup_mock_motioneye_config_entry, ) -from tests.common import async_capture_events +from tests.common import async_capture_events, async_fire_time_changed WEB_HOOK_MOTION_DETECTED_QUERY_STRING = ( "camera_id=%t&changed_pixels=%D&despeckle_labels=%Q&event=%v&fps=%{fps}" @@ -368,3 +372,151 @@ async def test_bad_query_cannot_decode( assert resp.status == HTTPStatus.BAD_REQUEST assert not motion_events assert not storage_events + + +async def test_event_media_data(hass: HomeAssistant, hass_client_no_auth: Any) -> None: + """Test an event with a file path generates media data.""" + await async_setup_component(hass, "http", {"http": {}}) + + device_registry = await dr.async_get_registry(hass) + client = create_mock_motioneye_client() + config_entry = await setup_mock_motioneye_config_entry(hass, client=client) + + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + hass_client = await hass_client_no_auth() + + events = async_capture_events(hass, f"{DOMAIN}.{EVENT_FILE_STORED}") + + client.get_movie_url = Mock(return_value="http://movie-url") + client.get_image_url = Mock(return_value="http://image-url") + + # Test: Movie storage. + client.is_file_type_image = Mock(return_value=False) + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": f"/var/lib/motioneye/{TEST_CAMERA_NAME}/dir/one", + "file_type": "8", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 1 + assert events[-1].data["file_url"] == "http://movie-url" + assert ( + events[-1].data["media_content_id"] + == f"media-source://motioneye/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/dir/one" + ) + assert client.get_movie_url.call_args == call(TEST_CAMERA_ID, "/dir/one") + + # Test: Image storage. + client.is_file_type_image = Mock(return_value=True) + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": f"/var/lib/motioneye/{TEST_CAMERA_NAME}/dir/two", + "file_type": "4", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 2 + assert events[-1].data["file_url"] == "http://image-url" + assert ( + events[-1].data["media_content_id"] + == f"media-source://motioneye/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/dir/two" + ) + assert client.get_image_url.call_args == call(TEST_CAMERA_ID, "/dir/two") + + # Test: Invalid file type. + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": f"/var/lib/motioneye/{TEST_CAMERA_NAME}/dir/three", + "file_type": "NOT_AN_INT", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 3 + assert "file_url" not in events[-1].data + assert "media_content_id" not in events[-1].data + + # Test: Different file path. + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": "/var/random", + "file_type": "8", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 4 + assert "file_url" not in events[-1].data + assert "media_content_id" not in events[-1].data + + # Test: Not a loaded motionEye config entry. + wrong_device = device_registry.async_get_or_create( + config_entry_id="wrong_config_id", identifiers={("motioneye", "a_1")} + ) + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: wrong_device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": "/var/random", + "file_type": "8", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 5 + assert "file_url" not in events[-1].data + assert "media_content_id" not in events[-1].data + + # Test: No root directory. + camera = copy.deepcopy(TEST_CAMERA) + del camera[KEY_ROOT_DIRECTORY] + client.async_get_cameras = AsyncMock(return_value={"cameras": [camera]}) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": f"/var/lib/motioneye/{TEST_CAMERA_NAME}/dir/four", + "file_type": "8", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 6 + assert "file_url" not in events[-1].data + assert "media_content_id" not in events[-1].data + + # Test: Device has incorrect device identifiers. + device_registry.async_update_device( + device_id=device.id, new_identifiers={("not", "motioneye")} + ) + resp = await hass_client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_DEVICE_ID: device.id, + ATTR_EVENT_TYPE: EVENT_FILE_STORED, + "file_path": f"/var/lib/motioneye/{TEST_CAMERA_NAME}/dir/five", + "file_type": "8", + }, + ) + assert resp.status == HTTPStatus.OK + assert len(events) == 7 + assert "file_url" not in events[-1].data + assert "media_content_id" not in events[-1].data From 3f1b4906bf4d34b13d77b1a517c267fc4c72ee64 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:32:17 +0100 Subject: [PATCH 0143/1452] Use assignment expressions 35 (#58824) --- .../aemet/weather_update_coordinator.py | 24 +++++-------------- .../components/bt_smarthub/device_tracker.py | 6 ++--- .../components/forked_daapd/media_player.py | 9 +++---- .../components/media_extractor/__init__.py | 4 +--- .../components/plugwise/binary_sensor.py | 4 +--- homeassistant/components/plugwise/sensor.py | 12 +++------- homeassistant/components/plugwise/switch.py | 4 +--- .../components/snmp/device_tracker.py | 3 +-- .../components/ubus/device_tracker.py | 10 +++----- homeassistant/helpers/area_registry.py | 3 +-- .../helpers/config_entry_oauth2_flow.py | 3 +-- homeassistant/helpers/template.py | 9 +++---- 12 files changed, 26 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 77f4a593fb0..68c979ee27c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -398,8 +398,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None def _convert_forecast_day(self, date, day): - condition = self._get_condition_day(day) - if not condition: + if not (condition := self._get_condition_day(day)): return None return { @@ -415,8 +414,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): } def _convert_forecast_hour(self, date, day, hour): - condition = self._get_condition(day, hour) - if not condition: + if not (condition := self._get_condition(day, hour)): return None forecast_dt = date.replace(hour=hour, minute=0, second=0) @@ -435,13 +433,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _calc_precipitation(self, day, hour): """Calculate the precipitation.""" - rain_value = self._get_rain(day, hour) - if not rain_value: - rain_value = 0 - - snow_value = self._get_snow(day, hour) - if not snow_value: - snow_value = 0 + rain_value = self._get_rain(day, hour) or 0 + snow_value = self._get_snow(day, hour) or 0 if round(rain_value + snow_value, 1) == 0: return None @@ -449,13 +442,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _calc_precipitation_prob(self, day, hour): """Calculate the precipitation probability (hour).""" - rain_value = self._get_rain_prob(day, hour) - if not rain_value: - rain_value = 0 - - snow_value = self._get_snow_prob(day, hour) - if not snow_value: - snow_value = 0 + rain_value = self._get_rain_prob(day, hour) or 0 + snow_value = self._get_snow_prob(day, hour) or 0 if rain_value == 0 and snow_value == 0: return None diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index ef4e89bd4bb..a4c8717e444 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -59,8 +59,7 @@ class BTSmartHubScanner(DeviceScanner): self.success_init = False # Test the router is accessible - data = self.get_bt_smarthub_data() - if data: + if self.get_bt_smarthub_data(): self.success_init = True else: _LOGGER.info("Failed to connect to %s", self.smarthub.router_ip) @@ -85,8 +84,7 @@ class BTSmartHubScanner(DeviceScanner): return _LOGGER.info("Scanning") - data = self.get_bt_smarthub_data() - if not data: + if not (data := self.get_bt_smarthub_data()): _LOGGER.warning("Error scanning devices") return self.last_results = data diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index aeb2350ce22..f19209a0b2d 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -797,8 +797,7 @@ class ForkedDaapdUpdater: if ( "queue" in update_types ): # update queue, queue before player for async_play_media - queue = await self._api.get_request("queue") - if queue: + if queue := await self._api.get_request("queue"): update_events["queue"] = asyncio.Event() async_dispatcher_send( self.hass, @@ -808,8 +807,7 @@ class ForkedDaapdUpdater: ) # order of below don't matter if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs - outputs = await self._api.get_request("outputs") - if outputs: + if outputs := await self._api.get_request("outputs"): outputs = outputs["outputs"] update_events[ "outputs" @@ -838,8 +836,7 @@ class ForkedDaapdUpdater: if not {"player", "options", "volume"}.isdisjoint( update_types ): # update player - player = await self._api.get_request("player") - if player: + if player := await self._api.get_request("player"): update_events["player"] = asyncio.Event() if update_events.get("queue"): await update_events[ diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index a223385d8e8..ded5e3e265e 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -89,9 +89,7 @@ class MediaExtractor: "Could not retrieve data for the URL: %s", self.get_media_url() ) else: - entities = self.get_entities() - - if not entities: + if not (entities := self.get_entities()): self.call_media_player_service(stream_selector, None) for entity_id in entities: diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 023ffa3de70..5faf5f00dde 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -112,9 +112,7 @@ class PwBinarySensor(SmileBinarySensor, BinarySensorEntity): @callback def _async_process_data(self): """Update the entity.""" - data = self._api.get_device_data(self._dev_id) - - if not data: + if not (data := self._api.get_device_data(self._dev_id)): _LOGGER.error("Received no data for device %s", self._binary_sensor) self.async_write_ha_state() return diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 6b33eccc753..59e7858f947 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -361,9 +361,7 @@ class PwThermostatSensor(SmileSensor): @callback def _async_process_data(self): """Update the entity.""" - data = self._api.get_device_data(self._dev_id) - - if not data: + if not (data := self._api.get_device_data(self._dev_id)): _LOGGER.error("Received no data for device %s", self._entity_name) self.async_write_ha_state() return @@ -388,9 +386,7 @@ class PwAuxDeviceSensor(SmileSensor): @callback def _async_process_data(self): """Update the entity.""" - data = self._api.get_device_data(self._dev_id) - - if not data: + if not (data := self._api.get_device_data(self._dev_id)): _LOGGER.error("Received no data for device %s", self._entity_name) self.async_write_ha_state() return @@ -434,9 +430,7 @@ class PwPowerSensor(SmileSensor): @callback def _async_process_data(self): """Update the entity.""" - data = self._api.get_device_data(self._dev_id) - - if not data: + if not (data := self._api.get_device_data(self._dev_id)): _LOGGER.error("Received no data for device %s", self._entity_name) self.async_write_ha_state() return diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index ce3be04681a..033fbcd7693 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -103,9 +103,7 @@ class GwSwitch(SmileGateway, SwitchEntity): @callback def _async_process_data(self): """Update the data from the Plugs.""" - data = self._api.get_device_data(self._dev_id) - - if not data: + if not (data := self._api.get_device_data(self._dev_id)): _LOGGER.error("Received no data for device %s", self._name) self.async_write_ha_state() return diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 30ec5cd41a3..aeaf3c72e0f 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -86,8 +86,7 @@ class SnmpScanner(DeviceScanner): if not self.success_init: return False - data = self.get_snmp_data() - if not data: + if not (data := self.get_snmp_data()): return False self.last_results = data diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index dc5cd8857f8..4ccba81d86c 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -125,9 +125,7 @@ class UbusDeviceScanner(DeviceScanner): results = 0 # for each access point for hostapd in self.hostapd: - result = self.ubus.get_hostapd_clients(hostapd) - - if result: + if result := self.ubus.get_hostapd_clients(hostapd): results = results + 1 # Check for each device is authorized (valid wpa key) for key in result["clients"].keys(): @@ -148,8 +146,7 @@ class DnsmasqUbusDeviceScanner(UbusDeviceScanner): def _generate_mac2name(self): if self.leasefile is None: - result = self.ubus.get_uci_config("dhcp", "dnsmasq") - if result: + if result := self.ubus.get_uci_config("dhcp", "dnsmasq"): values = result["values"].values() self.leasefile = next(iter(values))["leasefile"] else: @@ -170,8 +167,7 @@ class OdhcpdUbusDeviceScanner(UbusDeviceScanner): """Implement the Ubus device scanning for the odhcp DHCP server.""" def _generate_mac2name(self): - result = self.ubus.get_dhcp_method("ipv4leases") - if result: + if result := self.ubus.get_dhcp_method("ipv4leases"): self.mac2name = {} for device in result["device"].values(): for lease in device["leases"]: diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 0073ecfb44b..f08fbe36511 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -73,8 +73,7 @@ class AreaRegistry: @callback def async_get_or_create(self, name: str) -> AreaEntry: """Get or create an area.""" - area = self.async_get_area_by_name(name) - if area: + if area := self.async_get_area_by_name(name): return area return self.async_create(name) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 54f257cb781..42478e67bb9 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -356,8 +356,7 @@ async def async_get_implementations( registered = dict(registered) for provider_domain, get_impl in hass.data[DATA_PROVIDERS].items(): - implementation = await get_impl(hass, domain) - if implementation is not None: + if (implementation := await get_impl(hass, domain)) is not None: registered[provider_domain] = implementation return registered diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a181fbbfb44..f090b28210f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -886,8 +886,7 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: entity = search.pop() if isinstance(entity, str): entity_id = entity - entity = _get_state(hass, entity) - if entity is None: + if (entity := _get_state(hass, entity)) is None: continue elif isinstance(entity, State): entity_id = entity.entity_id @@ -1004,8 +1003,7 @@ def _get_area_name(area_reg: area_registry.AreaRegistry, valid_area_id: str) -> def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the area name from an area id, device id, or entity id.""" area_reg = area_registry.async_get(hass) - area = area_reg.async_get_area(lookup_value) - if area: + if area := area_reg.async_get_area(lookup_value): return area.name dev_reg = device_registry.async_get(hass) @@ -1226,8 +1224,7 @@ def is_state_attr(hass: HomeAssistant, entity_id: str, name: str, value: Any) -> def state_attr(hass: HomeAssistant, entity_id: str, name: str) -> Any: """Get a specific attribute from a state.""" - state_obj = _get_state(hass, entity_id) - if state_obj is not None: + if (state_obj := _get_state(hass, entity_id)) is not None: return state_obj.attributes.get(name) return None From 1ce889be602880cf6344613814f6b5b7e3af6cee Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:35:27 +0100 Subject: [PATCH 0144/1452] Use assignment expressions 36 (#58825) --- homeassistant/components/config/__init__.py | 4 +--- homeassistant/components/config/auth.py | 8 ++------ .../config/auth_provider_homeassistant.py | 7 ++----- homeassistant/components/config/config_entries.py | 3 +-- homeassistant/components/ddwrt/device_tracker.py | 9 +++------ .../components/digital_ocean/binary_sensor.py | 3 +-- homeassistant/components/digital_ocean/switch.py | 3 +-- homeassistant/components/epson/media_player.py | 3 +-- homeassistant/components/gpmdp/media_player.py | 15 +++++---------- homeassistant/components/intesishome/climate.py | 6 ++---- homeassistant/components/lcn/config_flow.py | 3 +-- .../components/lutron_caseta/device_trigger.py | 3 +-- .../components/meteoalarm/binary_sensor.py | 3 +-- .../components/squeezebox/media_player.py | 3 +-- homeassistant/components/subaru/__init__.py | 3 +-- .../components/swisscom/device_tracker.py | 3 +-- homeassistant/components/zoneminder/camera.py | 3 +-- homeassistant/components/zoneminder/sensor.py | 3 +-- homeassistant/components/zoneminder/switch.py | 3 +-- 19 files changed, 28 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 0815216ec79..c39a79f3e4a 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -226,9 +226,7 @@ class EditIdBasedConfigView(BaseEditConfigView): def _write_value(self, hass, data, config_key, new_value): """Set value.""" - value = self._get_value(hass, data, config_key) - - if value is None: + if (value := self._get_value(hass, data, config_key)) is None: value = {CONF_ID: config_key} data.append(value) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 54d992466f9..5c2e4ad2ed5 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -48,9 +48,7 @@ async def websocket_delete(hass, connection, msg): ) return - user = await hass.auth.async_get_user(msg["user_id"]) - - if not user: + if not (user := await hass.auth.async_get_user(msg["user_id"])): connection.send_message( websocket_api.error_message(msg["id"], "not_found", "User not found") ) @@ -92,9 +90,7 @@ async def websocket_create(hass, connection, msg): ) async def websocket_update(hass, connection, msg): """Update a user.""" - user = await hass.auth.async_get_user(msg.pop("user_id")) - - if not user: + if not (user := await hass.auth.async_get_user(msg.pop("user_id"))): connection.send_message( websocket_api.error_message( msg["id"], websocket_api.const.ERR_NOT_FOUND, "User not found" diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 78175678a58..590ab4bff1a 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -31,9 +31,8 @@ async def async_setup(hass): async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" provider = auth_ha.async_get_provider(hass) - user = await hass.auth.async_get_user(msg["user_id"]) - if user is None: + if (user := await hass.auth.async_get_user(msg["user_id"])) is None: connection.send_error(msg["id"], "not_found", "User not found") return @@ -149,9 +148,7 @@ async def websocket_admin_change_password(hass, connection, msg): if not connection.user.is_owner: raise Unauthorized(context=connection.context(msg)) - user = await hass.auth.async_get_user(msg["user_id"]) - - if user is None: + if (user := await hass.auth.async_get_user(msg["user_id"])) is None: connection.send_error(msg["id"], "user_not_found", "User not found") return diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index cf243137940..b1f686e23a4 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -249,8 +249,7 @@ def get_entry( msg_id: int, ) -> config_entries.ConfigEntry | None: """Get entry, send error message if it doesn't exist.""" - entry = hass.config_entries.async_get_entry(entry_id) - if entry is None: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: send_entry_not_found(connection, msg_id) return entry diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 303cfe72b97..8b7e7ed8c04 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -67,8 +67,7 @@ class DdWrtDeviceScanner(DeviceScanner): # Test the router is accessible url = f"{self.protocol}://{self.host}/Status_Wireless.live.asp" - data = self.get_ddwrt_data(url) - if not data: + if not self.get_ddwrt_data(url): raise ConnectionError("Cannot connect to DD-Wrt router") def scan_devices(self): @@ -82,9 +81,8 @@ class DdWrtDeviceScanner(DeviceScanner): # If not initialised and not already scanned and not found. if device not in self.mac2name: url = f"{self.protocol}://{self.host}/Status_Lan.live.asp" - data = self.get_ddwrt_data(url) - if not data: + if not (data := self.get_ddwrt_data(url)): return None if not (dhcp_leases := data.get("dhcp_leases")): @@ -115,9 +113,8 @@ class DdWrtDeviceScanner(DeviceScanner): endpoint = "Wireless" if self.wireless_only else "Lan" url = f"{self.protocol}://{self.host}/Status_{endpoint}.live.asp" - data = self.get_ddwrt_data(url) - if not data: + if not (data := self.get_ddwrt_data(url)): return False self.last_results = [] diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index b5188092862..172c50d20b2 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -43,8 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for droplet in droplets: - droplet_id = digital.get_droplet_id(droplet) - if droplet_id is None: + if (droplet_id := digital.get_droplet_id(droplet)) is None: _LOGGER.error("Droplet %s is not available", droplet) return False dev.append(DigitalOceanBinarySensor(digital, droplet_id)) diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index d52c223c866..b323d5be5b4 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -40,8 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for droplet in droplets: - droplet_id = digital.get_droplet_id(droplet) - if droplet_id is None: + if (droplet_id := digital.get_droplet_id(droplet)) is None: _LOGGER.error("Droplet %s is not available", droplet) return False dev.append(DigitalOceanSwitch(digital, droplet_id)) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 5223a9663d0..6abeb3b0ba6 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -100,8 +100,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): _LOGGER.debug("Setting unique_id for projector") if self._unique_id: return False - uid = await self._projector.get_serial_number() - if uid: + if uid := await self._projector.get_serial_number(): self.hass.config_entries.async_update_entry(self._entry, unique_id=uid) registry = async_get_entity_registry(self.hass) old_entity_id = registry.async_get_entity_id( diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 649e0283f5a..c6dbe41c996 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -211,8 +211,7 @@ class GPMDP(MediaPlayerEntity): """Send ws messages to GPMDP and verify request id in response.""" try: - websocket = self.get_ws() - if websocket is None: + if (websocket := self.get_ws()) is None: self._status = STATE_OFF return self._request_id += 1 @@ -342,8 +341,7 @@ class GPMDP(MediaPlayerEntity): def media_seek(self, position): """Send media_seek command to media player.""" - websocket = self.get_ws() - if websocket is None: + if (websocket := self.get_ws()) is None: return websocket.send( json.dumps( @@ -358,24 +356,21 @@ class GPMDP(MediaPlayerEntity): def volume_up(self): """Send volume_up command to media player.""" - websocket = self.get_ws() - if websocket is None: + if (websocket := self.get_ws()) is None: return websocket.send('{"namespace": "volume", "method": "increaseVolume"}') self.schedule_update_ha_state() def volume_down(self): """Send volume_down command to media player.""" - websocket = self.get_ws() - if websocket is None: + if (websocket := self.get_ws()) is None: return websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') self.schedule_update_ha_state() def set_volume_level(self, volume): """Set volume on media player, range(0..1).""" - websocket = self.get_ws() - if websocket is None: + if (websocket := self.get_ws()) is None: return websocket.send( json.dumps( diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index afe0fb519a8..4305489e25d 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -120,8 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("Error connecting to the %s server", device_type) raise PlatformNotReady from ex - ih_devices = controller.get_devices() - if ih_devices: + if ih_devices := controller.get_devices(): async_add_entities( [ IntesisAC(ih_device_id, device, controller) @@ -194,8 +193,7 @@ class IntesisAC(ClimateEntity): self._support |= SUPPORT_PRESET_MODE # Setup HVAC modes - modes = controller.get_mode_list(ih_device_id) - if modes: + if modes := controller.get_mode_list(ih_device_id): mode_list = [MAP_IH_TO_HVAC_MODE[mode] for mode in modes] self._hvac_mode_list.extend(mode_list) self._hvac_mode_list.append(HVAC_MODE_OFF) diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 20549ea32a3..9316d4309c9 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -91,8 +91,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="connection_timeout") # check if we already have a host with the same address configured - entry = get_config_entry(self.hass, data) - if entry: + if entry := get_config_entry(self.hass, data): entry.source = config_entries.SOURCE_IMPORT # Cleanup entity and device registry, if we imported from configuration.yaml to diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 857ef9b56c5..ce50923f2f5 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -235,8 +235,7 @@ async def async_get_triggers( """List device triggers for lutron caseta devices.""" triggers = [] - device = get_button_device_by_dr_id(hass, device_id) - if not device: + if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"], []) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index ce0fa97ecb9..079747afd3b 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -89,8 +89,7 @@ class MeteoAlertBinarySensor(BinarySensorEntity): self._attributes = {} self._state = False - alert = self._api.get_alert() - if alert: + if alert := self._api.get_alert(): expiration_date = dt_util.parse_datetime(alert["expires"]) now = dt_util.utcnow() diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index a0904c3178d..ef54f18bc9c 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -197,8 +197,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): known_players.append(entity) async_add_entities([entity]) - players = await lms.async_get_players() - if players: + if players := await lms.async_get_players(): for player in players: hass.async_create_task(_discovered_player(player)) diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index ce5ac6a95f5..220478da8bd 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -125,8 +125,7 @@ async def refresh_subaru_data(config_entry, vehicle_info, controller): await controller.fetch(vin, force=True) # Update our local data that will go to entity states - received_data = await controller.get_data(vin) - if received_data: + if received_data := await controller.get_data(vin): data[vin] = received_data return data diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index b50f0b31083..228f69d4c6c 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -65,8 +65,7 @@ class SwisscomDeviceScanner(DeviceScanner): return False _LOGGER.info("Loading data from Swisscom Internet Box") - data = self.get_swisscom_data() - if not data: + if not (data := self.get_swisscom_data()): return False active_clients = [client for client in data.values() if client["status"]] diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 6144fe11226..0f9f5e2f679 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -19,8 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): filter_urllib3_logging() cameras = [] for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: + if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s") return diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 0eb3e9d63a2..90c5f8d78eb 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -66,8 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: + if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch any monitors from ZoneMinder") for monitor in monitors: diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 0428ddbf888..b7ba3f48f10 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -28,8 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): switches = [] for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: + if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch monitors from ZoneMinder") return From 8e0310289234166218819b0f3070572b051f3a4f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 31 Oct 2021 13:41:55 -0400 Subject: [PATCH 0145/1452] Bump pyefergy to 0.1.3 (#58821) --- homeassistant/components/efergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index d95c0b69415..17f104c561f 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -3,7 +3,7 @@ "name": "Efergy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.1.2"], + "requirements": ["pyefergy==0.1.3"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a8b25240fcc..a6af04c2ab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.1.2 +pyefergy==0.1.3 # homeassistant.components.eight_sleep pyeight==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b99582c82d0..5c7f0062130 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,7 +862,7 @@ pydispatcher==2.0.5 pyeconet==0.1.14 # homeassistant.components.efergy -pyefergy==0.1.2 +pyefergy==0.1.3 # homeassistant.components.everlights pyeverlights==0.1.0 From b6d9e517c22bf44129d7dc27884bbef7f0f6133b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:45:27 +0100 Subject: [PATCH 0146/1452] Use assignment expressions 37 (#58827) --- .../components/arcam_fmj/media_player.py | 24 +++++++------------ .../components/cast/home_assistant_cast.py | 6 ++--- homeassistant/components/ihc/__init__.py | 3 +-- .../components/insteon/api/device.py | 3 +-- .../components/insteon/insteon_entity.py | 3 +-- homeassistant/components/kiwi/lock.py | 3 +-- .../components/mold_indicator/sensor.py | 7 ++---- homeassistant/components/rfxtrx/__init__.py | 13 +++++----- .../components/rfxtrx/binary_sensor.py | 3 +-- homeassistant/components/rfxtrx/cover.py | 3 +-- homeassistant/components/rfxtrx/light.py | 3 +-- homeassistant/components/rfxtrx/sensor.py | 3 +-- homeassistant/components/rfxtrx/switch.py | 3 +-- .../components/worxlandroid/sensor.py | 4 +--- 14 files changed, 29 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index b63279d9c26..553524dbcdf 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -255,8 +255,7 @@ class ArcamFmj(MediaPlayerEntity): @property def source(self): """Return the current input source.""" - value = self._state.get_source() - if value is None: + if (value := self._state.get_source()) is None: return None return value.name @@ -268,32 +267,28 @@ class ArcamFmj(MediaPlayerEntity): @property def sound_mode(self): """Name of the current sound mode.""" - value = self._state.get_decode_mode() - if value is None: + if (value := self._state.get_decode_mode()) is None: return None return value.name @property def sound_mode_list(self): """List of available sound modes.""" - values = self._state.get_decode_modes() - if values is None: + if (values := self._state.get_decode_modes()) is None: return None return [x.name for x in values] @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - value = self._state.get_mute() - if value is None: + if (value := self._state.get_mute()) is None: return None return value @property def volume_level(self): """Volume level of device.""" - value = self._state.get_volume() - if value is None: + if (value := self._state.get_volume()) is None: return None return value / 99.0 @@ -314,8 +309,7 @@ class ArcamFmj(MediaPlayerEntity): """Content type of current playing media.""" source = self._state.get_source() if source in (SourceCodes.DAB, SourceCodes.FM): - preset = self._state.get_tuner_preset() - if preset: + if preset := self._state.get_tuner_preset(): value = f"preset:{preset}" else: value = None @@ -339,8 +333,7 @@ class ArcamFmj(MediaPlayerEntity): @property def media_artist(self): """Artist of current playing media, music track only.""" - source = self._state.get_source() - if source == SourceCodes.DAB: + if self._state.get_source() == SourceCodes.DAB: value = self._state.get_dls_pdt() else: value = None @@ -349,8 +342,7 @@ class ArcamFmj(MediaPlayerEntity): @property def media_title(self): """Title of current playing media.""" - source = self._state.get_source() - if source is None: + if (source := self._state.get_source()) is None: return None if channel := self.media_channel: diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index fb2d790d03d..6127e466099 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -80,7 +80,5 @@ async def async_remove_user( """Remove Home Assistant Cast user.""" user_id: str | None = entry.data.get("user_id") - if user_id is not None: - user = await hass.auth.async_get_user(user_id) - if user: - await hass.auth.async_remove_user(user) + if user_id is not None and (user := await hass.auth.async_get_user(user_id)): + await hass.auth.async_remove_user(user) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index c0fe8944c66..3a75a841fde 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -286,8 +286,7 @@ def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): def autosetup_ihc_products(hass: HomeAssistant, config, ihc_controller, controller_id): """Auto setup of IHC products from the IHC project file.""" - project_xml = ihc_controller.get_project() - if not project_xml: + if not (project_xml := ihc_controller.get_project()): _LOGGER.error("Unable to read project from IHC controller") return False project = ElementTree.fromstring(project_xml) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index 6815ec43031..1071451876b 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -63,8 +63,7 @@ async def websocket_get_device( if not (ha_device := dev_registry.async_get(msg[DEVICE_ID])): notify_device_not_found(connection, msg, HA_DEVICE_NOT_FOUND) return - device = get_insteon_device_from_ha_device(ha_device) - if not device: + if not (device := get_insteon_device_from_ha_device(ha_device)): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return ha_name = compute_device_name(ha_device) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index aa2d4367225..719b58a3d9f 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -68,8 +68,7 @@ class InsteonEntity(Entity): if (description := self._insteon_device.description) is None: description = "Unknown Device" # Get an extension label if there is one - extension = self._get_label() - if extension: + if extension := self._get_label(): extension = f" {extension}" return f"{description} {self._insteon_device.address}{extension}" diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 8a0eeed83f0..650d6d34de8 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -39,8 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except KiwiException as exc: _LOGGER.error(exc) return - available_locks = kiwi.get_locks() - if not available_locks: + if not (available_locks := kiwi.get_locks()): # No locks found; abort setup routine. _LOGGER.info("No KIWI locks found in your account") return diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index c57903ce5b7..c178d5e2360 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -235,10 +235,7 @@ class MoldIndicator(SensorEntity): ) return None - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - hum = util.convert(state.state, float) - - if hum is None: + if (hum := util.convert(state.state, float)) is None: _LOGGER.error( "Unable to parse humidity sensor %s, state: %s", state.entity_id, @@ -246,7 +243,7 @@ class MoldIndicator(SensorEntity): ) return None - if unit != PERCENTAGE: + if (unit := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) != PERCENTAGE: _LOGGER.error( "Humidity sensor %s has unsupported unit: %s %s", state.entity_id, diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 9a5b7ecf57a..88bddc6afbe 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -124,8 +124,7 @@ def _get_device_lookup(devices): """Get a lookup structure for devices.""" lookup = {} for event_code, event_config in devices.items(): - event = get_rfx_object(event_code) - if event is None: + if (event := get_rfx_object(event_code)) is None: continue device_id = get_device_id( event.device, data_bits=event_config.get(CONF_DATA_BITS) @@ -313,10 +312,12 @@ def find_possible_pt2262_device(device_ids, device_id): def get_device_id(device, data_bits=None): """Calculate a device id for device.""" id_string = device.id_string - if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: - masked_id = get_pt2262_deviceid(id_string, data_bits) - if masked_id: - id_string = masked_id.decode("ASCII") + if ( + data_bits + and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4 + and (masked_id := get_pt2262_deviceid(id_string, data_bits)) + ): + id_string = masked_id.decode("ASCII") return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 914303f1468..cc11e94c526 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -114,8 +114,7 @@ async def async_setup_entry( return description for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: + if (event := get_rfx_object(packet_id)) is None: _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 26a938141a2..0244e3aa8a0 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -49,8 +49,7 @@ async def async_setup_entry( entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: + if (event := get_rfx_object(packet_id)) is None: _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index ea197b5ebc4..c67213ed6f8 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -50,8 +50,7 @@ async def async_setup_entry( # Add switch from config file entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: + if (event := get_rfx_object(packet_id)) is None: _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index afd1d6a12ce..8ce9843076d 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -237,8 +237,7 @@ async def async_setup_entry( entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: + if (event := get_rfx_object(packet_id)) is None: _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 2a09d027345..21e9e06b802 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -49,8 +49,7 @@ async def async_setup_entry( # Add switch from config file entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: + if (event := get_rfx_object(packet_id)) is None: _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index b34481d0990..464892e9126 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -134,9 +134,7 @@ class WorxLandroidSensor(SensorEntity): def get_state(self, obj): """Get the state of the mower.""" - state = self.get_error(obj) - - if state is None: + if (state := self.get_error(obj)) is None: if obj["batteryChargerState"] == "charging": return obj["batteryChargerState"] From 72801867d6f53c16846b8053b0ac367a6b5b51f3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:49:18 +0100 Subject: [PATCH 0147/1452] Use assignment expressions 38 (#58828) --- .../components/actiontec/device_tracker.py | 3 +-- homeassistant/components/avea/light.py | 3 +-- .../components/google_assistant/helpers.py | 3 +-- homeassistant/components/hassio/addon_panel.py | 3 +-- homeassistant/components/heos/__init__.py | 3 +-- .../components/homekit/type_cameras.py | 3 +-- .../components/homekit/type_sensors.py | 18 ++++++------------ homeassistant/components/homekit/util.py | 4 +--- homeassistant/components/lastfm/sensor.py | 9 +++------ .../components/nx584/binary_sensor.py | 3 +-- homeassistant/components/philips_js/light.py | 3 +-- homeassistant/components/vera/climate.py | 3 +-- homeassistant/components/yeelight/light.py | 7 ++----- homeassistant/components/zha/api.py | 12 ++++-------- homeassistant/components/zha/climate.py | 3 +-- 15 files changed, 26 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 8dc3f095437..cc26c191c8c 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -71,8 +71,7 @@ class ActiontecDeviceScanner(DeviceScanner): if not self.success_init: return False - actiontec_data = self.get_actiontec_data() - if actiontec_data is None: + if (actiontec_data := self.get_actiontec_data()) is None: return False self.last_results = [ device for device in actiontec_data if device.timevalid > -60 diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index d1df7ba3e46..ceb66ff39b6 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -59,7 +59,6 @@ class AveaLight(LightEntity): This is the only method that should fetch new data for Home Assistant. """ - brightness = self._light.get_brightness() - if brightness is not None: + if (brightness := self._light.get_brightness()) is not None: self._attr_is_on = brightness != 0 self._attr_brightness = round(255 * (brightness / 4095)) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 76933cecd91..238ee8d9576 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -521,8 +521,7 @@ class GoogleEntity: if area and area.name: device["roomHint"] = area.name - device_info = await _get_device_info(device_entry) - if device_info: + if device_info := await _get_device_info(device_entry): device["deviceInfo"] = device_info return device diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d6240896c84..41107a6fa55 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -21,8 +21,7 @@ async def async_setup_addon_panel(hass: HomeAssistant, hassio): hass.http.register_view(hassio_addon_panel) # If panels are exists - panels = await hassio_addon_panel.get_panels() - if not panels: + if not (panels := await hassio_addon_panel.get_panels()): return # Register available panels diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a4cdb6cdddc..610292f0389 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -335,9 +335,8 @@ class SourceManager: heos_const.EVENT_USER_CHANGED, heos_const.EVENT_CONNECTED, ): - sources = await get_sources() # If throttled, it will return None - if sources: + if sources := await get_sources(): self.favorites, self.inputs = sources self.source_list = self._build_source_list() _LOGGER.debug("Sources updated due to changed event") diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 6cf8735b075..14d065dc9bf 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -333,8 +333,7 @@ class Camera(HomeAccessory, PyhapCamera): session_info["id"], stream_config, ) - input_source = await self._async_get_stream_source() - if not input_source: + if not (input_source := await self._async_get_stream_source()): _LOGGER.error("Camera has no stream source") return False if "-i " not in input_source: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index c309e42a0f0..e43a899a9b2 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -114,8 +114,7 @@ class TemperatureSensor(HomeAccessory): def async_update_state(self, new_state): """Update temperature after state changed.""" unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) - temperature = convert_to_float(new_state.state) - if temperature: + if temperature := convert_to_float(new_state.state): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) _LOGGER.debug( @@ -142,8 +141,7 @@ class HumiditySensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - humidity = convert_to_float(new_state.state) - if humidity: + if humidity := convert_to_float(new_state.state): self.char_humidity.set_value(humidity) _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) @@ -170,8 +168,7 @@ class AirQualitySensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - density = convert_to_float(new_state.state) - if density: + if density := convert_to_float(new_state.state): if self.char_density.value != density: self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) @@ -206,8 +203,7 @@ class CarbonMonoxideSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - value = convert_to_float(new_state.state) - if value: + if value := convert_to_float(new_state.state): self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) @@ -242,8 +238,7 @@ class CarbonDioxideSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - value = convert_to_float(new_state.state) - if value: + if value := convert_to_float(new_state.state): self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) @@ -271,8 +266,7 @@ class LightSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - luminance = convert_to_float(new_state.state) - if luminance: + if luminance := convert_to_float(new_state.state): self.char_light.set_value(luminance) _LOGGER.debug("%s: Set to %d", self.entity_id, luminance) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a5c9f3937ea..7782c389e56 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -294,9 +294,7 @@ def get_media_player_features(state): def validate_media_player_features(state, feature_list): """Validate features for media players.""" - supported_modes = get_media_player_features(state) - - if not supported_modes: + if not (supported_modes := get_media_player_features(state)): _LOGGER.error("%s does not support any media_player features", state.entity_id) return False diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 66f05c5d34d..10623924c90 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -86,20 +86,17 @@ class LastfmSensor(SensorEntity): self._cover = self._user.get_image() self._playcount = self._user.get_playcount() - recent_tracks = self._user.get_recent_tracks(limit=2) - if recent_tracks: + if recent_tracks := self._user.get_recent_tracks(limit=2): last = recent_tracks[0] self._lastplayed = f"{last.track.artist} - {last.track.title}" - top_tracks = self._user.get_top_tracks(limit=1) - if top_tracks: + if top_tracks := self._user.get_top_tracks(limit=1): top = top_tracks[0] toptitle = re.search("', '(.+?)',", str(top)) topartist = re.search("'(.+?)',", str(top)) self._topplayed = f"{topartist.group(1)} - {toptitle.group(1)}" - now_playing = self._user.get_now_playing() - if now_playing is None: + if (now_playing := self._user.get_now_playing()) is None: self._state = STATE_NOT_SCROBBLING return diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 7a999263332..38787afb080 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -137,8 +137,7 @@ class NX584Watcher(threading.Thread): """Throw away any existing events so we don't replay history.""" self._client.get_events() while True: - events = self._client.get_events() - if events: + if events := self._client.get_events(): self._process_events(events) def run(self): diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 799b3b41631..93b65db90fa 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -200,8 +200,7 @@ class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): current = self._tv.ambilight_current_configuration if current and self._tv.ambilight_mode != "manual": if current["isExpert"]: - settings = _get_settings(current) - if settings: + if settings := _get_settings(current): return _get_effect( EFFECT_EXPERT, current["styleName"], settings["algorithm"] ) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 69d5a3ccbfa..f5dc16cf4ae 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -92,8 +92,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): @property def fan_mode(self) -> str | None: """Return the fan setting.""" - mode = self.vera_device.get_fan_mode() - if mode == "ContinuousOn": + if self.vera_device.get_fan_mode() == "ContinuousOn": return FAN_ON return FAN_AUTO diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 3d84a30f44e..e71b75755c0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -465,8 +465,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def color_temp(self) -> int: """Return the color temperature.""" - temp_in_k = self._get_property("ct") - if temp_in_k: + if temp_in_k := self._get_property("ct"): self._color_temp = kelvin_to_mired(int(temp_in_k)) return self._color_temp @@ -530,9 +529,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def rgb_color(self) -> tuple: """Return the color property.""" - rgb = self._get_property("rgb") - - if rgb is None: + if (rgb := self._get_property("rgb")) is None: return None rgb = int(rgb) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 403af7c6612..48e70c86c1f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1145,10 +1145,8 @@ def async_load_api(hass): strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) level = service.data.get(ATTR_LEVEL) - zha_device = zha_gateway.get_device(ieee) - if zha_device is not None: - channel = _get_ias_wd_channel(zha_device) - if channel: + if (zha_device := zha_gateway.get_device(ieee)) is not None: + if channel := _get_ias_wd_channel(zha_device): await channel.issue_squawk(mode, strobe, level) else: _LOGGER.error( @@ -1189,10 +1187,8 @@ def async_load_api(hass): duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) - zha_device = zha_gateway.get_device(ieee) - if zha_device is not None: - channel = _get_ias_wd_channel(zha_device) - if channel: + if (zha_device := zha_gateway.get_device(ieee)) is not None: + if channel := _get_ias_wd_channel(zha_device): await channel.issue_start_warning( mode, strobe, level, duration, duty_mode, intensity ) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index b82cccd5324..300fdb9ef66 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -405,8 +405,7 @@ class Thermostat(ZhaEntity, ClimateEntity): # occupancy attribute is an unreportable attribute, but if we get # an attribute update for an "occupied" setpoint, there's a chance # occupancy has changed - occupancy = await self._thrm.get_occupancy() - if occupancy is True: + if await self._thrm.get_occupancy() is True: self._preset = PRESET_NONE self.debug("Attribute '%s' = %s update", record.attr_name, record.value) From e0c0d008334b09bd1c34659e4c37a88598156d88 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:56:25 +0100 Subject: [PATCH 0148/1452] Use assignment expressions 39 (#58829) --- .../components/ambiclimate/config_flow.py | 4 +--- homeassistant/components/aruba/device_tracker.py | 3 +-- .../components/cisco_ios/device_tracker.py | 4 +--- homeassistant/components/dlna_dmr/config_flow.py | 3 +-- homeassistant/components/doorbird/__init__.py | 4 +--- homeassistant/components/dsmr/sensor.py | 3 +-- .../components/homematicip_cloud/services.py | 15 +++++---------- .../components/huawei_lte/device_tracker.py | 6 ++---- homeassistant/components/knx/expose.py | 3 +-- homeassistant/components/lametric/__init__.py | 3 +-- homeassistant/components/linode/binary_sensor.py | 3 +-- homeassistant/components/linode/switch.py | 3 +-- .../components/mobile_app/device_action.py | 4 +--- homeassistant/components/netatmo/sensor.py | 4 +--- homeassistant/components/repetier/sensor.py | 12 ++++-------- homeassistant/components/ripple/sensor.py | 4 +--- .../components/sky_hub/device_tracker.py | 4 +--- .../components/thomson/device_tracker.py | 3 +-- homeassistant/components/watson_iot/__init__.py | 3 +-- homeassistant/components/whirlpool/climate.py | 3 +-- homeassistant/components/xbox_live/sensor.py | 3 +-- homeassistant/components/zwave_js/sensor.py | 3 +-- homeassistant/util/location.py | 4 +--- 23 files changed, 31 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 623e96a4a67..b022b54c4fd 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -86,9 +86,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Received code for authentication.""" self._async_abort_entries_match() - token_info = await self._get_token_info(code) - - if token_info is None: + if await self._get_token_info(code) is None: return self.async_abort(reason="access_token") config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy() diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index e9799411197..721585fa391 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -74,8 +74,7 @@ class ArubaDeviceScanner(DeviceScanner): if not self.success_init: return False - data = self.get_aruba_data() - if not data: + if not (data := self.get_aruba_data()): return False self.last_results = data.values() diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index b30e9dae1f3..9861f657ff6 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -65,9 +65,7 @@ class CiscoDeviceScanner(DeviceScanner): Returns boolean if scanning successful. """ - string_result = self._get_arp_data() - - if string_result: + if string_result := self._get_arp_data(): self.last_results = [] last_results = [] diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index bfee37722b8..bfc5cb27129 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -89,8 +89,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_info_from_discovery(discovery) return self._create_entry() - discoveries = await self._async_get_discoveries() - if not discoveries: + if not (discoveries := await self._async_get_discoveries()): # Nothing found, maybe the user knows an URL to try return await self.async_step_manual() diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3bd82a7ff8e..7497304f9e1 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -269,9 +269,7 @@ class ConfiguredDoorBird: if not self.webhook_is_registered(url): self.device.change_favorite("http", f"Home Assistant ({event})", url) - fav_id = self.get_webhook_id(url) - - if not fav_id: + if not self.get_webhook_id(url): _LOGGER.warning( 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', url, diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 12b2a17016a..c016a25ad55 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -260,8 +260,7 @@ class DSMREntity(SensorEntity): @property 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: + if (value := self.get_dsmr_object_attr("value")) is None: return None if self.entity_description.key == obis_ref.ELECTRICITY_ACTIVE_TARIFF: diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 45b47b40efa..88c14c648d8 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -210,8 +210,7 @@ async def _async_activate_eco_mode_with_duration( duration = service.data[ATTR_DURATION] if hapid := service.data.get(ATTR_ACCESSPOINT_ID): - home = _get_home(hass, hapid) - if home: + if home := _get_home(hass, hapid): await home.activate_absence_with_duration(duration) else: for hap in hass.data[HMIPC_DOMAIN].values(): @@ -225,8 +224,7 @@ async def _async_activate_eco_mode_with_period( endtime = service.data[ATTR_ENDTIME] if hapid := service.data.get(ATTR_ACCESSPOINT_ID): - home = _get_home(hass, hapid) - if home: + if home := _get_home(hass, hapid): await home.activate_absence_with_period(endtime) else: for hap in hass.data[HMIPC_DOMAIN].values(): @@ -239,8 +237,7 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> temperature = service.data[ATTR_TEMPERATURE] if hapid := service.data.get(ATTR_ACCESSPOINT_ID): - home = _get_home(hass, hapid) - if home: + if home := _get_home(hass, hapid): await home.activate_vacation(endtime, temperature) else: for hap in hass.data[HMIPC_DOMAIN].values(): @@ -250,8 +247,7 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate eco mode.""" if hapid := service.data.get(ATTR_ACCESSPOINT_ID): - home = _get_home(hass, hapid) - if home: + if home := _get_home(hass, hapid): await home.deactivate_absence() else: for hap in hass.data[HMIPC_DOMAIN].values(): @@ -261,8 +257,7 @@ async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate vacation.""" if hapid := service.data.get(ATTR_ACCESSPOINT_ID): - home = _get_home(hass, hapid) - if home: + if home := _get_home(hass, hapid): await home.deactivate_vacation() else: for hap in hass.data[HMIPC_DOMAIN].values(): diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 5c451f71545..7c3f3d16c92 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -133,8 +133,7 @@ def async_add_new_entities( tracked: set[str], ) -> None: """Add new entities that are not already being tracked.""" - hosts = _get_hosts(router) - if not hosts: + if not (hosts := _get_hosts(router)): return track_wired_clients = router.config_entry.options.get( @@ -225,8 +224,7 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): async def async_update(self) -> None: """Update state.""" - hosts = _get_hosts(self.router) - if hosts is None: + if (hosts := _get_hosts(self.router)) is None: self._available = False return self._available = True diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index b4b15c977fd..6fa5a3ba728 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -137,8 +137,7 @@ class KNXExposeSensor: async def _async_entity_changed(self, event: Event) -> None: """Handle entity change.""" new_state = event.data.get("new_state") - new_value = self._get_expose_value(new_state) - if new_value is None: + if (new_value := self._get_expose_value(new_state)) is None: return old_state = event.data.get("old_state") # don't use default value for comparison on first state change (old_state is None) diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 797f0982f00..f51a34aafc1 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -34,8 +34,7 @@ def setup(hass, config): hlmn = HassLaMetricManager( client_id=conf[CONF_CLIENT_ID], client_secret=conf[CONF_CLIENT_SECRET] ) - devices = hlmn.manager.get_devices() - if not devices: + if not (devices := hlmn.manager.get_devices()): _LOGGER.error("No LaMetric devices found") return False diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 6769d72594b..4f602f5b81a 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -38,8 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for node in nodes: - node_id = linode.get_node_id(node) - if node_id is None: + if (node_id := linode.get_node_id(node)) is None: _LOGGER.error("Node %s is not available", node) return dev.append(LinodeBinarySensor(linode, node_id)) diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index 9002cb7bd11..a2a31ae62a8 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -35,8 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for node in nodes: - node_id = linode.get_node_id(node) - if node_id is None: + if (node_id := linode.get_node_id(node)) is None: _LOGGER.error("Node %s is not available", node) return dev.append(LinodeSwitch(linode, node_id)) diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index 193c25e482c..3ad43098225 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -45,9 +45,7 @@ async def async_call_action_from_config( "Unable to resolve webhook ID from the device ID" ) - service_name = get_notify_service(hass, webhook_id) - - if service_name is None: + if (service_name := get_notify_service(hass, webhook_id)) is None: raise InvalidDeviceAutomationConfig( "Unable to find notify service for webhook ID" ) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 5e7d5ae7893..5b93bdec27f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -490,9 +490,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._station_id = module_info.get("main_device", self._id) station = self._data.get_station(self._station_id) - device = self._data.get_module(self._id) - - if not device: + if not (device := self._data.get_module(self._id)): # Assume it's a station if module can't be found device = station diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index b21ff092c67..393d8a16ae3 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -106,8 +106,7 @@ class RepetierSensor(SensorEntity): def update(self): """Update the sensor.""" - data = self._get_data() - if data is None: + if (data := self._get_data()) is None: return state = data.pop("state") _LOGGER.debug("Printer %s State %s", self.name, state) @@ -127,8 +126,7 @@ class RepetierTempSensor(RepetierSensor): def update(self): """Update the sensor.""" - data = self._get_data() - if data is None: + if (data := self._get_data()) is None: return state = data.pop("state") temp_set = data["temp_set"] @@ -155,8 +153,7 @@ class RepetierJobEndSensor(RepetierSensor): def update(self): """Update the sensor.""" - data = self._get_data() - if data is None: + if (data := self._get_data()) is None: return job_name = data["job_name"] start = data["start"] @@ -180,8 +177,7 @@ class RepetierJobStartSensor(RepetierSensor): def update(self): """Update the sensor.""" - data = self._get_data() - if data is None: + if (data := self._get_data()) is None: return job_name = data["job_name"] start = data["start"] diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index 2746f5789cd..a99bb86c5ef 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -62,7 +62,5 @@ class RippleSensor(SensorEntity): def update(self): """Get the latest state of the sensor.""" - - balance = get_balance(self.address) - if balance is not None: + if (balance := get_balance(self.address)) is not None: self._state = balance diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index fda0b5e3774..8333e8e0cda 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -68,9 +68,7 @@ class SkyHubDeviceScanner(DeviceScanner): """Ensure the information from the Sky Hub is up to date.""" _LOGGER.debug("Scanning") - data = await self._hub.async_get_skyhub_data() - - if not data: + if not (data := await self._hub.async_get_skyhub_data()): return self.last_results = data diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 98302ad396a..8c52a6669dc 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -78,8 +78,7 @@ class ThomsonDeviceScanner(DeviceScanner): return False _LOGGER.info("Checking ARP") - data = self.get_thomson_data() - if not data: + if not (data := self.get_thomson_data()): return False # Flag C stands for CONNECTED diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index 8b81a9d741b..cd3599683d0 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -217,8 +217,7 @@ class WatsonIOTThread(threading.Thread): def run(self): """Process incoming events.""" while not self.shutdown: - event = self.get_events_json() - if event: + if event := self.get_events_json(): self.write_to_watson(event) self.queue.task_done() diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index eb1b88dcc13..ceb68ec29eb 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -64,8 +64,7 @@ SUPPORTED_TARGET_TEMPERATURE_STEP = 1 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" auth: Auth = hass.data[DOMAIN][config_entry.entry_id][AUTH_INSTANCE_KEY] - said_list = auth.get_said_list() - if not said_list: + if not (said_list := auth.get_said_list()): _LOGGER.debug("No appliances found") return diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index c09b707cba0..bbd44498dab 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -47,8 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): interval = config.get(CONF_SCAN_INTERVAL, interval) for xuid in users: - gamercard = get_user_gamercard(api, xuid) - if gamercard is None: + if (gamercard := get_user_gamercard(api, xuid)) is None: continue entities.append(XboxSensor(api, xuid, gamercard, interval)) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index a3d06a21f89..d8cf32d0f88 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -335,8 +335,7 @@ class ZWaveMeterSensor(ZWaveNumericSensor): @property def extra_state_attributes(self) -> Mapping[str, int | str] | None: """Return extra state attributes.""" - meter_type = get_meter_type(self.info.primary_value) - if meter_type: + if meter_type := get_meter_type(self.info.primary_value): return { ATTR_METER_TYPE: meter_type.value, ATTR_METER_TYPE_NAME: meter_type.name, diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index abe8fedd21a..b967a6a0b1e 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -52,9 +52,7 @@ async def async_detect_location_info( session: aiohttp.ClientSession, ) -> LocationInfo | None: """Detect location information.""" - data = await _get_whoami(session) - - if data is None: + if (data := await _get_whoami(session)) is None: return None data["use_metric"] = data["country_code"] not in ("US", "MM", "LR") From 4c68662612e8d9e63daa2be3646ef19e97ab8a7a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Oct 2021 19:01:16 +0100 Subject: [PATCH 0149/1452] Use assignment expressions 34 (#58823) --- homeassistant/components/air_quality/__init__.py | 3 +-- homeassistant/components/asuswrt/__init__.py | 3 +-- homeassistant/components/coronavirus/sensor.py | 3 +-- homeassistant/components/darksky/sensor.py | 5 ++--- homeassistant/components/energy/sensor.py | 3 +-- homeassistant/components/esphome/__init__.py | 3 +-- homeassistant/components/homeassistant/triggers/time.py | 3 +-- homeassistant/components/homekit/__init__.py | 3 +-- homeassistant/components/homekit_controller/config_flow.py | 3 +-- .../components/homematicip_cloud/binary_sensor.py | 6 ++---- .../components/homematicip_cloud/generic_entity.py | 6 ++---- homeassistant/components/homematicip_cloud/sensor.py | 3 +-- homeassistant/components/http/view.py | 4 +--- homeassistant/components/light/__init__.py | 6 ++---- homeassistant/components/lock/__init__.py | 3 +-- homeassistant/components/media_player/__init__.py | 3 +-- homeassistant/components/mqtt/debug_info.py | 3 +-- homeassistant/components/nest/legacy/sensor.py | 3 +-- homeassistant/components/plant/__init__.py | 3 +-- homeassistant/components/plex/media_player.py | 7 ++----- homeassistant/components/plex/services.py | 3 +-- homeassistant/components/repetier/__init__.py | 3 +-- homeassistant/components/shelly/entity.py | 4 +--- homeassistant/components/sonos/media_player.py | 3 +-- homeassistant/components/switch/__init__.py | 3 +-- homeassistant/components/tplink/sensor.py | 3 +-- homeassistant/components/wirelesstag/__init__.py | 6 ++---- homeassistant/components/xiaomi_miio/air_quality.py | 3 +-- homeassistant/data_entry_flow.py | 3 +-- homeassistant/helpers/entity_component.py | 4 +--- 30 files changed, 36 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 1e38bad55a8..d0aa1fd4a76 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -137,8 +137,7 @@ class AirQualityEntity(Entity): data: dict[str, str | int | float] = {} for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value is not None: + if (value := getattr(self, prop)) is not None: data[attr] = value return data diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 2d067d0e608..534ce6bd7df 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -92,8 +92,7 @@ async def async_setup(hass, config): return True # remove not required config keys - pub_key = conf.pop(CONF_PUB_KEY, "") - if pub_key: + if pub_key := conf.pop(CONF_PUB_KEY, ""): conf[CONF_SSH_KEY] = pub_key conf.pop(CONF_REQUIRE_IP, True) diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 92fdf232214..14f597299cf 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -58,8 +58,7 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): if self.country == OPTION_WORLDWIDE: sum_cases = 0 for case in self.coordinator.data.values(): - value = getattr(case, self.info_type) - if value is None: + if (value := getattr(case, self.info_type)) is None: continue sum_cases += value diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 228370be16a..3de47136d45 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -754,10 +754,9 @@ class DarkSkySensor(SensorEntity): """ sensor_type = self.entity_description.key lookup_type = convert_to_camel(sensor_type) - state = getattr(data, lookup_type, None) - if state is None: - return state + if (state := getattr(data, lookup_type, None)) is None: + return None if "summary" in sensor_type: self._icon = getattr(data, "icon", "") diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index bfcc2817633..0800b02330e 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -197,8 +197,7 @@ class SensorManager: ): return - current_entity = to_remove.pop(key, None) - if current_entity: + if current_entity := to_remove.pop(key, None): current_entity.update_config(config) return diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8965e5b9960..0c07d0732ef 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -456,8 +456,7 @@ async def _setup_services( for service in services: if service.key in old_services: # Already exists - matching = old_services.pop(service.key) - if matching != service: + if (matching := old_services.pop(service.key)) != service: # Need to re-register to_unregister.append(matching) to_register.append(service) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index be7ded37dc2..bdb66b718e8 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -84,8 +84,7 @@ async def async_attach_trigger(hass, config, action, automation_info): def update_entity_trigger(entity_id, new_state=None): """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. - remove = entities.pop(entity_id, None) - if remove: + if remove := entities.pop(entity_id, None): remove() remove = None diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 9d8c3d04302..3e2a2a65d18 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -659,8 +659,7 @@ class HomeKit: async def async_remove_bridge_accessory(self, aid): """Try adding accessory to bridge if configured beforehand.""" - acc = self.bridge.accessories.pop(aid, None) - if acc: + if acc := self.bridge.accessories.pop(aid, None): await acc.stop() return acc diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 02b4c396783..c8118f61b87 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -471,8 +471,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # available. Otherwise request a fresh copy from the API. # This removes the 'accessories' key from pairing_data at # the same time. - accessories = pairing_data.pop("accessories", None) - if not accessories: + if not (accessories := pairing_data.pop("accessories", None)): accessories = await pairing.list_accessories_and_characteristics() bridge_info = get_bridge_information(accessories) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b1261258bf4..0cc462ac1f4 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -218,8 +218,7 @@ class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): state_attr = super().extra_state_attributes for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: + if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value return state_attr @@ -490,8 +489,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE state_attr = super().extra_state_attributes for attr, attr_key in GROUP_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: + if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value window_state = getattr(self._device, "windowState", None) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index d3a3b1e34a4..8bcea5d1435 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -238,16 +238,14 @@ class HomematicipGenericEntity(Entity): if isinstance(self._device, AsyncDevice): for attr, attr_key in DEVICE_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: + if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value state_attr[ATTR_IS_GROUP] = False if isinstance(self._device, AsyncGroup): for attr, attr_key in GROUP_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: + if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value state_attr[ATTR_IS_GROUP] = True diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ae866bb42e2..d1c3f71a83f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -282,8 +282,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): state_attr = super().extra_state_attributes for attr, attr_key in ILLUMINATION_DEVICE_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: + if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value return state_attr diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index bf8dc4b432b..6123f83563c 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -86,9 +86,7 @@ class HomeAssistantView: routes: list[AbstractRoute] = [] for method in ("get", "post", "delete", "put", "patch", "head", "options"): - handler = getattr(self, method, None) - - if not handler: + if not (handler := getattr(self, method, None)): continue handler = request_handler_factory(self, handler) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c5ae88eaaa0..dcf4972706a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -252,16 +252,14 @@ def preprocess_turn_on_alternatives(hass, params): if ATTR_PROFILE in params: hass.data[DATA_PROFILES].apply_profile(params.pop(ATTR_PROFILE), params) - color_name = params.pop(ATTR_COLOR_NAME, None) - if color_name is not None: + if (color_name := params.pop(ATTR_COLOR_NAME, None)) is not None: try: params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) except ValueError: _LOGGER.warning("Got unknown color %s, falling back to white", color_name) params[ATTR_RGB_COLOR] = (255, 255, 255) - kelvin = params.pop(ATTR_KELVIN, None) - if kelvin is not None: + if (kelvin := params.pop(ATTR_KELVIN, None)) is not None: mired = color_util.color_temperature_kelvin_to_mired(kelvin) params[ATTR_COLOR_TEMP] = int(mired) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index aa3662da0c8..1f2e87fc864 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -162,8 +162,7 @@ class LockEntity(Entity): """Return the state attributes.""" state_attr = {} for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value is not None: + if (value := getattr(self, prop)) is not None: state_attr[attr] = value return state_attr diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 41ebd93f3b5..0b2a7959c13 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -905,8 +905,7 @@ class MediaPlayerEntity(Entity): return state_attr for attr in ATTR_TO_PROPERTY: - value = getattr(self, attr) - if value is not None: + if (value := getattr(self, attr)) is not None: state_attr[attr] = value if self.media_image_remotely_accessible: diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index f9bb3f1c91f..e462d76fa31 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -44,8 +44,7 @@ def log_messages( def add_subscription(hass, message_callback, subscription): """Prepare debug data for subscription.""" - entity_id = getattr(message_callback, "__entity_id", None) - if entity_id: + if entity_id := getattr(message_callback, "__entity_id", None): debug_info = hass.data.setdefault( DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} ) diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index f2c6670bf8b..7b3ba5ec2fe 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -210,8 +210,7 @@ class NestTempSensor(NestSensorDevice, SensorEntity): else: self._unit = TEMP_FAHRENHEIT - temp = getattr(self.device, self.variable) - if temp is None: + if (temp := getattr(self.device, self.variable)) is None: self._state = None if isinstance(temp, tuple): diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 5e734c7ba62..fe5b4b2483b 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -238,8 +238,7 @@ class Plant(Entity): result = [] for sensor_name in self._sensormap.values(): params = self.READINGS[sensor_name] - value = getattr(self, f"_{sensor_name}") - if value is not None: + if (value := getattr(self, f"_{sensor_name}")) is not None: if value == STATE_UNAVAILABLE: result.append(f"{sensor_name} unavailable") else: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index f210ebe8363..3bc0a080f8c 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -481,9 +481,7 @@ class PlexMediaPlayer(MediaPlayerEntity): if isinstance(src, int): src = {"plex_key": src} - playqueue_id = src.pop("playqueue_id", None) - - if playqueue_id: + if playqueue_id := src.pop("playqueue_id", None): try: playqueue = self.plex_server.get_playqueue(playqueue_id) except plexapi.exceptions.NotFound as err: @@ -518,8 +516,7 @@ class PlexMediaPlayer(MediaPlayerEntity): "media_summary", "username", ): - value = getattr(self, attr, None) - if value: + if value := getattr(self, attr, None): attributes[attr] = value return attributes diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 32af3c429dc..e07d94f5a1f 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -107,8 +107,7 @@ def lookup_plex_media(hass, content_type, content_id): plex_server_name = content.pop("plex_server", None) plex_server = get_plex_server(hass, plex_server_name) - playqueue_id = content.pop("playqueue_id", None) - if playqueue_id: + if playqueue_id := content.pop("playqueue_id", None): try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index a71038ffcb8..4c5534d1a28 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -262,8 +262,7 @@ class PrinterAPI: printer = self.printers[printer_id] methods = API_PRINTER_METHODS[sensor_type] for prop, offline in methods.offline.items(): - state = getattr(printer, prop) - if state == offline: + if getattr(printer, prop) == offline: # if state matches offline, sensor is offline return None diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index e33a2d87317..f8383ccd297 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -433,9 +433,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): @property def attribute_value(self) -> StateType: """Value of sensor.""" - value = getattr(self.block, self.attribute) - - if value is None: + if (value := getattr(self.block, self.attribute)) is None: return None return cast(StateType, self.description.value(value)) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9f2bc829eac..f5389c7966a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -663,8 +663,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_id, MEDIA_TYPES_TO_SONOS[media_content_type], ) - image_url = getattr(item, "album_art_uri", None) - if image_url: + if image_url := getattr(item, "album_art_uri", None): result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call] return result # type: ignore diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 0abbc6d9f97..b023b819a53 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -114,8 +114,7 @@ class SwitchEntity(ToggleEntity): data = {} for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value is not None: + if (value := getattr(self, prop)) is not None: data[attr] = value return data diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 9bd4a056d33..0ffe375e6ff 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -100,8 +100,7 @@ def async_emeter_from_device( ) -> float | None: """Map a sensor key to the device attribute.""" if attr := description.emeter_attr: - val = getattr(device.emeter_realtime, attr) - if val is None: + if (val := getattr(device.emeter_realtime, attr)) is None: return None return round(cast(float, val), description.precision) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 24afb6b0465..201041ba616 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -74,15 +74,13 @@ class WirelessTagPlatform: def arm(self, switch): """Arm entity sensor monitoring.""" func_name = f"arm_{switch.sensor_type}" - arm_func = getattr(self.api, func_name) - if arm_func is not None: + if (arm_func := getattr(self.api, func_name)) is not None: arm_func(switch.tag_id, switch.tag_manager_mac) def disarm(self, switch): """Disarm entity sensor monitoring.""" func_name = f"disarm_{switch.sensor_type}" - disarm_func = getattr(self.api, func_name) - if disarm_func is not None: + if (disarm_func := getattr(self.api, func_name)) is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) def start_monitoring(self): diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 372a1b62e73..271beae131c 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -117,8 +117,7 @@ class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): data = {} for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value is not None: + if (value := getattr(self, prop)) is not None: data[attr] = value return data diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 83135373b9b..2bc1a6c2278 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -293,8 +293,7 @@ class FlowManager(abc.ABC): @callback def _async_remove_flow_progress(self, flow_id: str) -> None: """Remove a flow from in progress.""" - flow = self._progress.pop(flow_id, None) - if flow is None: + if (flow := self._progress.pop(flow_id, None)) is None: raise UnknownFlow handler = flow.handler self._handler_progress_index[handler].remove(flow.flow_id) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index fb1abcbf7e9..c190fd5fc35 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -173,9 +173,7 @@ class EntityComponent: """Unload a config entry.""" key = config_entry.entry_id - platform = self._platforms.pop(key, None) - - if platform is None: + if (platform := self._platforms.pop(key, None)) is None: raise ValueError("Config entry was never loaded!") await platform.async_reset() From 8f51192cf05cfe037cf23e170deed1462b3d4052 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 31 Oct 2021 19:23:40 +0100 Subject: [PATCH 0150/1452] Cleanup old entity.unique_id migration from Synology DSM (#58811) Created on 2nd of june 2020 from #35565 --- .../components/synology_dsm/__init__.py | 75 +------------------ 1 file changed, 3 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a8e35178be4..bbc78ba42a3 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -42,7 +42,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -70,13 +70,9 @@ from .const import ( SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES, - STORAGE_DISK_BINARY_SENSORS, - STORAGE_DISK_SENSORS, - STORAGE_VOL_SENSORS, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, - UTILISATION_SENSORS, SynologyDSMEntityDescription, ) @@ -89,75 +85,10 @@ ATTRIBUTION = "Data provided by Synology" _LOGGER = logging.getLogger(__name__) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Synology DSM sensors.""" - # Migrate old unique_id - @callback - def _async_migrator( - entity_entry: entity_registry.RegistryEntry, - ) -> dict[str, str] | None: - """Migrate away from ID using label.""" - # Reject if new unique_id - if "SYNO." in entity_entry.unique_id: - return None - - entries = ( - *STORAGE_DISK_BINARY_SENSORS, - *STORAGE_DISK_SENSORS, - *STORAGE_VOL_SENSORS, - *UTILISATION_SENSORS, - ) - infos = entity_entry.unique_id.split("_") - serial = infos.pop(0) - label = infos.pop(0) - device_id = "_".join(infos) - - # Removed entity - if ( - "Type" in entity_entry.unique_id - or "Device" in entity_entry.unique_id - or "Name" in entity_entry.unique_id - ): - return None - - entity_type: str | None = None - for description in entries: - if ( - device_id - and description.name == "Status" - and "Status" in entity_entry.unique_id - and "(Smart)" not in entity_entry.unique_id - ): - if "sd" in device_id and "disk" in description.key: - entity_type = description.key - continue - if "volume" in device_id and "volume" in description.key: - entity_type = description.key - continue - - if description.name == label: - entity_type = description.key - - if entity_type is None: - return None - - new_unique_id = "_".join([serial, entity_type]) - if device_id: - new_unique_id += f"_{device_id}" - - _LOGGER.info( - "Migrating unique_id from [%s] to [%s]", - entity_entry.unique_id, - new_unique_id, - ) - return {"new_unique_id": new_unique_id} - - await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) - - # migrate device indetifiers + # Migrate device indentifiers dev_reg = await get_dev_reg(hass) devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id From ccad6a8f07067aa938d5822e1804b3af5bfbeb3a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:12:25 +0100 Subject: [PATCH 0151/1452] Add configuration url to AVM Fritz!Smarthome (#57711) * add configuration url * extend data update coordinator * improve exception handling during data update * store coordinator after first refresh * fix light init --- homeassistant/components/fritzbox/__init__.py | 62 +++-------------- .../components/fritzbox/binary_sensor.py | 4 +- .../components/fritzbox/coordinator.py | 68 +++++++++++++++++++ homeassistant/components/fritzbox/light.py | 6 +- tests/components/fritzbox/test_init.py | 18 ++++- 5 files changed, 98 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/fritzbox/coordinator.py diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 0ddd0b8d417..e72e1d86fc1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,10 +1,7 @@ """Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations -from datetime import timedelta - from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError -import requests from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,10 +15,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -32,6 +26,7 @@ from .const import ( LOGGER, PLATFORMS, ) +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzExtraAttributes @@ -53,52 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_CONNECTIONS: fritz, } - def _update_fritz_devices() -> dict[str, FritzhomeDevice]: - """Update all fritzbox device data.""" - try: - devices = fritz.get_devices() - except requests.exceptions.HTTPError: - # If the device rebooted, login again - try: - fritz.login() - except requests.exceptions.HTTPError as ex: - raise ConfigEntryAuthFailed from ex - devices = fritz.get_devices() - - data = {} - fritz.update_devices() - for device in devices: - # assume device as unavailable, see #55799 - if ( - device.has_powermeter - and device.present - and hasattr(device, "voltage") - and device.voltage <= 0 - and device.power <= 0 - and device.energy <= 0 - ): - LOGGER.debug("Assume device %s as unavailable", device.name) - device.present = False - - data[device.ain] = device - return data - - async def async_update_coordinator() -> dict[str, FritzhomeDevice]: - """Fetch all device data.""" - return await hass.async_add_executor_job(_update_fritz_devices) - - hass.data[DOMAIN][entry.entry_id][ - CONF_COORDINATOR - ] = coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{entry.entry_id}", - update_method=async_update_coordinator, - update_interval=timedelta(seconds=30), - ) + coordinator = FritzboxDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -142,9 +97,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class FritzBoxEntity(CoordinatorEntity): """Basis FritzBox entity.""" + coordinator: FritzboxDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: EntityDescription | None = None, ) -> None: @@ -174,11 +131,12 @@ class FritzBoxEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( + name=self.device.name, identifiers={(DOMAIN, self.ain)}, manufacturer=self.device.manufacturer, model=self.device.productname, - name=self.device.name, sw_version=self.device.fw_version, + configuration_url=self.coordinator.configuration_url, ) @property diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1317710c570..b0f5e63d424 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -15,10 +15,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import FritzBoxEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzEntityDescriptionMixinBase @@ -70,7 +70,7 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: FritzBinarySensorEntityDescription, ) -> None: diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py new file mode 100644 index 00000000000..69ab0b4c274 --- /dev/null +++ b/homeassistant/components/fritzbox/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for AVM FRITZ!SmartHome devices.""" +from __future__ import annotations + +from datetime import timedelta + +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, DOMAIN, LOGGER + + +class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): + """Fritzbox Smarthome device data update coordinator.""" + + configuration_url: str + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Fritzbox Smarthome device coordinator.""" + self.entry = entry + self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + self.configuration_url = self.fritz.get_prefixed_host() + super().__init__( + hass, + LOGGER, + name=entry.entry_id, + update_interval=timedelta(seconds=30), + ) + + def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = self.fritz.get_devices() + except requests.exceptions.ConnectionError as ex: + raise ConfigEntryNotReady from ex + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + self.fritz.login() + except LoginError as ex: + raise ConfigEntryAuthFailed from ex + devices = self.fritz.get_devices() + + data = {} + self.fritz.update_devices() + for device in devices: + # assume device as unavailable, see #55799 + if ( + device.has_powermeter + and device.present + and hasattr(device, "voltage") + and device.voltage <= 0 + and device.power <= 0 + and device.energy <= 0 + ): + LOGGER.debug("Assume device %s as unavailable", device.name) + device.present = False + + data[device.ain] = device + return data + + async def _async_update_data(self) -> dict[str, FritzhomeDevice]: + """Fetch all device data.""" + return await self.hass.async_add_executor_job(self._update_fritz_devices) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 3f9e3cabfa2..272d170e13d 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Any -from pyfritzhome.fritzhomedevice import FritzhomeDevice - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -16,7 +14,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import color from . import FritzBoxEntity @@ -26,6 +23,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .coordinator import FritzboxDataUpdateCoordinator SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} @@ -64,7 +62,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, supported_colors: dict, supported_color_temps: list[str], diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index ea0356c6af1..60828e83801 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import Mock, call, patch from pyfritzhome import LoginError -from requests.exceptions import HTTPError +from requests.exceptions import ConnectionError, HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -120,13 +120,27 @@ async def test_coordinator_update_after_password_change( ) entry.add_to_hass(hass) fritz().get_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", HTTPError()] + fritz().login.side_effect = ["", LoginError("some_user")] assert not await hass.config_entries.async_setup(entry.entry_id) assert fritz().get_devices.call_count == 1 assert fritz().login.call_count == 2 +async def test_coordinator_update_when_unreachable(hass: HomeAssistant, fritz: Mock): + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().get_devices.side_effect = [ConnectionError(), ""] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] From 9daf2ee65d617055b7b17783a369e4b5f5fe4006 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Oct 2021 20:19:51 +0100 Subject: [PATCH 0152/1452] Fix Plugwise not updating config entry with discovery information (#58819) --- .../components/plugwise/config_flow.py | 2 +- tests/components/plugwise/test_config_flow.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 450388b6f42..1dbf4324590 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -109,7 +109,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # unique_id is needed here, to be able to determine whether the discovered device is known, or not. unique_id = self.discovery_info.get("hostname").split(".")[0] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured({CONF_HOST: self.discovery_info[CONF_HOST]}) if DEFAULT_USERNAME not in unique_id: self.discovery_info[CONF_USERNAME] = STRETCH_USERNAME diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 75851f5c15a..7f270e23cc1 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -203,6 +203,29 @@ async def test_zeroconf_stretch_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zercoconf_discovery_update_configuration(hass): + """Test if a discovered device is configured and updated with new host.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={CONF_HOST: "0.0.0.0", CONF_PASSWORD: TEST_PASSWORD}, + unique_id=TEST_HOSTNAME, + ) + entry.add_to_hass(hass) + + assert entry.data[CONF_HOST] == "0.0.0.0" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "1.1.1.1" + + async def test_form_username(hass): """Test we get the username data back.""" From adfebaf51054f13a9f10eb0e352bd26899c860c0 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sun, 31 Oct 2021 15:25:19 -0700 Subject: [PATCH 0153/1452] Address late review of venstar (#58813) * Additional fixes from PR #58601 * Suggested fix to reduce attribute access --- homeassistant/components/venstar/__init__.py | 18 ++------- homeassistant/components/venstar/climate.py | 42 +++++--------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 5f2aabe6738..69bc1bf188c 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -70,14 +70,13 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): venstar_connection: VenstarColorTouch, ) -> None: """Initialize global Venstar data updater.""" - self.client = venstar_connection - super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=60), ) + self.client = venstar_connection async def _async_update_data(self) -> None: """Update the state.""" @@ -103,6 +102,8 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): class VenstarEntity(CoordinatorEntity): """Representation of a Venstar entity.""" + coordinator: VenstarDataUpdateCoordinator + def __init__( self, venstar_data_coordinator: VenstarDataUpdateCoordinator, @@ -111,22 +112,11 @@ class VenstarEntity(CoordinatorEntity): """Initialize the data object.""" super().__init__(venstar_data_coordinator) self._config = config - self._update_attr() - self.coordinator = venstar_data_coordinator - - @property - def _client(self): - """Return the venstar client.""" - return self.coordinator.client - - @callback - def _update_attr(self) -> None: - """Update the state and attributes.""" + self._client = venstar_data_coordinator.client @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._update_attr() self.async_write_ha_state() @property diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index c53cc9685e2..5d28d41db53 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,6 +1,4 @@ """Support for Venstar WiFi Thermostats.""" -from functools import partial - import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -276,12 +274,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _LOGGER.error("Failed to change the operation mode") return success - async def async_set_temperature(self, **kwargs): - """Set a new target temperature.""" - await self.hass.async_add_executor_job(partial(self._set_temperature, **kwargs)) - self.async_write_ha_state() - - def _set_temperature(self, **kwargs): + def set_temperature(self, **kwargs): """Set a new target temperature.""" set_temp = True operation_mode = kwargs.get(ATTR_HVAC_MODE) @@ -318,13 +311,9 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): if not success: _LOGGER.error("Failed to change the temperature") + self.schedule_update_ha_state() - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set a new target fan mode.""" - await self.hass.async_add_executor_job(self._set_fan_mode, fan_mode) - self.async_write_ha_state() - - def _set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode == STATE_ON: success = self._client.set_fan(self._client.FAN_ON) @@ -333,34 +322,22 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): if not success: _LOGGER.error("Failed to change the fan mode") + self.schedule_update_ha_state() - async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set a new target operation mode.""" - await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode) - self.async_write_ha_state() - - def _set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" self._set_operation_mode(hvac_mode) + self.schedule_update_ha_state() - async def async_set_humidity(self, humidity: int) -> None: - """Set a new target humidity.""" - await self.hass.async_add_executor_job(self._set_humidity, humidity) - self.async_write_ha_state() - - def _set_humidity(self, humidity): + def set_humidity(self, humidity): """Set new target humidity.""" success = self._client.set_hum_setpoint(humidity) if not success: _LOGGER.error("Failed to change the target humidity level") + self.schedule_update_ha_state() - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the hold mode.""" - await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode) - self.async_write_ha_state() - - def _set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode): """Set the hold mode.""" if preset_mode == PRESET_AWAY: success = self._client.set_away(self._client.AWAY_AWAY) @@ -376,3 +353,4 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): if not success: _LOGGER.error("Failed to change the schedule/hold state") + self.schedule_update_ha_state() From 3f61ff4f961ac71e528ded8345848bab3eed2c40 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:01:11 -0300 Subject: [PATCH 0154/1452] Make general code quality improvements in the Broadlink integration (#58848) * Create DEVICE_TYPES constant * Rename _auth_fetch_firmware() to _get_firmware_version() * Rename dev_type to device_type * Use SOURCE_REAUTH from config_entries namespace * Fix unidiomatic imports --- .../components/broadlink/config_flow.py | 11 ++++------- homeassistant/components/broadlink/const.py | 3 ++- homeassistant/components/broadlink/device.py | 10 +++++----- homeassistant/components/broadlink/helpers.py | 2 +- homeassistant/components/broadlink/remote.py | 16 ++++++++-------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 884a6a9d102..2ab21372fd9 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -14,11 +14,10 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, DOMAINS_AND_TYPES +from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DEVICE_TYPES, DOMAIN from .helpers import format_mac _LOGGER = logging.getLogger(__name__) @@ -35,8 +34,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" - supported_types = set.union(*DOMAINS_AND_TYPES.values()) - if device.type not in supported_types: + if device.type not in DEVICE_TYPES: _LOGGER.error( "Unsupported device: %s. If it worked before, please open " "an issue at https://github.com/home-assistant/core/issues", @@ -73,8 +71,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return self.async_abort(reason="unknown") - supported_types = set.union(*DOMAINS_AND_TYPES.values()) - if device.type not in supported_types: + if device.type not in DEVICE_TYPES: return self.async_abort(reason="not_supported") await self.async_set_device(device) @@ -110,7 +107,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: device.timeout = timeout - if self.source != SOURCE_REAUTH: + if self.source != config_entries.SOURCE_REAUTH: await self.async_set_device(device) self._abort_if_unique_id_configured( updates={CONF_HOST: device.host[0], CONF_TIMEOUT: timeout} diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index f40fd7785a1..174c7edde3a 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,4 +1,4 @@ -"""Constants for the Broadlink integration.""" +"""Constants.""" from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -36,6 +36,7 @@ DOMAINS_AND_TYPES = { }, LIGHT_DOMAIN: {"LB1"}, } +DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values()) DEFAULT_PORT = 80 DEFAULT_TIMEOUT = 5 diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index aada9ace84a..1d1fe273252 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -23,9 +23,9 @@ from .updater import get_update_manager _LOGGER = logging.getLogger(__name__) -def get_domains(dev_type): +def get_domains(device_type): """Return the domains available for a device type.""" - return {d for d, t in DOMAINS_AND_TYPES.items() if dev_type in t} + return {d for d, t in DOMAINS_AND_TYPES.items() if device_type in t} class BroadlinkDevice: @@ -67,8 +67,8 @@ class BroadlinkDevice: device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) - def _auth_fetch_firmware(self): - """Auth and fetch firmware.""" + def _get_firmware_version(self): + """Get firmware version.""" self.api.auth() with suppress(BroadlinkException, OSError): return self.api.get_fwversion() @@ -89,7 +89,7 @@ class BroadlinkDevice: try: self.fw_version = await self.hass.async_add_executor_job( - self._auth_fetch_firmware + self._get_firmware_version ) except AuthenticationError: diff --git a/homeassistant/components/broadlink/helpers.py b/homeassistant/components/broadlink/helpers.py index 6d81b98d5d1..bec61ba5bbd 100644 --- a/homeassistant/components/broadlink/helpers.py +++ b/homeassistant/components/broadlink/helpers.py @@ -3,7 +3,7 @@ from base64 import b64decode from homeassistant import config_entries from homeassistant.const import CONF_HOST -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from .const import DOMAIN diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index a0c5c4130e5..cc2d85204b8 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -34,10 +34,10 @@ from homeassistant.components.remote import ( ) from homeassistant.const import CONF_HOST, STATE_OFF from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.util.dt import utcnow +from homeassistant.util import dt from .const import DOMAIN from .entity import BroadlinkEntity @@ -332,8 +332,8 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): ) try: - start_time = utcnow() - while (utcnow() - start_time) < LEARNING_TIMEOUT: + start_time = dt.utcnow() + while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) try: code = await self._device.async_request(self._device.api.check_data) @@ -367,8 +367,8 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): ) try: - start_time = utcnow() - while (utcnow() - start_time) < LEARNING_TIMEOUT: + start_time = dt.utcnow() + while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) found = await self._device.async_request( self._device.api.check_frequency @@ -405,8 +405,8 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): ) try: - start_time = utcnow() - while (utcnow() - start_time) < LEARNING_TIMEOUT: + start_time = dt.utcnow() + while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) try: code = await self._device.async_request(self._device.api.check_data) From a3332410dccc023a321ab4914ede3bf985a9171d Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 1 Nov 2021 00:29:57 +0100 Subject: [PATCH 0155/1452] Add ROCKROBO_E2 to supported vacuums for xiaomi_miio (#58817) https://github.com/rytilahti/python-miio/blob/e1adea55f3be237f6e6904210b6f7b52162bf154/miio/vacuum.py#L129 --- homeassistant/components/xiaomi_miio/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 6630def38ef..578a6d3ffab 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -199,8 +199,10 @@ MODELS_LIGHT = ( # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, + ROCKROBO_E2, ROCKROBO_S4, ROCKROBO_S5, ROCKROBO_S5_MAX, @@ -209,6 +211,7 @@ MODELS_VACUUM = [ ROCKROBO_S7, ] MODELS_VACUUM_WITH_MOP = [ + ROCKROBO_E2, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From 4e419d8c6fa515bf6a67d93bd14cada40b92af2a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 1 Nov 2021 00:13:04 +0000 Subject: [PATCH 0156/1452] [ci skip] Translation update --- .../components/adax/translations/bg.json | 2 +- .../components/adguard/translations/ja.json | 7 ++ .../airvisual/translations/sensor.bg.json | 13 +++- .../ambee/translations/sensor.bg.json | 10 +++ .../components/auth/translations/ja.json | 19 +++++ .../components/axis/translations/ja.json | 12 +++ .../binary_sensor/translations/ja.json | 73 ++++++++++++++++++- .../components/bosch_shc/translations/bg.json | 22 ++++++ .../cert_expiry/translations/ca.json | 2 +- .../cert_expiry/translations/ja.json | 10 +++ .../components/cover/translations/hu.json | 6 +- .../components/daikin/translations/ja.json | 10 +++ .../components/deconz/translations/ja.json | 10 ++- .../devolo_home_network/translations/bg.json | 20 +++++ .../devolo_home_network/translations/it.json | 25 +++++++ .../translations/zh-Hant.json | 25 +++++++ .../dialogflow/translations/ja.json | 10 +++ .../components/dsmr/translations/bg.json | 4 +- .../components/ebusd/translations/ja.json | 6 ++ .../emulated_roku/translations/ja.json | 17 +++++ .../components/esphome/translations/ja.json | 21 ++++++ .../components/fritz/translations/bg.json | 2 +- .../garages_amsterdam/translations/bg.json | 2 +- .../geonetnz_quakes/translations/ja.json | 12 +++ .../components/gogogate2/translations/bg.json | 5 ++ .../google_travel_time/translations/bg.json | 11 +++ .../growatt_server/translations/bg.json | 8 +- .../homekit_controller/translations/ja.json | 8 ++ .../components/hue/translations/ja.json | 2 + .../components/ifttt/translations/ja.json | 10 +++ .../components/ipma/translations/ja.json | 7 ++ .../components/iqvia/translations/ja.json | 16 ++++ .../components/izone/translations/ja.json | 9 +++ .../components/life360/translations/ja.json | 7 ++ .../components/lifx/translations/ja.json | 9 +++ .../components/light/translations/ja.json | 15 ++++ .../components/luftdaten/translations/ja.json | 16 ++++ .../components/mailgun/translations/ja.json | 10 +++ .../modern_forms/translations/bg.json | 7 +- .../moon/translations/sensor.ja.json | 13 ++++ .../components/motioneye/translations/it.json | 1 + .../motioneye/translations/zh-Hant.json | 1 + .../components/mqtt/translations/ja.json | 8 +- .../components/nam/translations/bg.json | 13 ++++ .../components/nest/translations/ja.json | 3 +- .../components/netatmo/translations/bg.json | 3 + .../nfandroidtv/translations/bg.json | 5 +- .../components/notion/translations/ja.json | 12 +++ .../onboarding/translations/ja.json | 7 ++ .../components/openuv/translations/ja.json | 4 +- .../components/plex/translations/ja.json | 15 ++++ .../components/point/translations/ja.json | 7 ++ .../components/ps4/translations/ja.json | 29 ++++++++ .../rainmachine/translations/bg.json | 1 + .../rainmachine/translations/ja.json | 12 +++ .../components/ridwell/translations/bg.json | 28 +++++++ .../components/ridwell/translations/hu.json | 28 +++++++ .../components/ridwell/translations/it.json | 28 +++++++ .../ridwell/translations/zh-Hant.json | 28 +++++++ .../season/translations/sensor.ja.json | 10 +++ .../smartthings/translations/ja.json | 7 ++ .../components/smhi/translations/ja.json | 13 ++++ .../components/solaredge/translations/ja.json | 11 +++ .../components/switch/translations/ja.json | 15 ++++ .../components/syncthing/translations/bg.json | 18 +++++ .../system_bridge/translations/bg.json | 13 ++++ .../tellduslive/translations/ja.json | 10 +++ .../components/tplink/translations/ja.json | 9 +++ .../components/tradfri/translations/ja.json | 17 +++++ .../components/tuya/translations/ja.json | 19 +++++ .../tuya/translations/sensor.bg.json | 9 +++ .../twentemilieu/translations/ja.json | 11 +++ .../components/twilio/translations/ja.json | 9 +++ .../components/unifi/translations/ja.json | 12 +++ .../components/venstar/translations/bg.json | 7 ++ .../components/vesync/translations/ja.json | 9 +++ .../components/wallbox/translations/bg.json | 2 +- .../waze_travel_time/translations/bg.json | 11 +++ .../yamaha_musiccast/translations/bg.json | 3 + .../components/yeelight/translations/bg.json | 1 + .../components/zwave/translations/ja.json | 5 ++ .../components/zwave_js/translations/bg.json | 5 +- 82 files changed, 928 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/adguard/translations/ja.json create mode 100644 homeassistant/components/ambee/translations/sensor.bg.json create mode 100644 homeassistant/components/axis/translations/ja.json create mode 100644 homeassistant/components/cert_expiry/translations/ja.json create mode 100644 homeassistant/components/daikin/translations/ja.json create mode 100644 homeassistant/components/devolo_home_network/translations/bg.json create mode 100644 homeassistant/components/devolo_home_network/translations/it.json create mode 100644 homeassistant/components/devolo_home_network/translations/zh-Hant.json create mode 100644 homeassistant/components/dialogflow/translations/ja.json create mode 100644 homeassistant/components/ebusd/translations/ja.json create mode 100644 homeassistant/components/emulated_roku/translations/ja.json create mode 100644 homeassistant/components/esphome/translations/ja.json create mode 100644 homeassistant/components/geonetnz_quakes/translations/ja.json create mode 100644 homeassistant/components/gogogate2/translations/bg.json create mode 100644 homeassistant/components/google_travel_time/translations/bg.json create mode 100644 homeassistant/components/homekit_controller/translations/ja.json create mode 100644 homeassistant/components/ifttt/translations/ja.json create mode 100644 homeassistant/components/ipma/translations/ja.json create mode 100644 homeassistant/components/iqvia/translations/ja.json create mode 100644 homeassistant/components/izone/translations/ja.json create mode 100644 homeassistant/components/life360/translations/ja.json create mode 100644 homeassistant/components/lifx/translations/ja.json create mode 100644 homeassistant/components/luftdaten/translations/ja.json create mode 100644 homeassistant/components/mailgun/translations/ja.json create mode 100644 homeassistant/components/moon/translations/sensor.ja.json create mode 100644 homeassistant/components/notion/translations/ja.json create mode 100644 homeassistant/components/onboarding/translations/ja.json create mode 100644 homeassistant/components/plex/translations/ja.json create mode 100644 homeassistant/components/point/translations/ja.json create mode 100644 homeassistant/components/ps4/translations/ja.json create mode 100644 homeassistant/components/rainmachine/translations/ja.json create mode 100644 homeassistant/components/ridwell/translations/bg.json create mode 100644 homeassistant/components/ridwell/translations/hu.json create mode 100644 homeassistant/components/ridwell/translations/it.json create mode 100644 homeassistant/components/ridwell/translations/zh-Hant.json create mode 100644 homeassistant/components/season/translations/sensor.ja.json create mode 100644 homeassistant/components/smartthings/translations/ja.json create mode 100644 homeassistant/components/smhi/translations/ja.json create mode 100644 homeassistant/components/solaredge/translations/ja.json create mode 100644 homeassistant/components/syncthing/translations/bg.json create mode 100644 homeassistant/components/tellduslive/translations/ja.json create mode 100644 homeassistant/components/tplink/translations/ja.json create mode 100644 homeassistant/components/tradfri/translations/ja.json create mode 100644 homeassistant/components/tuya/translations/sensor.bg.json create mode 100644 homeassistant/components/twentemilieu/translations/ja.json create mode 100644 homeassistant/components/twilio/translations/ja.json create mode 100644 homeassistant/components/unifi/translations/ja.json create mode 100644 homeassistant/components/vesync/translations/ja.json create mode 100644 homeassistant/components/waze_travel_time/translations/bg.json diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json index 329b8fd8399..3d3795470ba 100644 --- a/homeassistant/components/adax/translations/bg.json +++ b/homeassistant/components/adax/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json new file mode 100644 index 00000000000..e35071d99bf --- /dev/null +++ b/homeassistant/components/adguard/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.bg.json b/homeassistant/components/airvisual/translations/sensor.bg.json index 311df560225..428050a2427 100644 --- a/homeassistant/components/airvisual/translations/sensor.bg.json +++ b/homeassistant/components/airvisual/translations/sensor.bg.json @@ -1,9 +1,20 @@ { "state": { "airvisual__pollutant_label": { + "co": "\u0412\u044a\u0433\u043b\u0435\u0440\u043e\u0434\u0435\u043d \u043e\u043a\u0438\u0441", + "n2": "\u0410\u0437\u043e\u0442\u0435\u043d \u0434\u0438\u043e\u043a\u0441\u0438\u0434", "o3": "\u041e\u0437\u043e\u043d", "p1": "PM10", - "p2": "PM2.5" + "p2": "PM2.5", + "s2": "\u0421\u0435\u0440\u0435\u043d \u0434\u0438\u043e\u043a\u0441\u0438\u0434" + }, + "airvisual__pollutant_level": { + "good": "\u0414\u043e\u0431\u0440\u043e", + "hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e", + "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043e", + "unhealthy": "\u041d\u0435\u0437\u0434\u0440\u0430\u0432\u043e\u0441\u043b\u043e\u0432\u043d\u043e", + "unhealthy_sensitive": "\u041d\u0435\u0437\u0434\u0440\u0430\u0432\u043e\u0441\u043b\u043e\u0432\u043d\u043e \u0437\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u0438 \u0433\u0440\u0443\u043f\u0438", + "very_unhealthy": "\u041c\u043d\u043e\u0433\u043e \u043d\u0435\u0437\u0434\u0440\u0430\u0432\u043e\u0441\u043b\u043e\u0432\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.bg.json b/homeassistant/components/ambee/translations/sensor.bg.json new file mode 100644 index 00000000000..07977ca4abf --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.bg.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "\u0412\u0438\u0441\u043e\u043a\u043e", + "low": "\u041d\u0438\u0441\u043a\u043e", + "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043e", + "very high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/ja.json b/homeassistant/components/auth/translations/ja.json index 1ef902e6fe2..beffdfe1f61 100644 --- a/homeassistant/components/auth/translations/ja.json +++ b/homeassistant/components/auth/translations/ja.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u5229\u7528\u3067\u304d\u308b\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "error": { + "invalid_code": "\u7121\u52b9\u306a\u30b3\u30fc\u30c9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "init": { + "description": "\u3069\u308c\u304b1\u3064\u3001\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044:", + "title": "\u901a\u77e5\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306b\u3088\u3063\u3066\u914d\u4fe1\u3055\u308c\u308b\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u8a2d\u5b9a" + }, + "setup": { + "description": "\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u304c **notify.{notify_service}** \u3092\u4ecb\u3057\u3066\u9001\u4fe1\u3055\u308c\u307e\u3057\u305f\u3002\u4ee5\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", + "title": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u78ba\u8a8d" + } + }, + "title": "\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u901a\u77e5" + }, "totp": { "title": "TOTP" } diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json new file mode 100644 index 00000000000..d3091e20d35 --- /dev/null +++ b/homeassistant/components/axis/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" + }, + "step": { + "user": { + "title": "Axis\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 54280a5334a..8c3cffa3166 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -1,4 +1,75 @@ { + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u304c\u5c11\u306a\u304f\u306a\u3063\u3066\u3044\u307e\u3059", + "is_cold": "{entity_name} \u51b7\u3048\u3066\u3044\u308b", + "is_connected": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", + "is_gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_hot": "{entity_name} \u71b1\u3044", + "is_light": "{\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u540d} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", + "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", + "is_moist": "{entity_name} \u306f\u6e7f\u3063\u3066\u3044\u307e\u3059", + "is_no_vibration": "{entity_name} \u306f\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u305b\u3093", + "is_not_bat_low": "{entity_name} \u30d0\u30c3\u30c6\u30ea\u30fc\u306f\u6b63\u5e38\u3067\u3059", + "is_not_cold": "{entity_name} \u51b7\u3048\u3066\u3044\u307e\u305b\u3093", + "is_not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", + "is_not_hot": "{entity_name} \u306f\u71b1\u304f\u3042\u308a\u307e\u305b\u3093", + "is_not_locked": "{entity_name} \u306e\u30ed\u30c3\u30af\u306f\u89e3\u9664\u3055\u308c\u3066\u3044\u307e\u3059", + "is_not_moist": "{entity_name} \u306f\u4e7e\u71e5\u3057\u3066\u3044\u307e\u3059", + "is_not_moving": "{entity_name} \u306f\u52d5\u3044\u3066\u3044\u307e\u305b\u3093", + "is_not_occupied": "{entity_name} \u306f\u5360\u6709\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "is_not_open": "{entity_name} \u306f\u9589\u3058\u3066\u3044\u307e\u3059", + "is_not_plugged_in": "{entity_name} \u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u3066\u3044\u307e\u3059", + "is_not_powered": "{entity_name} \u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "is_not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", + "is_not_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u3059", + "is_occupied": "{entity_name} \u306f\u5360\u6709\u3055\u308c\u3066\u3044\u307e\u3059", + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059", + "is_open": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059", + "is_plugged_in": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", + "is_powered": "{entity_name} \u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u3059", + "is_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u3059", + "is_problem": "{entity_name} \u304c\u554f\u984c\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_smoke": "{entity_name} \u304c\u7159\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", + "is_sound": "{entity_name} \u304c\u97f3\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", + "is_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "is_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u3059" + }, + "trigger_type": { + "bat_low": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u304c\u5c11\u306a\u304f\u306a\u3063\u3066\u3044\u307e\u3059", + "cold": "{entity_name} \u51b7\u3048\u3066\u3044\u307e\u3059", + "connected": "{entity_name} \u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", + "gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", + "no_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u306a\u304f\u306a\u3063\u305f", + "not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", + "not_hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u305b\u3093", + "not_locked": "{entity_name} \u306e\u30ed\u30c3\u30af\u304c\u89e3\u9664\u3055\u308c\u307e\u3057\u305f", + "not_moist": "{entity_name} \u306f\u4e7e\u3044\u3066\u3044\u307e\u305b\u3093", + "not_moving": "{entity_name} \u304c\u52d5\u304d\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "not_occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f", + "not_plugged_in": "{entity_name} \u306e\u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u307e\u3057\u305f", + "not_powered": "{entity_name} \u306e\u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", + "not_unsafe": "{entity_name} \u304c\u5b89\u5168\u306b\u306a\u308a\u307e\u3057\u305f", + "occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u307e\u3057\u305f", + "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", + "plugged_in": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", + "powered": "{entity_name} \uff5e\u3067\u52d5\u304f", + "present": "{entity_name} \u304c\u5b58\u5728", + "problem": "{entity_name} \u304c\u554f\u984c\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "smoke": "{entity_name} \u304c\u3001\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "sound": "{entity_name} \u304c\u3001\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", + "unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u306a\u304f\u306a\u308a\u307e\u3057\u305f", + "vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u59cb\u3081\u307e\u3057\u305f" + } + }, + "device_class": { + "vibration": "\u632f\u52d5" + }, "state": { "_": { "off": "\u30aa\u30d5", @@ -72,7 +143,7 @@ "on": "\u691c\u51fa" }, "vibration": { - "off": "\u672a\u691c\u51fa", + "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa" }, "window": { diff --git a/homeassistant/components/bosch_shc/translations/bg.json b/homeassistant/components/bosch_shc/translations/bg.json index 80f917a9793..759dd6b21fb 100644 --- a/homeassistant/components/bosch_shc/translations/bg.json +++ b/homeassistant/components/bosch_shc/translations/bg.json @@ -1,3 +1,25 @@ { + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, "title": "Bosch SHC" } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/ca.json b/homeassistant/components/cert_expiry/translations/ca.json index 42da690550b..1a9d3b109a5 100644 --- a/homeassistant/components/cert_expiry/translations/ca.json +++ b/homeassistant/components/cert_expiry/translations/ca.json @@ -20,5 +20,5 @@ } } }, - "title": "Caducitat del certificat" + "title": "Caducitat de certificat" } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/ja.json b/homeassistant/components/cert_expiry/translations/ja.json new file mode 100644 index 00000000000..612122b2b2c --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30c6\u30b9\u30c8\u3059\u308b\u8a3c\u660e\u66f8\u3092\u5b9a\u7fa9\u3059\u308b" + } + } + }, + "title": "\u8a3c\u660e\u66f8\u306e\u6709\u52b9\u671f\u9650" +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/hu.json b/homeassistant/components/cover/translations/hu.json index 87bd1c241c6..2155907cae2 100644 --- a/homeassistant/components/cover/translations/hu.json +++ b/homeassistant/components/cover/translations/hu.json @@ -29,10 +29,10 @@ "state": { "_": { "closed": "Z\u00e1rva", - "closing": "Z\u00e1r\u00e1s", + "closing": "Z\u00e1r\u00f3dik", "open": "Nyitva", - "opening": "Nyit\u00e1s", - "stopped": "Meg\u00e1ll\u00edtva" + "opening": "Ny\u00edlik", + "stopped": "Meg\u00e1llt" } }, "title": "Bor\u00edt\u00f3" diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json new file mode 100644 index 00000000000..fdbaf65af89 --- /dev/null +++ b/homeassistant/components/daikin/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API Key\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "title": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index be03f3b2036..5798618f463 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059", + "no_bridges": "deCONZ\u30d6\u30ea\u30c3\u30b8\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" @@ -11,5 +12,12 @@ "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" } } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", + "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/bg.json b/homeassistant/components/devolo_home_network/translations/bg.json new file mode 100644 index 00000000000..c1dc13fe2d7 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/it.json b/homeassistant/components/devolo_home_network/translations/it.json new file mode 100644 index 00000000000..118ad0e79c6 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "home_control": "L'unit\u00e0 centrale devolo Home Control non funziona con questa integrazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP" + }, + "description": "Vuoi iniziare la configurazione?" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il dispositivo di rete domestica devolo con il nome host `{host_name}` a Home Assistant?", + "title": "Rilevato dispositivo di rete domestica devolo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/zh-Hant.json b/homeassistant/components/devolo_home_network/translations/zh-Hant.json new file mode 100644 index 00000000000..17eb11eb070 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "home_control": "Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u8207\u6b64\u6574\u5408\u4e0d\u76f8\u5bb9\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + }, + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u5c07\u4e3b\u6a5f\u540d\u7a31\u70ba `{host_name}` \u7684 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json new file mode 100644 index 00000000000..ae8909faec9 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Dialogflow\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", + "title": "Dialogflow Webhook\u3092\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json index 439b8d63d8d..153afa164a2 100644 --- a/homeassistant/components/dsmr/translations/bg.json +++ b/homeassistant/components/dsmr/translations/bg.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "setup_network": { diff --git a/homeassistant/components/ebusd/translations/ja.json b/homeassistant/components/ebusd/translations/ja.json new file mode 100644 index 00000000000..c43ca27b22a --- /dev/null +++ b/homeassistant/components/ebusd/translations/ja.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u65e5", + "night": "\u591c" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/ja.json b/homeassistant/components/emulated_roku/translations/ja.json new file mode 100644 index 00000000000..62f80176b2d --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "advertise_ip": "IP\u30a2\u30c9\u30ec\u30b9\u3092\u30a2\u30c9\u30d0\u30bf\u30a4\u30ba\u3059\u308b", + "advertise_port": "\u30a2\u30c9\u30d0\u30bf\u30a4\u30ba \u30dd\u30fc\u30c8", + "host_ip": "\u30db\u30b9\u30c8\u306eIP\u30a2\u30c9\u30ec\u30b9", + "listen_port": "\u30ea\u30c3\u30b9\u30f3 \u30dd\u30fc\u30c8", + "upnp_bind_multicast": "\u30d0\u30a4\u30f3\u30c9 \u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8 (True/False)" + }, + "title": "\u30b5\u30fc\u30d0\u30fc\u69cb\u6210\u306e\u5b9a\u7fa9" + } + } + }, + "title": "Roku\u3092\u30a8\u30df\u30e5\u30ec\u30fc\u30c8" +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json new file mode 100644 index 00000000000..1205652aff2 --- /dev/null +++ b/homeassistant/components/esphome/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "description": "{name} \u306e\u69cb\u6210\u3067\u8a2d\u5b9a\u3057\u305f\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "discovery_confirm": { + "description": "ESPHome\u306e\u30ce\u30fc\u30c9 `{name}` \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "\u691c\u51fa\u3055\u308c\u305fESPHome\u306e\u30ce\u30fc\u30c9" + }, + "user": { + "description": "\u3042\u306a\u305f\u306e[ESPHome](https://esphomelib.com/)\u306e\u30ce\u30fc\u30c9\u306e\u63a5\u7d9a\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index b1ea395f077..3fca53d1013 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "start_config": { diff --git a/homeassistant/components/garages_amsterdam/translations/bg.json b/homeassistant/components/garages_amsterdam/translations/bg.json index 3348117ce6b..122ff7a6474 100644 --- a/homeassistant/components/garages_amsterdam/translations/bg.json +++ b/homeassistant/components/garages_amsterdam/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } }, diff --git a/homeassistant/components/geonetnz_quakes/translations/ja.json b/homeassistant/components/geonetnz_quakes/translations/ja.json new file mode 100644 index 00000000000..97dd549584c --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u534a\u5f84" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/bg.json b/homeassistant/components/gogogate2/translations/bg.json new file mode 100644 index 00000000000..94ea3d76554 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{device} ({ip_address})" + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/bg.json b/homeassistant/components/growatt_server/translations/bg.json index 02c83a6e916..46573dc14b4 100644 --- a/homeassistant/components/growatt_server/translations/bg.json +++ b/homeassistant/components/growatt_server/translations/bg.json @@ -1,9 +1,15 @@ { "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { - "url": "URL" + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json new file mode 100644 index 00000000000..248404e363a --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index f51e0680c67..1a430a03c97 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -8,6 +8,7 @@ "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "error": { + "linking": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044" }, "step": { @@ -18,6 +19,7 @@ "title": "Hue bridge\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" }, "link": { + "description": "\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Philips Hue\u304cHome Assistant\u306b\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "\u30ea\u30f3\u30af\u30cf\u30d6" }, "manual": { diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json new file mode 100644 index 00000000000..795beb33c9e --- /dev/null +++ b/homeassistant/components/ifttt/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "IFTTT\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", + "title": "IFTTT\u306eWebhook\u30a2\u30d7\u30ec\u30c3\u30c8\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json new file mode 100644 index 00000000000..91e89d74e32 --- /dev/null +++ b/homeassistant/components/ipma/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/ja.json b/homeassistant/components/iqvia/translations/ja.json new file mode 100644 index 00000000000..159fad10e03 --- /dev/null +++ b/homeassistant/components/iqvia/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_zip_code": "\u90f5\u4fbf\u756a\u53f7\u304c\u7121\u52b9\u3067\u3059" + }, + "step": { + "user": { + "data": { + "zip_code": "\u90f5\u4fbf\u756a\u53f7" + }, + "description": "\u7c73\u56fd\u307e\u305f\u306f\u30ab\u30ca\u30c0\u306e\u90f5\u4fbf\u756a\u53f7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "IQVIA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/ja.json b/homeassistant/components/izone/translations/ja.json new file mode 100644 index 00000000000..5b9bea94d7a --- /dev/null +++ b/homeassistant/components/izone/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "iZone\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json new file mode 100644 index 00000000000..7a239efcd3d --- /dev/null +++ b/homeassistant/components/life360/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json new file mode 100644 index 00000000000..09e11849452 --- /dev/null +++ b/homeassistant/components/lifx/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/ja.json b/homeassistant/components/light/translations/ja.json index d4ac27ea526..c8e4a666c25 100644 --- a/homeassistant/components/light/translations/ja.json +++ b/homeassistant/components/light/translations/ja.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u30c8\u30b0\u30eb {entity_name}", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" + }, + "trigger_type": { + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/luftdaten/translations/ja.json b/homeassistant/components/luftdaten/translations/ja.json new file mode 100644 index 00000000000..48f371ae432 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_sensor": "\u30bb\u30f3\u30b5\u30fc\u304c\u5229\u7528\u3067\u304d\u306a\u3044\u304b\u3001\u7121\u52b9\u3067\u3059" + }, + "step": { + "user": { + "data": { + "show_on_map": "\u5730\u56f3\u3067\u8868\u793a", + "station_id": "Luftdaten\u30bb\u30f3\u30b5\u30fcID" + }, + "title": "Luftdaten\u306e\u5b9a\u7fa9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json new file mode 100644 index 00000000000..8e896108021 --- /dev/null +++ b/homeassistant/components/mailgun/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Mailgun\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", + "title": "Mailgun Webhook\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/bg.json b/homeassistant/components/modern_forms/translations/bg.json index a6e2f383b1a..4200524546f 100644 --- a/homeassistant/components/modern_forms/translations/bg.json +++ b/homeassistant/components/modern_forms/translations/bg.json @@ -2,13 +2,16 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/moon/translations/sensor.ja.json b/homeassistant/components/moon/translations/sensor.ja.json new file mode 100644 index 00000000000..88bd2e4e95a --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.ja.json @@ -0,0 +1,13 @@ +{ + "state": { + "moon__phase": { + "full_moon": "\u6e80\u6708", + "last_quarter": "\u4e0b\u5f26\u306e\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u4e8c\u5341\u516d\u591c", + "waning_gibbous": "\u5341\u516b\u591c", + "waxing_crescent": "\u4e09\u65e5\u6708", + "waxing_gibbous": "\u5341\u4e09\u591c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json index 77307be07dd..114fdfc6052 100644 --- a/homeassistant/components/motioneye/translations/it.json +++ b/homeassistant/components/motioneye/translations/it.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Modello URL streaming", "webhook_set": "Configura i webhooks di motionEye per segnalare gli eventi a Home Assistant", "webhook_set_overwrite": "Sovrascrivi webhook non riconosciuti" } diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json index a443ee6954b..84e508b7737 100644 --- a/homeassistant/components/motioneye/translations/zh-Hant.json +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "\u4e32\u6d41 URL \u6a21\u677f", "webhook_set": "\u8a2d\u5b9a motionEye webhooks \u4ee5\u56de\u5831\u4e8b\u4ef6\u81f3 Home Assistant", "webhook_set_overwrite": "\u8986\u84cb\u7121\u6cd5\u8fa8\u8b58\u7684 Webhooks" } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 0ec3c953a00..161bef1c2cd 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -4,14 +4,18 @@ "broker": { "data": { "broker": "\u30d6\u30ed\u30fc\u30ab\u30fc", + "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "hassio_confirm": { "data": { "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b" - } + }, + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u63d0\u4f9b\u3059\u308bMQTT broker\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", + "title": "HomeAssistant\u30a2\u30c9\u30aa\u30f3\u3092\u4ecb\u3057\u305fMQTT Broker" } } } diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index c902368616e..efb0b252b1a 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -1,7 +1,20 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index bb80db0af5e..4a1f8502d5a 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -14,7 +14,8 @@ "link": { "data": { "code": "PIN\u30b3\u30fc\u30c9" - } + }, + "title": "Nest\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u30ea\u30f3\u30af" } } } diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index a27d52d9559..723b302203f 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -7,6 +7,9 @@ "step": { "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/nfandroidtv/translations/bg.json b/homeassistant/components/nfandroidtv/translations/bg.json index 78978419e43..484dd2b98e3 100644 --- a/homeassistant/components/nfandroidtv/translations/bg.json +++ b/homeassistant/components/nfandroidtv/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { @@ -12,7 +12,8 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "name": "\u0418\u043c\u0435" - } + }, + "title": "\u0418\u0437\u0432\u0435\u0441\u0442\u0438\u044f \u0437\u0430 Android TV / Fire TV" } } } diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json new file mode 100644 index 00000000000..bf28204a0df --- /dev/null +++ b/homeassistant/components/notion/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "step": { + "user": { + "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/translations/ja.json b/homeassistant/components/onboarding/translations/ja.json new file mode 100644 index 00000000000..c36d54519fa --- /dev/null +++ b/homeassistant/components/onboarding/translations/ja.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u5bdd\u5ba4", + "kitchen": "\u30ad\u30c3\u30c1\u30f3", + "living_room": "\u30ea\u30d3\u30f3\u30b0\u30eb\u30fc\u30e0" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/ja.json b/homeassistant/components/openuv/translations/ja.json index db717442b5e..b5d1f619ed1 100644 --- a/homeassistant/components/openuv/translations/ja.json +++ b/homeassistant/components/openuv/translations/ja.json @@ -7,9 +7,11 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "elevation": "\u6a29\u9650\u306e\u6607\u683c", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6" - } + }, + "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } } } diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json new file mode 100644 index 00000000000..89df73b9221 --- /dev/null +++ b/homeassistant/components/plex/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "step": { + "select_server": { + "data": { + "server": "\u30b5\u30fc\u30d0\u30fc" + }, + "title": "Plex\u30b5\u30fc\u30d0\u30fc\u3092\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json new file mode 100644 index 00000000000..09e474b989c --- /dev/null +++ b/homeassistant/components/point/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "follow_link": "\u9001\u4fe1 \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json new file mode 100644 index 00000000000..8d0bb1dd228 --- /dev/null +++ b/homeassistant/components/ps4/translations/ja.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "credential_error": "\u8cc7\u683c\u60c5\u5831\u306e\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", + "port_987_bind_error": "\u30dd\u30fc\u30c8 987\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "error": { + "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN Code\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "creds": { + "title": "Play Station 4" + }, + "link": { + "data": { + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + }, + "title": "Play Station 4" + }, + "mode": { + "data": { + "mode": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30e2\u30fc\u30c9" + }, + "title": "Play Station 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/bg.json b/homeassistant/components/rainmachine/translations/bg.json index 0ced0b2f334..b54660f8e9f 100644 --- a/homeassistant/components/rainmachine/translations/bg.json +++ b/homeassistant/components/rainmachine/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ja.json b/homeassistant/components/rainmachine/translations/ja.json new file mode 100644 index 00000000000..a86d3b073f3 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + }, + "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/bg.json b/homeassistant/components/ridwell/translations/bg.json new file mode 100644 index 00000000000..a0418dd4af0 --- /dev/null +++ b/homeassistant/components/ridwell/translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username}:", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0441\u0438 \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/hu.json b/homeassistant/components/ridwell/translations/hu.json new file mode 100644 index 00000000000..b79c5204f49 --- /dev/null +++ b/homeassistant/components/ridwell/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t {username} r\u00e9sz\u00e9re:", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/it.json b/homeassistant/components/ridwell/translations/it.json new file mode 100644 index 00000000000..bb7a0862268 --- /dev/null +++ b/homeassistant/components/ridwell/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Inserisci nuovamente la password per {username}:", + "title": "Autenticare nuovamente l'integrazione" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci il tuo nome utente e password:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/zh-Hant.json b/homeassistant/components/ridwell/translations/zh-Hant.json new file mode 100644 index 00000000000..f0ce6bd4327 --- /dev/null +++ b/homeassistant/components/ridwell/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 {username} \u5bc6\u78bc\uff1a", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\uff1a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.ja.json b/homeassistant/components/season/translations/sensor.ja.json new file mode 100644 index 00000000000..2f524d4f910 --- /dev/null +++ b/homeassistant/components/season/translations/sensor.ja.json @@ -0,0 +1,10 @@ +{ + "state": { + "season__season__": { + "autumn": "\u79cb", + "spring": "\u6625", + "summer": "\u590f", + "winter": "\u51ac" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json new file mode 100644 index 00000000000..93db79466a4 --- /dev/null +++ b/homeassistant/components/smartthings/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "webhook_error": "SmartThings\u304cWebhook URL\u3092\u691c\u8a3c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002Webhook URL\u304c\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u53ef\u80fd\u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/ja.json b/homeassistant/components/smhi/translations/ja.json new file mode 100644 index 00000000000..bfddae6169d --- /dev/null +++ b/homeassistant/components/smhi/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059", + "wrong_location": "\u6240\u5728\u5730 \u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u306e\u307f" + }, + "step": { + "user": { + "title": "\u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u3067\u306e\u4f4d\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ja.json b/homeassistant/components/solaredge/translations/ja.json new file mode 100644 index 00000000000..a81d9a23b71 --- /dev/null +++ b/homeassistant/components/solaredge/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u3053\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u540d\u524d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/ja.json b/homeassistant/components/switch/translations/ja.json index 42b7ddc7d06..bc55f9c8d2f 100644 --- a/homeassistant/components/switch/translations/ja.json +++ b/homeassistant/components/switch/translations/ja.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u30c8\u30b0\u30eb {entity_name}", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" + }, + "trigger_type": { + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/syncthing/translations/bg.json b/homeassistant/components/syncthing/translations/bg.json new file mode 100644 index 00000000000..5a326ca17cb --- /dev/null +++ b/homeassistant/components/syncthing/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/bg.json b/homeassistant/components/system_bridge/translations/bg.json index 4983c9a14b2..ccf68c66d57 100644 --- a/homeassistant/components/system_bridge/translations/bg.json +++ b/homeassistant/components/system_bridge/translations/bg.json @@ -1,8 +1,21 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", "step": { + "authenticate": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json new file mode 100644 index 00000000000..8aa43369fee --- /dev/null +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u7a7a", + "title": "\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u3092\u9078\u3076\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json new file mode 100644 index 00000000000..28f8763eea2 --- /dev/null +++ b/homeassistant/components/tplink/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "TP-Link\u30b9\u30de\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json new file mode 100644 index 00000000000..7e02bd92743 --- /dev/null +++ b/homeassistant/components/tradfri/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_key": "\u63d0\u4f9b\u3055\u308c\u305f\u30ad\u30fc\u3067\u306e\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u304c\u5f15\u304d\u7d9a\u304d\u767a\u751f\u3059\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3067\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "auth": { + "data": { + "host": "\u30db\u30b9\u30c8", + "security_code": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9" + }, + "title": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 6454194b1c7..8db8bd8d45e 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -17,5 +17,24 @@ } } } + }, + "options": { + "error": { + "dev_not_found": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", + "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8" + }, + "title": "Tuya\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" + }, + "init": { + "data": { + "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.bg.json b/homeassistant/components/tuya/translations/sensor.bg.json new file mode 100644 index 00000000000..a6bedcb419c --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "tuya__status": { + "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "heating": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "heating_temp": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json new file mode 100644 index 00000000000..5ff4d54e766 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "post_code": "\u90f5\u4fbf\u756a\u53f7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json new file mode 100644 index 00000000000..e19844627aa --- /dev/null +++ b/homeassistant/components/twilio/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Twilio Webhook\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json new file mode 100644 index 00000000000..295874b751a --- /dev/null +++ b/homeassistant/components/unifi/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "site": "\u30b5\u30a4\u30c8ID" + }, + "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/bg.json b/homeassistant/components/venstar/translations/bg.json index 9b2acbd7c42..a1401a7f1b2 100644 --- a/homeassistant/components/venstar/translations/bg.json +++ b/homeassistant/components/venstar/translations/bg.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json new file mode 100644 index 00000000000..85500dcafff --- /dev/null +++ b/homeassistant/components/vesync/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/bg.json b/homeassistant/components/wallbox/translations/bg.json index 648be54571d..5644c3b845c 100644 --- a/homeassistant/components/wallbox/translations/bg.json +++ b/homeassistant/components/wallbox/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/bg.json b/homeassistant/components/yamaha_musiccast/translations/bg.json index 6814831ecff..eec876d2be6 100644 --- a/homeassistant/components/yamaha_musiccast/translations/bg.json +++ b/homeassistant/components/yamaha_musiccast/translations/bg.json @@ -4,6 +4,9 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/yeelight/translations/bg.json b/homeassistant/components/yeelight/translations/bg.json index d68ec8b933c..a53214e40e4 100644 --- a/homeassistant/components/yeelight/translations/bg.json +++ b/homeassistant/components/yeelight/translations/bg.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{model} {id} ({host})", "step": { "pick_device": { "data": { diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index 3106439dc92..cbf5f7274a1 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -1,4 +1,9 @@ { + "config": { + "error": { + "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" + } + }, "state": { "_": { "initializing": "\u521d\u671f\u5316\u4e2d", diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index afde8dc1336..bdae86569ac 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -23,15 +23,16 @@ "options": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "configure_addon": { "data": { + "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447", "s0_legacy_key": "S0 \u043a\u043b\u044e\u0447 (\u043d\u0430\u0441\u043b\u0435\u0434\u0435\u043d)", "s2_access_control_key": "S2 \u043a\u043b\u044e\u0447 \u0437\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430 \u0434\u043e\u0441\u0442\u044a\u043f\u0430", "s2_authenticated_key": "S2 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447", From a122cbab6135544a9bd42c44b4c3e062f66cf573 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Oct 2021 20:21:46 -0700 Subject: [PATCH 0157/1452] Mobile app to update entity registry on re-register sensors (#58378) Co-authored-by: J. Nick Koston --- .../components/mobile_app/binary_sensor.py | 2 + homeassistant/components/mobile_app/sensor.py | 2 + .../components/mobile_app/webhook.py | 20 +++++ homeassistant/helpers/entity.py | 5 +- homeassistant/helpers/entity_registry.py | 82 +++++++++++-------- tests/components/mobile_app/test_webhook.py | 56 +++++++++++++ 6 files changed, 128 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 94bcd3b1c0d..2e3681b7618 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -9,6 +9,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, @@ -40,6 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ATTR_SENSOR_STATE: None, ATTR_SENSOR_TYPE: entry.domain, ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry)) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 533e33e84bb..ff4ca491411 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -11,6 +11,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, @@ -45,6 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ATTR_SENSOR_TYPE: entry.domain, ATTR_SENSOR_UNIQUE_ID: entry.unique_id, ATTR_SENSOR_UOM: entry.unit_of_measurement, + ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } entities.append(MobileAppSensor(config, entry.device_id, config_entry)) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 9a0d391373c..ebba383636b 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -446,6 +446,26 @@ async def webhook_register_sensor(hass, config_entry, data): "Re-register for %s of existing sensor %s", device_name, unique_id ) + entry = entity_registry.async_get(existing_sensor) + changes = {} + + if ( + new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}" + ) != entry.original_name: + changes["original_name"] = new_name + + for ent_reg_key, data_key in ( + ("device_class", ATTR_SENSOR_DEVICE_CLASS), + ("unit_of_measurement", ATTR_SENSOR_UOM), + ("entity_category", ATTR_SENSOR_ENTITY_CATEGORY), + ("original_icon", ATTR_SENSOR_ICON), + ): + if data_key in data and getattr(entry, ent_reg_key) != data[data_key]: + changes[ent_reg_key] = data[data_key] + + if changes: + entity_registry.async_update_entity(existing_sensor, **changes) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) else: register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3b2a12c687e..9ee7221ffa6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -38,7 +38,6 @@ from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.event import Event, async_track_entity_registry_updated_event from homeassistant.helpers.typing import StateType from homeassistant.loader import bind_hass @@ -227,7 +226,7 @@ class Entity(ABC): parallel_updates: asyncio.Semaphore | None = None # Entry in the entity registry - registry_entry: RegistryEntry | None = None + registry_entry: er.RegistryEntry | None = None # Hold list for functions to call on remove. _on_remove: list[CALLBACK_TYPE] | None = None @@ -806,7 +805,7 @@ class Entity(ABC): if data["action"] != "update": return - ent_reg = await self.hass.helpers.entity_registry.async_get_registry() + ent_reg = er.async_get(self.hass) old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) assert self.registry_entry is not None diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bedbdc51785..b7b0eed2f32 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -243,21 +243,21 @@ class EntityRegistry: unique_id: str, *, # To influence entity ID generation - suggested_object_id: str | None = None, known_object_ids: Iterable[str] | None = None, + suggested_object_id: str | None = None, # To disable an entity if it gets created disabled_by: str | None = None, # Data that we want entry to have - config_entry: ConfigEntry | None = None, - device_id: str | None = None, area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, - supported_features: int | None = None, + config_entry: ConfigEntry | None = None, device_class: str | None = None, - unit_of_measurement: str | None = None, - original_name: str | None = None, - original_icon: str | None = None, + device_id: str | None = None, entity_category: str | None = None, + original_icon: str | None = None, + original_name: str | None = None, + supported_features: int | None = None, + unit_of_measurement: str | None = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -300,20 +300,20 @@ class EntityRegistry: disabled_by = DISABLED_INTEGRATION entity = RegistryEntry( - entity_id=entity_id, - config_entry_id=config_entry_id, - device_id=device_id, area_id=area_id, - unique_id=unique_id, - platform=platform, - disabled_by=disabled_by, capabilities=capabilities, - supported_features=supported_features or 0, + config_entry_id=config_entry_id, device_class=device_class, - unit_of_measurement=unit_of_measurement, - original_name=original_name, - original_icon=original_icon, + device_id=device_id, + disabled_by=disabled_by, entity_category=entity_category, + entity_id=entity_id, + original_icon=original_icon, + original_name=original_name, + platform=platform, + supported_features=supported_features or 0, + unique_id=unique_id, + unit_of_measurement=unit_of_measurement, ) self._register_entry(entity) _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -383,24 +383,34 @@ class EntityRegistry: self, entity_id: str, *, - name: str | None | UndefinedType = UNDEFINED, - icon: str | None | UndefinedType = UNDEFINED, - config_entry_id: str | None | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, + device_class: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + entity_category: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Update properties of an entity.""" return self._async_update_entity( entity_id, - name=name, - icon=icon, - config_entry_id=config_entry_id, area_id=area_id, + config_entry_id=config_entry_id, + device_class=device_class, + disabled_by=disabled_by, + entity_category=entity_category, + icon=icon, + name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, - disabled_by=disabled_by, + original_icon=original_icon, + original_name=original_name, + unit_of_measurement=unit_of_measurement, ) @callback @@ -408,21 +418,21 @@ class EntityRegistry: self, entity_id: str, *, - name: str | None | UndefinedType = UNDEFINED, - icon: str | None | UndefinedType = UNDEFINED, - config_entry_id: str | None | UndefinedType = UNDEFINED, - new_entity_id: str | UndefinedType = UNDEFINED, - device_id: str | None | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, - new_unique_id: str | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, - supported_features: int | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, - unit_of_measurement: str | None | UndefinedType = UNDEFINED, - original_name: str | None | UndefinedType = UNDEFINED, - original_icon: str | None | UndefinedType = UNDEFINED, + device_id: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, entity_category: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + new_entity_id: str | UndefinedType = UNDEFINED, + new_unique_id: str | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + supported_features: int | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 623abf30e9e..41b939c7113 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -10,6 +10,7 @@ from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE @@ -515,3 +516,58 @@ async def test_register_sensor_limits_state_class( # This means it was ignored. assert reg_resp.status == HTTPStatus.OK + + +async def test_reregister_sensor(hass, create_registrations, webhook_client): + """Test that we can add more info in re-registration.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + ent_reg = er.async_get(hass) + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 Battery State" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon == "mdi:cellphone" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + "state_class": "total", + "device_class": "battery", + "entity_category": "diagnostic", + "icon": "mdi:new-icon", + "unit_of_measurement": "%", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 New Name" + assert entry.device_class == "battery" + assert entry.unit_of_measurement == "%" + assert entry.entity_category == "diagnostic" + assert entry.original_icon == "mdi:new-icon" From 7126c9b0defc46f24855f8e0a0abb2c8ec2d8c77 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 1 Nov 2021 04:22:13 +0100 Subject: [PATCH 0158/1452] Add `configuration_url` to GIOS integration (#58840) --- homeassistant/components/gios/const.py | 2 ++ homeassistant/components/gios/sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 4f19b0d8a68..9b98b0bda26 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -27,6 +27,8 @@ SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" MANUFACTURER: Final = "Główny Inspektorat Ochrony Środowiska" +URL = "http://powietrze.gios.gov.pl/pjp/current/station_details/info/{station_id}" + API_TIMEOUT: Final = 30 ATTR_INDEX: Final = "index" diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 5dd48656d12..f60a8e99d5a 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -25,6 +25,7 @@ from .const import ( DOMAIN, MANUFACTURER, SENSOR_TYPES, + URL, ) from .model import GiosSensorEntityDescription @@ -86,6 +87,7 @@ class GiosSensor(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, + configuration_url=URL.format(station_id=coordinator.gios.station_id), ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" From 9aaa92f366b5345f2297c43541a6daa2ac0e7ca7 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 1 Nov 2021 11:23:01 +0800 Subject: [PATCH 0159/1452] Improve part metadata in stream (#58822) --- homeassistant/components/stream/worker.py | 117 +++++++++++++--------- tests/components/stream/test_worker.py | 52 ++++++---- 2 files changed, 106 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 64a43f68aa0..881614b04a3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -66,9 +66,15 @@ class SegmentBuffer: memory_file: BytesIO, sequence: int, input_vstream: av.video.VideoStream, - ) -> av.container.OutputContainer: - """Make a new av OutputContainer.""" - return av.open( + input_astream: av.audio.stream.AudioStream, + ) -> tuple[ + av.container.OutputContainer, + av.video.VideoStream, + av.audio.stream.AudioStream | None, + ]: + """Make a new av OutputContainer and add output streams.""" + add_audio = input_astream and input_astream.name in AUDIO_CODECS + container = av.open( memory_file, mode="w", format=SEGMENT_CONTAINER_FORMAT, @@ -93,19 +99,21 @@ class SegmentBuffer: # Create a fragment every TARGET_PART_DURATION. The data from each fragment is stored in # a "Part" that can be combined with the data from all the other "Part"s, plus an init # section, to reconstitute the data in a "Segment". - # frag_duration is the threshold for determining part boundaries, and the dts of the last - # packet in the part should correspond to a duration that is smaller than this value. - # However, as the part duration includes the duration of the last frame, the part duration - # will be equal to or greater than this value. - # We previously scaled this number down by .85 to account for this while keeping within - # the 15% variance allowed in part duration. However, this did not work when inputs had - # an audio stream - sometimes the fragment would get cut on the audio packet, causing - # the durations to actually be to short. - # The current approach is to use this frag_duration for creating the media while - # adjusting the metadata duration to keep the durations in the metadata below the - # part_target_duration threshold. + # The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x] + # of the part target duration. We use the frag_duration option to tell ffmpeg to try to + # cut the fragments when they reach frag_duration. However, the resulting fragments can + # have variability in their durations and can end up being too short or too long. If + # there are two tracks, as in the case of a video feed with audio, the fragment cut seems + # to be done on the first track that crosses the desired threshold, and cutting on the + # audio track may result in a shorter video fragment than desired. Conversely, with a + # video track with no audio, the discrete nature of frames means that the frame at the + # end of a fragment will sometimes extend slightly beyond the desired frag_duration. + # Given this, our approach is to use a frag_duration near the upper end of the range for + # outputs with audio using a frag_duration at the lower end of the range for outputs with + # only video. "frag_duration": str( - self._stream_settings.part_target_duration * 1e6 + self._stream_settings.part_target_duration + * (98e4 if add_audio else 9e5) ), } if self._stream_settings.ll_hls @@ -113,6 +121,12 @@ class SegmentBuffer: ), }, ) + output_vstream = container.add_stream(template=input_vstream) + # Check if audio is requested + output_astream = None + if add_audio: + output_astream = container.add_stream(template=input_astream) + return container, output_vstream, output_astream def set_streams( self, @@ -128,26 +142,22 @@ class SegmentBuffer: """Initialize a new stream segment.""" # Keep track of the number of segments we've processed self._sequence += 1 - self._segment_start_dts = video_dts + self._part_start_dts = self._segment_start_dts = video_dts self._segment = None self._memory_file = BytesIO() self._memory_file_pos = 0 - self._av_output = self.make_new_av( + ( + self._av_output, + self._output_video_stream, + self._output_audio_stream, + ) = self.make_new_av( memory_file=self._memory_file, sequence=self._sequence, input_vstream=self._input_video_stream, - ) - self._output_video_stream = self._av_output.add_stream( - template=self._input_video_stream + input_astream=self._input_audio_stream, ) if self._output_video_stream.name == "hevc": self._output_video_stream.codec_tag = "hvc1" - # Check if audio is requested - self._output_audio_stream = None - if self._input_audio_stream and self._input_audio_stream.name in AUDIO_CODECS: - self._output_audio_stream = self._av_output.add_stream( - template=self._input_audio_stream - ) def mux_packet(self, packet: av.Packet) -> None: """Mux a packet to the appropriate output stream.""" @@ -186,13 +196,9 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. stream_outputs=self._outputs_callback().values(), - start_time=self._start_time - + datetime.timedelta( - seconds=float(self._segment_start_dts * packet.time_base) - ), + start_time=self._start_time, ) self._memory_file_pos = self._memory_file.tell() - self._part_start_dts = self._segment_start_dts else: # These are the ends of the part segments self.flush(packet, last_part=False) @@ -201,17 +207,23 @@ class SegmentBuffer: If last_part is True, also close the segment, give it a duration, and clean up the av_output and memory_file. + There are two different ways to enter this function, and when + last_part is True, packet has not yet been muxed, while when + last_part is False, the packet has already been muxed. However, + in both cases, packet is the next packet and is not included in + the Part. + This function writes the duration metadata for the Part and + for the Segment. However, as the fragmentation done by ffmpeg + may result in fragment durations which fall outside the + [0.85x,1.0x] tolerance band allowed by LL-HLS, we need to fudge + some durations a bit by reporting them as being within that + range. + Note that repeated adjustments may cause drift between the part + durations in the metadata and those in the media and result in + playback issues in some clients. """ - # In some cases using the current packet's dts (which is the start - # dts of the next part) to calculate the part duration will result in a - # value which exceeds the part_target_duration. This can muck up the - # duration of both this part and the next part. An easy fix is to just - # use the current packet dts and cap it by the part target duration. - # The adjustment may cause a drift between this adjusted duration - # (used in the metadata) and the media duration, but the drift should be - # automatically corrected when the part duration cleanly divides the - # framerate. - current_dts = min( + # Part durations should not exceed the part target duration + adjusted_dts = min( packet.dts, self._part_start_dts + self._stream_settings.part_target_duration / packet.time_base, @@ -220,29 +232,44 @@ class SegmentBuffer: # Closing the av_output will write the remaining buffered data to the # memory_file as a new moof/mdat. self._av_output.close() + elif not self._part_has_keyframe: + # Parts which are not the last part or an independent part should + # not have durations below 0.85 of the part target duration. + adjusted_dts = max( + adjusted_dts, + self._part_start_dts + + 0.85 * self._stream_settings.part_target_duration / packet.time_base, + ) assert self._segment self._memory_file.seek(self._memory_file_pos) self._hass.loop.call_soon_threadsafe( self._segment.async_add_part, Part( - duration=float((current_dts - self._part_start_dts) * packet.time_base), + duration=float( + (adjusted_dts - self._part_start_dts) * packet.time_base + ), has_keyframe=self._part_has_keyframe, data=self._memory_file.read(), ), - float((current_dts - self._segment_start_dts) * packet.time_base) + ( + segment_duration := float( + (adjusted_dts - self._segment_start_dts) * packet.time_base + ) + ) if last_part else 0, ) if last_part: # If we've written the last part, we can close the memory_file. self._memory_file.close() # We don't need the BytesIO object anymore + self._start_time += datetime.timedelta(seconds=segment_duration) # Reinitialize - self.reset(current_dts) + self.reset(packet.dts) else: # For the last part, these will get set again elsewhere so we can skip # setting them here. self._memory_file_pos = self._memory_file.tell() - self._part_start_dts = current_dts + self._part_start_dts = adjusted_dts self._part_has_keyframe = False def discontinuity(self) -> None: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 7c9ad91f543..97fe4bd0d37 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -677,6 +677,10 @@ async def test_worker_log(hass, caplog): async def test_durations(hass, record_worker_sync): """Test that the duration metadata matches the media.""" + + # Use a target part duration which has a slight mismatch + # with the incoming frame rate to better expose problems. + target_part_duration = TEST_PART_DURATION - 0.01 await async_setup_component( hass, "stream", @@ -684,12 +688,12 @@ async def test_durations(hass, record_worker_sync): "stream": { CONF_LL_HLS: True, CONF_SEGMENT_DURATION: SEGMENT_DURATION, - CONF_PART_DURATION: TEST_PART_DURATION, + CONF_PART_DURATION: target_part_duration, } }, ) - source = generate_h264_video() + source = generate_h264_video(duration=SEGMENT_DURATION + 1) stream = create_stream(hass, source, {}) # use record_worker_sync to grab output segments @@ -702,25 +706,37 @@ async def test_durations(hass, record_worker_sync): # check that the Part duration metadata matches the durations in the media running_metadata_duration = 0 for segment in complete_segments: - for part in segment.parts: + av_segment = av.open(io.BytesIO(segment.init + segment.get_data())) + av_segment.close() + for part_num, part in enumerate(segment.parts): av_part = av.open(io.BytesIO(segment.init + part.data)) running_metadata_duration += part.duration - # av_part.duration actually returns the dts of the first packet of - # the next av_part. When we normalize this by av.time_base we get - # the running duration of the media. - # The metadata duration is slightly different. The worker has - # some flexibility of where to set each metadata boundary, and - # when the media's duration is slightly too long, the metadata - # duration is adjusted down. This means that the running metadata - # duration may be up to one video frame duration smaller than the - # part duration. - assert running_metadata_duration < av_part.duration / av.time_base + 1e-6 - assert ( - running_metadata_duration - > av_part.duration / av.time_base - - 1 / av_part.streams.video[0].rate - - 1e-6 + # av_part.duration actually returns the dts of the first packet of the next + # av_part. When we normalize this by av.time_base we get the running + # duration of the media. + # The metadata duration may differ slightly from the media duration. + # The worker has some flexibility of where to set each metadata boundary, + # and when the media's duration is slightly too long or too short, the + # metadata duration may be adjusted up or down. + # We check here that the divergence between the metadata duration and the + # media duration is not too large (2 frames seems reasonable here). + assert math.isclose( + (av_part.duration - av_part.start_time) / av.time_base, + part.duration, + abs_tol=2 / av_part.streams.video[0].rate + 1e-6, ) + # Also check that the sum of the durations so far matches the last dts + # in the media. + assert math.isclose( + running_metadata_duration, + av_part.duration / av.time_base, + abs_tol=1e-6, + ) + # And check that the metadata duration is between 0.85x and 1.0x of + # the part target duration + if not (part.has_keyframe or part_num == len(segment.parts) - 1): + assert part.duration > 0.85 * target_part_duration - 1e-6 + assert part.duration < target_part_duration + 1e-6 av_part.close() # check that the Part durations are consistent with the Segment durations for segment in complete_segments: From a02055441a15d0767ce2790c0e69cf336188c001 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Nov 2021 04:43:42 +0100 Subject: [PATCH 0160/1452] Migrate attribution attribute for Zestimate (#58854) --- homeassistant/components/zestimate/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index bac32563776..36e13a780d9 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -7,14 +7,12 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "http://www.zillow.com/webservice/GetZestimate.htm" -ATTRIBUTION = "Data provided by Zillow.com" - CONF_ZPID = "zpid" DEFAULT_NAME = "Zestimate" @@ -58,6 +56,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ZestimateDataSensor(SensorEntity): """Implementation of a Zestimate sensor.""" + _attr_attribution = "Data provided by Zillow.com" + def __init__(self, name, params): """Initialize the sensor.""" self._name = name @@ -91,7 +91,6 @@ class ZestimateDataSensor(SensorEntity): if self.data is not None: attributes = self.data attributes["address"] = self.address - attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return attributes @property From 617144994bb9f17fea984d0dcd5513d5a315f752 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Nov 2021 04:44:01 +0100 Subject: [PATCH 0161/1452] Upgrade coverage to 6.1.1 (#58855) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 36b8f41fea1..3355cc82ef1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.0.2 +coverage==6.1.1 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 From 0baa6b1827ea961504b10aa85eb56f154cc164aa Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Mon, 1 Nov 2021 00:39:27 -0700 Subject: [PATCH 0162/1452] Add device_class for GEM power and voltage sensors (#58764) Co-authored-by: Martin Hjelmare --- homeassistant/components/greeneye_monitor/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index e14c24efbe6..067b3e220d8 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -12,7 +12,9 @@ from homeassistant.const import ( CONF_SENSOR_TYPE, CONF_SENSORS, CONF_TEMPERATURE_UNIT, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, POWER_WATT, TIME_HOURS, @@ -43,9 +45,6 @@ DATA_WATT_SECONDS = "watt_seconds" UNIT_WATTS = POWER_WATT COUNTER_ICON = "mdi:counter" -CURRENT_SENSOR_ICON = "mdi:flash" -TEMPERATURE_ICON = "mdi:thermometer" -VOLTAGE_ICON = "mdi:current-ac" async def async_setup_platform( @@ -171,8 +170,8 @@ class GEMSensor(Generic[T], SensorEntity): class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): """Entity showing power usage on one channel of the monitor.""" - _attr_icon = CURRENT_SENSOR_ICON _attr_native_unit_of_measurement = UNIT_WATTS + _attr_device_class = DEVICE_CLASS_POWER def __init__( self, monitor_serial_number: int, number: int, name: str, net_metering: bool @@ -279,7 +278,6 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): """Entity showing temperature from one temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_icon = TEMPERATURE_ICON def __init__( self, monitor_serial_number: int, number: int, name: str, unit: str @@ -310,8 +308,8 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): """Entity showing voltage.""" - _attr_icon = VOLTAGE_ICON _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT + _attr_device_class = DEVICE_CLASS_VOLTAGE def __init__(self, monitor_serial_number: int, number: int, name: str) -> None: """Construct the entity.""" From 2dca0805749bcc8382e7e8c4dce49f0013d4d0f9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Nov 2021 08:51:36 +0100 Subject: [PATCH 0163/1452] Upgrade black to 21.10b0 (#58870) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef58733100b..4a95dfbd869 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.10b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ffa11a0fc30..a21931423b9 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.9b0 +black==21.10b0 codespell==2.0.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 From ce2e3438ca5942fd15719c43078ea2594492eb57 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Nov 2021 08:53:27 +0100 Subject: [PATCH 0164/1452] Use attr and entity descriptions for Twente Milieu sensors (#58871) --- .../components/twentemilieu/sensor.py | 141 ++++++------------ 1 file changed, 48 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 14568fa5d87..4b76f3f475b 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -10,10 +10,10 @@ from twentemilieu import ( TwenteMilieuConnectionError, ) -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -23,6 +23,33 @@ from .const import DATA_UPDATE, DOMAIN PARALLEL_UPDATES = 1 +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=WASTE_TYPE_NON_RECYCLABLE, + name=f"{WASTE_TYPE_NON_RECYCLABLE} Waste Pickup", + icon="mdi:delete-empty", + device_class=DEVICE_CLASS_DATE, + ), + SensorEntityDescription( + key=WASTE_TYPE_ORGANIC, + name=f"{WASTE_TYPE_ORGANIC} Waste Pickup", + icon="mdi:delete-empty", + device_class=DEVICE_CLASS_DATE, + ), + SensorEntityDescription( + key=WASTE_TYPE_PAPER, + name=f"{WASTE_TYPE_PAPER} Waste Pickup", + icon="mdi:delete-empty", + device_class=DEVICE_CLASS_DATE, + ), + SensorEntityDescription( + key=WASTE_TYPE_PLASTIC, + name=f"{WASTE_TYPE_PLASTIC} Waste Pickup", + icon="mdi:delete-empty", + device_class=DEVICE_CLASS_DATE, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -37,118 +64,46 @@ async def async_setup_entry( except TwenteMilieuConnectionError as exception: raise PlatformNotReady from exception - sensors = [ - TwenteMilieuSensor( - twentemilieu, - unique_id=entry.data[CONF_ID], - name=f"{WASTE_TYPE_NON_RECYCLABLE} Waste Pickup", - waste_type=WASTE_TYPE_NON_RECYCLABLE, - icon="mdi:delete-empty", - ), - TwenteMilieuSensor( - twentemilieu, - unique_id=entry.data[CONF_ID], - name=f"{WASTE_TYPE_ORGANIC} Waste Pickup", - waste_type=WASTE_TYPE_ORGANIC, - icon="mdi:delete-empty", - ), - TwenteMilieuSensor( - twentemilieu, - unique_id=entry.data[CONF_ID], - name=f"{WASTE_TYPE_PAPER} Waste Pickup", - waste_type=WASTE_TYPE_PAPER, - icon="mdi:delete-empty", - ), - TwenteMilieuSensor( - twentemilieu, - unique_id=entry.data[CONF_ID], - name=f"{WASTE_TYPE_PLASTIC} Waste Pickup", - waste_type=WASTE_TYPE_PLASTIC, - icon="mdi:delete-empty", - ), - ] - - async_add_entities(sensors, True) + async_add_entities( + [ + TwenteMilieuSensor(twentemilieu, entry.data[CONF_ID], description) + for description in SENSORS + ], + True, + ) class TwenteMilieuSensor(SensorEntity): """Defines a Twente Milieu sensor.""" - _attr_device_class = DEVICE_CLASS_DATE + _attr_should_poll = False def __init__( self, twentemilieu: TwenteMilieu, unique_id: str, - name: str, - waste_type: str, - icon: str, + description: SensorEntityDescription, ) -> None: """Initialize the Twente Milieu entity.""" - self._available = True - self._unique_id = unique_id - self._icon = icon - self._name = name + self.entity_description = description self._twentemilieu = twentemilieu - self._waste_type = waste_type - - self._state = None - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{DOMAIN}_{self._unique_id}_{self._waste_type}" - - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False + self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Twente Milieu", + name="Twente Milieu", + ) async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( async_dispatcher_connect( - self.hass, DATA_UPDATE, self._schedule_immediate_update + self.hass, DATA_UPDATE, self.async_schedule_update_ha_state ) ) - @callback - def _schedule_immediate_update(self, unique_id: str) -> None: - """Schedule an immediate update of the entity.""" - if unique_id == self._unique_id: - self.async_schedule_update_ha_state(True) - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - async def async_update(self) -> None: """Update Twente Milieu entity.""" - next_pickup = await self._twentemilieu.next_pickup(self._waste_type) + next_pickup = await self._twentemilieu.next_pickup(self.entity_description.key) if next_pickup is not None: - self._state = next_pickup.date().isoformat() - - @property - def device_info(self) -> DeviceInfo: - """Return device information about Twente Milieu.""" - return DeviceInfo( - identifiers={(DOMAIN, self._unique_id)}, - manufacturer="Twente Milieu", - name="Twente Milieu", - ) + self._attr_native_value = next_pickup.date().isoformat() From f3d5768fb4f3108e69678358e503d794cf628080 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Nov 2021 08:54:05 +0100 Subject: [PATCH 0165/1452] Move WLED sensors to entity descriptions (#58839) Co-authored-by: Paulus Schoutsen --- homeassistant/components/wled/const.py | 2 - homeassistant/components/wled/sensor.py | 285 +++++++++++------------- tests/components/wled/test_sensor.py | 5 +- 3 files changed, 125 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 180ef89c1b7..1d76d6633dd 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -17,8 +17,6 @@ ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" ATTR_FADE = "fade" ATTR_INTENSITY = "intensity" -ATTR_LED_COUNT = "led_count" -ATTR_MAX_POWER = "max_power" ATTR_ON = "on" ATTR_PALETTE = "palette" ATTR_PRESET = "preset" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index b5982798852..2453c9d1604 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,10 +1,18 @@ """Support for WLED sensors.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from typing import Any +from typing import Callable -from homeassistant.components.sensor import DEVICE_CLASS_CURRENT, SensorEntity +from wled import Device as WLEDDevice + +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, @@ -17,13 +25,109 @@ 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.dt import utcnow -from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, DOMAIN +from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator from .models import WLEDEntity +@dataclass +class WLEDSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[WLEDDevice], StateType] + + +@dataclass +class WLEDSensorEntityDescription( + SensorEntityDescription, WLEDSensorEntityDescriptionMixin +): + """Describes WLED sensor entity.""" + + +SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( + WLEDSensorEntityDescription( + key="estimated_current", + name="Estimated Current", + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + value_fn=lambda device: device.info.leds.power, + ), + WLEDSensorEntityDescription( + key="info_leds_count", + name="LED Count", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + value_fn=lambda device: device.info.leds.count, + ), + WLEDSensorEntityDescription( + key="info_leds_max_power", + name="Max Current", + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=DEVICE_CLASS_CURRENT, + value_fn=lambda device: device.info.leds.max_power, + ), + WLEDSensorEntityDescription( + key="uptime", + name="Uptime", + device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: (utcnow() - timedelta(seconds=device.info.uptime)) + .replace(microsecond=0) + .isoformat(), + ), + WLEDSensorEntityDescription( + key="free_heap", + name="Free Memory", + icon="mdi:memory", + native_unit_of_measurement=DATA_BYTES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.info.free_heap, + ), + WLEDSensorEntityDescription( + key="wifi_signal", + name="Wi-Fi Signal", + icon="mdi:wifi", + native_unit_of_measurement=PERCENTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.info.wifi.signal if device.info.wifi else None, + ), + WLEDSensorEntityDescription( + key="wifi_rssi", + name="Wi-Fi RSSI", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.info.wifi.rssi if device.info.wifi else None, + ), + WLEDSensorEntityDescription( + key="wifi_channel", + name="Wi-Fi Channel", + icon="mdi:wifi", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.info.wifi.channel if device.info.wifi else None, + ), + WLEDSensorEntityDescription( + key="wifi_bssid", + name="Wi-Fi BSSID", + icon="mdi:wifi", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.info.wifi.bssid if device.info.wifi else None, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -31,169 +135,28 @@ async def async_setup_entry( ) -> None: """Set up WLED sensor based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - sensors = [ - WLEDEstimatedCurrentSensor(coordinator), - WLEDUptimeSensor(coordinator), - WLEDFreeHeapSensor(coordinator), - WLEDWifiBSSIDSensor(coordinator), - WLEDWifiChannelSensor(coordinator), - WLEDWifiRSSISensor(coordinator), - WLEDWifiSignalSensor(coordinator), - ] - - async_add_entities(sensors) + async_add_entities( + WLEDSensorEntity(coordinator, description) for description in SENSORS + ) -class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): - """Defines a WLED estimated current sensor.""" +class WLEDSensorEntity(WLEDEntity, SensorEntity): + """Defines a WLED sensor entity.""" - _attr_icon = "mdi:power" - _attr_native_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE - _attr_device_class = DEVICE_CLASS_CURRENT - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + entity_description: WLEDSensorEntityDescription - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED estimated current sensor.""" + def __init__( + self, + coordinator: WLEDDataUpdateCoordinator, + description: WLEDSensorEntityDescription, + ) -> None: + """Initialize a WLED sensor entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Estimated Current" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_estimated_current" + self.entity_description = description + self._attr_name = f"{coordinator.data.info.name} {description.name}" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_{description.key}" @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the entity.""" - return { - ATTR_LED_COUNT: self.coordinator.data.info.leds.count, - ATTR_MAX_POWER: self.coordinator.data.info.leds.max_power, - } - - @property - def native_value(self) -> int: + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.coordinator.data.info.leds.power - - -class WLEDUptimeSensor(WLEDEntity, SensorEntity): - """Defines a WLED uptime sensor.""" - - _attr_device_class = DEVICE_CLASS_TIMESTAMP - _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED uptime sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Uptime" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_uptime" - - @property - 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() - - -class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): - """Defines a WLED free heap sensor.""" - - _attr_icon = "mdi:memory" - _attr_entity_registry_enabled_default = False - _attr_native_unit_of_measurement = DATA_BYTES - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED free heap sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Free Memory" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_free_heap" - - @property - def native_value(self) -> int: - """Return the state of the sensor.""" - return self.coordinator.data.info.free_heap - - -class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): - """Defines a WLED Wi-Fi signal sensor.""" - - _attr_icon = "mdi:wifi" - _attr_native_unit_of_measurement = PERCENTAGE - _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED Wi-Fi signal sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Wi-Fi Signal" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" - - @property - def native_value(self) -> int | None: - """Return the state of the sensor.""" - if not self.coordinator.data.info.wifi: - return None - return self.coordinator.data.info.wifi.signal - - -class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): - """Defines a WLED Wi-Fi RSSI sensor.""" - - _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT - _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED Wi-Fi RSSI sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Wi-Fi RSSI" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" - - @property - def native_value(self) -> int | None: - """Return the state of the sensor.""" - if not self.coordinator.data.info.wifi: - return None - return self.coordinator.data.info.wifi.rssi - - -class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): - """Defines a WLED Wi-Fi Channel sensor.""" - - _attr_icon = "mdi:wifi" - _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED Wi-Fi Channel sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Wi-Fi Channel" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" - - @property - def native_value(self) -> int | None: - """Return the state of the sensor.""" - if not self.coordinator.data.info.wifi: - return None - return self.coordinator.data.info.wifi.channel - - -class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): - """Defines a WLED Wi-Fi BSSID sensor.""" - - _attr_icon = "mdi:wifi" - _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize WLED Wi-Fi BSSID sensor.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Wi-Fi BSSID" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" - - @property - def native_value(self) -> str | None: - """Return the state of the sensor.""" - if not self.coordinator.data.info.wifi: - return None - return self.coordinator.data.info.wifi.bssid + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 28effd2ff07..7810915825d 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TIMESTAMP, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.components.wled.const import ATTR_LED_COUNT, ATTR_MAX_POWER, DOMAIN +from homeassistant.components.wled.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -95,9 +95,6 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_estimated_current") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:power" - assert state.attributes.get(ATTR_LED_COUNT) == 30 - assert state.attributes.get(ATTR_MAX_POWER) == 850 assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_MILLIAMPERE ) From 39054d656b3db10969f5d38868755539f6bc7f07 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Nov 2021 02:03:37 -0600 Subject: [PATCH 0166/1452] Perform some AirVisual code cleanup (#58858) --- .../components/airvisual/__init__.py | 9 +++---- .../components/airvisual/config_flow.py | 24 +++++++------------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 72b063c9394..ed687a84b27 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -16,7 +16,6 @@ from pyairvisual.errors import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_IP_ADDRESS, CONF_LATITUDE, @@ -190,9 +189,6 @@ def _standardize_node_pro_config_entry(hass: HomeAssistant, entry: ConfigEntry) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - if CONF_API_KEY in entry.data: _standardize_geography_config_entry(hass, entry) @@ -271,7 +267,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} # Reassess the interval between 2 server requests if CONF_API_KEY in entry.data: @@ -355,7 +352,7 @@ class AirVisualEntity(CoordinatorEntity): """Initialize.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._entry = entry self.entity_description = description diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 636da54899f..ebd5373f1b9 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -91,6 +91,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, str], integration_type: str ) -> FlowResult: """Validate a Cloud API key.""" + errors = {} websession = aiohttp_client.async_get_clientsession(self.hass) cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) @@ -117,27 +118,20 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await coro except InvalidKeyError: - return self.async_show_form( - step_id=error_step, - data_schema=error_schema, - errors={CONF_API_KEY: "invalid_api_key"}, - ) + errors[CONF_API_KEY] = "invalid_api_key" except NotFoundError: - return self.async_show_form( - step_id=error_step, - data_schema=error_schema, - errors={CONF_CITY: "location_not_found"}, - ) + errors[CONF_CITY] = "location_not_found" except AirVisualError as err: LOGGER.error(err) - return self.async_show_form( - step_id=error_step, - data_schema=error_schema, - errors={"base": "unknown"}, - ) + errors["base"] = "unknown" valid_keys.add(user_input[CONF_API_KEY]) + if errors: + return self.async_show_form( + step_id=error_step, data_schema=error_schema, errors=errors + ) + existing_entry = await self.async_set_unique_id(self._geo_id) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) From 5836a39f1439c338c1492a34ffa8d427bd235daf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Nov 2021 02:03:48 -0600 Subject: [PATCH 0167/1452] Perform some WattTime code cleanup (#58869) --- homeassistant/components/watttime/__init__.py | 8 +++----- homeassistant/components/watttime/const.py | 2 -- homeassistant/components/watttime/sensor.py | 14 ++------------ 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 779fc0791b6..d33e6fceb2e 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -19,7 +19,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5) @@ -28,9 +28,6 @@ PLATFORMS: list[str] = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WattTime from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - session = aiohttp_client.async_get_clientsession(hass) try: @@ -65,7 +62,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/watttime/const.py b/homeassistant/components/watttime/const.py index 07ea0e47167..5bb8cb50d40 100644 --- a/homeassistant/components/watttime/const.py +++ b/homeassistant/components/watttime/const.py @@ -8,5 +8,3 @@ LOGGER = logging.getLogger(__package__) CONF_BALANCING_AUTHORITY = "balancing_authority" CONF_BALANCING_AUTHORITY_ABBREV = "balancing_authority_abbreviation" CONF_SHOW_ON_MAP = "show_on_map" - -DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index b1ebc262134..0b1ae54b5d1 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -10,13 +10,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, - MASS_POUNDS, - PERCENTAGE, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, MASS_POUNDS, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -29,14 +23,11 @@ from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, CONF_SHOW_ON_MAP, - DATA_COORDINATOR, DOMAIN, ) ATTR_BALANCING_AUTHORITY = "balancing_authority" -DEFAULT_ATTRIBUTION = "Pickup data provided by WattTime" - SENSOR_TYPE_REALTIME_EMISSIONS_MOER = "moer" SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" @@ -63,7 +54,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up WattTime sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ RealtimeEmissionsSensor(coordinator, entry, description) @@ -96,7 +87,6 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_BALANCING_AUTHORITY: self._entry.data[CONF_BALANCING_AUTHORITY], } From d024c5e698165c0641921a47be94fdb7e385e5af Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Nov 2021 02:03:54 -0600 Subject: [PATCH 0168/1452] Perform some Tile code cleanup (#58868) --- homeassistant/components/tile/__init__.py | 9 +++++---- homeassistant/components/tile/device_tracker.py | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 6d16ea79b68..75bf306a98b 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -30,8 +30,6 @@ CONF_SHOW_INACTIVE = "show_inactive" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} @callback def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None: @@ -100,8 +98,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_init_tasks.append(coordinator.async_refresh()) await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinators - hass.data[DOMAIN][entry.entry_id][DATA_TILE] = tiles + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinators, + DATA_TILE: tiles, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 1276dc3f5fd..9b8e2a94257 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -10,7 +10,7 @@ from pytile.tile import Tile from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -32,7 +32,6 @@ ATTR_RING_STATE = "ring_state" ATTR_TILE_NAME = "tile_name" ATTR_VOIP_STATE = "voip_state" -DEFAULT_ATTRIBUTION = "Data provided by Tile" DEFAULT_ICON = "mdi:view-grid" @@ -89,7 +88,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Initialize.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_name = tile.name self._attr_unique_id = f"{entry.data[CONF_USERNAME]}_{tile.uuid}" self._entry = entry From 0cc4b7219fdaf4afdd0dd72e3f3e1107d6096b72 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Nov 2021 02:04:00 -0600 Subject: [PATCH 0169/1452] Perform some SimpliSafe code cleanup (#58867) --- homeassistant/components/simplisafe/__init__.py | 8 +++----- .../components/simplisafe/alarm_control_panel.py | 3 +-- homeassistant/components/simplisafe/binary_sensor.py | 4 ++-- homeassistant/components/simplisafe/const.py | 2 -- homeassistant/components/simplisafe/lock.py | 4 ++-- homeassistant/components/simplisafe/sensor.py | 4 ++-- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8fc4c919ed8..3647c1bda57 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -80,7 +80,6 @@ from .const import ( ATTR_LIGHT, ATTR_VOICE_PROMPT_VOLUME, CONF_USER_ID, - DATA_CLIENT, DOMAIN, LOGGER, ) @@ -223,9 +222,6 @@ def _async_register_base_station( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SimpliSafe as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - _async_standardize_config_entry(hass, entry) _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -248,7 +244,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SimplipyError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = simplisafe + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = simplisafe + hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index bc2e2a8ac74..15887b91532 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -60,7 +60,6 @@ from .const import ( ATTR_EXIT_DELAY_HOME, ATTR_LIGHT, ATTR_VOICE_PROMPT_VOLUME, - DATA_CLIENT, DOMAIN, LOGGER, ) @@ -123,7 +122,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up a SimpliSafe alarm control panel based on a config entry.""" - simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + simplisafe = hass.data[DOMAIN][entry.entry_id] async_add_entities( [SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()], True, diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index f276a5fea66..eef38ffe003 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER SUPPORTED_BATTERY_SENSOR_TYPES = [ DeviceTypes.carbon_monoxide, @@ -50,7 +50,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe binary sensors based on a config entry.""" - simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + simplisafe = hass.data[DOMAIN][entry.entry_id] sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = [] diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index a0073fa8122..658ddfc13a6 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -16,5 +16,3 @@ ATTR_LIGHT = "light" ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" CONF_USER_ID = "user_id" - -DATA_CLIENT = "client" diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index dc09eb0b62e..263fd54f9d6 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" @@ -37,7 +37,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe locks based on a config entry.""" - simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + simplisafe = hass.data[DOMAIN][entry.entry_id] locks = [] for system in simplisafe.systems.values(): diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 97edd3008dd..0fb9c129a7c 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -12,14 +12,14 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe freeze sensors based on a config entry.""" - simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + simplisafe = hass.data[DOMAIN][entry.entry_id] sensors = [] for system in simplisafe.systems.values(): From f0bd6acd48356b1d388d24936a1b60176decf538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 1 Nov 2021 12:33:41 +0100 Subject: [PATCH 0170/1452] Set internal quality_scale for the hassio integration (#58881) --- homeassistant/components/hassio/manifest.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 27cc1eaf735..cc0bcc77265 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -2,8 +2,15 @@ "domain": "hassio", "name": "Home Assistant Supervisor", "documentation": "https://www.home-assistant.io/integrations/hassio", - "dependencies": ["http"], - "after_dependencies": ["panel_custom"], - "codeowners": ["@home-assistant/supervisor"], - "iot_class": "local_polling" -} + "dependencies": [ + "http" + ], + "after_dependencies": [ + "panel_custom" + ], + "codeowners": [ + "@home-assistant/supervisor" + ], + "iot_class": "local_polling", + "quality_scale": "internal" +} \ No newline at end of file From 93bc88be16b8f221f8a84cf148585eb2d21292aa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 1 Nov 2021 08:49:00 -0300 Subject: [PATCH 0171/1452] Simplify requests in the Broadlink integration (#58850) --- homeassistant/components/broadlink/entity.py | 14 +++--- homeassistant/components/broadlink/light.py | 6 +-- homeassistant/components/broadlink/remote.py | 52 ++++++++++---------- homeassistant/components/broadlink/switch.py | 18 ++++--- 4 files changed, 50 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index 080cd5bab71..25bfd87140b 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -53,11 +53,13 @@ class BroadlinkEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return device info.""" + device = self._device + return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, - identifiers={(DOMAIN, self._device.unique_id)}, - manufacturer=self._device.api.manufacturer, - model=self._device.api.model, - name=self._device.name, - sw_version=self._device.fw_version, + connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)}, + identifiers={(DOMAIN, device.unique_id)}, + manufacturer=device.api.manufacturer, + model=device.api.model, + name=device.name, + sw_version=device.fw_version, ) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index be4b08f5cee..698401f3e2e 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -124,10 +124,10 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): async def _async_set_state(self, state): """Set the state of the light.""" + device = self._device + try: - state = await self._device.async_request( - self._device.api.set_state, **state - ) + state = await device.async_request(device.api.set_state, **state) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to set state: %s", err) return diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index cc2d85204b8..5a939b68bb4 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -213,10 +213,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): kwargs[ATTR_COMMAND] = command kwargs = SERVICE_SEND_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] - device = kwargs.get(ATTR_DEVICE) + subdevice = kwargs.get(ATTR_DEVICE) repeat = kwargs[ATTR_NUM_REPEATS] delay = kwargs[ATTR_DELAY_SECS] service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}" + device = self._device if not self._attr_is_on: _LOGGER.warning( @@ -228,13 +229,13 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): await self._async_load_storage() try: - code_list = self._extract_codes(commands, device) + code_list = self._extract_codes(commands, subdevice) except ValueError as err: _LOGGER.error("Failed to call %s: %s", service, err) raise rf_flags = {0xB2, 0xD7} - if not hasattr(self._device.api, "sweep_frequency") and any( + if not hasattr(device.api, "sweep_frequency") and any( c[0] in rf_flags for codes in code_list for c in codes ): err_msg = f"{self.entity_id} doesn't support sending RF commands" @@ -247,18 +248,18 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): await asyncio.sleep(delay) if len(codes) > 1: - code = codes[self._flags[device]] + code = codes[self._flags[subdevice]] else: code = codes[0] try: - await self._device.async_request(self._device.api.send_data, code) + await device.async_request(device.api.send_data, code) except (BroadlinkException, OSError) as err: _LOGGER.error("Error during %s: %s", service, err) break if len(codes) > 1: - self._flags[device] ^= 1 + self._flags[subdevice] ^= 1 at_least_one_sent = True if at_least_one_sent: @@ -269,9 +270,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): kwargs = SERVICE_LEARN_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] command_type = kwargs[ATTR_COMMAND_TYPE] - device = kwargs[ATTR_DEVICE] + subdevice = kwargs[ATTR_DEVICE] toggle = kwargs[ATTR_ALTERNATIVE] service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}" + device = self._device if not self._attr_is_on: _LOGGER.warning( @@ -286,7 +288,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): if command_type == COMMAND_TYPE_IR: learn_command = self._async_learn_ir_command - elif hasattr(self._device.api, "sweep_frequency"): + elif hasattr(device.api, "sweep_frequency"): learn_command = self._async_learn_rf_command else: @@ -310,7 +312,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): _LOGGER.error("Failed to learn '%s': %s", command, err) continue - self._codes.setdefault(device, {}).update({command: code}) + self._codes.setdefault(subdevice, {}).update({command: code}) should_store = True if should_store: @@ -318,8 +320,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): async def _async_learn_ir_command(self, command): """Learn an infrared command.""" + device = self._device + try: - await self._device.async_request(self._device.api.enter_learning) + await device.async_request(device.api.enter_learning) except (BroadlinkException, OSError) as err: _LOGGER.debug("Failed to enter learning mode: %s", err) @@ -336,7 +340,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) try: - code = await self._device.async_request(self._device.api.check_data) + code = await device.async_request(device.api.check_data) except (ReadError, StorageError): continue return b64encode(code).decode("utf8") @@ -353,8 +357,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): async def _async_learn_rf_command(self, command): """Learn a radiofrequency command.""" + device = self._device + try: - await self._device.async_request(self._device.api.sweep_frequency) + await device.async_request(device.api.sweep_frequency) except (BroadlinkException, OSError) as err: _LOGGER.debug("Failed to sweep frequency: %s", err) @@ -370,15 +376,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): start_time = dt.utcnow() while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) - found = await self._device.async_request( - self._device.api.check_frequency - ) + found = await device.async_request(device.api.check_frequency) if found: break else: - await self._device.async_request( - self._device.api.cancel_sweep_frequency - ) + await device.async_request(device.api.cancel_sweep_frequency) raise TimeoutError( "No radiofrequency found within " f"{LEARNING_TIMEOUT.total_seconds()} seconds" @@ -392,7 +394,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): await asyncio.sleep(1) try: - await self._device.async_request(self._device.api.find_rf_packet) + await device.async_request(device.api.find_rf_packet) except (BroadlinkException, OSError) as err: _LOGGER.debug("Failed to enter learning mode: %s", err) @@ -409,7 +411,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): while (dt.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) try: - code = await self._device.async_request(self._device.api.check_data) + code = await device.async_request(device.api.check_data) except (ReadError, StorageError): continue return b64encode(code).decode("utf8") @@ -428,7 +430,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """Delete a list of commands from a remote.""" kwargs = SERVICE_DELETE_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] - device = kwargs[ATTR_DEVICE] + subdevice = kwargs[ATTR_DEVICE] service = f"{RM_DOMAIN}.{SERVICE_DELETE_COMMAND}" if not self._attr_is_on: @@ -443,9 +445,9 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): await self._async_load_storage() try: - codes = self._codes[device] + codes = self._codes[subdevice] except KeyError as err: - err_msg = f"Device not found: {repr(device)}" + err_msg = f"Device not found: {repr(subdevice)}" _LOGGER.error("Failed to call %s. %s", service, err_msg) raise ValueError(err_msg) from err @@ -470,8 +472,8 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): # Clean up if not codes: - del self._codes[device] - if self._flags.pop(device, None) is not None: + del self._codes[subdevice] + if self._flags.pop(subdevice, None) is not None: self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY) self._code_storage.async_delay_save(self._get_codes, CODE_SAVE_DELAY) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index a06e67defba..0649b526537 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -179,11 +179,13 @@ class BroadlinkRMSwitch(BroadlinkSwitch): async def _async_send_packet(self, packet): """Send a packet to the device.""" + device = self._device + if packet is None: return True try: - await self._device.async_request(self._device.api.send_data, packet) + await device.async_request(device.api.send_data, packet) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False @@ -200,8 +202,10 @@ class BroadlinkSP1Switch(BroadlinkSwitch): async def _async_send_packet(self, packet): """Send a packet to the device.""" + device = self._device + try: - await self._device.async_request(self._device.api.set_power, packet) + await device.async_request(device.api.set_power, packet) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False @@ -242,10 +246,10 @@ class BroadlinkMP1Slot(BroadlinkSwitch): async def _async_send_packet(self, packet): """Send a packet to the device.""" + device = self._device + try: - await self._device.async_request( - self._device.api.set_power, self._slot, packet - ) + await device.async_request(device.api.set_power, self._slot, packet) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False @@ -273,9 +277,11 @@ class BroadlinkBG1Slot(BroadlinkSwitch): async def _async_send_packet(self, packet): """Send a packet to the device.""" + device = self._device state = {f"pwr{self._slot}": packet} + try: - await self._device.async_request(self._device.api.set_state, **state) + await device.async_request(device.api.set_state, **state) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False From 1aa34b68927e119618338daea6ede47629212341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Mon, 1 Nov 2021 13:27:58 +0100 Subject: [PATCH 0172/1452] Fix OpenWeatherMap options not being initialized the first time (#58736) --- homeassistant/components/openweathermap/config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 507f1b6f721..0b7a3a1a25f 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -109,13 +109,15 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): vol.Optional( CONF_MODE, default=self.config_entry.options.get( - CONF_MODE, DEFAULT_FORECAST_MODE + CONF_MODE, + self.config_entry.data.get(CONF_MODE, DEFAULT_FORECAST_MODE), ), ): vol.In(FORECAST_MODES), vol.Optional( CONF_LANGUAGE, default=self.config_entry.options.get( - CONF_LANGUAGE, DEFAULT_LANGUAGE + CONF_LANGUAGE, + self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), ), ): vol.In(LANGUAGES), } From 51873573d31eb4d2422a20f1c209d9d92967598c Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:29:00 +0100 Subject: [PATCH 0173/1452] Add ROCKROBO_S4_MAX to supported xiaomi vacuums (#58826) --- homeassistant/components/xiaomi_miio/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 578a6d3ffab..69c279df493 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -198,12 +198,14 @@ MODELS_LIGHT = ( # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme ROCKROBO_S4 = "roborock.vacuum.s4" +ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, ROCKROBO_S4, + ROCKROBO_S4_MAX, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From d125dc7dbfcf19245317b24963b41af8e4106e6b Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Mon, 1 Nov 2021 06:25:02 -0700 Subject: [PATCH 0174/1452] Use _attr_ shorthand in greeneye_monitor sensors (#58784) --- .../components/greeneye_monitor/sensor.py | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 067b3e220d8..2a4692674c9 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -117,20 +117,13 @@ class GEMSensor(Generic[T], SensorEntity): ) -> None: """Construct the entity.""" self._monitor_serial_number = monitor_serial_number - self._name = name + self._attr_name = name self._sensor: T | None = None self._sensor_type = sensor_type self._number = number - - @property - def unique_id(self) -> str: - """Return a unique ID for this sensor.""" - return f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}" - - @property - def name(self) -> str: - """Return the name of the channel.""" - return self._name + self._attr_unique_id = ( + f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}" + ) async def async_added_to_hass(self) -> None: """Wait for and connect to the sensor.""" @@ -223,9 +216,9 @@ class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): ) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "pulse", number) - self._counted_quantity = counted_quantity self._counted_quantity_per_pulse = counted_quantity_per_pulse self._time_unit = time_unit + self._attr_native_unit_of_measurement = f"{counted_quantity}/{self._time_unit}" def _get_sensor( self, monitor: greeneye.monitor.Monitor @@ -260,11 +253,6 @@ class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): f"Invalid value for time unit: {self._time_unit}. Expected one of {TIME_SECONDS}, {TIME_MINUTES}, or {TIME_HOURS}" ) - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement for this pulse counter.""" - return f"{self._counted_quantity}/{self._time_unit}" - @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return total pulses in the data dictionary.""" @@ -284,7 +272,7 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): ) -> None: """Construct the entity.""" super().__init__(monitor_serial_number, name, "temp", number) - self._unit = unit + self._attr_native_unit_of_measurement = unit def _get_sensor( self, monitor: greeneye.monitor.Monitor @@ -299,11 +287,6 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): return cast(Optional[float], self._sensor.temperature) - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement for this sensor (user specified).""" - return self._unit - class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): """Entity showing voltage.""" From 0e1927830951900d9184fe9d3f327448cc1fa8b6 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Mon, 1 Nov 2021 14:28:30 +0100 Subject: [PATCH 0175/1452] Add type annotations to OpenWeatherMap (#58802) --- .../components/openweathermap/__init__.py | 15 +++--- .../components/openweathermap/sensor.py | 40 +++++++++------ .../components/openweathermap/weather.py | 49 +++++++++++-------- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 58219ee70b3..8fd7aaae7ad 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1,5 +1,8 @@ """The openweathermap component.""" +from __future__ import annotations + import logging +from typing import Any from pyowm import OWM from pyowm.utils.config import get_default_config @@ -62,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_migrate_entry(hass, entry): +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" config_entries = hass.config_entries data = entry.data @@ -83,7 +86,7 @@ async def async_migrate_entry(hass, entry): return True -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) @@ -99,17 +102,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def _filter_domain_configs(elements, domain): - return list(filter(lambda elem: elem["platform"] == domain, elements)) - - -def _get_config_value(config_entry, key): +def _get_config_value(config_entry: ConfigEntry, key: str) -> Any: if config_entry.options: return config_entry.options[key] return config_entry.data[key] -def _get_owm_config(language): +def _get_owm_config(language: str) -> dict[str, Any]: """Get OpenWeatherMap configuration and add language to it.""" config_dict = get_default_config() config_dict["language"] = language diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 13b282b5ef6..dcdb1c0c8fb 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -2,8 +2,12 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -20,7 +24,11 @@ from .const import ( from .weather_update_coordinator import WeatherUpdateCoordinator -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up OpenWeatherMap sensor entities based on a config entry.""" domain_data = hass.data[DOMAIN][config_entry.entry_id] name = domain_data[ENTRY_NAME] @@ -59,11 +67,11 @@ class AbstractOpenWeatherMapSensor(SensorEntity): def __init__( self, - name, - unique_id, + name: str, + unique_id: str, description: SensorEntityDescription, coordinator: DataUpdateCoordinator, - ): + ) -> None: """Initialize the sensor.""" self.entity_description = description self._coordinator = coordinator @@ -79,22 +87,22 @@ class AbstractOpenWeatherMapSensor(SensorEntity): ) @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._coordinator.last_update_success - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self._coordinator.async_add_listener(self.async_write_ha_state) ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from OWM and updates the states.""" await self._coordinator.async_request_refresh() @@ -104,17 +112,17 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): def __init__( self, - name, - unique_id, + name: str, + unique_id: str, description: SensorEntityDescription, weather_coordinator: WeatherUpdateCoordinator, - ): + ) -> None: """Initialize the sensor.""" super().__init__(name, unique_id, description, weather_coordinator) self._weather_coordinator = weather_coordinator @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the device.""" return self._weather_coordinator.data.get(self.entity_description.key, None) @@ -124,17 +132,17 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): def __init__( self, - name, - unique_id, + name: str, + unique_id: str, description: SensorEntityDescription, weather_coordinator: WeatherUpdateCoordinator, - ): + ) -> None: """Initialize the sensor.""" super().__init__(name, unique_id, description, weather_coordinator) self._weather_coordinator = weather_coordinator @property - def native_value(self): + def native_value(self) -> StateType: """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/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index f80566be329..d3b5c488c56 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,7 +1,12 @@ """Support for the OpenWeatherMap (OWM) service.""" -from homeassistant.components.weather import WeatherEntity +from __future__ import annotations + +from homeassistant.components.weather import Forecast, WeatherEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.pressure import convert as pressure_convert from .const import ( @@ -22,7 +27,11 @@ from .const import ( from .weather_update_coordinator import WeatherUpdateCoordinator -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up OpenWeatherMap weather entity based on a config entry.""" domain_data = hass.data[DOMAIN][config_entry.entry_id] name = domain_data[ENTRY_NAME] @@ -39,22 +48,22 @@ class OpenWeatherMapWeather(WeatherEntity): def __init__( self, - name, - unique_id, + name: str, + unique_id: str, weather_coordinator: WeatherUpdateCoordinator, - ): + ) -> None: """Initialize the sensor.""" self._name = name self._unique_id = unique_id self._weather_coordinator = weather_coordinator @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return self._unique_id @@ -69,32 +78,32 @@ class OpenWeatherMapWeather(WeatherEntity): ) @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling requirement of the entity.""" return False @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" return self._weather_coordinator.data[ATTR_API_CONDITION] @property - def temperature(self): + def temperature(self) -> float | None: """Return the temperature.""" return self._weather_coordinator.data[ATTR_API_TEMPERATURE] @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def pressure(self): + def pressure(self) -> float | None: """Return the pressure.""" pressure = self._weather_coordinator.data[ATTR_API_PRESSURE] # OpenWeatherMap returns pressure in hPA, so convert to @@ -104,12 +113,12 @@ class OpenWeatherMapWeather(WeatherEntity): return pressure @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self._weather_coordinator.data[ATTR_API_HUMIDITY] @property - def wind_speed(self): + def wind_speed(self) -> float | None: """Return the wind speed.""" wind_speed = self._weather_coordinator.data[ATTR_API_WIND_SPEED] if self.hass.config.units.name == "imperial": @@ -117,26 +126,26 @@ class OpenWeatherMapWeather(WeatherEntity): return round(wind_speed * 3.6, 2) @property - def wind_bearing(self): + def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" return self._weather_coordinator.data[ATTR_API_WIND_BEARING] @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" return self._weather_coordinator.data[ATTR_API_FORECAST] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._weather_coordinator.last_update_success - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self._weather_coordinator.async_add_listener(self.async_write_ha_state) ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from OWM and updates the states.""" await self._weather_coordinator.async_request_refresh() From f51e1fcb6700afe6ac7466973dde607d8e566927 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Nov 2021 06:33:09 -0700 Subject: [PATCH 0176/1452] Check for uncaught service not found exceptions (#58010) --- tests/components/alert/test_init.py | 7 --- tests/components/blueprint/conftest.py | 2 +- tests/components/config/test_automation.py | 59 +++++++++++++--------- tests/components/config/test_customize.py | 8 +++ tests/components/config/test_scene.py | 39 ++++++++------ tests/components/config/test_script.py | 10 ++++ tests/components/homekit/test_type_fans.py | 5 ++ tests/conftest.py | 3 -- 8 files changed, 84 insertions(+), 49 deletions(-) diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 199be9845ca..ef21b463a12 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -310,13 +310,6 @@ async def test_skipfirst(hass): assert len(events) == 0 -async def test_noack(hass): - """Test no ack feature.""" - entity = alert.Alert(hass, *TEST_NOACK) - hass.async_add_job(entity.begin_alerting) - await hass.async_block_till_done() - - async def test_done_message_state_tracker_reset_on_cancel(hass): """Test that the done message is reset when canceled.""" entity = alert.Alert(hass, *TEST_NOACK) diff --git a/tests/components/blueprint/conftest.py b/tests/components/blueprint/conftest.py index ec76451065c..fe0df5d8260 100644 --- a/tests/components/blueprint/conftest.py +++ b/tests/components/blueprint/conftest.py @@ -7,7 +7,7 @@ import pytest @pytest.fixture(autouse=True) def stub_blueprint_populate(): - """Stub copying the blueprint automations to the config folder.""" + """Stub copying the blueprints to the config folder.""" with patch( "homeassistant.components.blueprint.models.DomainBlueprints.async_populate" ): diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 0950e3d0358..80ee38350aa 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -3,6 +3,8 @@ from http import HTTPStatus import json from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.helpers import entity_registry as er @@ -10,7 +12,18 @@ from homeassistant.helpers import entity_registry as er from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -async def test_get_device_config(hass, hass_client): +@pytest.fixture +async def setup_automation( + hass, automation_config, stub_blueprint_populate # noqa: F811 +): + """Set up automation integration.""" + assert await async_setup_component( + hass, "automation", {"automation": automation_config} + ) + + +@pytest.mark.parametrize("automation_config", ({},)) +async def test_get_device_config(hass, hass_client, setup_automation): """Test getting device config.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) @@ -30,7 +43,8 @@ async def test_get_device_config(hass, hass_client): assert result == {"id": "moon"} -async def test_update_device_config(hass, hass_client): +@pytest.mark.parametrize("automation_config", ({},)) +async def test_update_device_config(hass, hass_client, setup_automation): """Test updating device config.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) @@ -66,7 +80,8 @@ async def test_update_device_config(hass, hass_client): assert written[0] == orig_data -async def test_bad_formatted_automations(hass, hass_client): +@pytest.mark.parametrize("automation_config", ({},)) +async def test_bad_formatted_automations(hass, hass_client, setup_automation): """Test that we handle automations without ID.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) @@ -110,29 +125,27 @@ async def test_bad_formatted_automations(hass, hass_client): assert orig_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} -async def test_delete_automation(hass, hass_client): +@pytest.mark.parametrize( + "automation_config", + ( + [ + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": "test.automation"}, + }, + { + "id": "moon", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": "test.automation"}, + }, + ], + ), +) +async def test_delete_automation(hass, hass_client, setup_automation): """Test deleting an automation.""" ent_reg = er.async_get(hass) - assert await async_setup_component( - hass, - "automation", - { - "automation": [ - { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, - }, - { - "id": "moon", - "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, - }, - ] - }, - ) - assert len(ent_reg.entities) == 2 with patch.object(config, "SECTIONS", ["automation"]): diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index d5b4c788bcf..9ea18ff2ae0 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -3,11 +3,19 @@ from http import HTTPStatus import json from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass): + """Set up homeassistant integration.""" + assert await async_setup_component(hass, "homeassistant", {}) + + async def test_get_entity(hass, hass_client): """Test getting entity.""" with patch.object(config, "SECTIONS", ["customize"]): diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index db938638d01..69f75cc5895 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -3,13 +3,22 @@ from http import HTTPStatus import json from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.helpers import entity_registry as er from homeassistant.util.yaml import dump -async def test_create_scene(hass, hass_client): +@pytest.fixture +async def setup_scene(hass, scene_config): + """Set up scene integration.""" + assert await async_setup_component(hass, "scene", {"scene": scene_config}) + + +@pytest.mark.parametrize("scene_config", ({},)) +async def test_create_scene(hass, hass_client, setup_scene): """Test creating a scene.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) @@ -58,7 +67,8 @@ async def test_create_scene(hass, hass_client): ) -async def test_update_scene(hass, hass_client): +@pytest.mark.parametrize("scene_config", ({},)) +async def test_update_scene(hass, hass_client, setup_scene): """Test updating a scene.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) @@ -110,7 +120,8 @@ async def test_update_scene(hass, hass_client): ) -async def test_bad_formatted_scene(hass, hass_client): +@pytest.mark.parametrize("scene_config", ({},)) +async def test_bad_formatted_scene(hass, hass_client, setup_scene): """Test that we handle scene without ID.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) @@ -163,21 +174,19 @@ async def test_bad_formatted_scene(hass, hass_client): } -async def test_delete_scene(hass, hass_client): +@pytest.mark.parametrize( + "scene_config", + ( + [ + {"id": "light_on", "name": "Light on", "entities": {}}, + {"id": "light_off", "name": "Light off", "entities": {}}, + ], + ), +) +async def test_delete_scene(hass, hass_client, setup_scene): """Test deleting a scene.""" ent_reg = er.async_get(hass) - assert await async_setup_component( - hass, - "scene", - { - "scene": [ - {"id": "light_on", "name": "Light on", "entities": {}}, - {"id": "light_off", "name": "Light off", "entities": {}}, - ] - }, - ) - assert len(ent_reg.entities) == 2 with patch.object(config, "SECTIONS", ["scene"]): diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 18ab87b8c40..dca9a8aa8a7 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -2,9 +2,19 @@ from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 + + +@pytest.fixture(autouse=True) +async def setup_script(hass, stub_blueprint_populate): # noqa: F811 + """Set up script integration.""" + assert await async_setup_component(hass, "script", {}) + async def test_delete_script(hass, hass_client): """Test deleting a script.""" diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 85d00dcb287..646d1baad63 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -312,6 +312,8 @@ async def test_fan_speed(hass, hk_driver, events): assert acc.char_speed.value == 50 assert acc.char_active.value == 0 + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -328,6 +330,9 @@ async def test_fan_speed(hass, hk_driver, events): assert acc.char_speed.value == 50 assert acc.char_active.value == 1 + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + async def test_fan_set_all_one_shot(hass, hk_driver, events): """Test fan with speed.""" diff --git a/tests/conftest.py b/tests/conftest.py index 845145c2ec2..80eb75ef2bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,6 @@ from homeassistant.components.websocket_api.auth import ( ) from homeassistant.components.websocket_api.http import URL from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED -from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -232,8 +231,6 @@ def hass(loop, load_registries, hass_storage, request): request.function.__name__, ) in IGNORE_UNCAUGHT_EXCEPTIONS: continue - if isinstance(ex, ServiceNotFound): - continue raise ex From 20a443ad6c0044dcb6bd7d0a848057e0a3905227 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 1 Nov 2021 15:18:01 +0100 Subject: [PATCH 0177/1452] Use entity_registry async_get for AsusWrt (#58885) --- homeassistant/components/asuswrt/router.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 03a15f80110..c186e182f5c 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -249,11 +250,9 @@ class AsusWrtRouter: self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" # Load tracked entities from registry - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() - track_entries = ( - self.hass.helpers.entity_registry.async_entries_for_config_entry( - entity_registry, self._entry.entry_id - ) + track_entries = er.async_entries_for_config_entry( + er.async_get(self.hass), + self._entry.entry_id, ) for entry in track_entries: if entry.domain == TRACKER_DOMAIN: From 108962b134afe95850e3fc9cf2fd0bbb9268532b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Mon, 1 Nov 2021 15:36:30 +0100 Subject: [PATCH 0178/1452] Update xknx to 0.18.12 (#58891) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6c0b1811a6b..bd79a815e7a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.11"], + "requirements": ["xknx==0.18.12"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a6af04c2ab5..02db16d8ee7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2436,7 +2436,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.11 +xknx==0.18.12 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c7f0062130..be6f6dca086 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1413,7 +1413,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.11 +xknx==0.18.12 # homeassistant.components.bluesound # homeassistant.components.fritz From f7b63e9fd7c13349ac2e9db4f028e2afd5e8976f Mon Sep 17 00:00:00 2001 From: purcell-lab <79175134+purcell-lab@users.noreply.github.com> Date: Tue, 2 Nov 2021 01:37:48 +1100 Subject: [PATCH 0179/1452] Fix renamed solaredge sensor keys (#58875) --- homeassistant/components/solaredge/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index e6c3fc3571a..a151a50a9c8 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -92,11 +92,11 @@ class SolarEdgeSensorFactory: self.services[key] = (SolarEdgeStorageLevelSensor, flow) for key in ( - "purchased_power", - "production_power", - "feedin_power", - "consumption_power", - "selfconsumption_power", + "purchased_energy", + "production_energy", + "feedin_energy", + "consumption_energy", + "selfconsumption_energy", ): self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) From 43ccf1d96745cfcab56a76ebf722b8f163e618c5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 1 Nov 2021 17:40:15 +0100 Subject: [PATCH 0180/1452] Handle `None` values in Xiaomi Miio integration (#58880) * Initial commit * Improve _handle_coordinator_update() * Fix entity_description define * Improve sensor & binary_sensor platforms * Log None value * Use coordinator variable * Improve log strings * Filter attributes with None values * Add hasattr condition * Update homeassistant/components/xiaomi_miio/sensor.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/binary_sensor.py | 32 +++++++-- .../components/xiaomi_miio/device.py | 13 +--- homeassistant/components/xiaomi_miio/fan.py | 9 --- .../components/xiaomi_miio/humidifier.py | 9 --- .../components/xiaomi_miio/number.py | 9 --- .../components/xiaomi_miio/select.py | 11 +--- .../components/xiaomi_miio/sensor.py | 66 ++++++++++++------- .../components/xiaomi_miio/switch.py | 9 --- 8 files changed, 72 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index d33059c20ef..1bdd647da79 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +import logging from typing import Callable from homeassistant.components.binary_sensor import ( @@ -12,6 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import callback from . import VacuumCoordinatorDataAttributes from .const import ( @@ -30,6 +32,8 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity +_LOGGER = logging.getLogger(__name__) + ATTR_NO_WATER = "no_water" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" @@ -108,21 +112,29 @@ HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) def _setup_vacuum_sensors(hass, config_entry, async_add_entities): """Only vacuums with mop should have binary sensor registered.""" - if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP: return device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] for sensor, description in VACUUM_SENSORS.items(): + parent_key_data = getattr(coordinator.data, description.parent_key) + if getattr(parent_key_data, description.key, None) is None: + _LOGGER.debug( + "It seems the %s does not support the %s as the initial value is None", + config_entry.data[CONF_MODEL], + description.key, + ) + continue entities.append( XiaomiGenericBinarySensor( f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) @@ -168,18 +180,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): """Representation of a Xiaomi Humidifier binary sensor.""" + entity_description: XiaomiMiioBinarySensorDescription + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) - self.entity_description: XiaomiMiioBinarySensorDescription = description + self.entity_description = description self._attr_entity_registry_enabled_default = ( description.entity_registry_enabled_default ) + self._attr_is_on = self._determine_native_value() - @property - def is_on(self): - """Return true if the binary sensor is on.""" + @callback + def _handle_coordinator_update(self) -> None: + self._attr_is_on = self._determine_native_value() + + super()._handle_coordinator_update() + + def _determine_native_value(self): + """Determine native value.""" if self.entity_description.parent_key is not None: return self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 8203b021ef9..488f4cc066f 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -169,17 +169,8 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): return cls._parse_datetime_datetime(value) if isinstance(value, datetime.timedelta): return cls._parse_time_delta(value) - if isinstance(value, float): - return value - if isinstance(value, int): - return value - - _LOGGER.warning( - "Could not determine how to parse state value of type %s for state %s and attribute %s", - type(value), - type(state), - attribute, - ) + if value is None: + _LOGGER.debug("Attribute %s is None, this is unexpected", attribute) return value diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 01304008b76..07ec4613270 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,7 +1,6 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" from abc import abstractmethod import asyncio -from enum import Enum import logging import math @@ -363,14 +362,6 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): return None - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - @callback def _handle_coordinator_update(self): """Fetch state from the device.""" diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 411d1428c70..9896bf8f0ea 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -1,5 +1,4 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity.""" -from enum import Enum import logging import math @@ -124,14 +123,6 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): """Return true if device is on.""" return self._state - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - @property def mode(self): """Get the current mode.""" diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 1461f33add6..161a690a0df 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES @@ -285,14 +284,6 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return False return super().available - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - async def async_set_value(self, value): """Set an option of the miio device.""" method = getattr(self, self.entity_description.method) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 2753fb09786..ec1be6f3219 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -2,7 +2,6 @@ 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 @@ -126,14 +125,6 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): self._attr_options = list(description.options) self.entity_description = description - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" @@ -153,7 +144,7 @@ class XiaomiAirHumidifierSelector(XiaomiSelector): ) # Sometimes (quite rarely) the device returns None as the LED brightness so we # check that the value is not None before updating the state. - if led_brightness: + if led_brightness is not None: self._current_led_brightness = led_brightness self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index e1e2d91ad1a..f818a809a5c 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -48,6 +48,7 @@ from homeassistant.const import ( TIME_SECONDS, VOLUME_CUBIC_METERS, ) +from homeassistant.core import callback from . import VacuumCoordinatorDataAttributes from .const import ( @@ -529,17 +530,27 @@ VACUUM_SENSORS = { def _setup_vacuum_sensors(hass, config_entry, async_add_entities): + """Set up the Xiaomi vacuum sensors.""" device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] for sensor, description in VACUUM_SENSORS.items(): + parent_key_data = getattr(coordinator.data, description.parent_key) + if getattr(parent_key_data, description.key, None) is None: + _LOGGER.debug( + "It seems the %s does not support the %s as the initial value is None", + config_entry.data[CONF_MODEL], + description.key, + ) + continue entities.append( XiaomiGenericSensor( f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) @@ -637,23 +648,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): """Representation of a Xiaomi generic sensor.""" - def __init__( - self, - name, - device, - entry, - unique_id, - coordinator, - description: XiaomiMiioSensorDescription, - ): + entity_description: XiaomiMiioSensorDescription + + 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 self._attr_unique_id = unique_id - self.entity_description: XiaomiMiioSensorDescription = description + self._attr_native_value = self._determine_native_value() + self._attr_extra_state_attributes = self._extract_attributes(coordinator.data) - @property - def native_value(self): - """Return the state of the device.""" + @callback + def _extract_attributes(self, data): + """Return state attributes with valid values.""" + return { + attr: value + for attr in self.entity_description.attributes + if hasattr(data, attr) + and (value := self._extract_value_from_attribute(data, attr)) is not None + } + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + native_value = self._determine_native_value() + # Sometimes (quite rarely) the device returns None as the sensor value so we + # check that the value is not None before updating the state. + if native_value is not None: + self._attr_native_value = native_value + self._attr_extra_state_attributes = self._extract_attributes( + self.coordinator.data + ) + self.async_write_ha_state() + + def _determine_native_value(self): + """Determine native value.""" if self.entity_description.parent_key is not None: return self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), @@ -664,15 +693,6 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): self.coordinator.data, self.entity_description.key ) - @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) - } - class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5c29253ae73..ab825e2485d 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from enum import Enum from functools import partial import logging @@ -474,14 +473,6 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): return False return super().available - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - async def async_turn_on(self, **kwargs) -> None: """Turn on an option of the miio device.""" method = getattr(self, self.entity_description.method_on) From a9c0f89c09975f8a8e1c6adf6884083b5e79c72f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 1 Nov 2021 17:45:13 +0100 Subject: [PATCH 0181/1452] Fix find_next_time_expression_time (#58894) * Better tests * Fix find_next_time_expression_time * Add tests for Nov 7th 2021, Chicago transtion * Update event tests * Update test_event.py * small performance improvement Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/util/dt.py | 67 ++++--- tests/helpers/test_event.py | 141 +++++++++++++-- tests/util/test_dt.py | 345 +++++++++++++++++++++++++++++++++--- 3 files changed, 489 insertions(+), 64 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index e2dd92a8b95..592b47ab6b1 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -272,7 +272,8 @@ def find_next_time_expression_time( return None return arr[left] - result = now.replace(microsecond=0) + # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + result = now.replace(microsecond=0, fold=0) # Match next second if (next_second := _lower_bound(seconds, result.second)) is None: @@ -309,40 +310,58 @@ def find_next_time_expression_time( result = result.replace(hour=next_hour) if result.tzinfo in (None, UTC): + # Using UTC, no DST checking needed return result - if _datetime_ambiguous(result): - # This happens when we're leaving daylight saving time and local - # clocks are rolled back. In this case, we want to trigger - # on both the DST and non-DST time. So when "now" is in the DST - # use the DST-on time, and if not, use the DST-off time. - fold = 1 if now.dst() else 0 - if result.fold != fold: - result = result.replace(fold=fold) - if not _datetime_exists(result): - # This happens when we're entering daylight saving time and local - # clocks are rolled forward, thus there are local times that do - # not exist. In this case, we want to trigger on the next time - # that *does* exist. - # In the worst case, this will run through all the seconds in the - # time shift, but that's max 3600 operations for once per year + # When entering DST and clocks are turned forward. + # There are wall clock times that don't "exist" (an hour is skipped). + + # -> trigger on the next time that 1. matches the pattern and 2. does exist + # for example: + # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour + # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) + # instead run at 02:30 the next day + + # We solve this edge case by just iterating one second until the result exists + # (max. 3600 operations, which should be fine for an edge case that happens once a year) return find_next_time_expression_time( result + dt.timedelta(seconds=1), seconds, minutes, hours ) - # Another edge-case when leaving DST: - # When now is in DST and ambiguous *and* the next trigger time we *should* - # trigger is ambiguous and outside DST, the excepts above won't catch it. - # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) - # we should trigger next on 28.10.2018 2:30 (out of DST), but our - # algorithm above would produce 29.10.2018 2:30 (out of DST) - if _datetime_ambiguous(now): + now_is_ambiguous = _datetime_ambiguous(now) + result_is_ambiguous = _datetime_ambiguous(result) + + # When leaving DST and clocks are turned backward. + # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST + # The logic above does not take into account if a given pattern matches _twice_ + # in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + + if now_is_ambiguous and result_is_ambiguous: + # `now` and `result` are both ambiguous, so the next match happens + # _within_ the current fold. + + # Examples: + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + return result.replace(fold=now.fold) + + if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches + # within the fold). + # -> Check if result matches in the next fold. If so, emit that match + + # Turn back the time by the DST offset, effectively run the algorithm on the first fold + # If it matches on the first fold, that means it will also match on the second one. + + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours ) if _datetime_ambiguous(check_result): - return check_result + return check_result.replace(fold=1) return result diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cf2e5ac13b8..f0b7a2c5d2d 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3399,9 +3399,19 @@ async def test_periodic_task_entering_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST starts early morning March 27th 2022 + yy = 2022 + mm = 3 + dd = 27 + + # There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30 time_that_will_not_match_right_away = datetime( - now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone + yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we enter DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() ) with patch( @@ -3416,25 +3426,25 @@ async def test_periodic_task_entering_dst(hass): ) async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -3448,10 +3458,19 @@ async def test_periodic_task_leaving_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 time_that_will_not_match_right_away = datetime( - now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1 + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() ) with patch( @@ -3465,38 +3484,134 @@ async def test_periodic_task_leaving_dst(hass): second=0, ) + # The task should not fire yet async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 0 + # The task should fire async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 1 + # The task should not fire again + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should fire again the next day async_fire_time_changed( - hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + unsub() + + +async def test_periodic_task_leaving_dst_2(hass): + """Test periodic task behavior when leaving dst.""" + timezone = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(timezone) + specific_runs = [] + + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 + + time_that_will_not_match_right_away = datetime( + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() + ) + + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + minute=30, + second=0, + ) + + # The task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1) ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + # The task should fire again the next hour + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + unsub() diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 628cb533681..63513c90360 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -224,120 +224,411 @@ def test_find_next_time_expression_time_dst(): tz = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) +# DST begins on 2021.03.28 2:00, clocks were turned forward 1h; 2:00-3:00 time does not exist +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 3, 28, 0, 0, 0), + datetime(2021, 3, 29, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_entering_dst(now_dt, expected_dt): + """Test entering daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + +# DST ends on 2021.10.31 2:00, clocks were turned backward 1h; 2:00-3:00 time is ambiguous +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 10, 31, 0, 0, 0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:00(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:15(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:00(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:01(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 1, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:45(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 45, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:15(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:01(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 30, 1, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 02:45(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 45, 0, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 08:00(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 8, 0, 1), + datetime(2021, 11, 1, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_exiting_dst(now_dt, expected_dt): + """Test exiting daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + def test_find_next_time_expression_time_dst_chicago(): """Test daylight saving time for find_next_time_expression_time.""" tz = dt_util.get_time_zone("America/Chicago") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) + + +def _get_matches(hours, minutes, seconds): + matching_hours = dt_util.parse_time_expression(hours, 0, 23) + matching_minutes = dt_util.parse_time_expression(minutes, 0, 59) + matching_seconds = dt_util.parse_time_expression(seconds, 0, 59) + return matching_hours, matching_minutes, matching_seconds + + +def test_find_next_time_expression_day_before_dst_change_the_same_time(): + """Test the day before DST to establish behavior without DST.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Not in DST yet + hour_minute_second = (12, 30, 1) + test_time = datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 10, 7, 17, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_30_s(): + """Test leaving daylight saving time for find_next_time_expression_time 30s into the future.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move ahead 30 seconds not folded yet + hour_minute_second = (1, 30, 31) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, 1, 30, 31, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 31, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time with the same time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move to the same time not folded yet + hour_minute_second = (0, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 5, 30, 1, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find the same time inside the fold + hour_minute_second = (1, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_ahead_1_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 01:20:01 fold=1 + hour_minute_second = (1, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 20, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_inside_the_fold_ahead_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 10m later while we are in the fold + # Start at 01:30:01 fold=0 + # Reach to 01:40:01 fold=1 + hour_minute_second = (1, 40, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=1) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 40, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_past_the_fold_ahead_2_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 02:20:01 past the fold + hour_minute_second = (2, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 # Time is no longer ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 8, 20, 1, tzinfo=dt_util.UTC + ) From 34e5596375078e51b7bd842b12440aa4f4bc1499 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Nov 2021 18:49:54 +0100 Subject: [PATCH 0182/1452] Use async_track_utc_time_change to schedule short term statistics (#58903) --- homeassistant/components/recorder/__init__.py | 3 ++- homeassistant/helpers/event.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 465209c7ed7..21acc183e50 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -41,6 +41,7 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, + async_track_utc_time_change, ) from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -622,7 +623,7 @@ class Recorder(threading.Thread): ) # Compile short term statistics every 5 minutes - async_track_time_change( + async_track_utc_time_change( self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 64cf3bf20bb..b157840db0d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1483,7 +1483,7 @@ def async_track_time_change( minute: Any | None = None, second: Any | None = None, ) -> CALLBACK_TYPE: - """Add a listener that will fire if UTC time matches a pattern.""" + """Add a listener that will fire if local time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, local=True) From 388cdf4e94efd1bb72714b5d536d90d249ce6316 Mon Sep 17 00:00:00 2001 From: emufan Date: Mon, 1 Nov 2021 19:13:30 +0100 Subject: [PATCH 0183/1452] Add classes for new Homematic devicetypes/devices (HmIP-SMI, HmIP-DRSI1) (#57521) * Update const.py Add new classes according to pyhomatic 0.1.75 * Update binary_sensor.py --- homeassistant/components/homematic/binary_sensor.py | 2 ++ homeassistant/components/homematic/const.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index c57e4cd15c7..b25fb2949aa 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -26,7 +26,9 @@ SENSOR_TYPES_CLASS = { "TiltSensor": None, "WeatherSensor": None, "IPContact": DEVICE_CLASS_OPENING, + "MotionIP": DEVICE_CLASS_MOTION, "MotionIPV2": DEVICE_CLASS_MOTION, + "MotionIPContactSabotage": DEVICE_CLASS_MOTION, "IPRemoteMotionV2": DEVICE_CLASS_MOTION, } diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 8aaa3ea21ac..78e00985c9f 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -47,6 +47,7 @@ HM_DEVICE_TYPES = { "IOSwitch", "IOSwitchNoInhibit", "IPSwitch", + "IPSwitchRssiDevice", "RFSiren", "IPSwitchPowermeter", "HMWIOSwitch", @@ -80,6 +81,8 @@ HM_DEVICE_TYPES = { "SwitchPowermeter", "Motion", "MotionV2", + "MotionIPV2", + "MotionIPContactSabotage", "RemoteMotion", "MotionIP", "ThermostatWall", @@ -114,7 +117,6 @@ HM_DEVICE_TYPES = { "IPBrightnessSensor", "IPGarage", "UniversalSensor", - "MotionIPV2", "IPMultiIO", "IPThermostatWall2", "IPRemoteMotionV2", @@ -151,6 +153,7 @@ HM_DEVICE_TYPES = { "Motion", "MotionV2", "MotionIP", + "MotionIPV2", "MotionIPContactSabotage", "RemoteMotion", "WeatherSensor", @@ -165,7 +168,6 @@ HM_DEVICE_TYPES = { "IPPassageSensor", "SmartwareMotion", "IPWeatherSensorPlus", - "MotionIPV2", "WaterIP", "IPMultiIO", "TiltIP", From 63c9cfdbc8f5335a133abe66d1ca1b48ff4be6da Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Mon, 1 Nov 2021 19:37:03 +0100 Subject: [PATCH 0184/1452] Add type annotations for MET (#58804) * Add Typing * Add missing types * define w/o Null * specify # type: ignore --- homeassistant/components/met/__init__.py | 36 ++++++----- homeassistant/components/met/weather.py | 80 ++++++++++++++++-------- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index dd932a75957..4010b0e0b79 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,10 +1,15 @@ """The met component.""" +from __future__ import annotations + from datetime import timedelta import logging from random import randrange +from types import MappingProxyType +from typing import Any, Callable import metno +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, @@ -13,6 +18,7 @@ from homeassistant.const import ( LENGTH_FEET, LENGTH_METERS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.distance import convert as convert_distance @@ -32,7 +38,7 @@ PLATFORMS = ["weather"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Met as config entry.""" # Don't setup if tracking home location and latitude or longitude isn't set. # Also, filters out our onboarding default location. @@ -62,7 +68,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS @@ -77,9 +83,9 @@ async def async_unload_entry(hass, config_entry): class MetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Met data.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize global Met data updater.""" - self._unsub_track_home = None + self._unsub_track_home: Callable | None = None self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) @@ -89,19 +95,19 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self): + async def _async_update_data(self) -> MetWeatherData: """Fetch data from Met.""" try: return await self.weather.fetch_data() except Exception as err: raise UpdateFailed(f"Update failed: {err}") from err - def track_home(self): + def track_home(self) -> None: """Start tracking changes to HA home setting.""" if self._unsub_track_home: return - async def _async_update_weather_data(_event=None): + async def _async_update_weather_data(_event: str | None = None) -> None: """Update weather data.""" if self.weather.set_coordinates(): await self.async_refresh() @@ -110,7 +116,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data ) - def untrack_home(self): + def untrack_home(self) -> None: """Stop tracking changes to HA home setting.""" if self._unsub_track_home: self._unsub_track_home() @@ -120,18 +126,20 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): class MetWeatherData: """Keep data for Met.no weather entities.""" - def __init__(self, hass, config, is_metric): + def __init__( + self, hass: HomeAssistant, config: MappingProxyType[str, Any], is_metric: bool + ) -> None: """Initialise the weather entity data.""" self.hass = hass self._config = config self._is_metric = is_metric - self._weather_data = None - self.current_weather_data = {} + self._weather_data: metno.MetWeatherData + self.current_weather_data: dict = {} self.daily_forecast = None self.hourly_forecast = None - self._coordinates = None + self._coordinates: dict[str, str] | None = None - def set_coordinates(self): + def set_coordinates(self) -> bool: """Weather data inialization - set the coordinates.""" if self._config.get(CONF_TRACK_HOME, False): latitude = self.hass.config.latitude @@ -161,7 +169,7 @@ class MetWeatherData: ) return True - async def fetch_data(self): + async def fetch_data(self) -> MetWeatherData: """Fetch data from API - (current weather and forecast).""" await self._weather_data.fetching_data() self.current_weather_data = self._weather_data.get_current_weather() diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 9cec6f93279..d938ede5cb2 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -1,5 +1,9 @@ """Support for Met.no weather service.""" +from __future__ import annotations + import logging +from types import MappingProxyType +from typing import Any import voluptuous as vol @@ -13,9 +17,10 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, PLATFORM_SCHEMA, + Forecast, WeatherEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, @@ -29,9 +34,16 @@ from homeassistant.const import ( PRESSURE_INHG, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + T, +) from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -67,7 +79,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Met.no weather platform.""" _LOGGER.warning("Loading Met.no via platform config is deprecated") @@ -84,7 +101,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( @@ -110,7 +131,13 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met.no weather condition.""" - def __init__(self, coordinator, config, is_metric, hourly): + def __init__( + self, + coordinator: DataUpdateCoordinator[T], + config: MappingProxyType[str, Any], + is_metric: bool, + hourly: bool, + ) -> None: """Initialise the platform with a data instance and site.""" super().__init__(coordinator) self._config = config @@ -118,12 +145,12 @@ class MetWeather(CoordinatorEntity, WeatherEntity): self._hourly = hourly @property - def track_home(self): + def track_home(self) -> (Any | bool): """Return if we are tracking home.""" return self._config.get(CONF_TRACK_HOME, False) @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID.""" name_appendix = "" if self._hourly: @@ -134,7 +161,7 @@ class MetWeather(CoordinatorEntity, WeatherEntity): return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" name = self._config.get(CONF_NAME) name_appendix = "" @@ -155,25 +182,25 @@ class MetWeather(CoordinatorEntity, WeatherEntity): return not self._hourly @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" condition = self.coordinator.data.current_weather_data.get("condition") return format_condition(condition) @property - def temperature(self): + def temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_TEMPERATURE] ) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def pressure(self): + def pressure(self) -> float | None: """Return the pressure.""" pressure_hpa = self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_PRESSURE] @@ -184,14 +211,14 @@ class MetWeather(CoordinatorEntity, WeatherEntity): return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_HUMIDITY] ) @property - def wind_speed(self): + def wind_speed(self) -> float | None: """Return the wind speed.""" speed_km_h = self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_SPEED] @@ -203,26 +230,26 @@ class MetWeather(CoordinatorEntity, WeatherEntity): return int(round(speed_mi_h)) @property - def wind_bearing(self): + def wind_bearing(self) -> float | str | None: """Return the wind direction.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_BEARING] ) @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" if self._hourly: met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} - ha_forecast = [] + ha_forecast: list[Forecast] = [] for met_item in met_forecast: if not set(met_item).issuperset(required_keys): continue @@ -232,26 +259,27 @@ class MetWeather(CoordinatorEntity, WeatherEntity): if met_item.get(v) is not None } if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) + if ha_item[ATTR_FORECAST_PRECIPITATION] is not None: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] ) - ha_forecast.append(ha_item) + ha_forecast.append(ha_item) # type: ignore[arg-type] return ha_forecast @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info.""" return DeviceInfo( default_name="Forecast", entry_type="service", - identifiers={(DOMAIN,)}, + identifiers={(DOMAIN,)}, # type: ignore[arg-type] manufacturer="Met.no", model="Forecast", ) From 1cd9be7538ebd5c979f5c62523c3476af6866b15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Nov 2021 18:11:17 -0500 Subject: [PATCH 0185/1452] Fix recursive limit in find_next_time_expression_time (#58914) * Fix recursive limit in find_next_time_expression_time * Add test case * Update test_event.py Co-authored-by: Erik Montnemery --- homeassistant/util/dt.py | 179 ++++++++++++++++++------------------ tests/helpers/test_event.py | 66 +++++++++++++ 2 files changed, 156 insertions(+), 89 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 592b47ab6b1..39f8a63e53f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -245,6 +245,16 @@ def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] +def _lower_bound(arr: list[int], cmp: int) -> int | None: + """Return the first value in arr greater or equal to cmp. + + Return None if no such value exists. + """ + if (left := bisect.bisect_left(arr, cmp)) == len(arr): + return None + return arr[left] + + def find_next_time_expression_time( now: dt.datetime, # pylint: disable=redefined-outer-name seconds: list[int], @@ -263,108 +273,99 @@ def find_next_time_expression_time( if not seconds or not minutes or not hours: raise ValueError("Cannot find a next time: Time expression never matches!") - def _lower_bound(arr: list[int], cmp: int) -> int | None: - """Return the first value in arr greater or equal to cmp. + while True: + # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + result = now.replace(microsecond=0, fold=0) - Return None if no such value exists. - """ - if (left := bisect.bisect_left(arr, cmp)) == len(arr): - return None - return arr[left] + # Match next second + if (next_second := _lower_bound(seconds, result.second)) is None: + # No second to match in this minute. Roll-over to next minute. + next_second = seconds[0] + result += dt.timedelta(minutes=1) - # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later - result = now.replace(microsecond=0, fold=0) + result = result.replace(second=next_second) - # Match next second - if (next_second := _lower_bound(seconds, result.second)) is None: - # No second to match in this minute. Roll-over to next minute. - next_second = seconds[0] - result += dt.timedelta(minutes=1) + # Match next minute + next_minute = _lower_bound(minutes, result.minute) + if next_minute != result.minute: + # We're in the next minute. Seconds needs to be reset. + result = result.replace(second=seconds[0]) - result = result.replace(second=next_second) + if next_minute is None: + # No minute to match in this hour. Roll-over to next hour. + next_minute = minutes[0] + result += dt.timedelta(hours=1) - # Match next minute - next_minute = _lower_bound(minutes, result.minute) - if next_minute != result.minute: - # We're in the next minute. Seconds needs to be reset. - result = result.replace(second=seconds[0]) + result = result.replace(minute=next_minute) - if next_minute is None: - # No minute to match in this hour. Roll-over to next hour. - next_minute = minutes[0] - result += dt.timedelta(hours=1) + # Match next hour + next_hour = _lower_bound(hours, result.hour) + if next_hour != result.hour: + # We're in the next hour. Seconds+minutes needs to be reset. + result = result.replace(second=seconds[0], minute=minutes[0]) - result = result.replace(minute=next_minute) + if next_hour is None: + # No minute to match in this day. Roll-over to next day. + next_hour = hours[0] + result += dt.timedelta(days=1) - # Match next hour - next_hour = _lower_bound(hours, result.hour) - if next_hour != result.hour: - # We're in the next hour. Seconds+minutes needs to be reset. - result = result.replace(second=seconds[0], minute=minutes[0]) + result = result.replace(hour=next_hour) - if next_hour is None: - # No minute to match in this day. Roll-over to next day. - next_hour = hours[0] - result += dt.timedelta(days=1) + if result.tzinfo in (None, UTC): + # Using UTC, no DST checking needed + return result - result = result.replace(hour=next_hour) + if not _datetime_exists(result): + # When entering DST and clocks are turned forward. + # There are wall clock times that don't "exist" (an hour is skipped). + + # -> trigger on the next time that 1. matches the pattern and 2. does exist + # for example: + # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour + # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) + # instead run at 02:30 the next day + + # We solve this edge case by just iterating one second until the result exists + # (max. 3600 operations, which should be fine for an edge case that happens once a year) + now += dt.timedelta(seconds=1) + continue + + now_is_ambiguous = _datetime_ambiguous(now) + result_is_ambiguous = _datetime_ambiguous(result) + + # When leaving DST and clocks are turned backward. + # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST + # The logic above does not take into account if a given pattern matches _twice_ + # in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + + if now_is_ambiguous and result_is_ambiguous: + # `now` and `result` are both ambiguous, so the next match happens + # _within_ the current fold. + + # Examples: + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + return result.replace(fold=now.fold) + + if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches + # within the fold). + # -> Check if result matches in the next fold. If so, emit that match + + # Turn back the time by the DST offset, effectively run the algorithm on the first fold + # If it matches on the first fold, that means it will also match on the second one. + + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + + check_result = find_next_time_expression_time( + now + _dst_offset_diff(now), seconds, minutes, hours + ) + if _datetime_ambiguous(check_result): + return check_result.replace(fold=1) - if result.tzinfo in (None, UTC): - # Using UTC, no DST checking needed return result - if not _datetime_exists(result): - # When entering DST and clocks are turned forward. - # There are wall clock times that don't "exist" (an hour is skipped). - - # -> trigger on the next time that 1. matches the pattern and 2. does exist - # for example: - # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour - # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) - # instead run at 02:30 the next day - - # We solve this edge case by just iterating one second until the result exists - # (max. 3600 operations, which should be fine for an edge case that happens once a year) - return find_next_time_expression_time( - result + dt.timedelta(seconds=1), seconds, minutes, hours - ) - - now_is_ambiguous = _datetime_ambiguous(now) - result_is_ambiguous = _datetime_ambiguous(result) - - # When leaving DST and clocks are turned backward. - # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST - # The logic above does not take into account if a given pattern matches _twice_ - # in a day. - # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour - - if now_is_ambiguous and result_is_ambiguous: - # `now` and `result` are both ambiguous, so the next match happens - # _within_ the current fold. - - # Examples: - # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 - # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 - return result.replace(fold=now.fold) - - if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: - # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches - # within the fold). - # -> Check if result matches in the next fold. If so, emit that match - - # Turn back the time by the DST offset, effectively run the algorithm on the first fold - # If it matches on the first fold, that means it will also match on the second one. - - # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 - - check_result = find_next_time_expression_time( - now + _dst_offset_diff(now), seconds, minutes, hours - ) - if _datetime_ambiguous(check_result): - return check_result.replace(fold=1) - - return result - def _datetime_exists(dattim: dt.datetime) -> bool: """Check if a datetime exists.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index f0b7a2c5d2d..9d48b0c0ada 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3452,6 +3452,72 @@ async def test_periodic_task_entering_dst(hass): unsub() +async def test_periodic_task_entering_dst_2(hass): + """Test periodic task behavior when entering dst. + + This tests a task firing every second in the range 0..58 (not *:*:59) + """ + timezone = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(timezone) + specific_runs = [] + + # DST starts early morning March 27th 2022 + yy = 2022 + mm = 3 + dd = 27 + + # There's no 2022-03-27 02:00:00, the event should not fire until 2022-03-28 03:00:00 + time_that_will_not_match_right_away = datetime( + yy, mm, dd, 1, 59, 59, tzinfo=timezone, fold=0 + ) + # Make sure we enter DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() + ) + + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + second=list(range(59)), + ) + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 1, 59, 59, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 0, 0, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 0, 1, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + async_fire_time_changed( + hass, datetime(yy, mm, dd + 1, 1, 59, 59, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + async_fire_time_changed( + hass, datetime(yy, mm, dd + 1, 2, 0, 0, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 4 + + unsub() + + async def test_periodic_task_leaving_dst(hass): """Test periodic task behavior when leaving dst.""" timezone = dt_util.get_time_zone("Europe/Vienna") From 0a94badb729990b8def573c765722edd734729a7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 2 Nov 2021 00:12:41 +0000 Subject: [PATCH 0186/1452] [ci skip] Translation update --- .../components/august/translations/ja.json | 9 ++++++ .../aurora_abb_powerone/translations/ja.json | 13 +++++++++ .../components/auth/translations/ja.json | 6 ++++ .../binary_sensor/translations/ja.json | 22 +++++++++++++-- .../components/blink/translations/ja.json | 12 ++++++++ .../devolo_home_network/translations/ja.json | 14 ++++++++++ .../devolo_home_network/translations/no.json | 25 +++++++++++++++++ .../components/dlna_dmr/translations/ja.json | 10 +++++++ .../environment_canada/translations/ja.json | 4 ++- .../components/epson/translations/ja.json | 7 +++++ .../forecast_solar/translations/ja.json | 11 ++++++++ .../components/hangouts/translations/ja.json | 2 ++ .../components/hive/translations/ja.json | 12 ++++++++ .../components/lookin/translations/ja.json | 10 +++++++ .../motion_blinds/translations/ja.json | 24 ++++++++++++++++ .../components/motioneye/translations/ja.json | 11 ++++++++ .../components/motioneye/translations/nl.json | 1 + .../components/motioneye/translations/no.json | 1 + .../components/mqtt/translations/ja.json | 13 ++++++++- .../components/netatmo/translations/ja.json | 9 ++++++ .../components/octoprint/translations/ja.json | 13 +++++++++ .../components/ridwell/translations/ja.json | 12 ++++++++ .../components/ridwell/translations/nl.json | 28 +++++++++++++++++++ .../components/ridwell/translations/no.json | 28 +++++++++++++++++++ .../components/ring/translations/ja.json | 12 ++++++++ .../components/sense/translations/ja.json | 11 ++++++++ .../components/sense/translations/no.json | 3 +- .../components/sensor/translations/ja.json | 8 ++++++ .../components/starline/translations/ja.json | 9 ++++++ .../tuya/translations/sensor.ja.json | 11 ++++++++ .../components/vizio/translations/ja.json | 7 +++++ .../vlc_telnet/translations/ja.json | 6 +++- .../components/watttime/translations/ja.json | 9 ++++++ 33 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/august/translations/ja.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/ja.json create mode 100644 homeassistant/components/blink/translations/ja.json create mode 100644 homeassistant/components/devolo_home_network/translations/ja.json create mode 100644 homeassistant/components/devolo_home_network/translations/no.json create mode 100644 homeassistant/components/epson/translations/ja.json create mode 100644 homeassistant/components/forecast_solar/translations/ja.json create mode 100644 homeassistant/components/hive/translations/ja.json create mode 100644 homeassistant/components/lookin/translations/ja.json create mode 100644 homeassistant/components/motion_blinds/translations/ja.json create mode 100644 homeassistant/components/motioneye/translations/ja.json create mode 100644 homeassistant/components/netatmo/translations/ja.json create mode 100644 homeassistant/components/octoprint/translations/ja.json create mode 100644 homeassistant/components/ridwell/translations/ja.json create mode 100644 homeassistant/components/ridwell/translations/nl.json create mode 100644 homeassistant/components/ridwell/translations/no.json create mode 100644 homeassistant/components/ring/translations/ja.json create mode 100644 homeassistant/components/sense/translations/ja.json create mode 100644 homeassistant/components/starline/translations/ja.json create mode 100644 homeassistant/components/tuya/translations/sensor.ja.json create mode 100644 homeassistant/components/vizio/translations/ja.json create mode 100644 homeassistant/components/watttime/translations/ja.json diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json new file mode 100644 index 00000000000..e410a949d7b --- /dev/null +++ b/homeassistant/components/august/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "validation": { + "title": "2\u8981\u7d20\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/ja.json b/homeassistant/components/aurora_abb_powerone/translations/ja.json new file mode 100644 index 00000000000..6df075f97e2 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u30a2\u30c9\u30ec\u30b9", + "port": "RS485\u3001\u307e\u305f\u306f USB-RS485 \u30a2\u30c0\u30d7\u30bf\u30fc \u30dd\u30fc\u30c8" + }, + "description": "\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306fRS485\u30a2\u30c0\u30d7\u30bf\u30fc\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002LCD\u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3057\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3068\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/ja.json b/homeassistant/components/auth/translations/ja.json index beffdfe1f61..ffeda27c9a5 100644 --- a/homeassistant/components/auth/translations/ja.json +++ b/homeassistant/components/auth/translations/ja.json @@ -20,6 +20,12 @@ "title": "\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u901a\u77e5" }, "totp": { + "step": { + "init": { + "description": "\u30bf\u30a4\u30e0\u30d9\u30fc\u30b9\u306e\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u4f7f\u7528\u3057\u30662\u8981\u7d20\u8a8d\u8a3c\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u3001\u8a8d\u8a3c\u30a2\u30d7\u30ea\u3067QR\u30b3\u30fc\u30c9\u3092\u30b9\u30ad\u30e3\u30f3\u3057\u307e\u3059\u3002\u8a8d\u8a3c\u30a2\u30d7\u30ea\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001[Google \u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0](https://support.google.com/accounts/answer/1066447)\u307e\u305f\u306f\u3001[Authy](https://authy.com/)\u306e\u3069\u3061\u3089\u304b\u3092\u63a8\u5968\u3057\u307e\u3059\u3002\n\n{qr_code}\n\n\u30b3\u30fc\u30c9\u3092\u30b9\u30ad\u30e3\u30f3\u3057\u305f\u5f8c\u3001\u30a2\u30d7\u30ea\u304b\u30896\u6841\u306e\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002QR\u30b3\u30fc\u30c9\u306e\u30b9\u30ad\u30e3\u30f3\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30b3\u30fc\u30c9 **`{code}`** \u3092\u4f7f\u7528\u3057\u3066\u624b\u52d5\u3067\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "TOTP\u3092\u4f7f\u7528\u3057\u30662\u8981\u7d20\u8a8d\u8a3c\u3092\u8a2d\u5b9a\u3059\u308b" + } + }, "title": "TOTP" } } diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 8c3cffa3166..9a58a1b831f 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -20,7 +20,7 @@ "is_not_occupied": "{entity_name} \u306f\u5360\u6709\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "is_not_open": "{entity_name} \u306f\u9589\u3058\u3066\u3044\u307e\u3059", "is_not_plugged_in": "{entity_name} \u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u3066\u3044\u307e\u3059", - "is_not_powered": "{entity_name} \u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "is_not_powered": "{entity_name} \u306f\u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "is_not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", "is_not_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u3059", "is_occupied": "{entity_name} \u306f\u5360\u6709\u3055\u308c\u3066\u3044\u307e\u3059", @@ -50,15 +50,17 @@ "not_moving": "{entity_name} \u304c\u52d5\u304d\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "not_occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f", "not_plugged_in": "{entity_name} \u306e\u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u307e\u3057\u305f", - "not_powered": "{entity_name} \u306e\u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "not_powered": "{entity_name} \u306f\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u305b\u3093", "not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", + "not_running": "{entity_name} \u306f\u3082\u3046\u5b9f\u884c\u3055\u308c\u3066\u3044\u306a\u3044", "not_unsafe": "{entity_name} \u304c\u5b89\u5168\u306b\u306a\u308a\u307e\u3057\u305f", "occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u307e\u3057\u305f", "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", "plugged_in": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", - "powered": "{entity_name} \uff5e\u3067\u52d5\u304f", + "powered": "{entity_name} \u96fb\u6e90", "present": "{entity_name} \u304c\u5b58\u5728", "problem": "{entity_name} \u304c\u554f\u984c\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "running": "{entity_name} \u306e\u5b9f\u884c\u3092\u958b\u59cb", "smoke": "{entity_name} \u304c\u3001\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "sound": "{entity_name} \u304c\u3001\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", @@ -68,6 +70,16 @@ } }, "device_class": { + "cold": "\u51b7\u305f\u3044", + "gas": "\u30ac\u30b9", + "heat": "\u71b1", + "moisture": "\u6e7f\u6c17", + "motion": "\u30e2\u30fc\u30b7\u30e7\u30f3", + "occupancy": "\u5360\u6709", + "power": "\u30d1\u30ef\u30fc", + "problem": "\u554f\u984c", + "smoke": "\u7159", + "sound": "\u97f3", "vibration": "\u632f\u52d5" }, "state": { @@ -130,6 +142,10 @@ "problem": { "off": "OK" }, + "running": { + "off": "\u30e9\u30f3\u30cb\u30f3\u30b0\u3067\u306f\u306a\u3044", + "on": "\u30e9\u30f3\u30cb\u30f3\u30b0" + }, "safety": { "off": "\u5b89\u5168", "on": "\u5371\u967a" diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json new file mode 100644 index 00000000000..dc544bbcb75 --- /dev/null +++ b/homeassistant/components/blink/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" + }, + "title": "2\u8981\u7d20\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json new file mode 100644 index 00000000000..c3eb278d18f --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" + }, + "flow_title": "{product} ({name})", + "step": { + "zeroconf_confirm": { + "description": "\u30db\u30b9\u30c8\u540d\u304c `{host_name}` \u306e devolo\u793e\u306e\u30db\u30fc\u30e0\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "devolo\u793e\u306e\u30db\u30fc\u30e0\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u6a5f\u5668\u3092\u767a\u898b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/no.json b/homeassistant/components/devolo_home_network/translations/no.json new file mode 100644 index 00000000000..405434abc4a --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "home_control": "Devolo Home Control Central Unit fungerer ikke med denne integrasjonen." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{product} ( {name} )", + "step": { + "user": { + "data": { + "ip_address": "IP adresse" + }, + "description": "Vil du starte oppsettet?" + }, + "zeroconf_confirm": { + "description": "Vil du legge til devolo hjemmenettverksenheten med vertsnavnet ` {host_name} ` til Home Assistant?", + "title": "Oppdaget devolo hjemmenettverksenhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 1a0c1d7fdf4..681117d07d9 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{name}", "step": { + "import_turn_on": { + "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u3092\u5165\u308c\u3001\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u79fb\u884c\u3092\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044" + }, + "manual": { + "description": "\u30c7\u30d0\u30a4\u30b9\u8a18\u8ff0XML\u30d5\u30a1\u30a4\u30eb\u3078\u306eURL", + "title": "\u624b\u52d5\u3067DLNA DMR\u6a5f\u5668\u306b\u63a5\u7d9a" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json index 0d27b8acbe5..4268a928cbb 100644 --- a/homeassistant/components/environment_canada/translations/ja.json +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -7,7 +7,9 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "station": "\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID" - } + }, + "description": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID\u307e\u305f\u306f\u7def\u5ea6/\u7d4c\u5ea6\u306e\u3044\u305a\u308c\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f7f\u7528\u3055\u308c\u308b\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u7def\u5ea6/\u7d4c\u5ea6\u306f\u3001Home Assistant\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u69cb\u6210\u3055\u308c\u305f\u5024\u3067\u3059\u3002\u5ea7\u6a19\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u306f\u3001\u5ea7\u6a19\u306b\u6700\u3082\u8fd1\u3044\u6c17\u8c61\u89b3\u6e2c\u6240\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u89b3\u6e2c\u6240\u30b3\u30fc\u30c9\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u3001PP/code\u306e\u5f62\u5f0f\u306b\u5f93\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u3053\u3053\u3067\u3001PP\u306f2\u6587\u5b57\u306e\u5dde\u3001code\u306f\u89b3\u6e2c\u6240ID\u3067\u3059\u3002\u89b3\u6e2c\u6240ID\u306e\u30ea\u30b9\u30c8\u306f\u3001https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv \u306b\u3042\u308a\u307e\u3059\u3002\u6c17\u8c61\u60c5\u5831\u306f\u82f1\u8a9e\u307e\u305f\u306f\u30d5\u30e9\u30f3\u30b9\u8a9e\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002", + "title": "\u30ab\u30ca\u30c0\u74b0\u5883\u7701: \u5929\u6c17\u306e\u5834\u6240\u3068\u8a00\u8a9e" } } } diff --git a/homeassistant/components/epson/translations/ja.json b/homeassistant/components/epson/translations/ja.json new file mode 100644 index 00000000000..f11e8609e01 --- /dev/null +++ b/homeassistant/components/epson/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "powered_off": "\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u306f\u5165\u3063\u3066\u3044\u307e\u3059\u304b\uff1f\u521d\u671f\u8a2d\u5b9a\u3092\u884c\u3046\u305f\u3081\u306b\u306f\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u304a\u304f\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json new file mode 100644 index 00000000000..b44ce74d2cb --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "damping": "\u6e1b\u8870\u4fc2\u6570(\u30c0\u30f3\u30d4\u30f3\u30b0\u30d5\u30a1\u30af\u30bf\u30fc): \u671d\u3068\u5915\u65b9\u306e\u7d50\u679c\u3092\u8abf\u6574\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json index 751e1ae41a1..a6ead972aee 100644 --- a/homeassistant/components/hangouts/translations/ja.json +++ b/homeassistant/components/hangouts/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "error": { + "invalid_2fa": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_2fa_method": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9(\u96fb\u8a71\u3067\u78ba\u8a8d)", "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "step": { diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json new file mode 100644 index 00000000000..91680d93274 --- /dev/null +++ b/homeassistant/components/hive/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" + }, + "title": "\u30cf\u30a4\u30d6(Hive)2\u8981\u7d20\u8a8d\u8a3c\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/ja.json b/homeassistant/components/lookin/translations/ja.json new file mode 100644 index 00000000000..8282b91ccd7 --- /dev/null +++ b/homeassistant/components/lookin/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "{name} ({host}) \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json new file mode 100644 index 00000000000..744606ae4e2 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" + }, + "step": { + "connect": { + "data": { + "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "\u66f4\u65b0\u6642\u306b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8 \u30d7\u30c3\u30b7\u30e5\u3092\u5f85\u6a5f\u3059\u308b" + }, + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json new file mode 100644 index 00000000000..5bded845e3f --- /dev/null +++ b/homeassistant/components/motioneye/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "stream_url_template": "\u30b9\u30c8\u30ea\u30fc\u30e0URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json index 81bb0365c33..dce66fcb5d1 100644 --- a/homeassistant/components/motioneye/translations/nl.json +++ b/homeassistant/components/motioneye/translations/nl.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Stream-URL template", "webhook_set": "MotionEye-webhooks configureren om gebeurtenissen aan Home Assistant te melden", "webhook_set_overwrite": "Overschrijf niet-herkende webhooks" } diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json index c0fd5e881c2..1d7f3bab29f 100644 --- a/homeassistant/components/motioneye/translations/no.json +++ b/homeassistant/components/motioneye/translations/no.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Stream URL-mal", "webhook_set": "Konfigurer motionEye webhooks for \u00e5 rapportere hendelser til Home Assistant", "webhook_set_overwrite": "Overskriv ukjente webhooks" } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 161bef1c2cd..cbe3e4d0fed 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -8,7 +8,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "hassio_confirm": { "data": { @@ -18,5 +19,15 @@ "title": "HomeAssistant\u30a2\u30c9\u30aa\u30f3\u3092\u4ecb\u3057\u305fMQTT Broker" } } + }, + "options": { + "step": { + "broker": { + "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "options": { + "description": "\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(Discovery(\u691c\u51fa)) - \u691c\u51fa\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408(\u63a8\u5968)\u3001Home Assistant\u306f\u3001MQTT broker\u306b\u8a2d\u5b9a\u3092\u516c\u958b\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3084\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3057\u307e\u3059\u3002\u691c\u51fa\u3092\u7121\u52b9\u306b\u3057\u305f\u5834\u5408\u306f\u3001\u3059\u3079\u3066\u306e\u8a2d\u5b9a\u3092\u624b\u52d5\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d0\u30fc\u30b9(Birth(\u8a95\u751f))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u8a95\u751f\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304cMQTT broker\u306b\u3001(\u518d)\u63a5\u7d9a\u3059\u308b\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\n\u30a6\u30a3\u30eb(Will(\u610f\u601d))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u30a6\u30a3\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304c\u30d6\u30ed\u30fc\u30ab\u30fc(broker)\u3078\u306e\u63a5\u7d9a\u3092\u5931\u3046\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f\u3001\u30af\u30ea\u30fc\u30f3\u306a\u63a5\u7d9a(Home Assistant\u306e\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3\u306a\u3069)\u306e\u5834\u5408\u3068\u3001\u30af\u30ea\u30fc\u30f3\u3067\u306f\u306a\u3044\u63a5\u7d9a(Home Assistant\u306e\u30af\u30e9\u30c3\u30b7\u30e5\u3084\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u5931\u3063\u305f\u5834\u5408)\u306e\u3069\u3061\u3089\u306e\u5834\u5408\u3067\u3042\u3063\u3066\u3082\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json new file mode 100644 index 00000000000..6fe56757002 --- /dev/null +++ b/homeassistant/components/netatmo/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json new file mode 100644 index 00000000000..17ec3918a5c --- /dev/null +++ b/homeassistant/components/octoprint/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "path": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30d1\u30b9", + "port": "\u30dd\u30fc\u30c8\u756a\u53f7", + "ssl": "SSL\u3092\u4f7f\u7528" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json new file mode 100644 index 00000000000..f802ddc0ab0 --- /dev/null +++ b/homeassistant/components/ridwell/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + }, + "user": { + "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/nl.json b/homeassistant/components/ridwell/translations/nl.json new file mode 100644 index 00000000000..afec8a578f5 --- /dev/null +++ b/homeassistant/components/ridwell/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord voor {username} opnieuw in:", + "title": "Verifieer de integratie opnieuw" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer uw gebruikersnaam en wachtwoord in:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/no.json b/homeassistant/components/ridwell/translations/no.json new file mode 100644 index 00000000000..bfc6dbd5bb3 --- /dev/null +++ b/homeassistant/components/ridwell/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Vennligst skriv inn passordet for {username} :", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Skriv inn brukernavn og passord:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/ja.json b/homeassistant/components/ring/translations/ja.json new file mode 100644 index 00000000000..dc544bbcb75 --- /dev/null +++ b/homeassistant/components/ring/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" + }, + "title": "2\u8981\u7d20\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json new file mode 100644 index 00000000000..45721ebbdc1 --- /dev/null +++ b/homeassistant/components/sense/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/no.json b/homeassistant/components/sense/translations/no.json index ec9502eed23..11f92bfccb4 100644 --- a/homeassistant/components/sense/translations/no.json +++ b/homeassistant/components/sense/translations/no.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-post", - "password": "Passord" + "password": "Passord", + "timeout": "Tidsavbrudd" }, "title": "Koble til din Sense Energy Monitor" } diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 0497959372c..9931bfd0a71 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,4 +1,12 @@ { + "device_automation": { + "condition_type": { + "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387" + }, + "trigger_type": { + "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json new file mode 100644 index 00000000000..c3e0d5ecbc6 --- /dev/null +++ b/homeassistant/components/starline/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth_mfa": { + "title": "2\u8981\u7d20\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.ja.json b/homeassistant/components/tuya/translations/sensor.ja.json new file mode 100644 index 00000000000..98142aa25cc --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "\u6cb8\u70b9", + "cooling": "\u51b7\u5374", + "heating": "\u6696\u623f", + "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4", + "warm": "\u4fdd\u6e29" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json new file mode 100644 index 00000000000..9a8572b2ee1 --- /dev/null +++ b/homeassistant/components/vizio/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/ja.json b/homeassistant/components/vlc_telnet/translations/ja.json index 4aa4b490324..a10999772e5 100644 --- a/homeassistant/components/vlc_telnet/translations/ja.json +++ b/homeassistant/components/vlc_telnet/translations/ja.json @@ -2,10 +2,14 @@ "config": { "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u306b\u63a5\u7d9a\u3057\u307e\u3059\u304b\uff1f" + }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "\u30db\u30b9\u30c8\u306e\u6b63\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {host}" }, "user": { "data": { diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json new file mode 100644 index 00000000000..0acaaea625d --- /dev/null +++ b/homeassistant/components/watttime/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + } + } + } +} \ No newline at end of file From 31153ac155b1ec37363f671d744c7857816faffd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Nov 2021 20:47:05 -0700 Subject: [PATCH 0187/1452] Move fixtures part 1 (#58902) --- tests/common.py | 19 +- tests/components/abode/conftest.py | 14 +- .../abode/fixtures/automation.json} | 0 .../abode/fixtures/automation_changed.json} | 0 .../abode/fixtures/devices.json} | 0 .../abode/fixtures/login.json} | 0 .../abode/fixtures/logout.json} | 0 .../abode/fixtures/oauth_claims.json} | 0 .../abode/fixtures/panel.json} | 0 .../fixtures}/current_conditions_data.json | 0 .../accuweather/fixtures}/forecast_data.json | 0 .../accuweather/fixtures}/location_data.json | 0 .../fixtures}/getSystemData.json | 0 .../advantage_air/fixtures}/setAircon.json | 0 .../aemet/fixtures}/station-3195-data.json | 0 .../aemet/fixtures}/station-3195.json | 0 .../aemet/fixtures}/station-list-data.json | 0 .../aemet/fixtures}/station-list.json | 0 .../town-28065-forecast-daily-data.json | 0 .../fixtures}/town-28065-forecast-daily.json | 0 .../town-28065-forecast-hourly-data.json | 0 .../fixtures}/town-28065-forecast-hourly.json | 0 .../aemet/fixtures}/town-id28065.json | 0 .../aemet/fixtures}/town-list.json | 0 .../agent_dvr/fixtures}/objects.json | 0 .../agent_dvr/fixtures}/status.json | 0 tests/components/airly/__init__.py | 2 +- .../airly/fixtures/no_station.json} | 0 .../airly/fixtures/valid_station.json} | 0 tests/components/airly/test_config_flow.py | 12 +- tests/components/airly/test_init.py | 10 +- tests/components/airly/test_sensor.py | 2 +- .../ambee/fixtures}/air_quality.json | 0 .../ambee/fixtures}/pollen.json | 0 .../get_activity.bridge_offline.json | 0 .../fixtures}/get_activity.bridge_online.json | 0 .../get_activity.doorbell_motion.json | 0 .../august/fixtures}/get_activity.jammed.json | 0 .../august/fixtures}/get_activity.lock.json | 0 .../get_activity.lock_from_autorelock.json | 0 .../get_activity.lock_from_bluetooth.json | 0 .../get_activity.lock_from_keypad.json | 0 .../fixtures}/get_activity.locking.json | 0 .../fixtures}/get_activity.unlocking.json | 0 .../august/fixtures}/get_doorbell.json | 0 .../fixtures}/get_doorbell.nobattery.json | 0 .../fixtures}/get_doorbell.offline.json | 0 .../fixtures}/get_lock.doorsense_init.json | 0 .../get_lock.low_keypad_battery.json | 0 .../august/fixtures}/get_lock.offline.json | 0 .../august/fixtures}/get_lock.online.json | 0 .../get_lock.online.unknown_state.json | 0 .../get_lock.online_missing_doorsense.json | 0 .../get_lock.online_with_doorsense.json | 0 .../august/fixtures}/get_locks.json | 0 .../august/fixtures}/lock_open.json | 0 .../august/fixtures}/unlock_closed.json | 0 .../awair/fixtures}/awair-offline.json | 0 .../awair/fixtures}/awair-r2.json | 0 .../awair/fixtures}/awair.json | 0 .../awair/fixtures}/devices.json | 0 .../awair/fixtures}/glow.json | 0 .../awair/fixtures}/mint.json | 0 .../awair/fixtures}/no_devices.json | 0 .../awair/fixtures}/omni.json | 0 .../awair/fixtures}/user.json | 0 .../bayesian/fixtures}/configuration.yaml | 0 .../components/bayesian/test_binary_sensor.py | 14 +- .../blueprint/fixtures}/community_post.json | 0 .../blueprint/fixtures}/github_gist.json | 0 tests/components/brother/__init__.py | 2 +- .../brother/fixtures/printer_data.json} | 0 tests/components/brother/test_config_flow.py | 12 +- tests/components/brother/test_sensor.py | 6 +- .../bsblan/fixtures}/info.json | 0 .../fixtures}/v3_forecast_daily.json | 0 .../fixtures}/v3_forecast_hourly.json | 0 .../fixtures}/v3_forecast_nowcast.json | 0 .../climacell/fixtures}/v3_realtime.json | 0 .../climacell/fixtures}/v4.json | 0 .../command_line/fixtures}/configuration.yaml | 0 tests/components/command_line/test_cover.py | 8 +- tests/components/dexcom/__init__.py | 2 +- .../dexcom/fixtures/data.json} | 0 .../directv/fixtures}/info-get-locations.json | 0 .../directv/fixtures}/info-get-version.json | 0 .../directv/fixtures}/info-mode-error.json | 0 .../directv/fixtures}/info-mode-standby.json | 0 .../directv/fixtures}/info-mode.json | 0 .../directv/fixtures}/remote-process-key.json | 0 .../directv/fixtures}/tv-get-tuned-movie.json | 0 .../directv/fixtures}/tv-get-tuned-music.json | 0 .../fixtures}/tv-get-tuned-restricted.json | 0 .../directv/fixtures}/tv-get-tuned.json | 0 .../directv/fixtures}/tv-tune.json | 0 .../ecobee/fixtures}/ecobee-data.json | 0 .../ecobee/fixtures}/ecobee-token.json | 0 .../efergy/fixtures}/budget.json | 0 .../fixtures}/current_values_multi.json | 0 .../fixtures}/current_values_single.json | 0 .../efergy/fixtures}/daily_cost.json | 0 .../efergy/fixtures}/daily_energy.json | 0 .../efergy/fixtures}/instant.json | 0 .../efergy/fixtures}/monthly_cost.json | 0 .../efergy/fixtures}/monthly_energy.json | 0 .../efergy/fixtures}/status.json | 0 .../efergy/fixtures}/weekly_cost.json | 0 .../efergy/fixtures}/weekly_energy.json | 0 .../efergy/fixtures}/yearly_cost.json | 0 .../efergy/fixtures}/yearly_energy.json | 0 .../elgato/fixtures}/info.json | 0 .../elgato/fixtures}/settings-color.json | 0 .../elgato/fixtures}/settings.json | 0 .../elgato/fixtures}/state-color.json | 0 .../elgato/fixtures}/state.json | 0 .../filesize/fixtures}/configuration.yaml | 0 tests/components/filesize/test_sensor.py | 12 +- .../filter/fixtures}/configuration.yaml | 0 tests/components/filter/test_sensor.py | 24 +- .../flo/fixtures}/device_info_response.json | 0 .../device_info_response_closed.json | 0 .../device_info_response_detector.json | 0 .../location_info_base_response.json | 0 ...location_info_expand_devices_response.json | 0 .../fixtures}/user_info_base_response.json | 0 .../user_info_expand_locations_response.json | 0 .../water_consumption_info_response.json | 0 .../foobot/fixtures/data.json} | 0 .../foobot/fixtures/devices.json} | 0 tests/components/foobot/test_sensor.py | 5 +- .../generic/fixtures}/configuration.yaml | 0 tests/components/generic/test_camera.py | 14 +- .../fixtures}/configuration.yaml | 0 .../generic_thermostat/test_climate.py | 12 +- .../gios/fixtures}/indexes.json | 0 .../gios/fixtures}/sensors.json | 0 .../gios/fixtures}/station.json | 0 .../google/fixtures/maps_elevation.json} | 0 .../group/fixtures}/configuration.yaml | 0 .../group/fixtures}/fan_configuration.yaml | 0 tests/components/group/test_binary_sensor.py | 6 - tests/components/group/test_fan.py | 13 +- tests/components/group/test_light.py | 25 +- tests/components/group/test_notify.py | 14 +- .../fixtures}/attribution_response.json | 0 .../fixtures}/bike_response.json | 0 .../fixtures}/car_enabled_response.json | 0 .../fixtures}/car_response.json | 0 .../fixtures}/car_shortest_response.json | 0 .../fixtures}/pedestrian_response.json | 0 .../fixtures}/public_response.json | 0 .../fixtures}/public_time_table_response.json | 0 .../routing_error_invalid_credentials.json | 0 .../routing_error_no_route_found.json | 0 .../fixtures}/truck_response.json | 0 .../fixtures}/configuration.yaml | 0 tests/components/history_stats/test_sensor.py | 12 +- .../homekit/fixtures}/configuration.yaml | 0 tests/components/homekit/test_homekit.py | 12 +- .../fixtures}/anker_eufycam.json | 0 .../fixtures}/aqara_gateway.json | 0 .../fixtures}/aqara_switch.json | 0 .../fixtures}/arlo_baby.json | 0 .../homekit_controller/fixtures}/ecobee3.json | 0 .../fixtures}/ecobee3_no_sensors.json | 0 .../fixtures}/ecobee_occupancy.json | 0 .../fixtures}/eve_degree.json | 0 .../homekit_controller/fixtures}/haa_fan.json | 0 .../fixtures}/home_assistant_bridge_fan.json | 0 .../fixtures}/hue_bridge.json | 0 .../fixtures}/koogeek_ls1.json | 0 .../fixtures}/koogeek_p1eu.json | 0 .../fixtures}/koogeek_sw2.json | 0 .../fixtures}/lennox_e30.json | 0 .../homekit_controller/fixtures}/lg_tv.json | 0 .../fixtures}/mysa_living.json | 0 .../fixtures}/netamo_doorbell.json | 0 .../fixtures}/rainmachine-pro-8.json | 0 .../fixtures}/ryse_smart_bridge.json | 0 .../ryse_smart_bridge_four_shades.json | 0 .../fixtures}/simpleconnect_fan.json | 0 .../fixtures}/velux_gateway.json | 0 .../fixtures}/vocolinc_flowerbud.json | 0 .../fixtures}/fwversion.json | 0 .../fixtures}/userdata.json | 0 .../fixtures}/userdata_v1.json | 0 .../hvv_departures/fixtures}/check_name.json | 0 .../fixtures}/config_entry.json | 0 .../fixtures}/departure_list.json | 0 .../hvv_departures/fixtures}/init.json | 0 .../hvv_departures/fixtures}/options.json | 0 .../fixtures}/station_information.json | 0 .../insteon/fixtures}/aldb_data.json | 0 .../insteon/fixtures}/kpl_properties.json | 0 tests/components/ipp/__init__.py | 14 +- .../get-printer-attributes-error-0x0503.bin | Bin .../get-printer-attributes-success-nodata.bin | Bin .../ipp/fixtures}/get-printer-attributes.bin | Bin .../lcn/fixtures}/config.json | 0 .../lcn/fixtures}/config_entry_myhome.json | 0 .../lcn/fixtures}/config_entry_pchk.json | 0 .../mazda/fixtures}/get_vehicle_status.json | 0 .../mazda/fixtures}/get_vehicles.json | 0 .../melissa/fixtures/cur_settings.json} | 0 .../melissa/fixtures/fetch_devices.json} | 0 .../melissa/fixtures/status.json} | 0 tests/components/melissa/test_climate.py | 6 +- .../min_max/fixtures}/configuration.yaml | 0 tests/components/min_max/test_sensor.py | 14 +- .../modern_forms/fixtures}/device_info.json | 0 .../fixtures}/device_info_no_light.json | 0 .../modern_forms/fixtures}/device_status.json | 0 .../fixtures}/device_status_no_light.json | 0 .../device_status_timers_active.json | 0 .../mqtt/fixtures}/configuration.yaml | 0 tests/components/mqtt/test_light.py | 18 +- .../myq/fixtures}/devices.json | 0 .../fixtures}/distance_sensor_state.json | 0 .../fixtures}/energy_sensor_state.json | 0 .../mysensors/fixtures}/gps_sensor_state.json | 0 .../fixtures}/power_sensor_state.json | 0 .../fixtures}/sound_sensor_state.json | 0 .../fixtures}/temperature_sensor_state.json | 0 .../netatmo/fixtures}/events.txt | 0 .../netatmo/fixtures}/gethomecoachsdata.json | 0 .../netatmo/fixtures}/gethomedata.json | 0 .../netatmo/fixtures}/getpublicdata.json | 0 .../netatmo/fixtures}/getstationsdata.json | 0 .../netatmo/fixtures}/homesdata.json | 0 .../netatmo/fixtures}/homestatus.json | 0 .../netatmo/fixtures}/ping.json | 0 .../nexia/fixtures}/mobile_houses_123456.json | 0 .../nexia/fixtures}/session_123456.json | 0 .../nexia/fixtures}/sign_in.json | 0 .../nut/fixtures}/5E650I.json | 0 .../nut/fixtures}/5E850I.json | 0 .../nut/fixtures}/BACKUPSES600M1.json | 0 .../nut/fixtures}/CP1350C.json | 0 .../nut/fixtures}/CP1500PFCLCD.json | 0 .../nut/fixtures}/DL650ELCD.json | 0 .../nut/fixtures}/PR3000RT2U.json | 0 .../nut/fixtures}/blazer_usb.json | 0 .../ozw/fixtures}/binary_sensor.json | 0 .../ozw/fixtures}/binary_sensor_alt.json | 0 .../ozw/fixtures}/climate.json | 0 .../ozw/fixtures}/climate_network_dump.csv | 0 .../ozw/fixtures}/cover.json | 0 .../ozw/fixtures}/cover_gdo.json | 0 .../ozw/fixtures}/cover_gdo_network_dump.csv | 0 .../ozw/fixtures}/cover_network_dump.csv | 0 .../ozw => components/ozw/fixtures}/fan.json | 0 .../ozw/fixtures}/fan_network_dump.csv | 0 .../ozw/fixtures}/generic_network_dump.csv | 0 .../ozw/fixtures}/light.json | 0 .../ozw/fixtures}/light_network_dump.csv | 0 .../fixtures}/light_new_ozw_network_dump.csv | 0 .../fixtures}/light_no_cw_network_dump.csv | 0 .../ozw/fixtures}/light_no_rgb.json | 0 .../fixtures}/light_no_ww_network_dump.csv | 0 .../ozw/fixtures}/light_pure_rgb.json | 0 .../ozw/fixtures}/light_rgb.json | 0 .../ozw/fixtures}/light_wc_network_dump.csv | 0 .../ozw => components/ozw/fixtures}/lock.json | 0 .../ozw/fixtures}/lock_network_dump.csv | 0 .../ozw/fixtures}/migration_fixture.csv | 0 .../ozw/fixtures}/sensor.json | 0 .../sensor_string_value_network_dump.csv | 0 .../ozw/fixtures}/switch.json | 0 .../p1_monitor/fixtures}/phases.json | 0 .../p1_monitor/fixtures}/settings.json | 0 .../p1_monitor/fixtures}/smartmeter.json | 0 .../ping/fixtures}/configuration.yaml | 0 tests/components/ping/test_binary_sensor.py | 13 +- .../plex/fixtures}/album.xml | 0 .../plex/fixtures}/artist_albums.xml | 0 .../plex/fixtures}/children_20.xml | 0 .../plex/fixtures}/children_200.xml | 0 .../plex/fixtures}/children_30.xml | 0 .../plex/fixtures}/children_300.xml | 0 .../plex/fixtures}/empty_library.xml | 0 .../plex/fixtures}/empty_payload.xml | 0 .../plex/fixtures}/grandchildren_300.xml | 0 .../plex/fixtures}/library.xml | 0 .../plex/fixtures}/library_movies_all.xml | 0 .../fixtures}/library_movies_collections.xml | 0 .../fixtures}/library_movies_filtertypes.xml | 0 .../fixtures}/library_movies_metadata.xml | 0 .../plex/fixtures}/library_movies_size.xml | 0 .../plex/fixtures}/library_movies_sort.xml | 0 .../plex/fixtures}/library_music_all.xml | 0 .../fixtures}/library_music_collections.xml | 0 .../plex/fixtures}/library_music_metadata.xml | 0 .../plex/fixtures}/library_music_size.xml | 0 .../plex/fixtures}/library_music_sort.xml | 0 .../plex/fixtures}/library_sections.xml | 0 .../plex/fixtures}/library_tvshows_all.xml | 0 .../fixtures}/library_tvshows_collections.xml | 0 .../fixtures}/library_tvshows_metadata.xml | 0 .../fixtures}/library_tvshows_most_recent.xml | 0 .../plex/fixtures}/library_tvshows_size.xml | 0 .../library_tvshows_size_episodes.xml | 0 .../library_tvshows_size_seasons.xml | 0 .../plex/fixtures}/library_tvshows_sort.xml | 0 .../plex/fixtures}/livetv_sessions.xml | 0 .../plex/fixtures}/media_1.xml | 0 .../plex/fixtures}/media_100.xml | 0 .../plex/fixtures}/media_200.xml | 0 .../plex/fixtures}/media_30.xml | 0 .../fixtures}/player_plexweb_resources.xml | 0 .../plex/fixtures}/playlist_500.xml | 0 .../plex/fixtures}/playlists.xml | 0 .../plex/fixtures}/playqueue_1234.xml | 0 .../plex/fixtures}/playqueue_created.xml | 0 .../plex/fixtures}/plex_server_accounts.xml | 0 .../plex/fixtures}/plex_server_base.xml | 0 .../plex/fixtures}/plex_server_clients.xml | 0 .../plex/fixtures}/plextv_account.xml | 0 .../plex/fixtures}/plextv_resources_base.xml | 0 .../plex/fixtures}/plextv_shared_users.xml | 0 .../plex/fixtures}/security_token.xml | 0 .../plex/fixtures}/session_base.xml | 0 .../plex/fixtures}/session_live_tv.xml | 0 .../plex/fixtures}/session_photo.xml | 0 .../plex/fixtures}/session_plexweb.xml | 0 .../plex/fixtures}/session_transient.xml | 0 .../plex/fixtures}/session_unknown.xml | 0 .../plex/fixtures}/show_seasons.xml | 0 .../plex/fixtures}/sonos_resources.xml | 0 .../get_all_devices.json | 0 .../02cf28bfec924855854c544690a609ef.json | 0 .../21f2b542c49845e6bb416884c55778d6.json | 0 .../4a810418d5394b3f82727340b91ba740.json | 0 .../675416a629f343c495449970e2ca37b5.json | 0 .../680423ff840043738f42cc7f1ff97a36.json | 0 .../6a3bf693d05e48e0b460c815a4fdd09d.json | 0 .../78d1126fc4c743db81b61c20e88342a7.json | 0 .../90986d591dcd426cae3ec3e8111ff730.json | 0 .../a28f588dc4a049a483fd03a30361ad3a.json | 0 .../a2c3583e0a6349358998b760cea82d2a.json | 0 .../b310b72a0e354bfab43089919b9a88bf.json | 0 .../b59bcebaf94b499ea7d46e4a66fb62d8.json | 0 .../cd0ddb54ef694e11ac18ed1cbce5dbbd.json | 0 .../d3da73bde12a47d5a6b8f9dad971f2ec.json | 0 .../df4a4a8169904cdb9c03d61a21f42140.json | 0 .../e7693eb9582644e5b865dba8d4447cf1.json | 0 .../f1fee6043d3642a9b0a65297455f008e.json | 0 .../fe799307f1624099878210aa0b9f1475.json | 0 .../notifications.json | 0 .../anna_heatpump/get_all_devices.json | 0 .../015ae9ea3f964e668e490fa39da3870b.json | 0 .../1cbf783bb11e4a7c8a6843dee3a86927.json | 0 .../3cb70739631c4d17a86b8b12e8a5161b.json | 0 .../anna_heatpump/notifications.json | 0 .../p1v3_full_option/get_all_devices.json | 0 .../e950c7d5e1ee407a858e2a8b5016c8b3.json | 0 .../p1v3_full_option/notifications.json | 0 .../stretch_v31/get_all_devices.json | 0 .../059e4d03c7a34d278add5c7a4a781d19.json | 0 .../5871317346d045bc9f6b987ef25ee638.json | 0 .../5ca521ac179d468e91d772eeeb8a2117.json | 0 .../71e1944f2a944b26ad73323e399efef0.json | 0 .../99f89d097be34fca88d8598c6dbc18ea.json | 0 .../aac7b735042c4832ac9ff33aae4f453b.json | 0 .../cfe95cf3de1948c0b8955125bf754614.json | 0 .../d03738edfcc947f7b8f4573571d90d2d.json | 0 .../d950b314e9d8499f968e6db8d82ef78c.json | 0 .../e1c884e7dede431dadee09506ec4f859.json | 0 .../e309b52ea5684cf1a22f30cf0cd15051.json | 0 .../fixtures}/stretch_v31/notifications.json | 0 .../powerwall/fixtures}/device_type.json | 0 .../powerwall/fixtures}/meters.json | 0 .../powerwall/fixtures}/site_info.json | 0 .../powerwall/fixtures}/sitemaster.json | 0 .../powerwall/fixtures}/status.json | 0 .../pushbullet/fixtures/devices.json} | 0 tests/components/pushbullet/test_notify.py | 2 +- .../fixtures}/PVPC_CURV_DD_2019_10_26.json | 0 .../fixtures}/PVPC_CURV_DD_2019_10_27.json | 0 .../fixtures}/PVPC_CURV_DD_2019_10_29.json | 0 .../fixtures}/PVPC_CURV_DD_2021_06_01.json | 0 .../fixtures}/action.set_ac_start.json | 0 .../renault/fixtures}/action.set_ac_stop.json | 0 .../fixtures}/action.set_charge_mode.json | 0 .../action.set_charge_schedules.json | 0 .../fixtures}/action.set_charge_start.json | 0 .../fixtures}/battery_status_charging.json | 0 .../battery_status_not_charging.json | 0 .../renault/fixtures}/charge_mode_always.json | 0 .../fixtures}/charge_mode_schedule.json | 0 .../renault/fixtures}/charging_settings.json | 0 .../renault/fixtures}/cockpit_ev.json | 0 .../renault/fixtures}/cockpit_fuel.json | 0 .../renault/fixtures}/hvac_status.json | 0 .../renault/fixtures}/location.json | 0 .../renault/fixtures}/no_data.json | 0 .../fixtures}/vehicle_captur_fuel.json | 0 .../fixtures}/vehicle_captur_phev.json | 0 .../renault/fixtures}/vehicle_zoe_40.json | 0 .../renault/fixtures}/vehicle_zoe_50.json | 0 .../rest/fixtures}/configuration.yaml | 0 .../rest/fixtures}/configuration_empty.yaml | 0 .../fixtures}/configuration_invalid.notyaml | 0 .../fixtures}/configuration_top_level.yaml | 0 tests/components/rest/test_binary_sensor.py | 13 +- tests/components/rest/test_init.py | 27 +- tests/components/rest/test_notify.py | 14 +- tests/components/rest/test_sensor.py | 13 +- tests/components/ring/conftest.py | 15 +- .../ring/fixtures/chime_health_attrs.json} | 0 .../ring/fixtures/devices.json} | 0 .../ring/fixtures/devices_updated.json} | 0 .../ring/fixtures/ding_active.json} | 0 .../ring/fixtures/doorboot_health_attrs.json} | 0 .../fixtures/doorbot_siren_on_response.json} | 0 .../ring/fixtures/doorbots.json} | 0 .../ring/fixtures/oauth.json} | 0 .../ring/fixtures/session.json} | 0 tests/components/ring/test_init.py | 10 +- tests/components/ring/test_light.py | 4 +- tests/components/ring/test_switch.py | 4 +- .../roku/fixtures}/active-app-netflix.xml | 0 .../roku/fixtures}/active-app-pluto.xml | 0 .../roku/fixtures}/active-app-roku.xml | 0 .../roku/fixtures}/active-app-screensaver.xml | 0 .../roku/fixtures}/active-app-tvinput-dtv.xml | 0 .../roku/fixtures}/apps-tv.xml | 0 .../roku/fixtures}/apps.xml | 0 .../roku/fixtures}/media-player-close.xml | 0 .../roku/fixtures}/media-player-live.xml | 0 .../roku/fixtures}/media-player-pause.xml | 0 .../roku/fixtures}/media-player-play.xml | 0 .../fixtures}/roku3-device-info-power-off.xml | 0 .../roku/fixtures}/roku3-device-info.xml | 0 .../rokutv-device-info-power-off.xml | 0 .../roku/fixtures}/rokutv-device-info.xml | 0 .../fixtures}/rokutv-tv-active-channel.xml | 0 .../roku/fixtures}/rokutv-tv-channels.xml | 0 .../components/smart_meter_texas/conftest.py | 3 +- .../fixtures}/latestodrread.json | 0 .../smart_meter_texas/fixtures}/meter.json | 0 .../smtp/fixtures}/configuration.yaml | 0 tests/components/smtp/test_notify.py | 13 +- .../sonarr/fixtures}/calendar.json | 0 .../sonarr/fixtures}/command.json | 0 .../sonarr/fixtures}/diskspace.json | 0 .../sonarr/fixtures}/queue.json | 0 .../sonarr/fixtures}/series.json | 0 .../sonarr/fixtures}/system-status.json | 0 .../sonarr/fixtures}/wanted-missing.json | 0 .../statistics/fixtures}/configuration.yaml | 0 tests/components/statistics/test_sensor.py | 12 +- .../fixtures}/ac_issue_32294.heat_mode.json | 0 .../tado/fixtures}/device_temp_offset.json | 0 .../tado/fixtures}/device_wr1.json | 0 .../tado/fixtures}/devices.json | 0 .../tado/fixtures}/hvac_action_heat.json | 0 .../tado => components/tado/fixtures}/me.json | 0 .../tado/fixtures}/smartac3.auto_mode.json | 0 .../tado/fixtures}/smartac3.cool_mode.json | 0 .../tado/fixtures}/smartac3.dry_mode.json | 0 .../tado/fixtures}/smartac3.fan_mode.json | 0 .../tado/fixtures}/smartac3.heat_mode.json | 0 .../tado/fixtures}/smartac3.hvac_off.json | 0 .../tado/fixtures}/smartac3.manual_off.json | 0 .../tado/fixtures}/smartac3.offline.json | 0 .../tado/fixtures}/smartac3.smart_mode.json | 0 .../tado/fixtures}/smartac3.turning_off.json | 0 .../tado/fixtures}/smartac3.with_swing.json | 0 .../fixtures}/tadov2.heating.auto_mode.json | 0 .../fixtures}/tadov2.heating.manual_mode.json | 0 .../fixtures}/tadov2.heating.off_mode.json | 0 .../tadov2.water_heater.auto_mode.json | 0 .../tadov2.water_heater.heating.json | 0 .../tadov2.water_heater.manual_mode.json | 0 .../tadov2.water_heater.off_mode.json | 0 .../fixtures}/tadov2.zone_capabilities.json | 0 .../tado/fixtures}/token.json | 0 .../water_heater_zone_capabilities.json | 0 .../tado/fixtures}/weather.json | 0 .../tado/fixtures}/zone_capabilities.json | 0 .../tado/fixtures}/zone_default_overlay.json | 0 .../tado/fixtures}/zone_state.json | 0 .../tado/fixtures}/zone_states.json | 0 .../zone_with_swing_capabilities.json | 0 .../tado/fixtures}/zones.json | 0 .../telegram/fixtures}/configuration.yaml | 0 tests/components/telegram/test_notify.py | 13 +- .../fixtures}/broken_configuration.yaml | 0 .../fixtures}/configuration.yaml.corrupt | Bin .../fixtures}/empty_configuration.yaml | 0 .../template/fixtures}/ref_configuration.yaml | 0 .../fixtures}/sensor_configuration.yaml | 0 tests/components/template/test_init.py | 13 +- .../fixtures}/automation_saved_traces.json | 0 .../trace/fixtures}/script_saved_traces.json | 0 .../trend/fixtures}/configuration.yaml | 0 tests/components/trend/test_binary_sensor.py | 17 +- .../unifi_direct/fixtures/data.txt} | 0 .../unifi_direct/test_device_tracker.py | 4 +- .../universal/fixtures}/configuration.yaml | 0 .../components/universal/test_media_player.py | 13 +- .../venstar/fixtures}/colortouch_info.json | 0 .../venstar/fixtures}/colortouch_root.json | 0 .../venstar/fixtures}/colortouch_sensors.json | 0 .../venstar/fixtures}/t2k_info.json | 0 .../venstar/fixtures}/t2k_root.json | 0 .../venstar/fixtures}/t2k_sensors.json | 0 .../vultr/fixtures/account_info.json} | 0 .../vultr/fixtures/server_list.json} | 0 tests/components/vultr/test_binary_sensor.py | 8 +- tests/components/vultr/test_init.py | 2 +- tests/components/vultr/test_sensor.py | 8 +- tests/components/vultr/test_switch.py | 12 +- .../wled/fixtures}/rgb.json | 0 .../wled/fixtures}/rgb_single_segment.json | 0 .../wled/fixtures}/rgb_websocket.json | 0 .../wled/fixtures}/rgbw.json | 0 .../fixtures}/aeon_smart_switch_6_state.json | 0 .../aeotec_radiator_thermostat_state.json | 0 .../fixtures}/aeotec_zw164_siren_state.json | 0 .../fixtures}/bulb_6_multi_color_state.json | 0 .../fixtures}/chain_actuator_zws12_state.json | 0 .../climate_danfoss_lc_13_state.json | 0 .../climate_eurotronic_spirit_z_state.json | 0 .../climate_heatit_z_trm2fx_state.json | 0 .../climate_heatit_z_trm3_no_value_state.json | 0 .../climate_heatit_z_trm3_state.json | 0 ...setpoint_on_different_endpoints_state.json | 0 ..._ct100_plus_different_endpoints_state.json | 0 ...ate_radio_thermostat_ct100_plus_state.json | 0 ...ostat_ct101_multiple_temp_units_state.json | 0 .../zwave_js/fixtures}/controller_state.json | 0 .../cover_aeotec_nano_shutter_state.json | 0 .../fixtures}/cover_fibaro_fgr222_state.json | 0 .../fixtures}/cover_iblinds_v2_state.json | 0 .../fixtures}/cover_qubino_shutter_state.json | 0 .../zwave_js/fixtures}/cover_zw062_state.json | 0 .../fixtures}/eaton_rf9640_dimmer_state.json | 0 .../fixtures}/ecolink_door_sensor_state.json | 0 .../fixtures}/fan_ge_12730_state.json | 0 .../fixtures}/fortrezz_ssa1_siren_state.json | 0 .../ge_in_wall_dimmer_switch_state.json | 0 .../fixtures}/hank_binary_switch_state.json | 0 .../in_wall_smart_fan_control_state.json | 0 .../fixtures}/inovelli_lzw36_state.json | 0 .../light_color_null_values_state.json | 0 .../fixtures}/lock_august_asl03_state.json | 0 .../lock_id_lock_as_id150_state.json | 0 ...pp_electric_strike_lock_control_state.json | 0 .../fixtures}/lock_schlage_be469_state.json | 0 .../fixtures}/multisensor_6_state.json | 0 .../nortek_thermostat_added_event.json | 0 .../nortek_thermostat_removed_event.json | 0 .../fixtures}/nortek_thermostat_state.json | 0 .../fixtures}/null_name_check_state.json | 0 .../fixtures}/srt321_hrt4_zw_state.json | 0 .../vision_security_zl7432_state.json | 0 .../wallmote_central_scene_state.json | 0 .../zwave_js/fixtures}/zen_31_state.json | 0 tests/fixtures/griddy/getnow.json | 600 ------------------ tests/helpers/test_reload.py | 42 +- 561 files changed, 188 insertions(+), 991 deletions(-) rename tests/{fixtures/abode_automation.json => components/abode/fixtures/automation.json} (100%) rename tests/{fixtures/abode_automation_changed.json => components/abode/fixtures/automation_changed.json} (100%) rename tests/{fixtures/abode_devices.json => components/abode/fixtures/devices.json} (100%) rename tests/{fixtures/abode_login.json => components/abode/fixtures/login.json} (100%) rename tests/{fixtures/abode_logout.json => components/abode/fixtures/logout.json} (100%) rename tests/{fixtures/abode_oauth_claims.json => components/abode/fixtures/oauth_claims.json} (100%) rename tests/{fixtures/abode_panel.json => components/abode/fixtures/panel.json} (100%) rename tests/{fixtures/accuweather => components/accuweather/fixtures}/current_conditions_data.json (100%) rename tests/{fixtures/accuweather => components/accuweather/fixtures}/forecast_data.json (100%) rename tests/{fixtures/accuweather => components/accuweather/fixtures}/location_data.json (100%) rename tests/{fixtures/advantage_air => components/advantage_air/fixtures}/getSystemData.json (100%) rename tests/{fixtures/advantage_air => components/advantage_air/fixtures}/setAircon.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/station-3195-data.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/station-3195.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/station-list-data.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/station-list.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-28065-forecast-daily-data.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-28065-forecast-daily.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-28065-forecast-hourly-data.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-28065-forecast-hourly.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-id28065.json (100%) rename tests/{fixtures/aemet => components/aemet/fixtures}/town-list.json (100%) rename tests/{fixtures/agent_dvr => components/agent_dvr/fixtures}/objects.json (100%) rename tests/{fixtures/agent_dvr => components/agent_dvr/fixtures}/status.json (100%) rename tests/{fixtures/airly_no_station.json => components/airly/fixtures/no_station.json} (100%) rename tests/{fixtures/airly_valid_station.json => components/airly/fixtures/valid_station.json} (100%) rename tests/{fixtures/ambee => components/ambee/fixtures}/air_quality.json (100%) rename tests/{fixtures/ambee => components/ambee/fixtures}/pollen.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.bridge_offline.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.bridge_online.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.doorbell_motion.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.jammed.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.lock.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.lock_from_autorelock.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.lock_from_bluetooth.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.lock_from_keypad.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.locking.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_activity.unlocking.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_doorbell.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_doorbell.nobattery.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_doorbell.offline.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.doorsense_init.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.low_keypad_battery.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.offline.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.online.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.online.unknown_state.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.online_missing_doorsense.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_lock.online_with_doorsense.json (100%) rename tests/{fixtures/august => components/august/fixtures}/get_locks.json (100%) rename tests/{fixtures/august => components/august/fixtures}/lock_open.json (100%) rename tests/{fixtures/august => components/august/fixtures}/unlock_closed.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/awair-offline.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/awair-r2.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/awair.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/devices.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/glow.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/mint.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/no_devices.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/omni.json (100%) rename tests/{fixtures/awair => components/awair/fixtures}/user.json (100%) rename tests/{fixtures/bayesian => components/bayesian/fixtures}/configuration.yaml (100%) rename tests/{fixtures/blueprint => components/blueprint/fixtures}/community_post.json (100%) rename tests/{fixtures/blueprint => components/blueprint/fixtures}/github_gist.json (100%) rename tests/{fixtures/brother_printer_data.json => components/brother/fixtures/printer_data.json} (100%) rename tests/{fixtures/bsblan => components/bsblan/fixtures}/info.json (100%) rename tests/{fixtures/climacell => components/climacell/fixtures}/v3_forecast_daily.json (100%) rename tests/{fixtures/climacell => components/climacell/fixtures}/v3_forecast_hourly.json (100%) rename tests/{fixtures/climacell => components/climacell/fixtures}/v3_forecast_nowcast.json (100%) rename tests/{fixtures/climacell => components/climacell/fixtures}/v3_realtime.json (100%) rename tests/{fixtures/climacell => components/climacell/fixtures}/v4.json (100%) rename tests/{fixtures/command_line => components/command_line/fixtures}/configuration.yaml (100%) rename tests/{fixtures/dexcom_data.json => components/dexcom/fixtures/data.json} (100%) rename tests/{fixtures/directv => components/directv/fixtures}/info-get-locations.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/info-get-version.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/info-mode-error.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/info-mode-standby.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/info-mode.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/remote-process-key.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/tv-get-tuned-movie.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/tv-get-tuned-music.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/tv-get-tuned-restricted.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/tv-get-tuned.json (100%) rename tests/{fixtures/directv => components/directv/fixtures}/tv-tune.json (100%) rename tests/{fixtures/ecobee => components/ecobee/fixtures}/ecobee-data.json (100%) rename tests/{fixtures/ecobee => components/ecobee/fixtures}/ecobee-token.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/budget.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/current_values_multi.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/current_values_single.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/daily_cost.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/daily_energy.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/instant.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/monthly_cost.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/monthly_energy.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/status.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/weekly_cost.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/weekly_energy.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/yearly_cost.json (100%) rename tests/{fixtures/efergy => components/efergy/fixtures}/yearly_energy.json (100%) rename tests/{fixtures/elgato => components/elgato/fixtures}/info.json (100%) rename tests/{fixtures/elgato => components/elgato/fixtures}/settings-color.json (100%) rename tests/{fixtures/elgato => components/elgato/fixtures}/settings.json (100%) rename tests/{fixtures/elgato => components/elgato/fixtures}/state-color.json (100%) rename tests/{fixtures/elgato => components/elgato/fixtures}/state.json (100%) rename tests/{fixtures/filesize => components/filesize/fixtures}/configuration.yaml (100%) rename tests/{fixtures/filter => components/filter/fixtures}/configuration.yaml (100%) rename tests/{fixtures/flo => components/flo/fixtures}/device_info_response.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/device_info_response_closed.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/device_info_response_detector.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/location_info_base_response.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/location_info_expand_devices_response.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/user_info_base_response.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/user_info_expand_locations_response.json (100%) rename tests/{fixtures/flo => components/flo/fixtures}/water_consumption_info_response.json (100%) rename tests/{fixtures/foobot_data.json => components/foobot/fixtures/data.json} (100%) rename tests/{fixtures/foobot_devices.json => components/foobot/fixtures/devices.json} (100%) rename tests/{fixtures/generic => components/generic/fixtures}/configuration.yaml (100%) rename tests/{fixtures/generic_thermostat => components/generic_thermostat/fixtures}/configuration.yaml (100%) rename tests/{fixtures/gios => components/gios/fixtures}/indexes.json (100%) rename tests/{fixtures/gios => components/gios/fixtures}/sensors.json (100%) rename tests/{fixtures/gios => components/gios/fixtures}/station.json (100%) rename tests/{fixtures/google_maps_elevation.json => components/google/fixtures/maps_elevation.json} (100%) rename tests/{fixtures/group => components/group/fixtures}/configuration.yaml (100%) rename tests/{fixtures/group => components/group/fixtures}/fan_configuration.yaml (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/attribution_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/bike_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/car_enabled_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/car_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/car_shortest_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/pedestrian_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/public_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/public_time_table_response.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/routing_error_invalid_credentials.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/routing_error_no_route_found.json (100%) rename tests/{fixtures/here_travel_time => components/here_travel_time/fixtures}/truck_response.json (100%) rename tests/{fixtures/history_stats => components/history_stats/fixtures}/configuration.yaml (100%) rename tests/{fixtures/homekit => components/homekit/fixtures}/configuration.yaml (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/anker_eufycam.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/aqara_gateway.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/aqara_switch.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/arlo_baby.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/ecobee3.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/ecobee3_no_sensors.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/ecobee_occupancy.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/eve_degree.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/haa_fan.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/home_assistant_bridge_fan.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/hue_bridge.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/koogeek_ls1.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/koogeek_p1eu.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/koogeek_sw2.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/lennox_e30.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/lg_tv.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/mysa_living.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/netamo_doorbell.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/rainmachine-pro-8.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/ryse_smart_bridge.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/ryse_smart_bridge_four_shades.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/simpleconnect_fan.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/velux_gateway.json (100%) rename tests/{fixtures/homekit_controller => components/homekit_controller/fixtures}/vocolinc_flowerbud.json (100%) rename tests/{fixtures/hunterdouglas_powerview => components/hunterdouglas_powerview/fixtures}/fwversion.json (100%) rename tests/{fixtures/hunterdouglas_powerview => components/hunterdouglas_powerview/fixtures}/userdata.json (100%) rename tests/{fixtures/hunterdouglas_powerview => components/hunterdouglas_powerview/fixtures}/userdata_v1.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/check_name.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/config_entry.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/departure_list.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/init.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/options.json (100%) rename tests/{fixtures/hvv_departures => components/hvv_departures/fixtures}/station_information.json (100%) rename tests/{fixtures/insteon => components/insteon/fixtures}/aldb_data.json (100%) rename tests/{fixtures/insteon => components/insteon/fixtures}/kpl_properties.json (100%) rename tests/{fixtures/ipp => components/ipp/fixtures}/get-printer-attributes-error-0x0503.bin (100%) rename tests/{fixtures/ipp => components/ipp/fixtures}/get-printer-attributes-success-nodata.bin (100%) rename tests/{fixtures/ipp => components/ipp/fixtures}/get-printer-attributes.bin (100%) rename tests/{fixtures/lcn => components/lcn/fixtures}/config.json (100%) rename tests/{fixtures/lcn => components/lcn/fixtures}/config_entry_myhome.json (100%) rename tests/{fixtures/lcn => components/lcn/fixtures}/config_entry_pchk.json (100%) rename tests/{fixtures/mazda => components/mazda/fixtures}/get_vehicle_status.json (100%) rename tests/{fixtures/mazda => components/mazda/fixtures}/get_vehicles.json (100%) rename tests/{fixtures/melissa_cur_settings.json => components/melissa/fixtures/cur_settings.json} (100%) rename tests/{fixtures/melissa_fetch_devices.json => components/melissa/fixtures/fetch_devices.json} (100%) rename tests/{fixtures/melissa_status.json => components/melissa/fixtures/status.json} (100%) rename tests/{fixtures/min_max => components/min_max/fixtures}/configuration.yaml (100%) rename tests/{fixtures/modern_forms => components/modern_forms/fixtures}/device_info.json (100%) rename tests/{fixtures/modern_forms => components/modern_forms/fixtures}/device_info_no_light.json (100%) rename tests/{fixtures/modern_forms => components/modern_forms/fixtures}/device_status.json (100%) rename tests/{fixtures/modern_forms => components/modern_forms/fixtures}/device_status_no_light.json (100%) rename tests/{fixtures/modern_forms => components/modern_forms/fixtures}/device_status_timers_active.json (100%) rename tests/{fixtures/mqtt => components/mqtt/fixtures}/configuration.yaml (100%) rename tests/{fixtures/myq => components/myq/fixtures}/devices.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/distance_sensor_state.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/energy_sensor_state.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/gps_sensor_state.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/power_sensor_state.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/sound_sensor_state.json (100%) rename tests/{fixtures/mysensors => components/mysensors/fixtures}/temperature_sensor_state.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/events.txt (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/gethomecoachsdata.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/gethomedata.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/getpublicdata.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/getstationsdata.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/homesdata.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/homestatus.json (100%) rename tests/{fixtures/netatmo => components/netatmo/fixtures}/ping.json (100%) rename tests/{fixtures/nexia => components/nexia/fixtures}/mobile_houses_123456.json (100%) rename tests/{fixtures/nexia => components/nexia/fixtures}/session_123456.json (100%) rename tests/{fixtures/nexia => components/nexia/fixtures}/sign_in.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/5E650I.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/5E850I.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/BACKUPSES600M1.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/CP1350C.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/CP1500PFCLCD.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/DL650ELCD.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/PR3000RT2U.json (100%) rename tests/{fixtures/nut => components/nut/fixtures}/blazer_usb.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/binary_sensor.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/binary_sensor_alt.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/climate.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/climate_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/cover.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/cover_gdo.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/cover_gdo_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/cover_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/fan.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/fan_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/generic_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_new_ozw_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_no_cw_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_no_rgb.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_no_ww_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_pure_rgb.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_rgb.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/light_wc_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/lock.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/lock_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/migration_fixture.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/sensor.json (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/sensor_string_value_network_dump.csv (100%) rename tests/{fixtures/ozw => components/ozw/fixtures}/switch.json (100%) rename tests/{fixtures/p1_monitor => components/p1_monitor/fixtures}/phases.json (100%) rename tests/{fixtures/p1_monitor => components/p1_monitor/fixtures}/settings.json (100%) rename tests/{fixtures/p1_monitor => components/p1_monitor/fixtures}/smartmeter.json (100%) rename tests/{fixtures/ping => components/ping/fixtures}/configuration.yaml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/album.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/artist_albums.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/children_20.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/children_200.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/children_30.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/children_300.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/empty_library.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/empty_payload.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/grandchildren_300.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_all.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_collections.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_filtertypes.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_metadata.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_size.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_movies_sort.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_music_all.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_music_collections.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_music_metadata.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_music_size.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_music_sort.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_sections.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_all.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_collections.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_metadata.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_most_recent.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_size.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_size_episodes.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_size_seasons.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/library_tvshows_sort.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/livetv_sessions.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/media_1.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/media_100.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/media_200.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/media_30.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/player_plexweb_resources.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/playlist_500.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/playlists.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/playqueue_1234.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/playqueue_created.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plex_server_accounts.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plex_server_base.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plex_server_clients.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plextv_account.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plextv_resources_base.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/plextv_shared_users.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/security_token.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_base.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_live_tv.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_photo.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_plexweb.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_transient.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/session_unknown.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/show_seasons.xml (100%) rename tests/{fixtures/plex => components/plex/fixtures}/sonos_resources.xml (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_all_devices.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/adam_multiple_devices_per_zone/notifications.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/anna_heatpump/get_all_devices.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/anna_heatpump/notifications.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/p1v3_full_option/get_all_devices.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/p1v3_full_option/notifications.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_all_devices.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json (100%) rename tests/{fixtures/plugwise => components/plugwise/fixtures}/stretch_v31/notifications.json (100%) rename tests/{fixtures/powerwall => components/powerwall/fixtures}/device_type.json (100%) rename tests/{fixtures/powerwall => components/powerwall/fixtures}/meters.json (100%) rename tests/{fixtures/powerwall => components/powerwall/fixtures}/site_info.json (100%) rename tests/{fixtures/powerwall => components/powerwall/fixtures}/sitemaster.json (100%) rename tests/{fixtures/powerwall => components/powerwall/fixtures}/status.json (100%) rename tests/{fixtures/pushbullet_devices.json => components/pushbullet/fixtures/devices.json} (100%) rename tests/{fixtures/pvpc_hourly_pricing => components/pvpc_hourly_pricing/fixtures}/PVPC_CURV_DD_2019_10_26.json (100%) rename tests/{fixtures/pvpc_hourly_pricing => components/pvpc_hourly_pricing/fixtures}/PVPC_CURV_DD_2019_10_27.json (100%) rename tests/{fixtures/pvpc_hourly_pricing => components/pvpc_hourly_pricing/fixtures}/PVPC_CURV_DD_2019_10_29.json (100%) rename tests/{fixtures/pvpc_hourly_pricing => components/pvpc_hourly_pricing/fixtures}/PVPC_CURV_DD_2021_06_01.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/action.set_ac_start.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/action.set_ac_stop.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/action.set_charge_mode.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/action.set_charge_schedules.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/action.set_charge_start.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/battery_status_charging.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/battery_status_not_charging.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/charge_mode_always.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/charge_mode_schedule.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/charging_settings.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/cockpit_ev.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/cockpit_fuel.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/hvac_status.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/location.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/no_data.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/vehicle_captur_fuel.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/vehicle_captur_phev.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/vehicle_zoe_40.json (100%) rename tests/{fixtures/renault => components/renault/fixtures}/vehicle_zoe_50.json (100%) rename tests/{fixtures/rest => components/rest/fixtures}/configuration.yaml (100%) rename tests/{fixtures/rest => components/rest/fixtures}/configuration_empty.yaml (100%) rename tests/{fixtures/rest => components/rest/fixtures}/configuration_invalid.notyaml (100%) rename tests/{fixtures/rest => components/rest/fixtures}/configuration_top_level.yaml (100%) rename tests/{fixtures/ring_chime_health_attrs.json => components/ring/fixtures/chime_health_attrs.json} (100%) rename tests/{fixtures/ring_devices.json => components/ring/fixtures/devices.json} (100%) rename tests/{fixtures/ring_devices_updated.json => components/ring/fixtures/devices_updated.json} (100%) rename tests/{fixtures/ring_ding_active.json => components/ring/fixtures/ding_active.json} (100%) rename tests/{fixtures/ring_doorboot_health_attrs.json => components/ring/fixtures/doorboot_health_attrs.json} (100%) rename tests/{fixtures/ring_doorbot_siren_on_response.json => components/ring/fixtures/doorbot_siren_on_response.json} (100%) rename tests/{fixtures/ring_doorbots.json => components/ring/fixtures/doorbots.json} (100%) rename tests/{fixtures/ring_oauth.json => components/ring/fixtures/oauth.json} (100%) rename tests/{fixtures/ring_session.json => components/ring/fixtures/session.json} (100%) rename tests/{fixtures/roku => components/roku/fixtures}/active-app-netflix.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/active-app-pluto.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/active-app-roku.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/active-app-screensaver.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/active-app-tvinput-dtv.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/apps-tv.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/apps.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/media-player-close.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/media-player-live.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/media-player-pause.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/media-player-play.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/roku3-device-info-power-off.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/roku3-device-info.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/rokutv-device-info-power-off.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/rokutv-device-info.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/rokutv-tv-active-channel.xml (100%) rename tests/{fixtures/roku => components/roku/fixtures}/rokutv-tv-channels.xml (100%) rename tests/{fixtures/smart_meter_texas => components/smart_meter_texas/fixtures}/latestodrread.json (100%) rename tests/{fixtures/smart_meter_texas => components/smart_meter_texas/fixtures}/meter.json (100%) rename tests/{fixtures/smtp => components/smtp/fixtures}/configuration.yaml (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/calendar.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/command.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/diskspace.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/queue.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/series.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/system-status.json (100%) rename tests/{fixtures/sonarr => components/sonarr/fixtures}/wanted-missing.json (100%) rename tests/{fixtures/statistics => components/statistics/fixtures}/configuration.yaml (100%) rename tests/{fixtures/tado => components/tado/fixtures}/ac_issue_32294.heat_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/device_temp_offset.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/device_wr1.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/devices.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/hvac_action_heat.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/me.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.auto_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.cool_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.dry_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.fan_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.heat_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.hvac_off.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.manual_off.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.offline.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.smart_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.turning_off.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/smartac3.with_swing.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.heating.auto_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.heating.manual_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.heating.off_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.water_heater.auto_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.water_heater.heating.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.water_heater.manual_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.water_heater.off_mode.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/tadov2.zone_capabilities.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/token.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/water_heater_zone_capabilities.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/weather.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zone_capabilities.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zone_default_overlay.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zone_state.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zone_states.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zone_with_swing_capabilities.json (100%) rename tests/{fixtures/tado => components/tado/fixtures}/zones.json (100%) rename tests/{fixtures/telegram => components/telegram/fixtures}/configuration.yaml (100%) rename tests/{fixtures/template => components/template/fixtures}/broken_configuration.yaml (100%) rename tests/{fixtures/template => components/template/fixtures}/configuration.yaml.corrupt (100%) rename tests/{fixtures/template => components/template/fixtures}/empty_configuration.yaml (100%) rename tests/{fixtures/template => components/template/fixtures}/ref_configuration.yaml (100%) rename tests/{fixtures/template => components/template/fixtures}/sensor_configuration.yaml (100%) rename tests/{fixtures/trace => components/trace/fixtures}/automation_saved_traces.json (100%) rename tests/{fixtures/trace => components/trace/fixtures}/script_saved_traces.json (100%) rename tests/{fixtures/trend => components/trend/fixtures}/configuration.yaml (100%) rename tests/{fixtures/unifi_direct.txt => components/unifi_direct/fixtures/data.txt} (100%) rename tests/{fixtures/universal => components/universal/fixtures}/configuration.yaml (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/colortouch_info.json (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/colortouch_root.json (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/colortouch_sensors.json (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/t2k_info.json (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/t2k_root.json (100%) rename tests/{fixtures/venstar => components/venstar/fixtures}/t2k_sensors.json (100%) rename tests/{fixtures/vultr_account_info.json => components/vultr/fixtures/account_info.json} (100%) rename tests/{fixtures/vultr_server_list.json => components/vultr/fixtures/server_list.json} (100%) rename tests/{fixtures/wled => components/wled/fixtures}/rgb.json (100%) rename tests/{fixtures/wled => components/wled/fixtures}/rgb_single_segment.json (100%) rename tests/{fixtures/wled => components/wled/fixtures}/rgb_websocket.json (100%) rename tests/{fixtures/wled => components/wled/fixtures}/rgbw.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/aeon_smart_switch_6_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/aeotec_radiator_thermostat_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/aeotec_zw164_siren_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/bulb_6_multi_color_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/chain_actuator_zws12_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_danfoss_lc_13_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_eurotronic_spirit_z_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_heatit_z_trm2fx_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_heatit_z_trm3_no_value_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_heatit_z_trm3_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_radio_thermostat_ct100_plus_different_endpoints_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_radio_thermostat_ct100_plus_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/climate_radio_thermostat_ct101_multiple_temp_units_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/controller_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/cover_aeotec_nano_shutter_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/cover_fibaro_fgr222_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/cover_iblinds_v2_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/cover_qubino_shutter_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/cover_zw062_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/eaton_rf9640_dimmer_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/ecolink_door_sensor_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/fan_ge_12730_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/fortrezz_ssa1_siren_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/ge_in_wall_dimmer_switch_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/hank_binary_switch_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/in_wall_smart_fan_control_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/inovelli_lzw36_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/light_color_null_values_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/lock_august_asl03_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/lock_id_lock_as_id150_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/lock_popp_electric_strike_lock_control_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/lock_schlage_be469_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/multisensor_6_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/nortek_thermostat_added_event.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/nortek_thermostat_removed_event.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/nortek_thermostat_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/null_name_check_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/srt321_hrt4_zw_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/vision_security_zl7432_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/wallmote_central_scene_state.json (100%) rename tests/{fixtures/zwave_js => components/zwave_js/fixtures}/zen_31_state.json (100%) delete mode 100644 tests/fixtures/griddy/getnow.json diff --git a/tests/common.py b/tests/common.py index 519b53cd991..72c18822e00 100644 --- a/tests/common.py +++ b/tests/common.py @@ -397,11 +397,22 @@ def async_fire_time_changed( fire_time_changed = threadsafe_callback_factory(async_fire_time_changed) -def load_fixture(filename): +def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.Path: + """Get path of fixture.""" + if integration is None and "/" in filename and not filename.startswith("helpers/"): + integration, filename = filename.split("/", 1) + + if integration is None: + return pathlib.Path(__file__).parent.joinpath("fixtures", filename) + else: + return pathlib.Path(__file__).parent.joinpath( + "components", integration, "fixtures", filename + ) + + +def load_fixture(filename, integration=None): """Load a fixture.""" - path = os.path.join(os.path.dirname(__file__), "fixtures", filename) - with open(path, encoding="utf-8") as fptr: - return fptr.read() + return get_fixture_path(filename, integration).read_text() def mock_state_change_event(hass, new_state, old_state=None): diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 21d64a644a9..472587781ca 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -10,16 +10,18 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 def requests_mock_fixture(requests_mock): """Fixture to provide a requests mocker.""" # Mocks the login response for abodepy. - requests_mock.post(CONST.LOGIN_URL, text=load_fixture("abode_login.json")) + requests_mock.post(CONST.LOGIN_URL, text=load_fixture("login.json", "abode")) # Mocks the logout response for abodepy. - requests_mock.post(CONST.LOGOUT_URL, text=load_fixture("abode_logout.json")) + requests_mock.post(CONST.LOGOUT_URL, text=load_fixture("logout.json", "abode")) # Mocks the oauth claims response for abodepy. requests_mock.get( - CONST.OAUTH_TOKEN_URL, text=load_fixture("abode_oauth_claims.json") + CONST.OAUTH_TOKEN_URL, text=load_fixture("oauth_claims.json", "abode") ) # Mocks the panel response for abodepy. - requests_mock.get(CONST.PANEL_URL, text=load_fixture("abode_panel.json")) + requests_mock.get(CONST.PANEL_URL, text=load_fixture("panel.json", "abode")) # Mocks the automations response for abodepy. - requests_mock.get(CONST.AUTOMATION_URL, text=load_fixture("abode_automation.json")) + requests_mock.get( + CONST.AUTOMATION_URL, text=load_fixture("automation.json", "abode") + ) # Mocks the devices response for abodepy. - requests_mock.get(CONST.DEVICES_URL, text=load_fixture("abode_devices.json")) + requests_mock.get(CONST.DEVICES_URL, text=load_fixture("devices.json", "abode")) diff --git a/tests/fixtures/abode_automation.json b/tests/components/abode/fixtures/automation.json similarity index 100% rename from tests/fixtures/abode_automation.json rename to tests/components/abode/fixtures/automation.json diff --git a/tests/fixtures/abode_automation_changed.json b/tests/components/abode/fixtures/automation_changed.json similarity index 100% rename from tests/fixtures/abode_automation_changed.json rename to tests/components/abode/fixtures/automation_changed.json diff --git a/tests/fixtures/abode_devices.json b/tests/components/abode/fixtures/devices.json similarity index 100% rename from tests/fixtures/abode_devices.json rename to tests/components/abode/fixtures/devices.json diff --git a/tests/fixtures/abode_login.json b/tests/components/abode/fixtures/login.json similarity index 100% rename from tests/fixtures/abode_login.json rename to tests/components/abode/fixtures/login.json diff --git a/tests/fixtures/abode_logout.json b/tests/components/abode/fixtures/logout.json similarity index 100% rename from tests/fixtures/abode_logout.json rename to tests/components/abode/fixtures/logout.json diff --git a/tests/fixtures/abode_oauth_claims.json b/tests/components/abode/fixtures/oauth_claims.json similarity index 100% rename from tests/fixtures/abode_oauth_claims.json rename to tests/components/abode/fixtures/oauth_claims.json diff --git a/tests/fixtures/abode_panel.json b/tests/components/abode/fixtures/panel.json similarity index 100% rename from tests/fixtures/abode_panel.json rename to tests/components/abode/fixtures/panel.json diff --git a/tests/fixtures/accuweather/current_conditions_data.json b/tests/components/accuweather/fixtures/current_conditions_data.json similarity index 100% rename from tests/fixtures/accuweather/current_conditions_data.json rename to tests/components/accuweather/fixtures/current_conditions_data.json diff --git a/tests/fixtures/accuweather/forecast_data.json b/tests/components/accuweather/fixtures/forecast_data.json similarity index 100% rename from tests/fixtures/accuweather/forecast_data.json rename to tests/components/accuweather/fixtures/forecast_data.json diff --git a/tests/fixtures/accuweather/location_data.json b/tests/components/accuweather/fixtures/location_data.json similarity index 100% rename from tests/fixtures/accuweather/location_data.json rename to tests/components/accuweather/fixtures/location_data.json diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/components/advantage_air/fixtures/getSystemData.json similarity index 100% rename from tests/fixtures/advantage_air/getSystemData.json rename to tests/components/advantage_air/fixtures/getSystemData.json diff --git a/tests/fixtures/advantage_air/setAircon.json b/tests/components/advantage_air/fixtures/setAircon.json similarity index 100% rename from tests/fixtures/advantage_air/setAircon.json rename to tests/components/advantage_air/fixtures/setAircon.json diff --git a/tests/fixtures/aemet/station-3195-data.json b/tests/components/aemet/fixtures/station-3195-data.json similarity index 100% rename from tests/fixtures/aemet/station-3195-data.json rename to tests/components/aemet/fixtures/station-3195-data.json diff --git a/tests/fixtures/aemet/station-3195.json b/tests/components/aemet/fixtures/station-3195.json similarity index 100% rename from tests/fixtures/aemet/station-3195.json rename to tests/components/aemet/fixtures/station-3195.json diff --git a/tests/fixtures/aemet/station-list-data.json b/tests/components/aemet/fixtures/station-list-data.json similarity index 100% rename from tests/fixtures/aemet/station-list-data.json rename to tests/components/aemet/fixtures/station-list-data.json diff --git a/tests/fixtures/aemet/station-list.json b/tests/components/aemet/fixtures/station-list.json similarity index 100% rename from tests/fixtures/aemet/station-list.json rename to tests/components/aemet/fixtures/station-list.json diff --git a/tests/fixtures/aemet/town-28065-forecast-daily-data.json b/tests/components/aemet/fixtures/town-28065-forecast-daily-data.json similarity index 100% rename from tests/fixtures/aemet/town-28065-forecast-daily-data.json rename to tests/components/aemet/fixtures/town-28065-forecast-daily-data.json diff --git a/tests/fixtures/aemet/town-28065-forecast-daily.json b/tests/components/aemet/fixtures/town-28065-forecast-daily.json similarity index 100% rename from tests/fixtures/aemet/town-28065-forecast-daily.json rename to tests/components/aemet/fixtures/town-28065-forecast-daily.json diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly-data.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json similarity index 100% rename from tests/fixtures/aemet/town-28065-forecast-hourly-data.json rename to tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly.json similarity index 100% rename from tests/fixtures/aemet/town-28065-forecast-hourly.json rename to tests/components/aemet/fixtures/town-28065-forecast-hourly.json diff --git a/tests/fixtures/aemet/town-id28065.json b/tests/components/aemet/fixtures/town-id28065.json similarity index 100% rename from tests/fixtures/aemet/town-id28065.json rename to tests/components/aemet/fixtures/town-id28065.json diff --git a/tests/fixtures/aemet/town-list.json b/tests/components/aemet/fixtures/town-list.json similarity index 100% rename from tests/fixtures/aemet/town-list.json rename to tests/components/aemet/fixtures/town-list.json diff --git a/tests/fixtures/agent_dvr/objects.json b/tests/components/agent_dvr/fixtures/objects.json similarity index 100% rename from tests/fixtures/agent_dvr/objects.json rename to tests/components/agent_dvr/fixtures/objects.json diff --git a/tests/fixtures/agent_dvr/status.json b/tests/components/agent_dvr/fixtures/status.json similarity index 100% rename from tests/fixtures/agent_dvr/status.json rename to tests/components/agent_dvr/fixtures/status.json diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 64f2059857a..452df6d9c27 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -23,7 +23,7 @@ async def init_integration(hass, aioclient_mock) -> MockConfigEntry: }, ) - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/fixtures/airly_no_station.json b/tests/components/airly/fixtures/no_station.json similarity index 100% rename from tests/fixtures/airly_no_station.json rename to tests/components/airly/fixtures/no_station.json diff --git a/tests/fixtures/airly_valid_station.json b/tests/components/airly/fixtures/valid_station.json similarity index 100% rename from tests/fixtures/airly_valid_station.json rename to tests/components/airly/fixtures/valid_station.json diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index c19618da0a7..8b593e85cf4 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -48,7 +48,7 @@ async def test_invalid_api_key(hass, aioclient_mock): async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("no_station.json", "airly")) aioclient_mock.get( API_NEAREST_URL, @@ -64,7 +64,7 @@ async def test_invalid_location(hass, aioclient_mock): async def test_duplicate_error(hass, aioclient_mock): """Test that errors are shown when duplicates are added.""" - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -77,7 +77,7 @@ async def test_duplicate_error(hass, aioclient_mock): async def test_create_entry(hass, aioclient_mock): """Test that the user step works.""" - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) with patch("homeassistant.components.airly.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( @@ -95,9 +95,11 @@ async def test_create_entry(hass, aioclient_mock): async def test_create_entry_with_nearest_method(hass, aioclient_mock): """Test that the user step works with nearest method.""" - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("no_station.json", "airly")) - aioclient_mock.get(API_NEAREST_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get( + API_NEAREST_URL, text=load_fixture("valid_station.json", "airly") + ) with patch("homeassistant.components.airly.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 252c01c124a..434c7e5022f 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -66,7 +66,7 @@ async def test_config_without_unique_id(hass, aioclient_mock): }, ) - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED @@ -87,7 +87,7 @@ async def test_config_with_turned_off_station(hass, aioclient_mock): }, ) - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("no_station.json", "airly")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY @@ -115,7 +115,7 @@ async def test_update_interval(hass, aioclient_mock): aioclient_mock.get( API_POINT_URL, - text=load_fixture("airly_valid_station.json"), + text=load_fixture("valid_station.json", "airly"), headers=HEADERS, ) entry.add_to_hass(hass) @@ -152,7 +152,7 @@ async def test_update_interval(hass, aioclient_mock): aioclient_mock.get( "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", - text=load_fixture("airly_valid_station.json"), + text=load_fixture("valid_station.json", "airly"), headers=HEADERS, ) entry.add_to_hass(hass) @@ -203,7 +203,7 @@ async def test_migrate_device_entry(hass, aioclient_mock, old_identifier): }, ) - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) config_entry.add_to_hass(hass) device_reg = mock_device_registry(hass) diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index cd17c692176..71912bb768b 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -149,7 +149,7 @@ async def test_availability(hass, aioclient_mock): assert state.state == STATE_UNAVAILABLE aioclient_mock.clear_requests() - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) future = utcnow() + timedelta(minutes=120) async_fire_time_changed(hass, future) await hass.async_block_till_done() diff --git a/tests/fixtures/ambee/air_quality.json b/tests/components/ambee/fixtures/air_quality.json similarity index 100% rename from tests/fixtures/ambee/air_quality.json rename to tests/components/ambee/fixtures/air_quality.json diff --git a/tests/fixtures/ambee/pollen.json b/tests/components/ambee/fixtures/pollen.json similarity index 100% rename from tests/fixtures/ambee/pollen.json rename to tests/components/ambee/fixtures/pollen.json diff --git a/tests/fixtures/august/get_activity.bridge_offline.json b/tests/components/august/fixtures/get_activity.bridge_offline.json similarity index 100% rename from tests/fixtures/august/get_activity.bridge_offline.json rename to tests/components/august/fixtures/get_activity.bridge_offline.json diff --git a/tests/fixtures/august/get_activity.bridge_online.json b/tests/components/august/fixtures/get_activity.bridge_online.json similarity index 100% rename from tests/fixtures/august/get_activity.bridge_online.json rename to tests/components/august/fixtures/get_activity.bridge_online.json diff --git a/tests/fixtures/august/get_activity.doorbell_motion.json b/tests/components/august/fixtures/get_activity.doorbell_motion.json similarity index 100% rename from tests/fixtures/august/get_activity.doorbell_motion.json rename to tests/components/august/fixtures/get_activity.doorbell_motion.json diff --git a/tests/fixtures/august/get_activity.jammed.json b/tests/components/august/fixtures/get_activity.jammed.json similarity index 100% rename from tests/fixtures/august/get_activity.jammed.json rename to tests/components/august/fixtures/get_activity.jammed.json diff --git a/tests/fixtures/august/get_activity.lock.json b/tests/components/august/fixtures/get_activity.lock.json similarity index 100% rename from tests/fixtures/august/get_activity.lock.json rename to tests/components/august/fixtures/get_activity.lock.json diff --git a/tests/fixtures/august/get_activity.lock_from_autorelock.json b/tests/components/august/fixtures/get_activity.lock_from_autorelock.json similarity index 100% rename from tests/fixtures/august/get_activity.lock_from_autorelock.json rename to tests/components/august/fixtures/get_activity.lock_from_autorelock.json diff --git a/tests/fixtures/august/get_activity.lock_from_bluetooth.json b/tests/components/august/fixtures/get_activity.lock_from_bluetooth.json similarity index 100% rename from tests/fixtures/august/get_activity.lock_from_bluetooth.json rename to tests/components/august/fixtures/get_activity.lock_from_bluetooth.json diff --git a/tests/fixtures/august/get_activity.lock_from_keypad.json b/tests/components/august/fixtures/get_activity.lock_from_keypad.json similarity index 100% rename from tests/fixtures/august/get_activity.lock_from_keypad.json rename to tests/components/august/fixtures/get_activity.lock_from_keypad.json diff --git a/tests/fixtures/august/get_activity.locking.json b/tests/components/august/fixtures/get_activity.locking.json similarity index 100% rename from tests/fixtures/august/get_activity.locking.json rename to tests/components/august/fixtures/get_activity.locking.json diff --git a/tests/fixtures/august/get_activity.unlocking.json b/tests/components/august/fixtures/get_activity.unlocking.json similarity index 100% rename from tests/fixtures/august/get_activity.unlocking.json rename to tests/components/august/fixtures/get_activity.unlocking.json diff --git a/tests/fixtures/august/get_doorbell.json b/tests/components/august/fixtures/get_doorbell.json similarity index 100% rename from tests/fixtures/august/get_doorbell.json rename to tests/components/august/fixtures/get_doorbell.json diff --git a/tests/fixtures/august/get_doorbell.nobattery.json b/tests/components/august/fixtures/get_doorbell.nobattery.json similarity index 100% rename from tests/fixtures/august/get_doorbell.nobattery.json rename to tests/components/august/fixtures/get_doorbell.nobattery.json diff --git a/tests/fixtures/august/get_doorbell.offline.json b/tests/components/august/fixtures/get_doorbell.offline.json similarity index 100% rename from tests/fixtures/august/get_doorbell.offline.json rename to tests/components/august/fixtures/get_doorbell.offline.json diff --git a/tests/fixtures/august/get_lock.doorsense_init.json b/tests/components/august/fixtures/get_lock.doorsense_init.json similarity index 100% rename from tests/fixtures/august/get_lock.doorsense_init.json rename to tests/components/august/fixtures/get_lock.doorsense_init.json diff --git a/tests/fixtures/august/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json similarity index 100% rename from tests/fixtures/august/get_lock.low_keypad_battery.json rename to tests/components/august/fixtures/get_lock.low_keypad_battery.json diff --git a/tests/fixtures/august/get_lock.offline.json b/tests/components/august/fixtures/get_lock.offline.json similarity index 100% rename from tests/fixtures/august/get_lock.offline.json rename to tests/components/august/fixtures/get_lock.offline.json diff --git a/tests/fixtures/august/get_lock.online.json b/tests/components/august/fixtures/get_lock.online.json similarity index 100% rename from tests/fixtures/august/get_lock.online.json rename to tests/components/august/fixtures/get_lock.online.json diff --git a/tests/fixtures/august/get_lock.online.unknown_state.json b/tests/components/august/fixtures/get_lock.online.unknown_state.json similarity index 100% rename from tests/fixtures/august/get_lock.online.unknown_state.json rename to tests/components/august/fixtures/get_lock.online.unknown_state.json diff --git a/tests/fixtures/august/get_lock.online_missing_doorsense.json b/tests/components/august/fixtures/get_lock.online_missing_doorsense.json similarity index 100% rename from tests/fixtures/august/get_lock.online_missing_doorsense.json rename to tests/components/august/fixtures/get_lock.online_missing_doorsense.json diff --git a/tests/fixtures/august/get_lock.online_with_doorsense.json b/tests/components/august/fixtures/get_lock.online_with_doorsense.json similarity index 100% rename from tests/fixtures/august/get_lock.online_with_doorsense.json rename to tests/components/august/fixtures/get_lock.online_with_doorsense.json diff --git a/tests/fixtures/august/get_locks.json b/tests/components/august/fixtures/get_locks.json similarity index 100% rename from tests/fixtures/august/get_locks.json rename to tests/components/august/fixtures/get_locks.json diff --git a/tests/fixtures/august/lock_open.json b/tests/components/august/fixtures/lock_open.json similarity index 100% rename from tests/fixtures/august/lock_open.json rename to tests/components/august/fixtures/lock_open.json diff --git a/tests/fixtures/august/unlock_closed.json b/tests/components/august/fixtures/unlock_closed.json similarity index 100% rename from tests/fixtures/august/unlock_closed.json rename to tests/components/august/fixtures/unlock_closed.json diff --git a/tests/fixtures/awair/awair-offline.json b/tests/components/awair/fixtures/awair-offline.json similarity index 100% rename from tests/fixtures/awair/awair-offline.json rename to tests/components/awair/fixtures/awair-offline.json diff --git a/tests/fixtures/awair/awair-r2.json b/tests/components/awair/fixtures/awair-r2.json similarity index 100% rename from tests/fixtures/awair/awair-r2.json rename to tests/components/awair/fixtures/awair-r2.json diff --git a/tests/fixtures/awair/awair.json b/tests/components/awair/fixtures/awair.json similarity index 100% rename from tests/fixtures/awair/awair.json rename to tests/components/awair/fixtures/awair.json diff --git a/tests/fixtures/awair/devices.json b/tests/components/awair/fixtures/devices.json similarity index 100% rename from tests/fixtures/awair/devices.json rename to tests/components/awair/fixtures/devices.json diff --git a/tests/fixtures/awair/glow.json b/tests/components/awair/fixtures/glow.json similarity index 100% rename from tests/fixtures/awair/glow.json rename to tests/components/awair/fixtures/glow.json diff --git a/tests/fixtures/awair/mint.json b/tests/components/awair/fixtures/mint.json similarity index 100% rename from tests/fixtures/awair/mint.json rename to tests/components/awair/fixtures/mint.json diff --git a/tests/fixtures/awair/no_devices.json b/tests/components/awair/fixtures/no_devices.json similarity index 100% rename from tests/fixtures/awair/no_devices.json rename to tests/components/awair/fixtures/no_devices.json diff --git a/tests/fixtures/awair/omni.json b/tests/components/awair/fixtures/omni.json similarity index 100% rename from tests/fixtures/awair/omni.json rename to tests/components/awair/fixtures/omni.json diff --git a/tests/fixtures/awair/user.json b/tests/components/awair/fixtures/user.json similarity index 100% rename from tests/fixtures/awair/user.json rename to tests/components/awair/fixtures/user.json diff --git a/tests/fixtures/bayesian/configuration.yaml b/tests/components/bayesian/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/bayesian/configuration.yaml rename to tests/components/bayesian/fixtures/configuration.yaml diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 9c181e90deb..c2f289b0697 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,6 +1,5 @@ """The test for the bayesian sensor platform.""" import json -from os import path from unittest.mock import patch from homeassistant import config as hass_config @@ -19,6 +18,8 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_load_values_when_added_to_hass(hass): """Test that sensor initializes with observations of relevant entities.""" @@ -666,11 +667,8 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "bayesian/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "bayesian") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -686,10 +684,6 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.test2") -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) - - async def test_template_triggers(hass): """Test sensor with template triggers.""" hass.states.async_set("input_boolean.test", STATE_OFF) diff --git a/tests/fixtures/blueprint/community_post.json b/tests/components/blueprint/fixtures/community_post.json similarity index 100% rename from tests/fixtures/blueprint/community_post.json rename to tests/components/blueprint/fixtures/community_post.json diff --git a/tests/fixtures/blueprint/github_gist.json b/tests/components/blueprint/fixtures/github_gist.json similarity index 100% rename from tests/fixtures/blueprint/github_gist.json rename to tests/components/blueprint/fixtures/github_gist.json diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index b4706d56fba..57cbe8b71de 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -22,7 +22,7 @@ async def init_integration(hass, skip_setup=False) -> MockConfigEntry: if not skip_setup: with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/fixtures/brother_printer_data.json b/tests/components/brother/fixtures/printer_data.json similarity index 100% rename from tests/fixtures/brother_printer_data.json rename to tests/components/brother/fixtures/printer_data.json diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index b7e4b31cfab..70c42884bf9 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -28,7 +28,7 @@ async def test_create_entry_with_hostname(hass): """Test that the user step works with printer hostname.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -44,7 +44,7 @@ async def test_create_entry_with_ipv4_address(hass): """Test that the user step works with printer IPv4 address.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -62,7 +62,7 @@ async def test_create_entry_with_ipv6_address(hass): """Test that the user step works with printer IPv6 address.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -123,7 +123,7 @@ async def test_device_exists_abort(hass): """Test we abort config flow if Brother printer already configured.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( hass @@ -172,7 +172,7 @@ async def test_zeroconf_device_exists_abort(hass): """Test we abort zeroconf flow if Brother printer already configured.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( hass @@ -209,7 +209,7 @@ async def test_zeroconf_confirm_create_entry(hass): """Test zeroconf confirmation and create config entry.""" with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index b51577b5f3d..1f103390b0a 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -46,7 +46,7 @@ async def test_sensors(hass): test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) with patch("brother.datetime", utcnow=Mock(return_value=test_time)), patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -296,7 +296,7 @@ async def test_availability(hass): future = utcnow() + timedelta(minutes=10) with patch( "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), + return_value=json.loads(load_fixture("printer_data.json", "brother")), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -311,7 +311,7 @@ async def test_manual_update_entity(hass): """Test manual update entity via service homeasasistant/update_entity.""" await init_integration(hass) - data = json.loads(load_fixture("brother_printer_data.json")) + data = json.loads(load_fixture("printer_data.json", "brother")) await async_setup_component(hass, "homeassistant", {}) with patch( diff --git a/tests/fixtures/bsblan/info.json b/tests/components/bsblan/fixtures/info.json similarity index 100% rename from tests/fixtures/bsblan/info.json rename to tests/components/bsblan/fixtures/info.json diff --git a/tests/fixtures/climacell/v3_forecast_daily.json b/tests/components/climacell/fixtures/v3_forecast_daily.json similarity index 100% rename from tests/fixtures/climacell/v3_forecast_daily.json rename to tests/components/climacell/fixtures/v3_forecast_daily.json diff --git a/tests/fixtures/climacell/v3_forecast_hourly.json b/tests/components/climacell/fixtures/v3_forecast_hourly.json similarity index 100% rename from tests/fixtures/climacell/v3_forecast_hourly.json rename to tests/components/climacell/fixtures/v3_forecast_hourly.json diff --git a/tests/fixtures/climacell/v3_forecast_nowcast.json b/tests/components/climacell/fixtures/v3_forecast_nowcast.json similarity index 100% rename from tests/fixtures/climacell/v3_forecast_nowcast.json rename to tests/components/climacell/fixtures/v3_forecast_nowcast.json diff --git a/tests/fixtures/climacell/v3_realtime.json b/tests/components/climacell/fixtures/v3_realtime.json similarity index 100% rename from tests/fixtures/climacell/v3_realtime.json rename to tests/components/climacell/fixtures/v3_realtime.json diff --git a/tests/fixtures/climacell/v4.json b/tests/components/climacell/fixtures/v4.json similarity index 100% rename from tests/fixtures/climacell/v4.json rename to tests/components/climacell/fixtures/v4.json diff --git a/tests/fixtures/command_line/configuration.yaml b/tests/components/command_line/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/command_line/configuration.yaml rename to tests/components/command_line/fixtures/configuration.yaml diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index d0b36d31c37..0a37449c184 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: @@ -133,11 +133,7 @@ async def test_reload(hass: HomeAssistant) -> None: assert entity_state assert entity_state.state == "unknown" - yaml_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - "fixtures", - "command_line/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "command_line") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "command_line", diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py index 16c6f4b4d45..acf03725d32 100644 --- a/tests/components/dexcom/__init__.py +++ b/tests/components/dexcom/__init__.py @@ -16,7 +16,7 @@ CONFIG = { CONF_SERVER: SERVER_US, } -GLUCOSE_READING = GlucoseReading(json.loads(load_fixture("dexcom_data.json"))) +GLUCOSE_READING = GlucoseReading(json.loads(load_fixture("data.json", "dexcom"))) async def init_integration(hass) -> MockConfigEntry: diff --git a/tests/fixtures/dexcom_data.json b/tests/components/dexcom/fixtures/data.json similarity index 100% rename from tests/fixtures/dexcom_data.json rename to tests/components/dexcom/fixtures/data.json diff --git a/tests/fixtures/directv/info-get-locations.json b/tests/components/directv/fixtures/info-get-locations.json similarity index 100% rename from tests/fixtures/directv/info-get-locations.json rename to tests/components/directv/fixtures/info-get-locations.json diff --git a/tests/fixtures/directv/info-get-version.json b/tests/components/directv/fixtures/info-get-version.json similarity index 100% rename from tests/fixtures/directv/info-get-version.json rename to tests/components/directv/fixtures/info-get-version.json diff --git a/tests/fixtures/directv/info-mode-error.json b/tests/components/directv/fixtures/info-mode-error.json similarity index 100% rename from tests/fixtures/directv/info-mode-error.json rename to tests/components/directv/fixtures/info-mode-error.json diff --git a/tests/fixtures/directv/info-mode-standby.json b/tests/components/directv/fixtures/info-mode-standby.json similarity index 100% rename from tests/fixtures/directv/info-mode-standby.json rename to tests/components/directv/fixtures/info-mode-standby.json diff --git a/tests/fixtures/directv/info-mode.json b/tests/components/directv/fixtures/info-mode.json similarity index 100% rename from tests/fixtures/directv/info-mode.json rename to tests/components/directv/fixtures/info-mode.json diff --git a/tests/fixtures/directv/remote-process-key.json b/tests/components/directv/fixtures/remote-process-key.json similarity index 100% rename from tests/fixtures/directv/remote-process-key.json rename to tests/components/directv/fixtures/remote-process-key.json diff --git a/tests/fixtures/directv/tv-get-tuned-movie.json b/tests/components/directv/fixtures/tv-get-tuned-movie.json similarity index 100% rename from tests/fixtures/directv/tv-get-tuned-movie.json rename to tests/components/directv/fixtures/tv-get-tuned-movie.json diff --git a/tests/fixtures/directv/tv-get-tuned-music.json b/tests/components/directv/fixtures/tv-get-tuned-music.json similarity index 100% rename from tests/fixtures/directv/tv-get-tuned-music.json rename to tests/components/directv/fixtures/tv-get-tuned-music.json diff --git a/tests/fixtures/directv/tv-get-tuned-restricted.json b/tests/components/directv/fixtures/tv-get-tuned-restricted.json similarity index 100% rename from tests/fixtures/directv/tv-get-tuned-restricted.json rename to tests/components/directv/fixtures/tv-get-tuned-restricted.json diff --git a/tests/fixtures/directv/tv-get-tuned.json b/tests/components/directv/fixtures/tv-get-tuned.json similarity index 100% rename from tests/fixtures/directv/tv-get-tuned.json rename to tests/components/directv/fixtures/tv-get-tuned.json diff --git a/tests/fixtures/directv/tv-tune.json b/tests/components/directv/fixtures/tv-tune.json similarity index 100% rename from tests/fixtures/directv/tv-tune.json rename to tests/components/directv/fixtures/tv-tune.json diff --git a/tests/fixtures/ecobee/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json similarity index 100% rename from tests/fixtures/ecobee/ecobee-data.json rename to tests/components/ecobee/fixtures/ecobee-data.json diff --git a/tests/fixtures/ecobee/ecobee-token.json b/tests/components/ecobee/fixtures/ecobee-token.json similarity index 100% rename from tests/fixtures/ecobee/ecobee-token.json rename to tests/components/ecobee/fixtures/ecobee-token.json diff --git a/tests/fixtures/efergy/budget.json b/tests/components/efergy/fixtures/budget.json similarity index 100% rename from tests/fixtures/efergy/budget.json rename to tests/components/efergy/fixtures/budget.json diff --git a/tests/fixtures/efergy/current_values_multi.json b/tests/components/efergy/fixtures/current_values_multi.json similarity index 100% rename from tests/fixtures/efergy/current_values_multi.json rename to tests/components/efergy/fixtures/current_values_multi.json diff --git a/tests/fixtures/efergy/current_values_single.json b/tests/components/efergy/fixtures/current_values_single.json similarity index 100% rename from tests/fixtures/efergy/current_values_single.json rename to tests/components/efergy/fixtures/current_values_single.json diff --git a/tests/fixtures/efergy/daily_cost.json b/tests/components/efergy/fixtures/daily_cost.json similarity index 100% rename from tests/fixtures/efergy/daily_cost.json rename to tests/components/efergy/fixtures/daily_cost.json diff --git a/tests/fixtures/efergy/daily_energy.json b/tests/components/efergy/fixtures/daily_energy.json similarity index 100% rename from tests/fixtures/efergy/daily_energy.json rename to tests/components/efergy/fixtures/daily_energy.json diff --git a/tests/fixtures/efergy/instant.json b/tests/components/efergy/fixtures/instant.json similarity index 100% rename from tests/fixtures/efergy/instant.json rename to tests/components/efergy/fixtures/instant.json diff --git a/tests/fixtures/efergy/monthly_cost.json b/tests/components/efergy/fixtures/monthly_cost.json similarity index 100% rename from tests/fixtures/efergy/monthly_cost.json rename to tests/components/efergy/fixtures/monthly_cost.json diff --git a/tests/fixtures/efergy/monthly_energy.json b/tests/components/efergy/fixtures/monthly_energy.json similarity index 100% rename from tests/fixtures/efergy/monthly_energy.json rename to tests/components/efergy/fixtures/monthly_energy.json diff --git a/tests/fixtures/efergy/status.json b/tests/components/efergy/fixtures/status.json similarity index 100% rename from tests/fixtures/efergy/status.json rename to tests/components/efergy/fixtures/status.json diff --git a/tests/fixtures/efergy/weekly_cost.json b/tests/components/efergy/fixtures/weekly_cost.json similarity index 100% rename from tests/fixtures/efergy/weekly_cost.json rename to tests/components/efergy/fixtures/weekly_cost.json diff --git a/tests/fixtures/efergy/weekly_energy.json b/tests/components/efergy/fixtures/weekly_energy.json similarity index 100% rename from tests/fixtures/efergy/weekly_energy.json rename to tests/components/efergy/fixtures/weekly_energy.json diff --git a/tests/fixtures/efergy/yearly_cost.json b/tests/components/efergy/fixtures/yearly_cost.json similarity index 100% rename from tests/fixtures/efergy/yearly_cost.json rename to tests/components/efergy/fixtures/yearly_cost.json diff --git a/tests/fixtures/efergy/yearly_energy.json b/tests/components/efergy/fixtures/yearly_energy.json similarity index 100% rename from tests/fixtures/efergy/yearly_energy.json rename to tests/components/efergy/fixtures/yearly_energy.json diff --git a/tests/fixtures/elgato/info.json b/tests/components/elgato/fixtures/info.json similarity index 100% rename from tests/fixtures/elgato/info.json rename to tests/components/elgato/fixtures/info.json diff --git a/tests/fixtures/elgato/settings-color.json b/tests/components/elgato/fixtures/settings-color.json similarity index 100% rename from tests/fixtures/elgato/settings-color.json rename to tests/components/elgato/fixtures/settings-color.json diff --git a/tests/fixtures/elgato/settings.json b/tests/components/elgato/fixtures/settings.json similarity index 100% rename from tests/fixtures/elgato/settings.json rename to tests/components/elgato/fixtures/settings.json diff --git a/tests/fixtures/elgato/state-color.json b/tests/components/elgato/fixtures/state-color.json similarity index 100% rename from tests/fixtures/elgato/state-color.json rename to tests/components/elgato/fixtures/state-color.json diff --git a/tests/fixtures/elgato/state.json b/tests/components/elgato/fixtures/state.json similarity index 100% rename from tests/fixtures/elgato/state.json rename to tests/components/elgato/fixtures/state.json diff --git a/tests/fixtures/filesize/configuration.yaml b/tests/components/filesize/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/filesize/configuration.yaml rename to tests/components/filesize/fixtures/configuration.yaml diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 8bb79a8088a..72d0d112f17 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -10,6 +10,8 @@ from homeassistant.components.filesize.sensor import CONF_FILE_PATHS from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") @@ -70,11 +72,7 @@ async def test_reload(hass, tmpdir): assert hass.states.get("sensor.file") - yaml_path = os.path.join( - _get_fixtures_base_path(), - "fixtures", - "filesize/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "filesize") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch.object( hass.config, "is_allowed_path", return_value=True ): @@ -87,7 +85,3 @@ async def test_reload(hass, tmpdir): await hass.async_block_till_done() assert hass.states.get("sensor.file") is None - - -def _get_fixtures_base_path(): - return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) diff --git a/tests/fixtures/filter/configuration.yaml b/tests/components/filter/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/filter/configuration.yaml rename to tests/components/filter/fixtures/configuration.yaml diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 89e8758c661..ec831b79670 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,6 +1,5 @@ """The test for the data filter sensor platform.""" from datetime import timedelta -from os import path from unittest.mock import patch from pytest import fixture @@ -30,7 +29,11 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import assert_setup_component, async_init_recorder_component +from tests.common import ( + assert_setup_component, + async_init_recorder_component, + get_fixture_path, +) @fixture @@ -184,11 +187,7 @@ async def test_source_state_none(hass, values): assert state.state == "0.0" # Force Template Reload - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "template/sensor_configuration.yaml", - ) + yaml_path = get_fixture_path("sensor_configuration.yaml", "template") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "template", @@ -478,11 +477,8 @@ async def test_reload(hass): assert hass.states.get("sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "filter/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "filter") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -496,7 +492,3 @@ async def test_reload(hass): assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.filtered_realistic_humidity") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/flo/device_info_response.json b/tests/components/flo/fixtures/device_info_response.json similarity index 100% rename from tests/fixtures/flo/device_info_response.json rename to tests/components/flo/fixtures/device_info_response.json diff --git a/tests/fixtures/flo/device_info_response_closed.json b/tests/components/flo/fixtures/device_info_response_closed.json similarity index 100% rename from tests/fixtures/flo/device_info_response_closed.json rename to tests/components/flo/fixtures/device_info_response_closed.json diff --git a/tests/fixtures/flo/device_info_response_detector.json b/tests/components/flo/fixtures/device_info_response_detector.json similarity index 100% rename from tests/fixtures/flo/device_info_response_detector.json rename to tests/components/flo/fixtures/device_info_response_detector.json diff --git a/tests/fixtures/flo/location_info_base_response.json b/tests/components/flo/fixtures/location_info_base_response.json similarity index 100% rename from tests/fixtures/flo/location_info_base_response.json rename to tests/components/flo/fixtures/location_info_base_response.json diff --git a/tests/fixtures/flo/location_info_expand_devices_response.json b/tests/components/flo/fixtures/location_info_expand_devices_response.json similarity index 100% rename from tests/fixtures/flo/location_info_expand_devices_response.json rename to tests/components/flo/fixtures/location_info_expand_devices_response.json diff --git a/tests/fixtures/flo/user_info_base_response.json b/tests/components/flo/fixtures/user_info_base_response.json similarity index 100% rename from tests/fixtures/flo/user_info_base_response.json rename to tests/components/flo/fixtures/user_info_base_response.json diff --git a/tests/fixtures/flo/user_info_expand_locations_response.json b/tests/components/flo/fixtures/user_info_expand_locations_response.json similarity index 100% rename from tests/fixtures/flo/user_info_expand_locations_response.json rename to tests/components/flo/fixtures/user_info_expand_locations_response.json diff --git a/tests/fixtures/flo/water_consumption_info_response.json b/tests/components/flo/fixtures/water_consumption_info_response.json similarity index 100% rename from tests/fixtures/flo/water_consumption_info_response.json rename to tests/components/flo/fixtures/water_consumption_info_response.json diff --git a/tests/fixtures/foobot_data.json b/tests/components/foobot/fixtures/data.json similarity index 100% rename from tests/fixtures/foobot_data.json rename to tests/components/foobot/fixtures/data.json diff --git a/tests/fixtures/foobot_devices.json b/tests/components/foobot/fixtures/devices.json similarity index 100% rename from tests/fixtures/foobot_devices.json rename to tests/components/foobot/fixtures/devices.json diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index ec19fb7b94f..7ffa3987110 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -32,10 +32,11 @@ async def test_default_setup(hass, aioclient_mock): """Test the default setup.""" aioclient_mock.get( re.compile("api.foobot.io/v2/owner/.*"), - text=load_fixture("foobot_devices.json"), + text=load_fixture("devices.json", "foobot"), ) aioclient_mock.get( - re.compile("api.foobot.io/v2/device/.*"), text=load_fixture("foobot_data.json") + re.compile("api.foobot.io/v2/device/.*"), + text=load_fixture("data.json", "foobot"), ) assert await async_setup_component(hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) await hass.async_block_till_done() diff --git a/tests/fixtures/generic/configuration.yaml b/tests/components/generic/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/generic/configuration.yaml rename to tests/components/generic/fixtures/configuration.yaml diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 36a9304cdc1..9f72520105e 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,7 +1,6 @@ """The tests for generic camera component.""" import asyncio from http import HTTPStatus -from os import path from unittest.mock import patch import httpx @@ -13,6 +12,8 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + @respx.mock async def test_fetching_url(hass, hass_client): @@ -379,11 +380,8 @@ async def test_reloading(hass, hass_client): body = await resp.text() assert body == "hello world" - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "generic/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "generic") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -456,7 +454,3 @@ async def test_timeout_cancelled(hass, hass_client): assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK assert await resp.text() == "hello world" - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/generic_thermostat/configuration.yaml b/tests/components/generic_thermostat/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/generic_thermostat/configuration.yaml rename to tests/components/generic_thermostat/fixtures/configuration.yaml diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 4015887efbb..a1896c94d2f 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,6 +1,5 @@ """The tests for the generic_thermostat.""" import datetime -from os import path from unittest.mock import patch import pytest @@ -43,6 +42,7 @@ from tests.common import ( assert_setup_component, async_fire_time_changed, async_mock_service, + get_fixture_path, mock_restore_cache, ) from tests.components.climate import common @@ -1486,11 +1486,7 @@ async def test_reload(hass): assert len(hass.states.async_all()) == 1 assert hass.states.get("climate.test") is not None - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "generic_thermostat/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "generic_thermostat") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( GENERIC_THERMOSTAT_DOMAIN, @@ -1503,7 +1499,3 @@ async def test_reload(hass): assert len(hass.states.async_all()) == 1 assert hass.states.get("climate.test") is None assert hass.states.get("climate.reload") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/gios/indexes.json b/tests/components/gios/fixtures/indexes.json similarity index 100% rename from tests/fixtures/gios/indexes.json rename to tests/components/gios/fixtures/indexes.json diff --git a/tests/fixtures/gios/sensors.json b/tests/components/gios/fixtures/sensors.json similarity index 100% rename from tests/fixtures/gios/sensors.json rename to tests/components/gios/fixtures/sensors.json diff --git a/tests/fixtures/gios/station.json b/tests/components/gios/fixtures/station.json similarity index 100% rename from tests/fixtures/gios/station.json rename to tests/components/gios/fixtures/station.json diff --git a/tests/fixtures/google_maps_elevation.json b/tests/components/google/fixtures/maps_elevation.json similarity index 100% rename from tests/fixtures/google_maps_elevation.json rename to tests/components/google/fixtures/maps_elevation.json diff --git a/tests/fixtures/group/configuration.yaml b/tests/components/group/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/group/configuration.yaml rename to tests/components/group/fixtures/configuration.yaml diff --git a/tests/fixtures/group/fan_configuration.yaml b/tests/components/group/fixtures/fan_configuration.yaml similarity index 100% rename from tests/fixtures/group/fan_configuration.yaml rename to tests/components/group/fixtures/fan_configuration.yaml diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 7bf62a16a42..0738c31eb62 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -1,6 +1,4 @@ """The tests for the Group Binary Sensor platform.""" -from os import path - from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.group import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -145,7 +143,3 @@ async def test_state_reporting_any(hass): entry = entity_registry.async_get("binary_sensor.binary_sensor_group") assert entry assert entry.unique_id == "unique_identifier" - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 10770e3de06..abb1dcf245a 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,5 +1,4 @@ """The tests for the group fan platform.""" -from os import path from unittest.mock import patch import pytest @@ -38,7 +37,7 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from tests.common import assert_setup_component, get_fixture_path FAN_GROUP = "fan.fan_group" @@ -379,11 +378,7 @@ async def test_reload(hass, setup_comp): await hass.async_block_till_done() assert hass.states.get(FAN_GROUP).state == STATE_OFF - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "group/fan_configuration.yaml", - ) + yaml_path = get_fixture_path("fan_configuration.yaml", "group") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "group", @@ -397,10 +392,6 @@ async def test_reload(hass, setup_comp): assert hass.states.get("fan.upstairs_fans") is not None -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) - - @pytest.mark.parametrize("config_count", [(CONFIG_FULL_SUPPORT, 2)]) async def test_service_calls(hass, setup_comp): """Test calling services.""" diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index e1a45d6fe53..843f15c7113 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,4 @@ """The tests for the Group Light platform.""" -from os import path import unittest.mock from unittest.mock import MagicMock, patch @@ -53,6 +52,8 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_default_state(hass): """Test light group default state.""" @@ -1379,11 +1380,7 @@ async def test_reload(hass): await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_ON - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "group/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "group") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -1421,11 +1418,7 @@ async def test_reload_with_platform_not_setup(hass): ) await hass.async_block_till_done() - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "group/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "group") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -1458,11 +1451,7 @@ async def test_reload_with_base_integration_platform_not_setup(hass): hass.states.async_set("light.outside_patio_lights", STATE_OFF) hass.states.async_set("light.outside_patio_lights_2", STATE_OFF) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "group/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "group") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -1513,7 +1502,3 @@ async def test_nested_group(hass): assert state is not None assert state.state == STATE_ON assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom_group"] - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index a0f210c68a2..ad0be58dd16 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,5 +1,4 @@ """The tests for the notify.group platform.""" -from os import path from unittest.mock import MagicMock, patch from homeassistant import config as hass_config @@ -9,6 +8,8 @@ import homeassistant.components.group.notify as group import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_send_message_with_data(hass): """Test sending a message with to a notify group.""" @@ -110,11 +111,8 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, "demo2") assert hass.services.has_service(notify.DOMAIN, "group_notify") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "group/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "group", @@ -128,7 +126,3 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, "demo2") assert not hass.services.has_service(notify.DOMAIN, "group_notify") assert hass.services.has_service(notify.DOMAIN, "new_group_notify") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/here_travel_time/attribution_response.json b/tests/components/here_travel_time/fixtures/attribution_response.json similarity index 100% rename from tests/fixtures/here_travel_time/attribution_response.json rename to tests/components/here_travel_time/fixtures/attribution_response.json diff --git a/tests/fixtures/here_travel_time/bike_response.json b/tests/components/here_travel_time/fixtures/bike_response.json similarity index 100% rename from tests/fixtures/here_travel_time/bike_response.json rename to tests/components/here_travel_time/fixtures/bike_response.json diff --git a/tests/fixtures/here_travel_time/car_enabled_response.json b/tests/components/here_travel_time/fixtures/car_enabled_response.json similarity index 100% rename from tests/fixtures/here_travel_time/car_enabled_response.json rename to tests/components/here_travel_time/fixtures/car_enabled_response.json diff --git a/tests/fixtures/here_travel_time/car_response.json b/tests/components/here_travel_time/fixtures/car_response.json similarity index 100% rename from tests/fixtures/here_travel_time/car_response.json rename to tests/components/here_travel_time/fixtures/car_response.json diff --git a/tests/fixtures/here_travel_time/car_shortest_response.json b/tests/components/here_travel_time/fixtures/car_shortest_response.json similarity index 100% rename from tests/fixtures/here_travel_time/car_shortest_response.json rename to tests/components/here_travel_time/fixtures/car_shortest_response.json diff --git a/tests/fixtures/here_travel_time/pedestrian_response.json b/tests/components/here_travel_time/fixtures/pedestrian_response.json similarity index 100% rename from tests/fixtures/here_travel_time/pedestrian_response.json rename to tests/components/here_travel_time/fixtures/pedestrian_response.json diff --git a/tests/fixtures/here_travel_time/public_response.json b/tests/components/here_travel_time/fixtures/public_response.json similarity index 100% rename from tests/fixtures/here_travel_time/public_response.json rename to tests/components/here_travel_time/fixtures/public_response.json diff --git a/tests/fixtures/here_travel_time/public_time_table_response.json b/tests/components/here_travel_time/fixtures/public_time_table_response.json similarity index 100% rename from tests/fixtures/here_travel_time/public_time_table_response.json rename to tests/components/here_travel_time/fixtures/public_time_table_response.json diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json similarity index 100% rename from tests/fixtures/here_travel_time/routing_error_invalid_credentials.json rename to tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json diff --git a/tests/fixtures/here_travel_time/routing_error_no_route_found.json b/tests/components/here_travel_time/fixtures/routing_error_no_route_found.json similarity index 100% rename from tests/fixtures/here_travel_time/routing_error_no_route_found.json rename to tests/components/here_travel_time/fixtures/routing_error_no_route_found.json diff --git a/tests/fixtures/here_travel_time/truck_response.json b/tests/components/here_travel_time/fixtures/truck_response.json similarity index 100% rename from tests/fixtures/here_travel_time/truck_response.json rename to tests/components/here_travel_time/fixtures/truck_response.json diff --git a/tests/fixtures/history_stats/configuration.yaml b/tests/components/history_stats/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/history_stats/configuration.yaml rename to tests/components/history_stats/fixtures/configuration.yaml diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 01ce5bf06b3..105943d4444 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,7 +1,6 @@ """The test for the History Statistics sensor platform.""" # pylint: disable=protected-access from datetime import datetime, timedelta -from os import path import unittest from unittest.mock import patch @@ -18,6 +17,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( async_init_recorder_component, + get_fixture_path, get_test_home_assistant, init_recorder_component, ) @@ -253,11 +253,7 @@ async def test_reload(hass): assert hass.states.get("sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "history_stats/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "history_stats") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -427,7 +423,3 @@ async def async_test_measure(hass): assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "50.0" - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/homekit/configuration.yaml b/tests/components/homekit/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/homekit/configuration.yaml rename to tests/components/homekit/fixtures/configuration.yaml diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index e77980d4c2c..58aa9741fa3 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -68,7 +68,7 @@ from homeassistant.util import json as json_util from .util import PATH_HOMEKIT, async_init_entry, async_init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, get_fixture_path IP_ADDRESS = "127.0.0.1" @@ -1536,11 +1536,7 @@ async def test_reload(hass, mock_zeroconf): entry.title, devices=[], ) - yaml_path = os.path.join( - _get_fixtures_base_path(), - "fixtures", - "homekit/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "homekit") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( f"{PATH_HOMEKIT}.HomeKit" ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( @@ -1577,10 +1573,6 @@ async def test_reload(hass, mock_zeroconf): ) -def _get_fixtures_base_path(): - return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - - async def test_homekit_start_in_accessory_mode( hass, hk_driver, mock_zeroconf, device_reg ): diff --git a/tests/fixtures/homekit_controller/anker_eufycam.json b/tests/components/homekit_controller/fixtures/anker_eufycam.json similarity index 100% rename from tests/fixtures/homekit_controller/anker_eufycam.json rename to tests/components/homekit_controller/fixtures/anker_eufycam.json diff --git a/tests/fixtures/homekit_controller/aqara_gateway.json b/tests/components/homekit_controller/fixtures/aqara_gateway.json similarity index 100% rename from tests/fixtures/homekit_controller/aqara_gateway.json rename to tests/components/homekit_controller/fixtures/aqara_gateway.json diff --git a/tests/fixtures/homekit_controller/aqara_switch.json b/tests/components/homekit_controller/fixtures/aqara_switch.json similarity index 100% rename from tests/fixtures/homekit_controller/aqara_switch.json rename to tests/components/homekit_controller/fixtures/aqara_switch.json diff --git a/tests/fixtures/homekit_controller/arlo_baby.json b/tests/components/homekit_controller/fixtures/arlo_baby.json similarity index 100% rename from tests/fixtures/homekit_controller/arlo_baby.json rename to tests/components/homekit_controller/fixtures/arlo_baby.json diff --git a/tests/fixtures/homekit_controller/ecobee3.json b/tests/components/homekit_controller/fixtures/ecobee3.json similarity index 100% rename from tests/fixtures/homekit_controller/ecobee3.json rename to tests/components/homekit_controller/fixtures/ecobee3.json diff --git a/tests/fixtures/homekit_controller/ecobee3_no_sensors.json b/tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json similarity index 100% rename from tests/fixtures/homekit_controller/ecobee3_no_sensors.json rename to tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json diff --git a/tests/fixtures/homekit_controller/ecobee_occupancy.json b/tests/components/homekit_controller/fixtures/ecobee_occupancy.json similarity index 100% rename from tests/fixtures/homekit_controller/ecobee_occupancy.json rename to tests/components/homekit_controller/fixtures/ecobee_occupancy.json diff --git a/tests/fixtures/homekit_controller/eve_degree.json b/tests/components/homekit_controller/fixtures/eve_degree.json similarity index 100% rename from tests/fixtures/homekit_controller/eve_degree.json rename to tests/components/homekit_controller/fixtures/eve_degree.json diff --git a/tests/fixtures/homekit_controller/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan.json similarity index 100% rename from tests/fixtures/homekit_controller/haa_fan.json rename to tests/components/homekit_controller/fixtures/haa_fan.json diff --git a/tests/fixtures/homekit_controller/home_assistant_bridge_fan.json b/tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json similarity index 100% rename from tests/fixtures/homekit_controller/home_assistant_bridge_fan.json rename to tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json diff --git a/tests/fixtures/homekit_controller/hue_bridge.json b/tests/components/homekit_controller/fixtures/hue_bridge.json similarity index 100% rename from tests/fixtures/homekit_controller/hue_bridge.json rename to tests/components/homekit_controller/fixtures/hue_bridge.json diff --git a/tests/fixtures/homekit_controller/koogeek_ls1.json b/tests/components/homekit_controller/fixtures/koogeek_ls1.json similarity index 100% rename from tests/fixtures/homekit_controller/koogeek_ls1.json rename to tests/components/homekit_controller/fixtures/koogeek_ls1.json diff --git a/tests/fixtures/homekit_controller/koogeek_p1eu.json b/tests/components/homekit_controller/fixtures/koogeek_p1eu.json similarity index 100% rename from tests/fixtures/homekit_controller/koogeek_p1eu.json rename to tests/components/homekit_controller/fixtures/koogeek_p1eu.json diff --git a/tests/fixtures/homekit_controller/koogeek_sw2.json b/tests/components/homekit_controller/fixtures/koogeek_sw2.json similarity index 100% rename from tests/fixtures/homekit_controller/koogeek_sw2.json rename to tests/components/homekit_controller/fixtures/koogeek_sw2.json diff --git a/tests/fixtures/homekit_controller/lennox_e30.json b/tests/components/homekit_controller/fixtures/lennox_e30.json similarity index 100% rename from tests/fixtures/homekit_controller/lennox_e30.json rename to tests/components/homekit_controller/fixtures/lennox_e30.json diff --git a/tests/fixtures/homekit_controller/lg_tv.json b/tests/components/homekit_controller/fixtures/lg_tv.json similarity index 100% rename from tests/fixtures/homekit_controller/lg_tv.json rename to tests/components/homekit_controller/fixtures/lg_tv.json diff --git a/tests/fixtures/homekit_controller/mysa_living.json b/tests/components/homekit_controller/fixtures/mysa_living.json similarity index 100% rename from tests/fixtures/homekit_controller/mysa_living.json rename to tests/components/homekit_controller/fixtures/mysa_living.json diff --git a/tests/fixtures/homekit_controller/netamo_doorbell.json b/tests/components/homekit_controller/fixtures/netamo_doorbell.json similarity index 100% rename from tests/fixtures/homekit_controller/netamo_doorbell.json rename to tests/components/homekit_controller/fixtures/netamo_doorbell.json diff --git a/tests/fixtures/homekit_controller/rainmachine-pro-8.json b/tests/components/homekit_controller/fixtures/rainmachine-pro-8.json similarity index 100% rename from tests/fixtures/homekit_controller/rainmachine-pro-8.json rename to tests/components/homekit_controller/fixtures/rainmachine-pro-8.json diff --git a/tests/fixtures/homekit_controller/ryse_smart_bridge.json b/tests/components/homekit_controller/fixtures/ryse_smart_bridge.json similarity index 100% rename from tests/fixtures/homekit_controller/ryse_smart_bridge.json rename to tests/components/homekit_controller/fixtures/ryse_smart_bridge.json diff --git a/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json b/tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json similarity index 100% rename from tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json rename to tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json diff --git a/tests/fixtures/homekit_controller/simpleconnect_fan.json b/tests/components/homekit_controller/fixtures/simpleconnect_fan.json similarity index 100% rename from tests/fixtures/homekit_controller/simpleconnect_fan.json rename to tests/components/homekit_controller/fixtures/simpleconnect_fan.json diff --git a/tests/fixtures/homekit_controller/velux_gateway.json b/tests/components/homekit_controller/fixtures/velux_gateway.json similarity index 100% rename from tests/fixtures/homekit_controller/velux_gateway.json rename to tests/components/homekit_controller/fixtures/velux_gateway.json diff --git a/tests/fixtures/homekit_controller/vocolinc_flowerbud.json b/tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json similarity index 100% rename from tests/fixtures/homekit_controller/vocolinc_flowerbud.json rename to tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json diff --git a/tests/fixtures/hunterdouglas_powerview/fwversion.json b/tests/components/hunterdouglas_powerview/fixtures/fwversion.json similarity index 100% rename from tests/fixtures/hunterdouglas_powerview/fwversion.json rename to tests/components/hunterdouglas_powerview/fixtures/fwversion.json diff --git a/tests/fixtures/hunterdouglas_powerview/userdata.json b/tests/components/hunterdouglas_powerview/fixtures/userdata.json similarity index 100% rename from tests/fixtures/hunterdouglas_powerview/userdata.json rename to tests/components/hunterdouglas_powerview/fixtures/userdata.json diff --git a/tests/fixtures/hunterdouglas_powerview/userdata_v1.json b/tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json similarity index 100% rename from tests/fixtures/hunterdouglas_powerview/userdata_v1.json rename to tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json diff --git a/tests/fixtures/hvv_departures/check_name.json b/tests/components/hvv_departures/fixtures/check_name.json similarity index 100% rename from tests/fixtures/hvv_departures/check_name.json rename to tests/components/hvv_departures/fixtures/check_name.json diff --git a/tests/fixtures/hvv_departures/config_entry.json b/tests/components/hvv_departures/fixtures/config_entry.json similarity index 100% rename from tests/fixtures/hvv_departures/config_entry.json rename to tests/components/hvv_departures/fixtures/config_entry.json diff --git a/tests/fixtures/hvv_departures/departure_list.json b/tests/components/hvv_departures/fixtures/departure_list.json similarity index 100% rename from tests/fixtures/hvv_departures/departure_list.json rename to tests/components/hvv_departures/fixtures/departure_list.json diff --git a/tests/fixtures/hvv_departures/init.json b/tests/components/hvv_departures/fixtures/init.json similarity index 100% rename from tests/fixtures/hvv_departures/init.json rename to tests/components/hvv_departures/fixtures/init.json diff --git a/tests/fixtures/hvv_departures/options.json b/tests/components/hvv_departures/fixtures/options.json similarity index 100% rename from tests/fixtures/hvv_departures/options.json rename to tests/components/hvv_departures/fixtures/options.json diff --git a/tests/fixtures/hvv_departures/station_information.json b/tests/components/hvv_departures/fixtures/station_information.json similarity index 100% rename from tests/fixtures/hvv_departures/station_information.json rename to tests/components/hvv_departures/fixtures/station_information.json diff --git a/tests/fixtures/insteon/aldb_data.json b/tests/components/insteon/fixtures/aldb_data.json similarity index 100% rename from tests/fixtures/insteon/aldb_data.json rename to tests/components/insteon/fixtures/aldb_data.json diff --git a/tests/fixtures/insteon/kpl_properties.json b/tests/components/insteon/fixtures/kpl_properties.json similarity index 100% rename from tests/fixtures/insteon/kpl_properties.json rename to tests/components/insteon/fixtures/kpl_properties.json diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index 1e269438ad5..ec1001b5772 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -1,6 +1,4 @@ """Tests for the IPP integration.""" -import os - import aiohttp from pyipp import IPPConnectionUpgradeRequired, IPPError @@ -15,7 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, get_fixture_path from tests.test_util.aiohttp import AiohttpClientMocker ATTR_HOSTNAME = "hostname" @@ -63,9 +61,7 @@ MOCK_ZEROCONF_IPPS_SERVICE_INFO = { def load_fixture_binary(filename): """Load a binary fixture.""" - path = os.path.join(os.path.dirname(__file__), "..", "..", "fixtures", filename) - with open(path, "rb") as fptr: - return fptr.read() + return get_fixture_path(filename, "ipp").read_bytes() def mock_connection( @@ -97,11 +93,11 @@ def mock_connection( aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPConnectionUpgradeRequired) return - fixture = "ipp/get-printer-attributes.bin" + fixture = "get-printer-attributes.bin" if no_unique_id: - fixture = "ipp/get-printer-attributes-success-nodata.bin" + fixture = "get-printer-attributes-success-nodata.bin" elif version_not_supported: - fixture = "ipp/get-printer-attributes-error-0x0503.bin" + fixture = "get-printer-attributes-error-0x0503.bin" if parse_error: content = "BAD" diff --git a/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin b/tests/components/ipp/fixtures/get-printer-attributes-error-0x0503.bin similarity index 100% rename from tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin rename to tests/components/ipp/fixtures/get-printer-attributes-error-0x0503.bin diff --git a/tests/fixtures/ipp/get-printer-attributes-success-nodata.bin b/tests/components/ipp/fixtures/get-printer-attributes-success-nodata.bin similarity index 100% rename from tests/fixtures/ipp/get-printer-attributes-success-nodata.bin rename to tests/components/ipp/fixtures/get-printer-attributes-success-nodata.bin diff --git a/tests/fixtures/ipp/get-printer-attributes.bin b/tests/components/ipp/fixtures/get-printer-attributes.bin similarity index 100% rename from tests/fixtures/ipp/get-printer-attributes.bin rename to tests/components/ipp/fixtures/get-printer-attributes.bin diff --git a/tests/fixtures/lcn/config.json b/tests/components/lcn/fixtures/config.json similarity index 100% rename from tests/fixtures/lcn/config.json rename to tests/components/lcn/fixtures/config.json diff --git a/tests/fixtures/lcn/config_entry_myhome.json b/tests/components/lcn/fixtures/config_entry_myhome.json similarity index 100% rename from tests/fixtures/lcn/config_entry_myhome.json rename to tests/components/lcn/fixtures/config_entry_myhome.json diff --git a/tests/fixtures/lcn/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json similarity index 100% rename from tests/fixtures/lcn/config_entry_pchk.json rename to tests/components/lcn/fixtures/config_entry_pchk.json diff --git a/tests/fixtures/mazda/get_vehicle_status.json b/tests/components/mazda/fixtures/get_vehicle_status.json similarity index 100% rename from tests/fixtures/mazda/get_vehicle_status.json rename to tests/components/mazda/fixtures/get_vehicle_status.json diff --git a/tests/fixtures/mazda/get_vehicles.json b/tests/components/mazda/fixtures/get_vehicles.json similarity index 100% rename from tests/fixtures/mazda/get_vehicles.json rename to tests/components/mazda/fixtures/get_vehicles.json diff --git a/tests/fixtures/melissa_cur_settings.json b/tests/components/melissa/fixtures/cur_settings.json similarity index 100% rename from tests/fixtures/melissa_cur_settings.json rename to tests/components/melissa/fixtures/cur_settings.json diff --git a/tests/fixtures/melissa_fetch_devices.json b/tests/components/melissa/fixtures/fetch_devices.json similarity index 100% rename from tests/fixtures/melissa_fetch_devices.json rename to tests/components/melissa/fixtures/fetch_devices.json diff --git a/tests/fixtures/melissa_status.json b/tests/components/melissa/fixtures/status.json similarity index 100% rename from tests/fixtures/melissa_status.json rename to tests/components/melissa/fixtures/status.json diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 590c5149f9e..224a878b065 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -25,13 +25,13 @@ def melissa_mock(): """Use this to mock the melissa api.""" api = Mock() api.async_fetch_devices = AsyncMock( - return_value=json.loads(load_fixture("melissa_fetch_devices.json")) + return_value=json.loads(load_fixture("fetch_devices.json", "melissa")) ) api.async_status = AsyncMock( - return_value=json.loads(load_fixture("melissa_status.json")) + return_value=json.loads(load_fixture("status.json", "melissa")) ) api.async_cur_settings = AsyncMock( - return_value=json.loads(load_fixture("melissa_cur_settings.json")) + return_value=json.loads(load_fixture("cur_settings.json", "melissa")) ) api.async_send = AsyncMock(return_value=True) diff --git a/tests/fixtures/min_max/configuration.yaml b/tests/components/min_max/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/min_max/configuration.yaml rename to tests/components/min_max/fixtures/configuration.yaml diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index c6c352a8c3e..1712a9027ca 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,5 +1,4 @@ """The test for the min/max sensor platform.""" -from os import path import statistics from unittest.mock import patch @@ -16,6 +15,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + VALUES = [17, 20, 15.3] COUNT = len(VALUES) MIN_VALUE = min(VALUES) @@ -365,11 +366,8 @@ async def test_reload(hass): assert hass.states.get("sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "min_max/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "min_max") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -383,7 +381,3 @@ async def test_reload(hass): assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.second_test") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/modern_forms/device_info.json b/tests/components/modern_forms/fixtures/device_info.json similarity index 100% rename from tests/fixtures/modern_forms/device_info.json rename to tests/components/modern_forms/fixtures/device_info.json diff --git a/tests/fixtures/modern_forms/device_info_no_light.json b/tests/components/modern_forms/fixtures/device_info_no_light.json similarity index 100% rename from tests/fixtures/modern_forms/device_info_no_light.json rename to tests/components/modern_forms/fixtures/device_info_no_light.json diff --git a/tests/fixtures/modern_forms/device_status.json b/tests/components/modern_forms/fixtures/device_status.json similarity index 100% rename from tests/fixtures/modern_forms/device_status.json rename to tests/components/modern_forms/fixtures/device_status.json diff --git a/tests/fixtures/modern_forms/device_status_no_light.json b/tests/components/modern_forms/fixtures/device_status_no_light.json similarity index 100% rename from tests/fixtures/modern_forms/device_status_no_light.json rename to tests/components/modern_forms/fixtures/device_status_no_light.json diff --git a/tests/fixtures/modern_forms/device_status_timers_active.json b/tests/components/modern_forms/fixtures/device_status_timers_active.json similarity index 100% rename from tests/fixtures/modern_forms/device_status_timers_active.json rename to tests/components/modern_forms/fixtures/device_status_timers_active.json diff --git a/tests/fixtures/mqtt/configuration.yaml b/tests/components/mqtt/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/mqtt/configuration.yaml rename to tests/components/mqtt/fixtures/configuration.yaml diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index bf327796f57..58b607bb777 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,7 +153,6 @@ light: payload_off: "off" """ -from os import path from unittest.mock import call, patch import pytest @@ -198,7 +197,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message +from tests.common import ( + assert_setup_component, + async_fire_mqtt_message, + get_fixture_path, +) from tests.components.light import common DEFAULT_CONFIG = { @@ -3389,11 +3392,8 @@ async def test_reloadable(hass, mqtt_mock): assert hass.states.get("light.test") assert len(hass.states.async_all("light")) == 1 - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "mqtt/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "mqtt") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "mqtt", @@ -3407,7 +3407,3 @@ async def test_reloadable(hass, mqtt_mock): assert hass.states.get("light.test") is None assert hass.states.get("light.reload") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/myq/devices.json b/tests/components/myq/fixtures/devices.json similarity index 100% rename from tests/fixtures/myq/devices.json rename to tests/components/myq/fixtures/devices.json diff --git a/tests/fixtures/mysensors/distance_sensor_state.json b/tests/components/mysensors/fixtures/distance_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/distance_sensor_state.json rename to tests/components/mysensors/fixtures/distance_sensor_state.json diff --git a/tests/fixtures/mysensors/energy_sensor_state.json b/tests/components/mysensors/fixtures/energy_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/energy_sensor_state.json rename to tests/components/mysensors/fixtures/energy_sensor_state.json diff --git a/tests/fixtures/mysensors/gps_sensor_state.json b/tests/components/mysensors/fixtures/gps_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/gps_sensor_state.json rename to tests/components/mysensors/fixtures/gps_sensor_state.json diff --git a/tests/fixtures/mysensors/power_sensor_state.json b/tests/components/mysensors/fixtures/power_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/power_sensor_state.json rename to tests/components/mysensors/fixtures/power_sensor_state.json diff --git a/tests/fixtures/mysensors/sound_sensor_state.json b/tests/components/mysensors/fixtures/sound_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/sound_sensor_state.json rename to tests/components/mysensors/fixtures/sound_sensor_state.json diff --git a/tests/fixtures/mysensors/temperature_sensor_state.json b/tests/components/mysensors/fixtures/temperature_sensor_state.json similarity index 100% rename from tests/fixtures/mysensors/temperature_sensor_state.json rename to tests/components/mysensors/fixtures/temperature_sensor_state.json diff --git a/tests/fixtures/netatmo/events.txt b/tests/components/netatmo/fixtures/events.txt similarity index 100% rename from tests/fixtures/netatmo/events.txt rename to tests/components/netatmo/fixtures/events.txt diff --git a/tests/fixtures/netatmo/gethomecoachsdata.json b/tests/components/netatmo/fixtures/gethomecoachsdata.json similarity index 100% rename from tests/fixtures/netatmo/gethomecoachsdata.json rename to tests/components/netatmo/fixtures/gethomecoachsdata.json diff --git a/tests/fixtures/netatmo/gethomedata.json b/tests/components/netatmo/fixtures/gethomedata.json similarity index 100% rename from tests/fixtures/netatmo/gethomedata.json rename to tests/components/netatmo/fixtures/gethomedata.json diff --git a/tests/fixtures/netatmo/getpublicdata.json b/tests/components/netatmo/fixtures/getpublicdata.json similarity index 100% rename from tests/fixtures/netatmo/getpublicdata.json rename to tests/components/netatmo/fixtures/getpublicdata.json diff --git a/tests/fixtures/netatmo/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json similarity index 100% rename from tests/fixtures/netatmo/getstationsdata.json rename to tests/components/netatmo/fixtures/getstationsdata.json diff --git a/tests/fixtures/netatmo/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json similarity index 100% rename from tests/fixtures/netatmo/homesdata.json rename to tests/components/netatmo/fixtures/homesdata.json diff --git a/tests/fixtures/netatmo/homestatus.json b/tests/components/netatmo/fixtures/homestatus.json similarity index 100% rename from tests/fixtures/netatmo/homestatus.json rename to tests/components/netatmo/fixtures/homestatus.json diff --git a/tests/fixtures/netatmo/ping.json b/tests/components/netatmo/fixtures/ping.json similarity index 100% rename from tests/fixtures/netatmo/ping.json rename to tests/components/netatmo/fixtures/ping.json diff --git a/tests/fixtures/nexia/mobile_houses_123456.json b/tests/components/nexia/fixtures/mobile_houses_123456.json similarity index 100% rename from tests/fixtures/nexia/mobile_houses_123456.json rename to tests/components/nexia/fixtures/mobile_houses_123456.json diff --git a/tests/fixtures/nexia/session_123456.json b/tests/components/nexia/fixtures/session_123456.json similarity index 100% rename from tests/fixtures/nexia/session_123456.json rename to tests/components/nexia/fixtures/session_123456.json diff --git a/tests/fixtures/nexia/sign_in.json b/tests/components/nexia/fixtures/sign_in.json similarity index 100% rename from tests/fixtures/nexia/sign_in.json rename to tests/components/nexia/fixtures/sign_in.json diff --git a/tests/fixtures/nut/5E650I.json b/tests/components/nut/fixtures/5E650I.json similarity index 100% rename from tests/fixtures/nut/5E650I.json rename to tests/components/nut/fixtures/5E650I.json diff --git a/tests/fixtures/nut/5E850I.json b/tests/components/nut/fixtures/5E850I.json similarity index 100% rename from tests/fixtures/nut/5E850I.json rename to tests/components/nut/fixtures/5E850I.json diff --git a/tests/fixtures/nut/BACKUPSES600M1.json b/tests/components/nut/fixtures/BACKUPSES600M1.json similarity index 100% rename from tests/fixtures/nut/BACKUPSES600M1.json rename to tests/components/nut/fixtures/BACKUPSES600M1.json diff --git a/tests/fixtures/nut/CP1350C.json b/tests/components/nut/fixtures/CP1350C.json similarity index 100% rename from tests/fixtures/nut/CP1350C.json rename to tests/components/nut/fixtures/CP1350C.json diff --git a/tests/fixtures/nut/CP1500PFCLCD.json b/tests/components/nut/fixtures/CP1500PFCLCD.json similarity index 100% rename from tests/fixtures/nut/CP1500PFCLCD.json rename to tests/components/nut/fixtures/CP1500PFCLCD.json diff --git a/tests/fixtures/nut/DL650ELCD.json b/tests/components/nut/fixtures/DL650ELCD.json similarity index 100% rename from tests/fixtures/nut/DL650ELCD.json rename to tests/components/nut/fixtures/DL650ELCD.json diff --git a/tests/fixtures/nut/PR3000RT2U.json b/tests/components/nut/fixtures/PR3000RT2U.json similarity index 100% rename from tests/fixtures/nut/PR3000RT2U.json rename to tests/components/nut/fixtures/PR3000RT2U.json diff --git a/tests/fixtures/nut/blazer_usb.json b/tests/components/nut/fixtures/blazer_usb.json similarity index 100% rename from tests/fixtures/nut/blazer_usb.json rename to tests/components/nut/fixtures/blazer_usb.json diff --git a/tests/fixtures/ozw/binary_sensor.json b/tests/components/ozw/fixtures/binary_sensor.json similarity index 100% rename from tests/fixtures/ozw/binary_sensor.json rename to tests/components/ozw/fixtures/binary_sensor.json diff --git a/tests/fixtures/ozw/binary_sensor_alt.json b/tests/components/ozw/fixtures/binary_sensor_alt.json similarity index 100% rename from tests/fixtures/ozw/binary_sensor_alt.json rename to tests/components/ozw/fixtures/binary_sensor_alt.json diff --git a/tests/fixtures/ozw/climate.json b/tests/components/ozw/fixtures/climate.json similarity index 100% rename from tests/fixtures/ozw/climate.json rename to tests/components/ozw/fixtures/climate.json diff --git a/tests/fixtures/ozw/climate_network_dump.csv b/tests/components/ozw/fixtures/climate_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/climate_network_dump.csv rename to tests/components/ozw/fixtures/climate_network_dump.csv diff --git a/tests/fixtures/ozw/cover.json b/tests/components/ozw/fixtures/cover.json similarity index 100% rename from tests/fixtures/ozw/cover.json rename to tests/components/ozw/fixtures/cover.json diff --git a/tests/fixtures/ozw/cover_gdo.json b/tests/components/ozw/fixtures/cover_gdo.json similarity index 100% rename from tests/fixtures/ozw/cover_gdo.json rename to tests/components/ozw/fixtures/cover_gdo.json diff --git a/tests/fixtures/ozw/cover_gdo_network_dump.csv b/tests/components/ozw/fixtures/cover_gdo_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/cover_gdo_network_dump.csv rename to tests/components/ozw/fixtures/cover_gdo_network_dump.csv diff --git a/tests/fixtures/ozw/cover_network_dump.csv b/tests/components/ozw/fixtures/cover_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/cover_network_dump.csv rename to tests/components/ozw/fixtures/cover_network_dump.csv diff --git a/tests/fixtures/ozw/fan.json b/tests/components/ozw/fixtures/fan.json similarity index 100% rename from tests/fixtures/ozw/fan.json rename to tests/components/ozw/fixtures/fan.json diff --git a/tests/fixtures/ozw/fan_network_dump.csv b/tests/components/ozw/fixtures/fan_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/fan_network_dump.csv rename to tests/components/ozw/fixtures/fan_network_dump.csv diff --git a/tests/fixtures/ozw/generic_network_dump.csv b/tests/components/ozw/fixtures/generic_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/generic_network_dump.csv rename to tests/components/ozw/fixtures/generic_network_dump.csv diff --git a/tests/fixtures/ozw/light.json b/tests/components/ozw/fixtures/light.json similarity index 100% rename from tests/fixtures/ozw/light.json rename to tests/components/ozw/fixtures/light.json diff --git a/tests/fixtures/ozw/light_network_dump.csv b/tests/components/ozw/fixtures/light_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/light_network_dump.csv rename to tests/components/ozw/fixtures/light_network_dump.csv diff --git a/tests/fixtures/ozw/light_new_ozw_network_dump.csv b/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/light_new_ozw_network_dump.csv rename to tests/components/ozw/fixtures/light_new_ozw_network_dump.csv diff --git a/tests/fixtures/ozw/light_no_cw_network_dump.csv b/tests/components/ozw/fixtures/light_no_cw_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/light_no_cw_network_dump.csv rename to tests/components/ozw/fixtures/light_no_cw_network_dump.csv diff --git a/tests/fixtures/ozw/light_no_rgb.json b/tests/components/ozw/fixtures/light_no_rgb.json similarity index 100% rename from tests/fixtures/ozw/light_no_rgb.json rename to tests/components/ozw/fixtures/light_no_rgb.json diff --git a/tests/fixtures/ozw/light_no_ww_network_dump.csv b/tests/components/ozw/fixtures/light_no_ww_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/light_no_ww_network_dump.csv rename to tests/components/ozw/fixtures/light_no_ww_network_dump.csv diff --git a/tests/fixtures/ozw/light_pure_rgb.json b/tests/components/ozw/fixtures/light_pure_rgb.json similarity index 100% rename from tests/fixtures/ozw/light_pure_rgb.json rename to tests/components/ozw/fixtures/light_pure_rgb.json diff --git a/tests/fixtures/ozw/light_rgb.json b/tests/components/ozw/fixtures/light_rgb.json similarity index 100% rename from tests/fixtures/ozw/light_rgb.json rename to tests/components/ozw/fixtures/light_rgb.json diff --git a/tests/fixtures/ozw/light_wc_network_dump.csv b/tests/components/ozw/fixtures/light_wc_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/light_wc_network_dump.csv rename to tests/components/ozw/fixtures/light_wc_network_dump.csv diff --git a/tests/fixtures/ozw/lock.json b/tests/components/ozw/fixtures/lock.json similarity index 100% rename from tests/fixtures/ozw/lock.json rename to tests/components/ozw/fixtures/lock.json diff --git a/tests/fixtures/ozw/lock_network_dump.csv b/tests/components/ozw/fixtures/lock_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/lock_network_dump.csv rename to tests/components/ozw/fixtures/lock_network_dump.csv diff --git a/tests/fixtures/ozw/migration_fixture.csv b/tests/components/ozw/fixtures/migration_fixture.csv similarity index 100% rename from tests/fixtures/ozw/migration_fixture.csv rename to tests/components/ozw/fixtures/migration_fixture.csv diff --git a/tests/fixtures/ozw/sensor.json b/tests/components/ozw/fixtures/sensor.json similarity index 100% rename from tests/fixtures/ozw/sensor.json rename to tests/components/ozw/fixtures/sensor.json diff --git a/tests/fixtures/ozw/sensor_string_value_network_dump.csv b/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv similarity index 100% rename from tests/fixtures/ozw/sensor_string_value_network_dump.csv rename to tests/components/ozw/fixtures/sensor_string_value_network_dump.csv diff --git a/tests/fixtures/ozw/switch.json b/tests/components/ozw/fixtures/switch.json similarity index 100% rename from tests/fixtures/ozw/switch.json rename to tests/components/ozw/fixtures/switch.json diff --git a/tests/fixtures/p1_monitor/phases.json b/tests/components/p1_monitor/fixtures/phases.json similarity index 100% rename from tests/fixtures/p1_monitor/phases.json rename to tests/components/p1_monitor/fixtures/phases.json diff --git a/tests/fixtures/p1_monitor/settings.json b/tests/components/p1_monitor/fixtures/settings.json similarity index 100% rename from tests/fixtures/p1_monitor/settings.json rename to tests/components/p1_monitor/fixtures/settings.json diff --git a/tests/fixtures/p1_monitor/smartmeter.json b/tests/components/p1_monitor/fixtures/smartmeter.json similarity index 100% rename from tests/fixtures/p1_monitor/smartmeter.json rename to tests/components/p1_monitor/fixtures/smartmeter.json diff --git a/tests/fixtures/ping/configuration.yaml b/tests/components/ping/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/ping/configuration.yaml rename to tests/components/ping/fixtures/configuration.yaml diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index 3ffb2bb95d5..e5021ad6ac2 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -1,5 +1,4 @@ """The test for the ping binary_sensor platform.""" -from os import path from unittest.mock import patch import pytest @@ -8,6 +7,8 @@ from homeassistant import config as hass_config, setup from homeassistant.components.ping import DOMAIN from homeassistant.const import SERVICE_RELOAD +from tests.common import get_fixture_path + @pytest.fixture def mock_ping(): @@ -37,11 +38,7 @@ async def test_reload(hass, mock_ping): assert hass.states.get("binary_sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "ping/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "ping") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -55,7 +52,3 @@ async def test_reload(hass, mock_ping): assert hass.states.get("binary_sensor.test") is None assert hass.states.get("binary_sensor.test2") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/plex/album.xml b/tests/components/plex/fixtures/album.xml similarity index 100% rename from tests/fixtures/plex/album.xml rename to tests/components/plex/fixtures/album.xml diff --git a/tests/fixtures/plex/artist_albums.xml b/tests/components/plex/fixtures/artist_albums.xml similarity index 100% rename from tests/fixtures/plex/artist_albums.xml rename to tests/components/plex/fixtures/artist_albums.xml diff --git a/tests/fixtures/plex/children_20.xml b/tests/components/plex/fixtures/children_20.xml similarity index 100% rename from tests/fixtures/plex/children_20.xml rename to tests/components/plex/fixtures/children_20.xml diff --git a/tests/fixtures/plex/children_200.xml b/tests/components/plex/fixtures/children_200.xml similarity index 100% rename from tests/fixtures/plex/children_200.xml rename to tests/components/plex/fixtures/children_200.xml diff --git a/tests/fixtures/plex/children_30.xml b/tests/components/plex/fixtures/children_30.xml similarity index 100% rename from tests/fixtures/plex/children_30.xml rename to tests/components/plex/fixtures/children_30.xml diff --git a/tests/fixtures/plex/children_300.xml b/tests/components/plex/fixtures/children_300.xml similarity index 100% rename from tests/fixtures/plex/children_300.xml rename to tests/components/plex/fixtures/children_300.xml diff --git a/tests/fixtures/plex/empty_library.xml b/tests/components/plex/fixtures/empty_library.xml similarity index 100% rename from tests/fixtures/plex/empty_library.xml rename to tests/components/plex/fixtures/empty_library.xml diff --git a/tests/fixtures/plex/empty_payload.xml b/tests/components/plex/fixtures/empty_payload.xml similarity index 100% rename from tests/fixtures/plex/empty_payload.xml rename to tests/components/plex/fixtures/empty_payload.xml diff --git a/tests/fixtures/plex/grandchildren_300.xml b/tests/components/plex/fixtures/grandchildren_300.xml similarity index 100% rename from tests/fixtures/plex/grandchildren_300.xml rename to tests/components/plex/fixtures/grandchildren_300.xml diff --git a/tests/fixtures/plex/library.xml b/tests/components/plex/fixtures/library.xml similarity index 100% rename from tests/fixtures/plex/library.xml rename to tests/components/plex/fixtures/library.xml diff --git a/tests/fixtures/plex/library_movies_all.xml b/tests/components/plex/fixtures/library_movies_all.xml similarity index 100% rename from tests/fixtures/plex/library_movies_all.xml rename to tests/components/plex/fixtures/library_movies_all.xml diff --git a/tests/fixtures/plex/library_movies_collections.xml b/tests/components/plex/fixtures/library_movies_collections.xml similarity index 100% rename from tests/fixtures/plex/library_movies_collections.xml rename to tests/components/plex/fixtures/library_movies_collections.xml diff --git a/tests/fixtures/plex/library_movies_filtertypes.xml b/tests/components/plex/fixtures/library_movies_filtertypes.xml similarity index 100% rename from tests/fixtures/plex/library_movies_filtertypes.xml rename to tests/components/plex/fixtures/library_movies_filtertypes.xml diff --git a/tests/fixtures/plex/library_movies_metadata.xml b/tests/components/plex/fixtures/library_movies_metadata.xml similarity index 100% rename from tests/fixtures/plex/library_movies_metadata.xml rename to tests/components/plex/fixtures/library_movies_metadata.xml diff --git a/tests/fixtures/plex/library_movies_size.xml b/tests/components/plex/fixtures/library_movies_size.xml similarity index 100% rename from tests/fixtures/plex/library_movies_size.xml rename to tests/components/plex/fixtures/library_movies_size.xml diff --git a/tests/fixtures/plex/library_movies_sort.xml b/tests/components/plex/fixtures/library_movies_sort.xml similarity index 100% rename from tests/fixtures/plex/library_movies_sort.xml rename to tests/components/plex/fixtures/library_movies_sort.xml diff --git a/tests/fixtures/plex/library_music_all.xml b/tests/components/plex/fixtures/library_music_all.xml similarity index 100% rename from tests/fixtures/plex/library_music_all.xml rename to tests/components/plex/fixtures/library_music_all.xml diff --git a/tests/fixtures/plex/library_music_collections.xml b/tests/components/plex/fixtures/library_music_collections.xml similarity index 100% rename from tests/fixtures/plex/library_music_collections.xml rename to tests/components/plex/fixtures/library_music_collections.xml diff --git a/tests/fixtures/plex/library_music_metadata.xml b/tests/components/plex/fixtures/library_music_metadata.xml similarity index 100% rename from tests/fixtures/plex/library_music_metadata.xml rename to tests/components/plex/fixtures/library_music_metadata.xml diff --git a/tests/fixtures/plex/library_music_size.xml b/tests/components/plex/fixtures/library_music_size.xml similarity index 100% rename from tests/fixtures/plex/library_music_size.xml rename to tests/components/plex/fixtures/library_music_size.xml diff --git a/tests/fixtures/plex/library_music_sort.xml b/tests/components/plex/fixtures/library_music_sort.xml similarity index 100% rename from tests/fixtures/plex/library_music_sort.xml rename to tests/components/plex/fixtures/library_music_sort.xml diff --git a/tests/fixtures/plex/library_sections.xml b/tests/components/plex/fixtures/library_sections.xml similarity index 100% rename from tests/fixtures/plex/library_sections.xml rename to tests/components/plex/fixtures/library_sections.xml diff --git a/tests/fixtures/plex/library_tvshows_all.xml b/tests/components/plex/fixtures/library_tvshows_all.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_all.xml rename to tests/components/plex/fixtures/library_tvshows_all.xml diff --git a/tests/fixtures/plex/library_tvshows_collections.xml b/tests/components/plex/fixtures/library_tvshows_collections.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_collections.xml rename to tests/components/plex/fixtures/library_tvshows_collections.xml diff --git a/tests/fixtures/plex/library_tvshows_metadata.xml b/tests/components/plex/fixtures/library_tvshows_metadata.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_metadata.xml rename to tests/components/plex/fixtures/library_tvshows_metadata.xml diff --git a/tests/fixtures/plex/library_tvshows_most_recent.xml b/tests/components/plex/fixtures/library_tvshows_most_recent.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_most_recent.xml rename to tests/components/plex/fixtures/library_tvshows_most_recent.xml diff --git a/tests/fixtures/plex/library_tvshows_size.xml b/tests/components/plex/fixtures/library_tvshows_size.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_size.xml rename to tests/components/plex/fixtures/library_tvshows_size.xml diff --git a/tests/fixtures/plex/library_tvshows_size_episodes.xml b/tests/components/plex/fixtures/library_tvshows_size_episodes.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_size_episodes.xml rename to tests/components/plex/fixtures/library_tvshows_size_episodes.xml diff --git a/tests/fixtures/plex/library_tvshows_size_seasons.xml b/tests/components/plex/fixtures/library_tvshows_size_seasons.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_size_seasons.xml rename to tests/components/plex/fixtures/library_tvshows_size_seasons.xml diff --git a/tests/fixtures/plex/library_tvshows_sort.xml b/tests/components/plex/fixtures/library_tvshows_sort.xml similarity index 100% rename from tests/fixtures/plex/library_tvshows_sort.xml rename to tests/components/plex/fixtures/library_tvshows_sort.xml diff --git a/tests/fixtures/plex/livetv_sessions.xml b/tests/components/plex/fixtures/livetv_sessions.xml similarity index 100% rename from tests/fixtures/plex/livetv_sessions.xml rename to tests/components/plex/fixtures/livetv_sessions.xml diff --git a/tests/fixtures/plex/media_1.xml b/tests/components/plex/fixtures/media_1.xml similarity index 100% rename from tests/fixtures/plex/media_1.xml rename to tests/components/plex/fixtures/media_1.xml diff --git a/tests/fixtures/plex/media_100.xml b/tests/components/plex/fixtures/media_100.xml similarity index 100% rename from tests/fixtures/plex/media_100.xml rename to tests/components/plex/fixtures/media_100.xml diff --git a/tests/fixtures/plex/media_200.xml b/tests/components/plex/fixtures/media_200.xml similarity index 100% rename from tests/fixtures/plex/media_200.xml rename to tests/components/plex/fixtures/media_200.xml diff --git a/tests/fixtures/plex/media_30.xml b/tests/components/plex/fixtures/media_30.xml similarity index 100% rename from tests/fixtures/plex/media_30.xml rename to tests/components/plex/fixtures/media_30.xml diff --git a/tests/fixtures/plex/player_plexweb_resources.xml b/tests/components/plex/fixtures/player_plexweb_resources.xml similarity index 100% rename from tests/fixtures/plex/player_plexweb_resources.xml rename to tests/components/plex/fixtures/player_plexweb_resources.xml diff --git a/tests/fixtures/plex/playlist_500.xml b/tests/components/plex/fixtures/playlist_500.xml similarity index 100% rename from tests/fixtures/plex/playlist_500.xml rename to tests/components/plex/fixtures/playlist_500.xml diff --git a/tests/fixtures/plex/playlists.xml b/tests/components/plex/fixtures/playlists.xml similarity index 100% rename from tests/fixtures/plex/playlists.xml rename to tests/components/plex/fixtures/playlists.xml diff --git a/tests/fixtures/plex/playqueue_1234.xml b/tests/components/plex/fixtures/playqueue_1234.xml similarity index 100% rename from tests/fixtures/plex/playqueue_1234.xml rename to tests/components/plex/fixtures/playqueue_1234.xml diff --git a/tests/fixtures/plex/playqueue_created.xml b/tests/components/plex/fixtures/playqueue_created.xml similarity index 100% rename from tests/fixtures/plex/playqueue_created.xml rename to tests/components/plex/fixtures/playqueue_created.xml diff --git a/tests/fixtures/plex/plex_server_accounts.xml b/tests/components/plex/fixtures/plex_server_accounts.xml similarity index 100% rename from tests/fixtures/plex/plex_server_accounts.xml rename to tests/components/plex/fixtures/plex_server_accounts.xml diff --git a/tests/fixtures/plex/plex_server_base.xml b/tests/components/plex/fixtures/plex_server_base.xml similarity index 100% rename from tests/fixtures/plex/plex_server_base.xml rename to tests/components/plex/fixtures/plex_server_base.xml diff --git a/tests/fixtures/plex/plex_server_clients.xml b/tests/components/plex/fixtures/plex_server_clients.xml similarity index 100% rename from tests/fixtures/plex/plex_server_clients.xml rename to tests/components/plex/fixtures/plex_server_clients.xml diff --git a/tests/fixtures/plex/plextv_account.xml b/tests/components/plex/fixtures/plextv_account.xml similarity index 100% rename from tests/fixtures/plex/plextv_account.xml rename to tests/components/plex/fixtures/plextv_account.xml diff --git a/tests/fixtures/plex/plextv_resources_base.xml b/tests/components/plex/fixtures/plextv_resources_base.xml similarity index 100% rename from tests/fixtures/plex/plextv_resources_base.xml rename to tests/components/plex/fixtures/plextv_resources_base.xml diff --git a/tests/fixtures/plex/plextv_shared_users.xml b/tests/components/plex/fixtures/plextv_shared_users.xml similarity index 100% rename from tests/fixtures/plex/plextv_shared_users.xml rename to tests/components/plex/fixtures/plextv_shared_users.xml diff --git a/tests/fixtures/plex/security_token.xml b/tests/components/plex/fixtures/security_token.xml similarity index 100% rename from tests/fixtures/plex/security_token.xml rename to tests/components/plex/fixtures/security_token.xml diff --git a/tests/fixtures/plex/session_base.xml b/tests/components/plex/fixtures/session_base.xml similarity index 100% rename from tests/fixtures/plex/session_base.xml rename to tests/components/plex/fixtures/session_base.xml diff --git a/tests/fixtures/plex/session_live_tv.xml b/tests/components/plex/fixtures/session_live_tv.xml similarity index 100% rename from tests/fixtures/plex/session_live_tv.xml rename to tests/components/plex/fixtures/session_live_tv.xml diff --git a/tests/fixtures/plex/session_photo.xml b/tests/components/plex/fixtures/session_photo.xml similarity index 100% rename from tests/fixtures/plex/session_photo.xml rename to tests/components/plex/fixtures/session_photo.xml diff --git a/tests/fixtures/plex/session_plexweb.xml b/tests/components/plex/fixtures/session_plexweb.xml similarity index 100% rename from tests/fixtures/plex/session_plexweb.xml rename to tests/components/plex/fixtures/session_plexweb.xml diff --git a/tests/fixtures/plex/session_transient.xml b/tests/components/plex/fixtures/session_transient.xml similarity index 100% rename from tests/fixtures/plex/session_transient.xml rename to tests/components/plex/fixtures/session_transient.xml diff --git a/tests/fixtures/plex/session_unknown.xml b/tests/components/plex/fixtures/session_unknown.xml similarity index 100% rename from tests/fixtures/plex/session_unknown.xml rename to tests/components/plex/fixtures/session_unknown.xml diff --git a/tests/fixtures/plex/show_seasons.xml b/tests/components/plex/fixtures/show_seasons.xml similarity index 100% rename from tests/fixtures/plex/show_seasons.xml rename to tests/components/plex/fixtures/show_seasons.xml diff --git a/tests/fixtures/plex/sonos_resources.xml b/tests/components/plex/fixtures/sonos_resources.xml similarity index 100% rename from tests/fixtures/plex/sonos_resources.xml rename to tests/components/plex/fixtures/sonos_resources.xml diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_all_devices.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_all_devices.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json similarity index 100% rename from tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json rename to tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/components/plugwise/fixtures/anna_heatpump/get_all_devices.json similarity index 100% rename from tests/fixtures/plugwise/anna_heatpump/get_all_devices.json rename to tests/components/plugwise/fixtures/anna_heatpump/get_all_devices.json diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json similarity index 100% rename from tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json rename to tests/components/plugwise/fixtures/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json similarity index 100% rename from tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json rename to tests/components/plugwise/fixtures/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json similarity index 100% rename from tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json rename to tests/components/plugwise/fixtures/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json diff --git a/tests/fixtures/plugwise/anna_heatpump/notifications.json b/tests/components/plugwise/fixtures/anna_heatpump/notifications.json similarity index 100% rename from tests/fixtures/plugwise/anna_heatpump/notifications.json rename to tests/components/plugwise/fixtures/anna_heatpump/notifications.json diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/components/plugwise/fixtures/p1v3_full_option/get_all_devices.json similarity index 100% rename from tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json rename to tests/components/plugwise/fixtures/p1v3_full_option/get_all_devices.json diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/components/plugwise/fixtures/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json similarity index 100% rename from tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json rename to tests/components/plugwise/fixtures/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json diff --git a/tests/fixtures/plugwise/p1v3_full_option/notifications.json b/tests/components/plugwise/fixtures/p1v3_full_option/notifications.json similarity index 100% rename from tests/fixtures/plugwise/p1v3_full_option/notifications.json rename to tests/components/plugwise/fixtures/p1v3_full_option/notifications.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_all_devices.json b/tests/components/plugwise/fixtures/stretch_v31/get_all_devices.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_all_devices.json rename to tests/components/plugwise/fixtures/stretch_v31/get_all_devices.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json rename to tests/components/plugwise/fixtures/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json diff --git a/tests/fixtures/plugwise/stretch_v31/notifications.json b/tests/components/plugwise/fixtures/stretch_v31/notifications.json similarity index 100% rename from tests/fixtures/plugwise/stretch_v31/notifications.json rename to tests/components/plugwise/fixtures/stretch_v31/notifications.json diff --git a/tests/fixtures/powerwall/device_type.json b/tests/components/powerwall/fixtures/device_type.json similarity index 100% rename from tests/fixtures/powerwall/device_type.json rename to tests/components/powerwall/fixtures/device_type.json diff --git a/tests/fixtures/powerwall/meters.json b/tests/components/powerwall/fixtures/meters.json similarity index 100% rename from tests/fixtures/powerwall/meters.json rename to tests/components/powerwall/fixtures/meters.json diff --git a/tests/fixtures/powerwall/site_info.json b/tests/components/powerwall/fixtures/site_info.json similarity index 100% rename from tests/fixtures/powerwall/site_info.json rename to tests/components/powerwall/fixtures/site_info.json diff --git a/tests/fixtures/powerwall/sitemaster.json b/tests/components/powerwall/fixtures/sitemaster.json similarity index 100% rename from tests/fixtures/powerwall/sitemaster.json rename to tests/components/powerwall/fixtures/sitemaster.json diff --git a/tests/fixtures/powerwall/status.json b/tests/components/powerwall/fixtures/status.json similarity index 100% rename from tests/fixtures/powerwall/status.json rename to tests/components/powerwall/fixtures/status.json diff --git a/tests/fixtures/pushbullet_devices.json b/tests/components/pushbullet/fixtures/devices.json similarity index 100% rename from tests/fixtures/pushbullet_devices.json rename to tests/components/pushbullet/fixtures/devices.json diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 6e1de0b9824..a9186652f64 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -18,7 +18,7 @@ def mock_pushbullet(): with patch.object( PushBullet, "_get_data", - return_value=json.loads(load_fixture("pushbullet_devices.json")), + return_value=json.loads(load_fixture("devices.json", "pushbullet")), ): yield diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json similarity index 100% rename from tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json rename to tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json similarity index 100% rename from tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json rename to tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json similarity index 100% rename from tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json rename to tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json similarity index 100% rename from tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json rename to tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json diff --git a/tests/fixtures/renault/action.set_ac_start.json b/tests/components/renault/fixtures/action.set_ac_start.json similarity index 100% rename from tests/fixtures/renault/action.set_ac_start.json rename to tests/components/renault/fixtures/action.set_ac_start.json diff --git a/tests/fixtures/renault/action.set_ac_stop.json b/tests/components/renault/fixtures/action.set_ac_stop.json similarity index 100% rename from tests/fixtures/renault/action.set_ac_stop.json rename to tests/components/renault/fixtures/action.set_ac_stop.json diff --git a/tests/fixtures/renault/action.set_charge_mode.json b/tests/components/renault/fixtures/action.set_charge_mode.json similarity index 100% rename from tests/fixtures/renault/action.set_charge_mode.json rename to tests/components/renault/fixtures/action.set_charge_mode.json diff --git a/tests/fixtures/renault/action.set_charge_schedules.json b/tests/components/renault/fixtures/action.set_charge_schedules.json similarity index 100% rename from tests/fixtures/renault/action.set_charge_schedules.json rename to tests/components/renault/fixtures/action.set_charge_schedules.json diff --git a/tests/fixtures/renault/action.set_charge_start.json b/tests/components/renault/fixtures/action.set_charge_start.json similarity index 100% rename from tests/fixtures/renault/action.set_charge_start.json rename to tests/components/renault/fixtures/action.set_charge_start.json diff --git a/tests/fixtures/renault/battery_status_charging.json b/tests/components/renault/fixtures/battery_status_charging.json similarity index 100% rename from tests/fixtures/renault/battery_status_charging.json rename to tests/components/renault/fixtures/battery_status_charging.json diff --git a/tests/fixtures/renault/battery_status_not_charging.json b/tests/components/renault/fixtures/battery_status_not_charging.json similarity index 100% rename from tests/fixtures/renault/battery_status_not_charging.json rename to tests/components/renault/fixtures/battery_status_not_charging.json diff --git a/tests/fixtures/renault/charge_mode_always.json b/tests/components/renault/fixtures/charge_mode_always.json similarity index 100% rename from tests/fixtures/renault/charge_mode_always.json rename to tests/components/renault/fixtures/charge_mode_always.json diff --git a/tests/fixtures/renault/charge_mode_schedule.json b/tests/components/renault/fixtures/charge_mode_schedule.json similarity index 100% rename from tests/fixtures/renault/charge_mode_schedule.json rename to tests/components/renault/fixtures/charge_mode_schedule.json diff --git a/tests/fixtures/renault/charging_settings.json b/tests/components/renault/fixtures/charging_settings.json similarity index 100% rename from tests/fixtures/renault/charging_settings.json rename to tests/components/renault/fixtures/charging_settings.json diff --git a/tests/fixtures/renault/cockpit_ev.json b/tests/components/renault/fixtures/cockpit_ev.json similarity index 100% rename from tests/fixtures/renault/cockpit_ev.json rename to tests/components/renault/fixtures/cockpit_ev.json diff --git a/tests/fixtures/renault/cockpit_fuel.json b/tests/components/renault/fixtures/cockpit_fuel.json similarity index 100% rename from tests/fixtures/renault/cockpit_fuel.json rename to tests/components/renault/fixtures/cockpit_fuel.json diff --git a/tests/fixtures/renault/hvac_status.json b/tests/components/renault/fixtures/hvac_status.json similarity index 100% rename from tests/fixtures/renault/hvac_status.json rename to tests/components/renault/fixtures/hvac_status.json diff --git a/tests/fixtures/renault/location.json b/tests/components/renault/fixtures/location.json similarity index 100% rename from tests/fixtures/renault/location.json rename to tests/components/renault/fixtures/location.json diff --git a/tests/fixtures/renault/no_data.json b/tests/components/renault/fixtures/no_data.json similarity index 100% rename from tests/fixtures/renault/no_data.json rename to tests/components/renault/fixtures/no_data.json diff --git a/tests/fixtures/renault/vehicle_captur_fuel.json b/tests/components/renault/fixtures/vehicle_captur_fuel.json similarity index 100% rename from tests/fixtures/renault/vehicle_captur_fuel.json rename to tests/components/renault/fixtures/vehicle_captur_fuel.json diff --git a/tests/fixtures/renault/vehicle_captur_phev.json b/tests/components/renault/fixtures/vehicle_captur_phev.json similarity index 100% rename from tests/fixtures/renault/vehicle_captur_phev.json rename to tests/components/renault/fixtures/vehicle_captur_phev.json diff --git a/tests/fixtures/renault/vehicle_zoe_40.json b/tests/components/renault/fixtures/vehicle_zoe_40.json similarity index 100% rename from tests/fixtures/renault/vehicle_zoe_40.json rename to tests/components/renault/fixtures/vehicle_zoe_40.json diff --git a/tests/fixtures/renault/vehicle_zoe_50.json b/tests/components/renault/fixtures/vehicle_zoe_50.json similarity index 100% rename from tests/fixtures/renault/vehicle_zoe_50.json rename to tests/components/renault/fixtures/vehicle_zoe_50.json diff --git a/tests/fixtures/rest/configuration.yaml b/tests/components/rest/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/rest/configuration.yaml rename to tests/components/rest/fixtures/configuration.yaml diff --git a/tests/fixtures/rest/configuration_empty.yaml b/tests/components/rest/fixtures/configuration_empty.yaml similarity index 100% rename from tests/fixtures/rest/configuration_empty.yaml rename to tests/components/rest/fixtures/configuration_empty.yaml diff --git a/tests/fixtures/rest/configuration_invalid.notyaml b/tests/components/rest/fixtures/configuration_invalid.notyaml similarity index 100% rename from tests/fixtures/rest/configuration_invalid.notyaml rename to tests/components/rest/fixtures/configuration_invalid.notyaml diff --git a/tests/fixtures/rest/configuration_top_level.yaml b/tests/components/rest/fixtures/configuration_top_level.yaml similarity index 100% rename from tests/fixtures/rest/configuration_top_level.yaml rename to tests/components/rest/fixtures/configuration_top_level.yaml diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index a0cd7d5108c..6daffcb2a5e 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -2,7 +2,6 @@ import asyncio from http import HTTPStatus -from os import path from unittest.mock import MagicMock, patch import httpx @@ -21,6 +20,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_setup_missing_basic_config(hass): """Test setup with configuration missing required entries.""" @@ -397,11 +398,7 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.mockrest") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "rest") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", @@ -433,7 +430,3 @@ async def test_setup_query_params(hass): ) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 1 - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index ddd5356525d..988f88b348e 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta from http import HTTPStatus -from os import path from unittest.mock import patch import respx @@ -19,7 +18,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path @respx.mock @@ -220,11 +219,8 @@ async def test_reload(hass): assert hass.states.get("sensor.mockrest") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration_top_level.yaml", - ) + yaml_path = get_fixture_path("configuration_top_level.yaml", "rest") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", @@ -272,11 +268,8 @@ async def test_reload_and_remove_all(hass): assert hass.states.get("sensor.mockrest") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration_empty.yaml", - ) + yaml_path = get_fixture_path("configuration_empty.yaml", "rest") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", @@ -320,11 +313,7 @@ async def test_reload_fails_to_read_configuration(hass): assert len(hass.states.async_all()) == 1 - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration_invalid.notyaml", - ) + yaml_path = get_fixture_path("configuration_invalid.notyaml", "rest") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", @@ -337,10 +326,6 @@ async def test_reload_fails_to_read_configuration(hass): assert len(hass.states.async_all()) == 1 -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) - - @respx.mock async def test_multiple_rest_endpoints(hass): """Test multiple rest endpoints.""" diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index fb7b8a31238..31567ae63f0 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -1,5 +1,4 @@ """The tests for the rest.notify platform.""" -from os import path from unittest.mock import patch import respx @@ -10,6 +9,8 @@ from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + @respx.mock async def test_reload_notify(hass): @@ -33,11 +34,8 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, DOMAIN) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "rest") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -49,7 +47,3 @@ async def test_reload_notify(hass): assert not hass.services.has_service(notify.DOMAIN, DOMAIN) assert hass.services.has_service(notify.DOMAIN, "rest_reloaded") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a576acb2fe3..d37fb047f8f 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the REST sensor platform.""" import asyncio from http import HTTPStatus -from os import path from unittest.mock import MagicMock, patch import httpx @@ -23,6 +22,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" @@ -786,11 +787,7 @@ async def test_reload(hass): assert hass.states.get("sensor.mockrest") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "rest/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "rest") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", @@ -802,7 +799,3 @@ async def test_reload(hass): assert hass.states.get("sensor.mockreset") is None assert hass.states.get("sensor.rollout") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index cda662aab64..2b6edf86132 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -18,38 +18,39 @@ def requests_mock_fixture(): # Mocks the response for authenticating mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") + "https://oauth.ring.com/oauth/token", + text=load_fixture("oauth.json", "ring"), ) # Mocks the response for getting the login session mock.post( "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), + text=load_fixture("session.json", "ring"), ) # Mocks the response for getting all the devices mock.get( "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), + text=load_fixture("devices.json", "ring"), ) mock.get( "https://api.ring.com/clients_api/dings/active", - text=load_fixture("ring_ding_active.json"), + text=load_fixture("ding_active.json", "ring"), ) # Mocks the response for getting the history of a device mock.get( re.compile( r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" ), - text=load_fixture("ring_doorbots.json"), + text=load_fixture("doorbots.json", "ring"), ) # Mocks the response for getting the health of a device mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), - text=load_fixture("ring_doorboot_health_attrs.json"), + text=load_fixture("doorboot_health_attrs.json", "ring"), ) # Mocks the response for getting a chimes health mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), - text=load_fixture("ring_chime_health_attrs.json"), + text=load_fixture("chime_health_attrs.json", "ring"), ) yield mock diff --git a/tests/fixtures/ring_chime_health_attrs.json b/tests/components/ring/fixtures/chime_health_attrs.json similarity index 100% rename from tests/fixtures/ring_chime_health_attrs.json rename to tests/components/ring/fixtures/chime_health_attrs.json diff --git a/tests/fixtures/ring_devices.json b/tests/components/ring/fixtures/devices.json similarity index 100% rename from tests/fixtures/ring_devices.json rename to tests/components/ring/fixtures/devices.json diff --git a/tests/fixtures/ring_devices_updated.json b/tests/components/ring/fixtures/devices_updated.json similarity index 100% rename from tests/fixtures/ring_devices_updated.json rename to tests/components/ring/fixtures/devices_updated.json diff --git a/tests/fixtures/ring_ding_active.json b/tests/components/ring/fixtures/ding_active.json similarity index 100% rename from tests/fixtures/ring_ding_active.json rename to tests/components/ring/fixtures/ding_active.json diff --git a/tests/fixtures/ring_doorboot_health_attrs.json b/tests/components/ring/fixtures/doorboot_health_attrs.json similarity index 100% rename from tests/fixtures/ring_doorboot_health_attrs.json rename to tests/components/ring/fixtures/doorboot_health_attrs.json diff --git a/tests/fixtures/ring_doorbot_siren_on_response.json b/tests/components/ring/fixtures/doorbot_siren_on_response.json similarity index 100% rename from tests/fixtures/ring_doorbot_siren_on_response.json rename to tests/components/ring/fixtures/doorbot_siren_on_response.json diff --git a/tests/fixtures/ring_doorbots.json b/tests/components/ring/fixtures/doorbots.json similarity index 100% rename from tests/fixtures/ring_doorbots.json rename to tests/components/ring/fixtures/doorbots.json diff --git a/tests/fixtures/ring_oauth.json b/tests/components/ring/fixtures/oauth.json similarity index 100% rename from tests/fixtures/ring_oauth.json rename to tests/components/ring/fixtures/oauth.json diff --git a/tests/fixtures/ring_session.json b/tests/components/ring/fixtures/session.json similarity index 100% rename from tests/fixtures/ring_session.json rename to tests/components/ring/fixtures/session.json diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index c7a87ae880a..860102f9fc8 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -18,23 +18,23 @@ async def test_setup(hass, requests_mock): await async_setup_component(hass, ring.DOMAIN, {}) requests_mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") + "https://oauth.ring.com/oauth/token", text=load_fixture("oauth.json", "ring") ) requests_mock.post( "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), + text=load_fixture("session.json", "ring"), ) requests_mock.get( "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), + text=load_fixture("devices.json", "ring"), ) requests_mock.get( "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("ring_chime_health_attrs.json"), + text=load_fixture("chime_health_attrs.json", "ring"), ) requests_mock.get( "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("ring_doorboot_health_attrs.json"), + text=load_fixture("doorboot_health_attrs.json", "ring"), ) assert await ring.async_setup(hass, VALID_CONFIG) diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 603c0bf3e84..1b8150364bc 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -44,7 +44,7 @@ async def test_light_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a light on requests_mock.put( "https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on", - text=load_fixture("ring_doorbot_siren_on_response.json"), + text=load_fixture("doorbot_siren_on_response.json", "ring"), ) state = hass.states.get("light.front_light") @@ -67,7 +67,7 @@ async def test_updates_work(hass, requests_mock): # Changes the return to indicate that the light is now on. requests_mock.get( "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices_updated.json"), + text=load_fixture("devices_updated.json", "ring"), ) await hass.services.async_call("ring", "update", {}, blocking=True) diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index ab81ed5c69a..ed4e9024292 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -45,7 +45,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a siren on requests_mock.put( "https://api.ring.com/clients_api/doorbots/765432/siren_on", - text=load_fixture("ring_doorbot_siren_on_response.json"), + text=load_fixture("doorbot_siren_on_response.json", "ring"), ) state = hass.states.get("switch.front_siren") @@ -68,7 +68,7 @@ async def test_updates_work(hass, requests_mock): # Changes the return to indicate that the siren is now on. requests_mock.get( "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices_updated.json"), + text=load_fixture("devices_updated.json", "ring"), ) await hass.services.async_call("ring", "update", {}, blocking=True) diff --git a/tests/fixtures/roku/active-app-netflix.xml b/tests/components/roku/fixtures/active-app-netflix.xml similarity index 100% rename from tests/fixtures/roku/active-app-netflix.xml rename to tests/components/roku/fixtures/active-app-netflix.xml diff --git a/tests/fixtures/roku/active-app-pluto.xml b/tests/components/roku/fixtures/active-app-pluto.xml similarity index 100% rename from tests/fixtures/roku/active-app-pluto.xml rename to tests/components/roku/fixtures/active-app-pluto.xml diff --git a/tests/fixtures/roku/active-app-roku.xml b/tests/components/roku/fixtures/active-app-roku.xml similarity index 100% rename from tests/fixtures/roku/active-app-roku.xml rename to tests/components/roku/fixtures/active-app-roku.xml diff --git a/tests/fixtures/roku/active-app-screensaver.xml b/tests/components/roku/fixtures/active-app-screensaver.xml similarity index 100% rename from tests/fixtures/roku/active-app-screensaver.xml rename to tests/components/roku/fixtures/active-app-screensaver.xml diff --git a/tests/fixtures/roku/active-app-tvinput-dtv.xml b/tests/components/roku/fixtures/active-app-tvinput-dtv.xml similarity index 100% rename from tests/fixtures/roku/active-app-tvinput-dtv.xml rename to tests/components/roku/fixtures/active-app-tvinput-dtv.xml diff --git a/tests/fixtures/roku/apps-tv.xml b/tests/components/roku/fixtures/apps-tv.xml similarity index 100% rename from tests/fixtures/roku/apps-tv.xml rename to tests/components/roku/fixtures/apps-tv.xml diff --git a/tests/fixtures/roku/apps.xml b/tests/components/roku/fixtures/apps.xml similarity index 100% rename from tests/fixtures/roku/apps.xml rename to tests/components/roku/fixtures/apps.xml diff --git a/tests/fixtures/roku/media-player-close.xml b/tests/components/roku/fixtures/media-player-close.xml similarity index 100% rename from tests/fixtures/roku/media-player-close.xml rename to tests/components/roku/fixtures/media-player-close.xml diff --git a/tests/fixtures/roku/media-player-live.xml b/tests/components/roku/fixtures/media-player-live.xml similarity index 100% rename from tests/fixtures/roku/media-player-live.xml rename to tests/components/roku/fixtures/media-player-live.xml diff --git a/tests/fixtures/roku/media-player-pause.xml b/tests/components/roku/fixtures/media-player-pause.xml similarity index 100% rename from tests/fixtures/roku/media-player-pause.xml rename to tests/components/roku/fixtures/media-player-pause.xml diff --git a/tests/fixtures/roku/media-player-play.xml b/tests/components/roku/fixtures/media-player-play.xml similarity index 100% rename from tests/fixtures/roku/media-player-play.xml rename to tests/components/roku/fixtures/media-player-play.xml diff --git a/tests/fixtures/roku/roku3-device-info-power-off.xml b/tests/components/roku/fixtures/roku3-device-info-power-off.xml similarity index 100% rename from tests/fixtures/roku/roku3-device-info-power-off.xml rename to tests/components/roku/fixtures/roku3-device-info-power-off.xml diff --git a/tests/fixtures/roku/roku3-device-info.xml b/tests/components/roku/fixtures/roku3-device-info.xml similarity index 100% rename from tests/fixtures/roku/roku3-device-info.xml rename to tests/components/roku/fixtures/roku3-device-info.xml diff --git a/tests/fixtures/roku/rokutv-device-info-power-off.xml b/tests/components/roku/fixtures/rokutv-device-info-power-off.xml similarity index 100% rename from tests/fixtures/roku/rokutv-device-info-power-off.xml rename to tests/components/roku/fixtures/rokutv-device-info-power-off.xml diff --git a/tests/fixtures/roku/rokutv-device-info.xml b/tests/components/roku/fixtures/rokutv-device-info.xml similarity index 100% rename from tests/fixtures/roku/rokutv-device-info.xml rename to tests/components/roku/fixtures/rokutv-device-info.xml diff --git a/tests/fixtures/roku/rokutv-tv-active-channel.xml b/tests/components/roku/fixtures/rokutv-tv-active-channel.xml similarity index 100% rename from tests/fixtures/roku/rokutv-tv-active-channel.xml rename to tests/components/roku/fixtures/rokutv-tv-active-channel.xml diff --git a/tests/fixtures/roku/rokutv-tv-channels.xml b/tests/components/roku/fixtures/rokutv-tv-channels.xml similarity index 100% rename from tests/fixtures/roku/rokutv-tv-channels.xml rename to tests/components/roku/fixtures/rokutv-tv-channels.xml diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 0d474503582..b5193b59d08 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -2,7 +2,6 @@ import asyncio from http import HTTPStatus import json -from pathlib import Path import pytest from smart_meter_texas.const import ( @@ -29,7 +28,7 @@ TEST_ENTITY_ID = "sensor.electric_meter_123456789" def load_smt_fixture(name): """Return a dict of the json fixture.""" - json_fixture = load_fixture(Path() / DOMAIN / f"{name}.json") + json_fixture = load_fixture(f"{name}.json", DOMAIN) return json.loads(json_fixture) diff --git a/tests/fixtures/smart_meter_texas/latestodrread.json b/tests/components/smart_meter_texas/fixtures/latestodrread.json similarity index 100% rename from tests/fixtures/smart_meter_texas/latestodrread.json rename to tests/components/smart_meter_texas/fixtures/latestodrread.json diff --git a/tests/fixtures/smart_meter_texas/meter.json b/tests/components/smart_meter_texas/fixtures/meter.json similarity index 100% rename from tests/fixtures/smart_meter_texas/meter.json rename to tests/components/smart_meter_texas/fixtures/meter.json diff --git a/tests/fixtures/smtp/configuration.yaml b/tests/components/smtp/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/smtp/configuration.yaml rename to tests/components/smtp/fixtures/configuration.yaml diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 5af1e5fcdbc..38f48c169ac 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,5 +1,4 @@ """The tests for the notify smtp platform.""" -from os import path import re from unittest.mock import patch @@ -12,6 +11,8 @@ from homeassistant.components.smtp.notify import MailNotificationService from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + class MockSMTP(MailNotificationService): """Test SMTP object that doesn't need a working server.""" @@ -45,11 +46,7 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, DOMAIN) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "smtp/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "smtp") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" ): @@ -65,10 +62,6 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, "smtp_reloaded") -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) - - @pytest.fixture def message(): """Return MockSMTP object with test data.""" diff --git a/tests/fixtures/sonarr/calendar.json b/tests/components/sonarr/fixtures/calendar.json similarity index 100% rename from tests/fixtures/sonarr/calendar.json rename to tests/components/sonarr/fixtures/calendar.json diff --git a/tests/fixtures/sonarr/command.json b/tests/components/sonarr/fixtures/command.json similarity index 100% rename from tests/fixtures/sonarr/command.json rename to tests/components/sonarr/fixtures/command.json diff --git a/tests/fixtures/sonarr/diskspace.json b/tests/components/sonarr/fixtures/diskspace.json similarity index 100% rename from tests/fixtures/sonarr/diskspace.json rename to tests/components/sonarr/fixtures/diskspace.json diff --git a/tests/fixtures/sonarr/queue.json b/tests/components/sonarr/fixtures/queue.json similarity index 100% rename from tests/fixtures/sonarr/queue.json rename to tests/components/sonarr/fixtures/queue.json diff --git a/tests/fixtures/sonarr/series.json b/tests/components/sonarr/fixtures/series.json similarity index 100% rename from tests/fixtures/sonarr/series.json rename to tests/components/sonarr/fixtures/series.json diff --git a/tests/fixtures/sonarr/system-status.json b/tests/components/sonarr/fixtures/system-status.json similarity index 100% rename from tests/fixtures/sonarr/system-status.json rename to tests/components/sonarr/fixtures/system-status.json diff --git a/tests/fixtures/sonarr/wanted-missing.json b/tests/components/sonarr/fixtures/wanted-missing.json similarity index 100% rename from tests/fixtures/sonarr/wanted-missing.json rename to tests/components/sonarr/fixtures/wanted-missing.json diff --git a/tests/fixtures/statistics/configuration.yaml b/tests/components/statistics/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/statistics/configuration.yaml rename to tests/components/statistics/fixtures/configuration.yaml diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 6cb53c9b93f..41ec88deaec 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,6 +1,5 @@ """The test for the statistics sensor platform.""" from datetime import datetime, timedelta -from os import path import statistics import unittest from unittest.mock import patch @@ -21,6 +20,7 @@ from homeassistant.util import dt as dt_util from tests.common import ( fire_time_changed, + get_fixture_path, get_test_home_assistant, init_recorder_component, ) @@ -496,11 +496,7 @@ async def test_reload(hass): assert hass.states.get("sensor.test") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "statistics/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "statistics") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -514,7 +510,3 @@ async def test_reload(hass): assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.cputest") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/tado/ac_issue_32294.heat_mode.json b/tests/components/tado/fixtures/ac_issue_32294.heat_mode.json similarity index 100% rename from tests/fixtures/tado/ac_issue_32294.heat_mode.json rename to tests/components/tado/fixtures/ac_issue_32294.heat_mode.json diff --git a/tests/fixtures/tado/device_temp_offset.json b/tests/components/tado/fixtures/device_temp_offset.json similarity index 100% rename from tests/fixtures/tado/device_temp_offset.json rename to tests/components/tado/fixtures/device_temp_offset.json diff --git a/tests/fixtures/tado/device_wr1.json b/tests/components/tado/fixtures/device_wr1.json similarity index 100% rename from tests/fixtures/tado/device_wr1.json rename to tests/components/tado/fixtures/device_wr1.json diff --git a/tests/fixtures/tado/devices.json b/tests/components/tado/fixtures/devices.json similarity index 100% rename from tests/fixtures/tado/devices.json rename to tests/components/tado/fixtures/devices.json diff --git a/tests/fixtures/tado/hvac_action_heat.json b/tests/components/tado/fixtures/hvac_action_heat.json similarity index 100% rename from tests/fixtures/tado/hvac_action_heat.json rename to tests/components/tado/fixtures/hvac_action_heat.json diff --git a/tests/fixtures/tado/me.json b/tests/components/tado/fixtures/me.json similarity index 100% rename from tests/fixtures/tado/me.json rename to tests/components/tado/fixtures/me.json diff --git a/tests/fixtures/tado/smartac3.auto_mode.json b/tests/components/tado/fixtures/smartac3.auto_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.auto_mode.json rename to tests/components/tado/fixtures/smartac3.auto_mode.json diff --git a/tests/fixtures/tado/smartac3.cool_mode.json b/tests/components/tado/fixtures/smartac3.cool_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.cool_mode.json rename to tests/components/tado/fixtures/smartac3.cool_mode.json diff --git a/tests/fixtures/tado/smartac3.dry_mode.json b/tests/components/tado/fixtures/smartac3.dry_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.dry_mode.json rename to tests/components/tado/fixtures/smartac3.dry_mode.json diff --git a/tests/fixtures/tado/smartac3.fan_mode.json b/tests/components/tado/fixtures/smartac3.fan_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.fan_mode.json rename to tests/components/tado/fixtures/smartac3.fan_mode.json diff --git a/tests/fixtures/tado/smartac3.heat_mode.json b/tests/components/tado/fixtures/smartac3.heat_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.heat_mode.json rename to tests/components/tado/fixtures/smartac3.heat_mode.json diff --git a/tests/fixtures/tado/smartac3.hvac_off.json b/tests/components/tado/fixtures/smartac3.hvac_off.json similarity index 100% rename from tests/fixtures/tado/smartac3.hvac_off.json rename to tests/components/tado/fixtures/smartac3.hvac_off.json diff --git a/tests/fixtures/tado/smartac3.manual_off.json b/tests/components/tado/fixtures/smartac3.manual_off.json similarity index 100% rename from tests/fixtures/tado/smartac3.manual_off.json rename to tests/components/tado/fixtures/smartac3.manual_off.json diff --git a/tests/fixtures/tado/smartac3.offline.json b/tests/components/tado/fixtures/smartac3.offline.json similarity index 100% rename from tests/fixtures/tado/smartac3.offline.json rename to tests/components/tado/fixtures/smartac3.offline.json diff --git a/tests/fixtures/tado/smartac3.smart_mode.json b/tests/components/tado/fixtures/smartac3.smart_mode.json similarity index 100% rename from tests/fixtures/tado/smartac3.smart_mode.json rename to tests/components/tado/fixtures/smartac3.smart_mode.json diff --git a/tests/fixtures/tado/smartac3.turning_off.json b/tests/components/tado/fixtures/smartac3.turning_off.json similarity index 100% rename from tests/fixtures/tado/smartac3.turning_off.json rename to tests/components/tado/fixtures/smartac3.turning_off.json diff --git a/tests/fixtures/tado/smartac3.with_swing.json b/tests/components/tado/fixtures/smartac3.with_swing.json similarity index 100% rename from tests/fixtures/tado/smartac3.with_swing.json rename to tests/components/tado/fixtures/smartac3.with_swing.json diff --git a/tests/fixtures/tado/tadov2.heating.auto_mode.json b/tests/components/tado/fixtures/tadov2.heating.auto_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.heating.auto_mode.json rename to tests/components/tado/fixtures/tadov2.heating.auto_mode.json diff --git a/tests/fixtures/tado/tadov2.heating.manual_mode.json b/tests/components/tado/fixtures/tadov2.heating.manual_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.heating.manual_mode.json rename to tests/components/tado/fixtures/tadov2.heating.manual_mode.json diff --git a/tests/fixtures/tado/tadov2.heating.off_mode.json b/tests/components/tado/fixtures/tadov2.heating.off_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.heating.off_mode.json rename to tests/components/tado/fixtures/tadov2.heating.off_mode.json diff --git a/tests/fixtures/tado/tadov2.water_heater.auto_mode.json b/tests/components/tado/fixtures/tadov2.water_heater.auto_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.water_heater.auto_mode.json rename to tests/components/tado/fixtures/tadov2.water_heater.auto_mode.json diff --git a/tests/fixtures/tado/tadov2.water_heater.heating.json b/tests/components/tado/fixtures/tadov2.water_heater.heating.json similarity index 100% rename from tests/fixtures/tado/tadov2.water_heater.heating.json rename to tests/components/tado/fixtures/tadov2.water_heater.heating.json diff --git a/tests/fixtures/tado/tadov2.water_heater.manual_mode.json b/tests/components/tado/fixtures/tadov2.water_heater.manual_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.water_heater.manual_mode.json rename to tests/components/tado/fixtures/tadov2.water_heater.manual_mode.json diff --git a/tests/fixtures/tado/tadov2.water_heater.off_mode.json b/tests/components/tado/fixtures/tadov2.water_heater.off_mode.json similarity index 100% rename from tests/fixtures/tado/tadov2.water_heater.off_mode.json rename to tests/components/tado/fixtures/tadov2.water_heater.off_mode.json diff --git a/tests/fixtures/tado/tadov2.zone_capabilities.json b/tests/components/tado/fixtures/tadov2.zone_capabilities.json similarity index 100% rename from tests/fixtures/tado/tadov2.zone_capabilities.json rename to tests/components/tado/fixtures/tadov2.zone_capabilities.json diff --git a/tests/fixtures/tado/token.json b/tests/components/tado/fixtures/token.json similarity index 100% rename from tests/fixtures/tado/token.json rename to tests/components/tado/fixtures/token.json diff --git a/tests/fixtures/tado/water_heater_zone_capabilities.json b/tests/components/tado/fixtures/water_heater_zone_capabilities.json similarity index 100% rename from tests/fixtures/tado/water_heater_zone_capabilities.json rename to tests/components/tado/fixtures/water_heater_zone_capabilities.json diff --git a/tests/fixtures/tado/weather.json b/tests/components/tado/fixtures/weather.json similarity index 100% rename from tests/fixtures/tado/weather.json rename to tests/components/tado/fixtures/weather.json diff --git a/tests/fixtures/tado/zone_capabilities.json b/tests/components/tado/fixtures/zone_capabilities.json similarity index 100% rename from tests/fixtures/tado/zone_capabilities.json rename to tests/components/tado/fixtures/zone_capabilities.json diff --git a/tests/fixtures/tado/zone_default_overlay.json b/tests/components/tado/fixtures/zone_default_overlay.json similarity index 100% rename from tests/fixtures/tado/zone_default_overlay.json rename to tests/components/tado/fixtures/zone_default_overlay.json diff --git a/tests/fixtures/tado/zone_state.json b/tests/components/tado/fixtures/zone_state.json similarity index 100% rename from tests/fixtures/tado/zone_state.json rename to tests/components/tado/fixtures/zone_state.json diff --git a/tests/fixtures/tado/zone_states.json b/tests/components/tado/fixtures/zone_states.json similarity index 100% rename from tests/fixtures/tado/zone_states.json rename to tests/components/tado/fixtures/zone_states.json diff --git a/tests/fixtures/tado/zone_with_swing_capabilities.json b/tests/components/tado/fixtures/zone_with_swing_capabilities.json similarity index 100% rename from tests/fixtures/tado/zone_with_swing_capabilities.json rename to tests/components/tado/fixtures/zone_with_swing_capabilities.json diff --git a/tests/fixtures/tado/zones.json b/tests/components/tado/fixtures/zones.json similarity index 100% rename from tests/fixtures/tado/zones.json rename to tests/components/tado/fixtures/zones.json diff --git a/tests/fixtures/telegram/configuration.yaml b/tests/components/telegram/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/telegram/configuration.yaml rename to tests/components/telegram/fixtures/configuration.yaml diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index 6f8d1f989f9..8c67c5af3a2 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -1,5 +1,4 @@ """The tests for the telegram.notify platform.""" -from os import path from unittest.mock import patch from homeassistant import config as hass_config @@ -8,6 +7,8 @@ from homeassistant.components.telegram import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +from tests.common import get_fixture_path + async def test_reload_notify(hass): """Verify we can reload the notify service.""" @@ -30,11 +31,7 @@ async def test_reload_notify(hass): assert hass.services.has_service(notify.DOMAIN, DOMAIN) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "telegram/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "telegram") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -46,7 +43,3 @@ async def test_reload_notify(hass): assert not hass.services.has_service(notify.DOMAIN, DOMAIN) assert hass.services.has_service(notify.DOMAIN, "telegram_reloaded") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/template/broken_configuration.yaml b/tests/components/template/fixtures/broken_configuration.yaml similarity index 100% rename from tests/fixtures/template/broken_configuration.yaml rename to tests/components/template/fixtures/broken_configuration.yaml diff --git a/tests/fixtures/template/configuration.yaml.corrupt b/tests/components/template/fixtures/configuration.yaml.corrupt similarity index 100% rename from tests/fixtures/template/configuration.yaml.corrupt rename to tests/components/template/fixtures/configuration.yaml.corrupt diff --git a/tests/fixtures/template/empty_configuration.yaml b/tests/components/template/fixtures/empty_configuration.yaml similarity index 100% rename from tests/fixtures/template/empty_configuration.yaml rename to tests/components/template/fixtures/empty_configuration.yaml diff --git a/tests/fixtures/template/ref_configuration.yaml b/tests/components/template/fixtures/ref_configuration.yaml similarity index 100% rename from tests/fixtures/template/ref_configuration.yaml rename to tests/components/template/fixtures/ref_configuration.yaml diff --git a/tests/fixtures/template/sensor_configuration.yaml b/tests/components/template/fixtures/sensor_configuration.yaml similarity index 100% rename from tests/fixtures/template/sensor_configuration.yaml rename to tests/components/template/fixtures/sensor_configuration.yaml diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 28caf673d01..08ffdbf6cb0 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -1,6 +1,5 @@ """The test for the Template sensor platform.""" from datetime import timedelta -from os import path from unittest.mock import patch import pytest @@ -11,7 +10,7 @@ from homeassistant.helpers.reload import SERVICE_RELOAD from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path @pytest.mark.parametrize("count,domain", [(1, "sensor")]) @@ -250,17 +249,9 @@ async def test_reload_sensors_that_reference_other_template_sensors(hass, start_ assert hass.states.get("sensor.test3").state == "2" -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) - - async def async_yaml_patch_helper(hass, filename): """Help update configuration.yaml.""" - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - f"template/{filename}", - ) + yaml_path = get_fixture_path(filename, "template") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, diff --git a/tests/fixtures/trace/automation_saved_traces.json b/tests/components/trace/fixtures/automation_saved_traces.json similarity index 100% rename from tests/fixtures/trace/automation_saved_traces.json rename to tests/components/trace/fixtures/automation_saved_traces.json diff --git a/tests/fixtures/trace/script_saved_traces.json b/tests/components/trace/fixtures/script_saved_traces.json similarity index 100% rename from tests/fixtures/trace/script_saved_traces.json rename to tests/components/trace/fixtures/script_saved_traces.json diff --git a/tests/fixtures/trend/configuration.yaml b/tests/components/trend/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/trend/configuration.yaml rename to tests/components/trend/fixtures/configuration.yaml diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index be414b73042..4716762d2e7 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,6 +1,5 @@ """The test for the Trend sensor platform.""" from datetime import timedelta -from os import path from unittest.mock import patch from homeassistant import config as hass_config, setup @@ -8,7 +7,11 @@ from homeassistant.components.trend import DOMAIN from homeassistant.const import SERVICE_RELOAD import homeassistant.util.dt as dt_util -from tests.common import assert_setup_component, get_test_home_assistant +from tests.common import ( + assert_setup_component, + get_fixture_path, + get_test_home_assistant, +) class TestTrendBinarySensor: @@ -395,11 +398,7 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.test_trend_sensor") - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "trend/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "trend") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( DOMAIN, @@ -413,7 +412,3 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.test_trend_sensor") is None assert hass.states.get("binary_sensor.second_test_trend_sensor") - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/unifi_direct.txt b/tests/components/unifi_direct/fixtures/data.txt similarity index 100% rename from tests/fixtures/unifi_direct.txt rename to tests/components/unifi_direct/fixtures/data.txt diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 9f594901e94..700f490bba5 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -74,7 +74,7 @@ async def test_get_device_name(mock_ssh, hass): CONF_CONSIDER_HOME: timedelta(seconds=180), } } - mock_ssh.return_value.before = load_fixture("unifi_direct.txt") + mock_ssh.return_value.before = load_fixture("data.txt", "unifi_direct") scanner = get_scanner(hass, conf_dict) devices = scanner.scan_devices() assert len(devices) == 23 @@ -132,7 +132,7 @@ async def test_to_get_update(mock_sendline, mock_prompt, mock_login, mock_logout def test_good_response_parses(hass): """Test that the response form the AP parses to JSON correctly.""" - response = _response_to_json(load_fixture("unifi_direct.txt")) + response = _response_to_json(load_fixture("data.txt", "unifi_direct")) assert response != {} diff --git a/tests/fixtures/universal/configuration.yaml b/tests/components/universal/fixtures/configuration.yaml similarity index 100% rename from tests/fixtures/universal/configuration.yaml rename to tests/components/universal/fixtures/configuration.yaml diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 737d37052c2..4c8b125cbce 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the Universal Media player platform.""" import asyncio from copy import copy -from os import path import unittest from unittest.mock import patch @@ -24,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, mock_service +from tests.common import get_fixture_path, get_test_home_assistant, mock_service def validate_config(config): @@ -1177,11 +1176,7 @@ async def test_reload(hass): {"activity_list": ["act1", "act2"], "current_activity": "act2"}, ) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "universal/configuration.yaml", - ) + yaml_path = get_fixture_path("configuration.yaml", "universal") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "universal", @@ -1199,7 +1194,3 @@ async def test_reload(hass): assert ( "device_class" not in hass.states.get("media_player.master_bed_tv").attributes ) - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/venstar/colortouch_info.json b/tests/components/venstar/fixtures/colortouch_info.json similarity index 100% rename from tests/fixtures/venstar/colortouch_info.json rename to tests/components/venstar/fixtures/colortouch_info.json diff --git a/tests/fixtures/venstar/colortouch_root.json b/tests/components/venstar/fixtures/colortouch_root.json similarity index 100% rename from tests/fixtures/venstar/colortouch_root.json rename to tests/components/venstar/fixtures/colortouch_root.json diff --git a/tests/fixtures/venstar/colortouch_sensors.json b/tests/components/venstar/fixtures/colortouch_sensors.json similarity index 100% rename from tests/fixtures/venstar/colortouch_sensors.json rename to tests/components/venstar/fixtures/colortouch_sensors.json diff --git a/tests/fixtures/venstar/t2k_info.json b/tests/components/venstar/fixtures/t2k_info.json similarity index 100% rename from tests/fixtures/venstar/t2k_info.json rename to tests/components/venstar/fixtures/t2k_info.json diff --git a/tests/fixtures/venstar/t2k_root.json b/tests/components/venstar/fixtures/t2k_root.json similarity index 100% rename from tests/fixtures/venstar/t2k_root.json rename to tests/components/venstar/fixtures/t2k_root.json diff --git a/tests/fixtures/venstar/t2k_sensors.json b/tests/components/venstar/fixtures/t2k_sensors.json similarity index 100% rename from tests/fixtures/venstar/t2k_sensors.json rename to tests/components/venstar/fixtures/t2k_sensors.json diff --git a/tests/fixtures/vultr_account_info.json b/tests/components/vultr/fixtures/account_info.json similarity index 100% rename from tests/fixtures/vultr_account_info.json rename to tests/components/vultr/fixtures/account_info.json diff --git a/tests/fixtures/vultr_server_list.json b/tests/components/vultr/fixtures/server_list.json similarity index 100% rename from tests/fixtures/vultr_server_list.json rename to tests/components/vultr/fixtures/server_list.json diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index e0d71bf20a8..1b84ddff291 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -53,12 +53,12 @@ class TestVultrBinarySensorSetup(unittest.TestCase): """Test successful instance.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) @@ -113,12 +113,12 @@ class TestVultrBinarySensorSetup(unittest.TestCase): """Test the VultrBinarySensor fails.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index 80480a2cec2..040eac1a674 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -32,7 +32,7 @@ class TestVultr(unittest.TestCase): """Test successful setup.""" with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): response = vultr.setup(self.hass, self.config) assert response diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index bacffe8e6af..ac7008d066b 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -59,12 +59,12 @@ class TestVultrSensorSetup(unittest.TestCase): """Test the Vultr sensor class and methods.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) @@ -143,12 +143,12 @@ class TestVultrSensorSetup(unittest.TestCase): """Test the VultrSensor fails.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index d6b7392ca9c..ab3698b48f4 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -53,12 +53,12 @@ class TestVultrSwitchSetup(unittest.TestCase): """Test successful instance.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) @@ -114,7 +114,7 @@ class TestVultrSwitchSetup(unittest.TestCase): """Test turning a subscription on.""" with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ), patch("vultr.Vultr.server_start") as mock_start: for device in self.DEVICES: if device.name == "Failed Server": @@ -128,7 +128,7 @@ class TestVultrSwitchSetup(unittest.TestCase): """Test turning a subscription off.""" with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ), patch("vultr.Vultr.server_halt") as mock_halt: for device in self.DEVICES: if device.name == "A Server": @@ -147,12 +147,12 @@ class TestVultrSwitchSetup(unittest.TestCase): """Test the VultrSwitch fails.""" mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("vultr_account_info.json"), + text=load_fixture("account_info.json", "vultr"), ) with patch( "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("vultr_server_list.json")), + return_value=json.loads(load_fixture("server_list.json", "vultr")), ): # Setup hub base_vultr.setup(self.hass, VALID_CONFIG) diff --git a/tests/fixtures/wled/rgb.json b/tests/components/wled/fixtures/rgb.json similarity index 100% rename from tests/fixtures/wled/rgb.json rename to tests/components/wled/fixtures/rgb.json diff --git a/tests/fixtures/wled/rgb_single_segment.json b/tests/components/wled/fixtures/rgb_single_segment.json similarity index 100% rename from tests/fixtures/wled/rgb_single_segment.json rename to tests/components/wled/fixtures/rgb_single_segment.json diff --git a/tests/fixtures/wled/rgb_websocket.json b/tests/components/wled/fixtures/rgb_websocket.json similarity index 100% rename from tests/fixtures/wled/rgb_websocket.json rename to tests/components/wled/fixtures/rgb_websocket.json diff --git a/tests/fixtures/wled/rgbw.json b/tests/components/wled/fixtures/rgbw.json similarity index 100% rename from tests/fixtures/wled/rgbw.json rename to tests/components/wled/fixtures/rgbw.json diff --git a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json similarity index 100% rename from tests/fixtures/zwave_js/aeon_smart_switch_6_state.json rename to tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json diff --git a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json similarity index 100% rename from tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json rename to tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json diff --git a/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json similarity index 100% rename from tests/fixtures/zwave_js/aeotec_zw164_siren_state.json rename to tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json similarity index 100% rename from tests/fixtures/zwave_js/bulb_6_multi_color_state.json rename to tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json diff --git a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json similarity index 100% rename from tests/fixtures/zwave_js/chain_actuator_zws12_state.json rename to tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json diff --git a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json rename to tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json diff --git a/tests/fixtures/zwave_js/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_eurotronic_spirit_z_state.json rename to tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json rename to tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json rename to tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json rename to tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json rename to tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json rename to tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json rename to tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json similarity index 100% rename from tests/fixtures/zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json rename to tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/components/zwave_js/fixtures/controller_state.json similarity index 100% rename from tests/fixtures/zwave_js/controller_state.json rename to tests/components/zwave_js/fixtures/controller_state.json diff --git a/tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json similarity index 100% rename from tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json rename to tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json diff --git a/tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json similarity index 100% rename from tests/fixtures/zwave_js/cover_fibaro_fgr222_state.json rename to tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json diff --git a/tests/fixtures/zwave_js/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json similarity index 100% rename from tests/fixtures/zwave_js/cover_iblinds_v2_state.json rename to tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json diff --git a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json similarity index 100% rename from tests/fixtures/zwave_js/cover_qubino_shutter_state.json rename to tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json diff --git a/tests/fixtures/zwave_js/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json similarity index 100% rename from tests/fixtures/zwave_js/cover_zw062_state.json rename to tests/components/zwave_js/fixtures/cover_zw062_state.json diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json similarity index 100% rename from tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json rename to tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json diff --git a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json similarity index 100% rename from tests/fixtures/zwave_js/ecolink_door_sensor_state.json rename to tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json diff --git a/tests/fixtures/zwave_js/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json similarity index 100% rename from tests/fixtures/zwave_js/fan_ge_12730_state.json rename to tests/components/zwave_js/fixtures/fan_ge_12730_state.json diff --git a/tests/fixtures/zwave_js/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json similarity index 100% rename from tests/fixtures/zwave_js/fortrezz_ssa1_siren_state.json rename to tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json diff --git a/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json similarity index 100% rename from tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json rename to tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json similarity index 100% rename from tests/fixtures/zwave_js/hank_binary_switch_state.json rename to tests/components/zwave_js/fixtures/hank_binary_switch_state.json diff --git a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json b/tests/components/zwave_js/fixtures/in_wall_smart_fan_control_state.json similarity index 100% rename from tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json rename to tests/components/zwave_js/fixtures/in_wall_smart_fan_control_state.json diff --git a/tests/fixtures/zwave_js/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json similarity index 100% rename from tests/fixtures/zwave_js/inovelli_lzw36_state.json rename to tests/components/zwave_js/fixtures/inovelli_lzw36_state.json diff --git a/tests/fixtures/zwave_js/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json similarity index 100% rename from tests/fixtures/zwave_js/light_color_null_values_state.json rename to tests/components/zwave_js/fixtures/light_color_null_values_state.json diff --git a/tests/fixtures/zwave_js/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json similarity index 100% rename from tests/fixtures/zwave_js/lock_august_asl03_state.json rename to tests/components/zwave_js/fixtures/lock_august_asl03_state.json diff --git a/tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json similarity index 100% rename from tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json rename to tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json diff --git a/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json similarity index 100% rename from tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json rename to tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json similarity index 100% rename from tests/fixtures/zwave_js/lock_schlage_be469_state.json rename to tests/components/zwave_js/fixtures/lock_schlage_be469_state.json diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/components/zwave_js/fixtures/multisensor_6_state.json similarity index 100% rename from tests/fixtures/zwave_js/multisensor_6_state.json rename to tests/components/zwave_js/fixtures/multisensor_6_state.json diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json similarity index 100% rename from tests/fixtures/zwave_js/nortek_thermostat_added_event.json rename to tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json diff --git a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json similarity index 100% rename from tests/fixtures/zwave_js/nortek_thermostat_removed_event.json rename to tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json diff --git a/tests/fixtures/zwave_js/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json similarity index 100% rename from tests/fixtures/zwave_js/nortek_thermostat_state.json rename to tests/components/zwave_js/fixtures/nortek_thermostat_state.json diff --git a/tests/fixtures/zwave_js/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json similarity index 100% rename from tests/fixtures/zwave_js/null_name_check_state.json rename to tests/components/zwave_js/fixtures/null_name_check_state.json diff --git a/tests/fixtures/zwave_js/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json similarity index 100% rename from tests/fixtures/zwave_js/srt321_hrt4_zw_state.json rename to tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json diff --git a/tests/fixtures/zwave_js/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json similarity index 100% rename from tests/fixtures/zwave_js/vision_security_zl7432_state.json rename to tests/components/zwave_js/fixtures/vision_security_zl7432_state.json diff --git a/tests/fixtures/zwave_js/wallmote_central_scene_state.json b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json similarity index 100% rename from tests/fixtures/zwave_js/wallmote_central_scene_state.json rename to tests/components/zwave_js/fixtures/wallmote_central_scene_state.json diff --git a/tests/fixtures/zwave_js/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json similarity index 100% rename from tests/fixtures/zwave_js/zen_31_state.json rename to tests/components/zwave_js/fixtures/zen_31_state.json diff --git a/tests/fixtures/griddy/getnow.json b/tests/fixtures/griddy/getnow.json deleted file mode 100644 index 2bf685dac44..00000000000 --- a/tests/fixtures/griddy/getnow.json +++ /dev/null @@ -1,600 +0,0 @@ -{ - "now": { - "date": "2020-03-08T18:10:16Z", - "hour_num": "18", - "min_num": "10", - "settlement_point": "LZ_HOUSTON", - "price_type": "lmp", - "price_ckwh": "1.26900000000000000000", - "value_score": "11", - "mean_price_ckwh": "1.429706", - "diff_mean_ckwh": "-0.160706", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.544065", - "price_display": "1.3", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T13:10:16-05:00" - }, - "forecast": [ - { - "date": "2020-03-08T19:00:00Z", - "hour_num": "19", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.32000000", - "value_score": "12", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.113030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.3", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T14:00:00-05:00" - }, - { - "date": "2020-03-08T20:00:00Z", - "hour_num": "20", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.37400000", - "value_score": "12", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.059030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.4", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T15:00:00-05:00" - }, - { - "date": "2020-03-08T21:00:00Z", - "hour_num": "21", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.44700000", - "value_score": "13", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.013970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.4", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T16:00:00-05:00" - }, - { - "date": "2020-03-08T22:00:00Z", - "hour_num": "22", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.52600000", - "value_score": "13", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.092970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.5", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T17:00:00-05:00" - }, - { - "date": "2020-03-08T23:00:00Z", - "hour_num": "23", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "2.05100000", - "value_score": "17", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.617970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "2.1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T18:00:00-05:00" - }, - { - "date": "2020-03-09T00:00:00Z", - "hour_num": "0", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "2.07400000", - "value_score": "17", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.640970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "2.1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T19:00:00-05:00" - }, - { - "date": "2020-03-09T01:00:00Z", - "hour_num": "1", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.94400000", - "value_score": "16", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.510970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.9", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T20:00:00-05:00" - }, - { - "date": "2020-03-09T02:00:00Z", - "hour_num": "2", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.57500000", - "value_score": "14", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.141970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.6", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T21:00:00-05:00" - }, - { - "date": "2020-03-09T03:00:00Z", - "hour_num": "3", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.23700000", - "value_score": "11", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.196030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T22:00:00-05:00" - }, - { - "date": "2020-03-09T04:00:00Z", - "hour_num": "4", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.96200000", - "value_score": "9", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.471030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-08T23:00:00-05:00" - }, - { - "date": "2020-03-09T05:00:00Z", - "hour_num": "5", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.80000000", - "value_score": "8", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.633030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.8", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T00:00:00-05:00" - }, - { - "date": "2020-03-09T06:00:00Z", - "hour_num": "6", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.70000000", - "value_score": "7", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.733030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.7", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T01:00:00-05:00" - }, - { - "date": "2020-03-09T07:00:00Z", - "hour_num": "7", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.70000000", - "value_score": "7", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.733030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.7", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T02:00:00-05:00" - }, - { - "date": "2020-03-09T08:00:00Z", - "hour_num": "8", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.70000000", - "value_score": "7", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.733030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.7", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T03:00:00-05:00" - }, - { - "date": "2020-03-09T09:00:00Z", - "hour_num": "9", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.70000000", - "value_score": "7", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.733030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.7", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T04:00:00-05:00" - }, - { - "date": "2020-03-09T10:00:00Z", - "hour_num": "10", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "0.90000000", - "value_score": "8", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.533030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "0.9", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T05:00:00-05:00" - }, - { - "date": "2020-03-09T11:00:00Z", - "hour_num": "11", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.00000000", - "value_score": "9", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.433030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T06:00:00-05:00" - }, - { - "date": "2020-03-09T12:00:00Z", - "hour_num": "12", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.10000000", - "value_score": "10", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.333030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T07:00:00-05:00" - }, - { - "date": "2020-03-09T13:00:00Z", - "hour_num": "13", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.10000000", - "value_score": "10", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.333030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T08:00:00-05:00" - }, - { - "date": "2020-03-09T14:00:00Z", - "hour_num": "14", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.20000000", - "value_score": "11", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.233030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T09:00:00-05:00" - }, - { - "date": "2020-03-09T15:00:00Z", - "hour_num": "15", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.20000000", - "value_score": "11", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.233030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T10:00:00-05:00" - }, - { - "date": "2020-03-09T16:00:00Z", - "hour_num": "16", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.30000000", - "value_score": "11", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.133030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.3", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T11:00:00-05:00" - }, - { - "date": "2020-03-09T17:00:00Z", - "hour_num": "17", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.40000000", - "value_score": "12", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.033030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.4", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T12:00:00-05:00" - }, - { - "date": "2020-03-09T18:00:00Z", - "hour_num": "18", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.50000000", - "value_score": "13", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.066970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.5", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T13:00:00-05:00" - }, - { - "date": "2020-03-09T19:00:00Z", - "hour_num": "19", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.60000000", - "value_score": "14", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.166970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.6", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T14:00:00-05:00" - }, - { - "date": "2020-03-09T20:00:00Z", - "hour_num": "20", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.60000000", - "value_score": "14", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.166970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.6", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T15:00:00-05:00" - }, - { - "date": "2020-03-09T21:00:00Z", - "hour_num": "21", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.60000000", - "value_score": "14", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.166970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.6", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T16:00:00-05:00" - }, - { - "date": "2020-03-09T22:00:00Z", - "hour_num": "22", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "2.10000000", - "value_score": "18", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.666970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "2.1", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T17:00:00-05:00" - }, - { - "date": "2020-03-09T23:00:00Z", - "hour_num": "23", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "3.20000000", - "value_score": "27", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "1.766970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "3.2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T18:00:00-05:00" - }, - { - "date": "2020-03-10T00:00:00Z", - "hour_num": "0", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "2.40000000", - "value_score": "20", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.966970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "2.4", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T19:00:00-05:00" - }, - { - "date": "2020-03-10T01:00:00Z", - "hour_num": "1", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "2.00000000", - "value_score": "17", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.566970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T20:00:00-05:00" - }, - { - "date": "2020-03-10T02:00:00Z", - "hour_num": "2", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.70000000", - "value_score": "15", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "0.266970", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.7", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T21:00:00-05:00" - }, - { - "date": "2020-03-10T03:00:00Z", - "hour_num": "3", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.40000000", - "value_score": "12", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.033030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.4", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T22:00:00-05:00" - }, - { - "date": "2020-03-10T04:00:00Z", - "hour_num": "4", - "min_num": "0", - "settlement_point": "LZ_HOUSTON", - "price_type": "dam", - "price_ckwh": "1.20000000", - "value_score": "11", - "mean_price_ckwh": "1.433030", - "diff_mean_ckwh": "-0.233030", - "high_ckwh": "3.200000", - "low_ckwh": "0.700000", - "std_dev_ckwh": "0.552149", - "price_display": "1.2", - "price_display_sign": "¢", - "date_local_tz": "2020-03-09T23:00:00-05:00" - } - ], - "seconds_until_refresh": "26" -} diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index b4f47fa65b1..b99951493ca 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -1,6 +1,5 @@ """Tests for the reload helper.""" import logging -from os import path from unittest.mock import AsyncMock, Mock, patch import pytest @@ -20,6 +19,7 @@ from homeassistant.loader import async_get_integration from tests.common import ( MockModule, MockPlatform, + get_fixture_path, mock_entity_platform, mock_integration, ) @@ -57,11 +57,7 @@ async def test_reload_platform(hass): assert platform.platform_name == PLATFORM assert platform.domain == DOMAIN - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "helpers/reload_configuration.yaml", - ) + yaml_path = get_fixture_path("helpers/reload_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await async_reload_integration_platforms(hass, PLATFORM, [DOMAIN]) @@ -99,11 +95,7 @@ async def test_setup_reload_service(hass): await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "helpers/reload_configuration.yaml", - ) + yaml_path = get_fixture_path("helpers/reload_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( PLATFORM, @@ -142,11 +134,7 @@ async def test_setup_reload_service_when_async_process_component_config_fails(ha await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "helpers/reload_configuration.yaml", - ) + yaml_path = get_fixture_path("helpers/reload_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object( config, "async_process_component_config", return_value=None ): @@ -196,11 +184,7 @@ async def test_setup_reload_service_with_platform_that_provides_async_reset_plat await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "helpers/reload_configuration.yaml", - ) + yaml_path = get_fixture_path("helpers/reload_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( PLATFORM, @@ -218,11 +202,7 @@ async def test_async_integration_yaml_config(hass): """Test loading yaml config for an integration.""" mock_integration(hass, MockModule(DOMAIN)) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - f"helpers/{DOMAIN}_configuration.yaml", - ) + yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): processed_config = await async_integration_yaml_config(hass, DOMAIN) @@ -233,16 +213,8 @@ async def test_async_integration_missing_yaml_config(hass): """Test loading missing yaml config for an integration.""" mock_integration(hass, MockModule(DOMAIN)) - yaml_path = path.join( - _get_fixtures_base_path(), - "fixtures", - "helpers/does_not_exist_configuration.yaml", - ) + yaml_path = get_fixture_path("helpers/does_not_exist_configuration.yaml") with pytest.raises(FileNotFoundError), patch.object( config, "YAML_CONFIG_FILE", yaml_path ): await async_integration_yaml_config(hass, DOMAIN) - - -def _get_fixtures_base_path(): - return path.dirname(path.dirname(__file__)) From 8dcaae69e1e62c7b9a9861aa72f47343d3fcce6b Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 2 Nov 2021 04:31:30 -0400 Subject: [PATCH 0188/1452] Bump pyinsteon to 1.0.13 (#58908) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index f5f9d57d8a8..c17f441a159 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.12" + "pyinsteon==1.0.13" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index 02db16d8ee7..c84aac7e3ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1541,7 +1541,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.12 +pyinsteon==1.0.13 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be6f6dca086..c5b5ad5f9cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -920,7 +920,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.12 +pyinsteon==1.0.13 # homeassistant.components.ipma pyipma==2.0.5 From c14dcdb077ca64b6ac2aebddc7254415e93aaa9e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Nov 2021 09:39:23 +0100 Subject: [PATCH 0189/1452] Add `configuration_url` to Airly integration (#58911) --- homeassistant/components/airly/const.py | 1 + homeassistant/components/airly/sensor.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index c583a56c22b..801bca58412 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -32,3 +32,4 @@ MANUFACTURER: Final = "Airly sp. z o.o." MAX_UPDATE_INTERVAL: Final = 90 MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." +URL = "https://airly.org/map/#{latitude},{longitude}" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a331e99497e..76b4e7d9d48 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -55,6 +55,7 @@ from .const import ( MANUFACTURER, SUFFIX_LIMIT, SUFFIX_PERCENT, + URL, ) PARALLEL_UPDATES = 1 @@ -157,6 +158,9 @@ class AirlySensor(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, + configuration_url=URL.format( + latitude=coordinator.latitude, longitude=coordinator.longitude + ), ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( From 9d7786f887c0ba5914024a049eec89bd82ec4c3f Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Tue, 2 Nov 2021 09:54:28 +0100 Subject: [PATCH 0190/1452] Add ROCKROBO_S6_PURE to supported vacuums for xiaomi_miio (#58901) --- homeassistant/components/xiaomi_miio/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 69c279df493..c140ad526e2 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -200,6 +200,7 @@ MODELS_LIGHT = ( ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, @@ -210,6 +211,7 @@ MODELS_VACUUM = [ ROCKROBO_S5_MAX, ROCKROBO_S6, ROCKROBO_S6_MAXV, + ROCKROBO_S6_PURE, ROCKROBO_S7, ] MODELS_VACUUM_WITH_MOP = [ @@ -218,6 +220,7 @@ MODELS_VACUUM_WITH_MOP = [ ROCKROBO_S5_MAX, ROCKROBO_S6, ROCKROBO_S6_MAXV, + ROCKROBO_S6_PURE, ROCKROBO_S7, ] From bfb0d8dd19a3cb261a17324605cffb6028eb7e24 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Tue, 2 Nov 2021 11:11:46 +0100 Subject: [PATCH 0191/1452] Split wallbox sensor type and number type (#58807) --- homeassistant/components/wallbox/__init__.py | 11 +- homeassistant/components/wallbox/const.py | 100 --------------- homeassistant/components/wallbox/number.py | 49 ++++++-- homeassistant/components/wallbox/sensor.py | 121 ++++++++++++++++++- tests/components/wallbox/__init__.py | 13 +- 5 files changed, 161 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index a1361984606..aade0c430f6 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -18,7 +18,6 @@ from .const import ( CONF_MAX_CHARGING_CURRENT_KEY, CONF_STATION, DOMAIN, - SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -71,16 +70,8 @@ class WallboxCoordinator(DataUpdateCoordinator): CONF_MAX_CHARGING_CURRENT_KEY ] - filtered_data = {k: data[k] for k in SENSOR_TYPES if k in data} + return data - for key, value in filtered_data.items(): - if (sensor_round := SENSOR_TYPES[key].precision) is not None: - try: - filtered_data[key] = round(value, sensor_round) - except TypeError: - _LOGGER.debug("Cannot format %s", key) - - return filtered_data except requests.exceptions.HTTPError as wallbox_connection_error: raise ConnectionError from wallbox_connection_error diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 26af7f4c499..e753d548987 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -1,20 +1,4 @@ """Constants for the Wallbox integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ELECTRIC_CURRENT_AMPERE, - ENERGY_KILO_WATT_HOUR, - LENGTH_KILOMETERS, - PERCENTAGE, - POWER_KILO_WATT, -) DOMAIN = "wallbox" @@ -32,88 +16,4 @@ CONF_MAX_AVAILABLE_POWER_KEY = "max_available_power" CONF_MAX_CHARGING_CURRENT_KEY = "max_charging_current" CONF_STATE_OF_CHARGE_KEY = "state_of_charge" CONF_STATUS_DESCRIPTION_KEY = "status_description" - CONF_CONNECTIONS = "connections" - - -@dataclass -class WallboxSensorEntityDescription(SensorEntityDescription): - """Describes Wallbox sensor entity.""" - - precision: int | None = None - - -SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { - CONF_CHARGING_POWER_KEY: WallboxSensorEntityDescription( - key=CONF_CHARGING_POWER_KEY, - name="Charging Power", - precision=2, - native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - ), - CONF_MAX_AVAILABLE_POWER_KEY: WallboxSensorEntityDescription( - key=CONF_MAX_AVAILABLE_POWER_KEY, - name="Max Available Power", - precision=0, - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - ), - CONF_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( - key=CONF_CHARGING_SPEED_KEY, - icon="mdi:speedometer", - name="Charging Speed", - precision=0, - ), - CONF_ADDED_RANGE_KEY: WallboxSensorEntityDescription( - key=CONF_ADDED_RANGE_KEY, - icon="mdi:map-marker-distance", - name="Added Range", - precision=0, - native_unit_of_measurement=LENGTH_KILOMETERS, - ), - CONF_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( - key=CONF_ADDED_ENERGY_KEY, - name="Added Energy", - precision=2, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - ), - CONF_CHARGING_TIME_KEY: WallboxSensorEntityDescription( - key=CONF_CHARGING_TIME_KEY, - icon="mdi:timer", - name="Charging Time", - ), - CONF_COST_KEY: WallboxSensorEntityDescription( - key=CONF_COST_KEY, - icon="mdi:ev-station", - name="Cost", - ), - CONF_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( - key=CONF_STATE_OF_CHARGE_KEY, - name="State of Charge", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - ), - CONF_CURRENT_MODE_KEY: WallboxSensorEntityDescription( - key=CONF_CURRENT_MODE_KEY, - icon="mdi:ev-station", - name="Current Mode", - ), - CONF_DEPOT_PRICE_KEY: WallboxSensorEntityDescription( - key=CONF_DEPOT_PRICE_KEY, - icon="mdi:ev-station", - name="Depot Price", - precision=2, - ), - CONF_STATUS_DESCRIPTION_KEY: WallboxSensorEntityDescription( - key=CONF_STATUS_DESCRIPTION_KEY, - icon="mdi:ev-station", - name="Status Description", - ), - CONF_MAX_CHARGING_CURRENT_KEY: WallboxSensorEntityDescription( - key=CONF_MAX_CHARGING_CURRENT_KEY, - name="Max. Charging Current", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - ), -} diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 01e453593f3..837e880afcb 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -1,5 +1,10 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" -from homeassistant.components.number import NumberEntity +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.const import DEVICE_CLASS_CURRENT from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import InvalidAuth @@ -8,35 +13,59 @@ from .const import ( CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, DOMAIN, - SENSOR_TYPES, ) +@dataclass +class WallboxNumberEntityDescription(NumberEntityDescription): + """Describes Wallbox sensor entity.""" + + min_value: float = 0 + + +NUMBER_TYPES: dict[str, NumberEntityDescription] = { + CONF_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( + key=CONF_MAX_CHARGING_CURRENT_KEY, + name="Max. Charging Current", + device_class=DEVICE_CLASS_CURRENT, + min_value=6, + ), +} + + async def async_setup_entry(hass, config, async_add_entities): """Create wallbox sensor entities in HASS.""" coordinator = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] + # Check if the user is authorized to change current, if so, add number component: try: await coordinator.async_set_charging_current( coordinator.data[CONF_MAX_CHARGING_CURRENT_KEY] ) except InvalidAuth: - pass - else: - async_add_entities([WallboxNumber(coordinator, config)]) + return + + async_add_entities( + [ + WallboxNumber(coordinator, config, description) + for ent in coordinator.data + if (description := NUMBER_TYPES.get(ent)) + ] + ) class WallboxNumber(CoordinatorEntity, NumberEntity): """Representation of the Wallbox portal.""" - def __init__(self, coordinator, config): + def __init__( + self, coordinator, config, description: WallboxNumberEntityDescription + ): """Initialize a Wallbox sensor.""" super().__init__(coordinator) - sensor_description = SENSOR_TYPES[CONF_MAX_CHARGING_CURRENT_KEY] + self.entity_description = description self._coordinator = coordinator - self._attr_name = f"{config.title} {sensor_description.name}" - self._attr_min_value = 6 - self._attr_device_class = sensor_description.device_class + self._attr_name = f"{config.title} {description.name}" + self._attr_min_value = description.min_value @property def max_value(self): diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 3b87d3b29c3..25c0796d0bd 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -1,17 +1,122 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" -from homeassistant.components.sensor import SensorEntity +from __future__ import annotations + +from dataclasses import dataclass +import logging + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ELECTRIC_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + CONF_ADDED_ENERGY_KEY, + CONF_ADDED_RANGE_KEY, + CONF_CHARGING_POWER_KEY, + CONF_CHARGING_SPEED_KEY, CONF_CONNECTIONS, + CONF_COST_KEY, + CONF_CURRENT_MODE_KEY, + CONF_DEPOT_PRICE_KEY, + CONF_MAX_AVAILABLE_POWER_KEY, + CONF_MAX_CHARGING_CURRENT_KEY, + CONF_STATE_OF_CHARGE_KEY, + CONF_STATUS_DESCRIPTION_KEY, DOMAIN, - SENSOR_TYPES, - WallboxSensorEntityDescription, ) CONF_STATION = "station" UPDATE_INTERVAL = 30 +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class WallboxSensorEntityDescription(SensorEntityDescription): + """Describes Wallbox sensor entity.""" + + precision: int | None = None + + +SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { + CONF_CHARGING_POWER_KEY: WallboxSensorEntityDescription( + key=CONF_CHARGING_POWER_KEY, + name="Charging Power", + precision=2, + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + CONF_MAX_AVAILABLE_POWER_KEY: WallboxSensorEntityDescription( + key=CONF_MAX_AVAILABLE_POWER_KEY, + name="Max Available Power", + precision=0, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( + key=CONF_CHARGING_SPEED_KEY, + icon="mdi:speedometer", + name="Charging Speed", + precision=0, + ), + CONF_ADDED_RANGE_KEY: WallboxSensorEntityDescription( + key=CONF_ADDED_RANGE_KEY, + icon="mdi:map-marker-distance", + name="Added Range", + precision=0, + native_unit_of_measurement=LENGTH_KILOMETERS, + ), + CONF_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( + key=CONF_ADDED_ENERGY_KEY, + name="Added Energy", + precision=2, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + CONF_COST_KEY: WallboxSensorEntityDescription( + key=CONF_COST_KEY, + icon="mdi:ev-station", + name="Cost", + ), + CONF_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( + key=CONF_STATE_OF_CHARGE_KEY, + name="State of Charge", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + CONF_CURRENT_MODE_KEY: WallboxSensorEntityDescription( + key=CONF_CURRENT_MODE_KEY, + icon="mdi:ev-station", + name="Current Mode", + ), + CONF_DEPOT_PRICE_KEY: WallboxSensorEntityDescription( + key=CONF_DEPOT_PRICE_KEY, + icon="mdi:ev-station", + name="Depot Price", + precision=2, + ), + CONF_STATUS_DESCRIPTION_KEY: WallboxSensorEntityDescription( + key=CONF_STATUS_DESCRIPTION_KEY, + icon="mdi:ev-station", + name="Status Description", + ), + CONF_MAX_CHARGING_CURRENT_KEY: WallboxSensorEntityDescription( + key=CONF_MAX_CHARGING_CURRENT_KEY, + name="Max. Charging Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), +} + async def async_setup_entry(hass, config, async_add_entities): """Create wallbox sensor entities in HASS.""" @@ -21,7 +126,7 @@ async def async_setup_entry(hass, config, async_add_entities): [ WallboxSensor(coordinator, config, description) for ent in coordinator.data - if (description := SENSOR_TYPES[ent]) + if (description := SENSOR_TYPES.get(ent)) ] ) @@ -42,4 +147,12 @@ class WallboxSensor(CoordinatorEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" + if (sensor_round := self.entity_description.precision) is not None: + try: + return round( + self.coordinator.data[self.entity_description.key], sensor_round + ) + except TypeError: + _LOGGER.debug("Cannot format %s", self._attr_name) + return None return self.coordinator.data[self.entity_description.key] diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index f8031bd86a4..21d1f0acbc5 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -18,14 +18,9 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from .const import CONF_ERROR, CONF_JWT, CONF_STATUS, CONF_TTL, CONF_USER_ID + from tests.common import MockConfigEntry -from tests.components.wallbox.const import ( - CONF_ERROR, - CONF_JWT, - CONF_STATUS, - CONF_TTL, - CONF_USER_ID, -) test_response = json.loads( json.dumps( @@ -33,8 +28,8 @@ test_response = json.loads( CONF_CHARGING_POWER_KEY: 0, CONF_MAX_AVAILABLE_POWER_KEY: 25.2, CONF_CHARGING_SPEED_KEY: 0, - CONF_ADDED_RANGE_KEY: "xx", - CONF_ADDED_ENERGY_KEY: "44.697", + CONF_ADDED_RANGE_KEY: 150, + CONF_ADDED_ENERGY_KEY: 44.697, CONF_DATA_KEY: {CONF_MAX_CHARGING_CURRENT_KEY: 24}, } ) From 4746ff379823fe552cb241a5fa65ebb59cdc20f3 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 2 Nov 2021 05:33:08 -0700 Subject: [PATCH 0192/1452] Address late review of motionEye services (#58924) --- homeassistant/components/motioneye/camera.py | 15 ++++++++------- tests/components/motioneye/test_camera.py | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 428e2f31c81..dac5dd7093f 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -77,12 +77,14 @@ SCHEMA_TEXT_OVERLAY = vol.In( ) SCHEMA_SERVICE_SET_TEXT = vol.Schema( vol.All( - { - vol.Optional(KEY_TEXT_OVERLAY_LEFT): SCHEMA_TEXT_OVERLAY, - vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT): cv.string, - vol.Optional(KEY_TEXT_OVERLAY_RIGHT): SCHEMA_TEXT_OVERLAY, - vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT): cv.string, - }, + cv.make_entity_service_schema( + { + vol.Optional(KEY_TEXT_OVERLAY_LEFT): SCHEMA_TEXT_OVERLAY, + vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT): cv.string, + vol.Optional(KEY_TEXT_OVERLAY_RIGHT): SCHEMA_TEXT_OVERLAY, + vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT): cv.string, + }, + ), cv.has_at_least_one_key( KEY_TEXT_OVERLAY_LEFT, KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT, @@ -90,7 +92,6 @@ SCHEMA_SERVICE_SET_TEXT = vol.Schema( KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT, ), ), - extra=vol.ALLOW_EXTRA, ) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index b3d19237165..c1144256ae2 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -414,9 +414,9 @@ async def test_set_text_overlay_bad_entity_identifier(hass: HomeAssistant) -> No } client.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) - await hass.async_block_till_done() - assert not client.async_set_camera.called + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) + await hass.async_block_till_done() async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None: From f73c734fb6da1f09a7ad1909d727fb39ab6a21a1 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 2 Nov 2021 21:32:02 +0800 Subject: [PATCH 0193/1452] Add libav.mpegts to logging filter (#58937) --- homeassistant/components/stream/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index cef70a2e809..77e946770e6 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -120,6 +120,7 @@ def filter_libav_logging() -> None: "libav.rtsp", "libav.tcp", "libav.tls", + "libav.mpegts", "libav.NULL", ): logging.getLogger(logging_namespace).addFilter(libav_filter) From 8d910c50797f007724669fdeb55eecd0a24c2b2f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Nov 2021 16:56:38 +0100 Subject: [PATCH 0194/1452] Revert "Add offset support to time trigger" (#58947) --- .../components/homeassistant/triggers/time.py | 51 ++--- .../homeassistant/triggers/test_time.py | 184 ------------------ 2 files changed, 11 insertions(+), 224 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index bdb66b718e8..1bfdc0e4e58 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -1,5 +1,5 @@ """Offer time listening automation rules.""" -from datetime import datetime, timedelta +from datetime import datetime from functools import partial import voluptuous as vol @@ -8,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, - CONF_ENTITY_ID, - CONF_OFFSET, CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -25,21 +23,9 @@ import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs -_TIME_TRIGGER_ENTITY_REFERENCE = vol.All( - str, cv.entity_domain(["input_datetime", "sensor"]) -) - -_TIME_TRIGGER_WITH_OFFSET_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY_ID): _TIME_TRIGGER_ENTITY_REFERENCE, - vol.Required(CONF_OFFSET): cv.time_period, - } -) - _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, - _TIME_TRIGGER_ENTITY_REFERENCE, - _TIME_TRIGGER_WITH_OFFSET_SCHEMA, + vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'", ) @@ -57,7 +43,6 @@ async def async_attach_trigger(hass, config, action, automation_info): entities = {} removes = [] job = HassJob(action) - offsets = {} @callback def time_automation_listener(description, now, *, entity_id=None): @@ -91,8 +76,6 @@ async def async_attach_trigger(hass, config, action, automation_info): if not new_state: return - offset = offsets[entity_id] if entity_id in offsets else timedelta(0) - # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": if has_date := new_state.attributes["has_date"]: @@ -109,17 +92,14 @@ async def async_attach_trigger(hass, config, action, automation_info): if has_date: # If input_datetime has date, then track point in time. - trigger_dt = ( - datetime( - year, - month, - day, - hour, - minute, - second, - tzinfo=dt_util.DEFAULT_TIME_ZONE, - ) - + offset + trigger_dt = datetime( + year, + month, + day, + hour, + minute, + second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, ) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): @@ -151,7 +131,7 @@ async def async_attach_trigger(hass, config, action, automation_info): == sensor.DEVICE_CLASS_TIMESTAMP and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) ): - trigger_dt = dt_util.parse_datetime(new_state.state) + offset + trigger_dt = dt_util.parse_datetime(new_state.state) if trigger_dt is not None and trigger_dt > dt_util.utcnow(): remove = async_track_point_in_time( @@ -175,15 +155,6 @@ async def async_attach_trigger(hass, config, action, automation_info): # entity to_track.append(at_time) update_entity_trigger(at_time, new_state=hass.states.get(at_time)) - elif isinstance(at_time, dict) and CONF_OFFSET in at_time: - # entity with offset - entity_id = at_time.get(CONF_ENTITY_ID) - to_track.append(entity_id) - offsets[entity_id] = at_time.get(CONF_OFFSET) - update_entity_trigger( - entity_id, - new_state=hass.states.get(entity_id), - ) else: # datetime.time removes.append( diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 7961ce25026..499fcf8611e 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -150,96 +150,6 @@ async def test_if_fires_using_at_input_datetime(hass, calls, has_date, has_time) ) -@pytest.mark.parametrize( - "offset,delta", - [ - ("00:00:10", timedelta(seconds=10)), - ("-00:00:10", timedelta(seconds=-10)), - ({"minutes": 5}, timedelta(minutes=5)), - ], -) -async def test_if_fires_using_at_input_datetime_with_offset(hass, calls, offset, delta): - """Test for firing at input_datetime.""" - await async_setup_component( - hass, - "input_datetime", - {"input_datetime": {"trigger": {"has_date": True, "has_time": True}}}, - ) - now = dt_util.now() - - set_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) - trigger_dt = set_dt + delta - - await hass.services.async_call( - "input_datetime", - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.trigger", - "datetime": str(set_dt.replace(tzinfo=None)), - }, - blocking=True, - ) - - time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) - - some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" - with patch( - "homeassistant.util.dt.utcnow", - return_value=dt_util.as_utc(time_that_will_not_match_right_away), - ): - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time", - "at": { - "entity_id": "input_datetime.trigger", - "offset": offset, - }, - }, - "action": { - "service": "test.automation", - "data_template": {"some": some_data}, - }, - } - }, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 1 - assert ( - calls[0].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" - ) - - set_dt += timedelta(days=1, hours=1) - trigger_dt += timedelta(days=1, hours=1) - - await hass.services.async_call( - "input_datetime", - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.trigger", - "datetime": str(set_dt.replace(tzinfo=None)), - }, - blocking=True, - ) - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 2 - assert ( - calls[1].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" - ) - - async def test_if_fires_using_multiple_at(hass, calls): """Test for firing at.""" @@ -588,103 +498,12 @@ async def test_if_fires_using_at_sensor(hass, calls): assert len(calls) == 2 -@pytest.mark.parametrize( - "offset,delta", - [ - ("00:00:10", timedelta(seconds=10)), - ("-00:00:10", timedelta(seconds=-10)), - ({"minutes": 5}, timedelta(minutes=5)), - ], -) -async def test_if_fires_using_at_sensor_with_offset(hass, calls, offset, delta): - """Test for firing at sensor time.""" - now = dt_util.now() - - start_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) - trigger_dt = start_dt + delta - - hass.states.async_set( - "sensor.next_alarm", - start_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, - ) - - time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) - - some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" - with patch( - "homeassistant.util.dt.utcnow", - return_value=dt_util.as_utc(time_that_will_not_match_right_away), - ): - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time", - "at": { - "entity_id": "sensor.next_alarm", - "offset": offset, - }, - }, - "action": { - "service": "test.automation", - "data_template": {"some": some_data}, - }, - } - }, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 1 - assert ( - calls[0].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" - ) - - start_dt += timedelta(days=1, hours=1) - trigger_dt += timedelta(days=1, hours=1) - - hass.states.async_set( - "sensor.next_alarm", - start_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 2 - assert ( - calls[1].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" - ) - - @pytest.mark.parametrize( "conf", [ {"platform": "time", "at": "input_datetime.bla"}, {"platform": "time", "at": "sensor.bla"}, {"platform": "time", "at": "12:34"}, - { - "platform": "time", - "at": {"entity_id": "input_datetime.bla", "offset": "00:01"}, - }, - {"platform": "time", "at": {"entity_id": "sensor.bla", "offset": "-00:01"}}, - { - "platform": "time", - "at": [{"entity_id": "input_datetime.bla", "offset": "01:00:00"}], - }, - { - "platform": "time", - "at": [{"entity_id": "sensor.bla", "offset": "-01:00:00"}], - }, ], ) def test_schema_valid(conf): @@ -698,9 +517,6 @@ def test_schema_valid(conf): {"platform": "time", "at": "binary_sensor.bla"}, {"platform": "time", "at": 745}, {"platform": "time", "at": "25:00"}, - {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "0:"}}, - {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "a"}}, - {"platform": "time", "at": {"entity_id": "13:00:00", "offset": "0:10"}}, ], ) def test_schema_invalid(conf): From 9307cbf861e199adccf33fb83f797e71032680a6 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 2 Nov 2021 17:29:28 +0100 Subject: [PATCH 0195/1452] Add home/not_home icons to Asuswrt trackers (#58883) * add home/not_home icons to Asuswrt trackers like the Nmap trackers have their icons set in https://github.com/home-assistant/core/blob/2df13d01187a4fac2f9038facc180eb2c2543712/homeassistant/components/nmap_tracker/device_tracker.py#L186 * white space --- homeassistant/components/asuswrt/device_tracker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 380f7a60c32..bb96cb184a1 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -84,6 +84,11 @@ class AsusWrtDevice(ScannerEntity): """Return the hostname of device.""" return self._device.name + @property + def icon(self) -> str: + """Return device icon.""" + return "mdi:lan-connect" if self._device.is_connected else "mdi:lan-disconnect" + @property def ip_address(self) -> str: """Return the primary ip address of the device.""" From 339117aceb1932a53a7d04b980def047d935c150 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 2 Nov 2021 17:33:23 +0100 Subject: [PATCH 0196/1452] Add command_template to mqtt select platform (#58934) --- homeassistant/components/mqtt/select.py | 24 +++++--- tests/components/mqtt/test_select.py | 80 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 3857184a330..c43593dfc4b 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -19,6 +19,8 @@ from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +CONF_COMMAND_TEMPLATE = "command_template" + _LOGGER = logging.getLogger(__name__) CONF_OPTIONS = "options" @@ -43,6 +45,7 @@ def validate_config(config): _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Required(CONF_OPTIONS): cv.ensure_list, @@ -110,9 +113,16 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): self._optimistic = config[CONF_OPTIMISTIC] self._attr_options = config[CONF_OPTIONS] - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._templates = { + CONF_COMMAND_TEMPLATE: config.get(CONF_COMMAND_TEMPLATE), + CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), + } + for key, tpl in self._templates.items(): + if tpl is None: + self._templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._templates[key] = tpl.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -121,10 +131,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) if payload.lower() == "none": payload = None @@ -162,6 +169,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): async def async_select_option(self, option: str) -> None: """Update the current value.""" + payload = self._templates[CONF_COMMAND_TEMPLATE](option) if self._optimistic: self._attr_current_option = option self.async_write_ha_state() @@ -169,7 +177,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], - option, + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 4843631f98b..731aca2e178 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -171,6 +171,50 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): assert state.state == "beer" +async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mock): + """Test that set_value service works in optimistic mode and with a command_template.""" + topic = "test/select" + + fake_state = ha.State("select.test", "milk") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "milk" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "beer"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + topic, '{"option": "beer"}', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("select.test_select") + assert state.state == "beer" + + async def test_run_select_service(hass, mqtt_mock): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/select/set" @@ -206,6 +250,42 @@ async def test_run_select_service(hass, mqtt_mock): assert state.state == "beer" +async def test_run_select_service_with_command_template(hass, mqtt_mock): + """Test that set_value service works in non optimistic mode and with a command_template.""" + cmd_topic = "test/select/set" + state_topic = "test/select" + + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, state_topic, "beer") + state = hass.states.get("select.test_select") + assert state.state == "beer" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "milk"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + cmd_topic, '{"option": "milk"}', 0, False + ) + + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( From 2df1ba23460ab0e4c502bfb736cb254130e93f12 Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Tue, 2 Nov 2021 17:40:05 +0100 Subject: [PATCH 0197/1452] Add device_class to MQTT switch (#58931) --- homeassistant/components/mqtt/switch.py | 11 ++++++++++- tests/components/mqtt/test_switch.py | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index d3252525b76..9cc13ac94bd 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,11 +1,14 @@ """Support for MQTT switches.""" +from __future__ import annotations + import functools import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, @@ -48,6 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_STATE_ON): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -158,6 +162,11 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Return true if we do optimistic updates.""" return self._optimistic + @property + def device_class(self) -> str | None: + """Return the device class of the sensor.""" + return self._config.get(CONF_DEVICE_CLASS) + async def async_turn_on(self, **kwargs): """Turn the device on. diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 263ec0a2825..a3ef29d0d08 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -6,7 +6,12 @@ import pytest from homeassistant.components import switch from homeassistant.components.mqtt.switch import MQTT_SWITCH_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + STATE_OFF, + STATE_ON, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -56,6 +61,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "command_topic": "command-topic", "payload_on": 1, "payload_off": 0, + "device_class": "switch", } }, ) @@ -63,6 +69,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("switch.test") assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == "switch" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "1") @@ -387,6 +394,7 @@ async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): """Test update of discovered switch.""" data1 = ( '{ "name": "Beer",' + ' "device_class": "switch",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) From 30f7bc0f1844e97503afa90ab8a575a19bfdb66f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Nov 2021 18:11:39 +0100 Subject: [PATCH 0198/1452] Use freezegun in DST tests (#58939) --- requirements_test.txt | 2 + tests/common.py | 7 +- tests/conftest.py | 49 ++++++- tests/helpers/test_event.py | 270 +++++++++++++++--------------------- 4 files changed, 166 insertions(+), 162 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 3355cc82ef1..69af0476c5c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,6 +9,7 @@ -r requirements_test_pre_commit.txt codecov==2.1.12 coverage==6.1.1 +freezegun==1.1.0 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 @@ -18,6 +19,7 @@ pipdeptree==2.1.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.12.1 +pytest-freezegun==0.4.2 pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 diff --git a/tests/common.py b/tests/common.py index 72c18822e00..a9ba4ae86b4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -371,9 +371,12 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback def async_fire_time_changed( - hass: HomeAssistant, datetime_: datetime, fire_all: bool = False + hass: HomeAssistant, datetime_: datetime = None, fire_all: bool = False ) -> None: - """Fire a time changes event.""" + """Fire a time changed event.""" + if datetime_ is None: + datetime_ = date_util.utcnow() + hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) for task in list(hass.loop._scheduled): diff --git a/tests/conftest.py b/tests/conftest.py index 80eb75ef2bd..61dcd40e53f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import threading from unittest.mock import MagicMock, patch from aiohttp.test_utils import make_mocked_request +import freezegun import multidict import pytest import pytest_socket @@ -63,15 +64,24 @@ def pytest_configure(config): def pytest_runtest_setup(): - """Throw if tests attempt to open sockets. + """Prepare pytest_socket and freezegun. + + pytest_socket: + Throw if tests attempt to open sockets. allow_unix_socket is set to True because it's needed by asyncio. Important: socket_allow_hosts must be called before disable_socket, otherwise all destinations will be allowed. + + freezegun: + Modified to include https://github.com/spulec/freezegun/pull/424 """ pytest_socket.socket_allow_hosts(["127.0.0.1"]) disable_socket(allow_unix_socket=True) + freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime + freezegun.api.FakeDatetime = HAFakeDatetime + @pytest.fixture def socket_disabled(pytestconfig): @@ -126,6 +136,43 @@ def disable_socket(allow_unix_socket=False): socket.socket = GuardedSocket +def ha_datetime_to_fakedatetime(datetime): + """Convert datetime to FakeDatetime. + + Modified to include https://github.com/spulec/freezegun/pull/424. + """ + return freezegun.api.FakeDatetime( + datetime.year, + datetime.month, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.microsecond, + datetime.tzinfo, + fold=datetime.fold, + ) + + +class HAFakeDatetime(freezegun.api.FakeDatetime): + """Modified to include https://github.com/spulec/freezegun/pull/424.""" + + @classmethod + def now(cls, tz=None): + """Return frozen now.""" + now = cls._time_to_freeze() or freezegun.api.real_datetime.now() + if tz: + result = tz.fromutc(now.replace(tzinfo=tz)) + else: + result = now + + # Add the _tz_offset only if it's non-zero to preserve fold + if cls._tz_offset(): + result += cls._tz_offset() + + return ha_datetime_to_fakedatetime(result) + + def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9d48b0c0ada..2e8f6264f1a 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,7 +1,7 @@ """Test event helpers.""" # pylint: disable=protected-access import asyncio -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from unittest.mock import patch from astral import LocationInfo @@ -3393,66 +3393,56 @@ async def test_periodic_task_duplicate_time(hass): unsub() -async def test_periodic_task_entering_dst(hass): +# DST starts early morning March 28th 2021 +@pytest.mark.freeze_time("2021-03-28 01:28:00+01:00") +async def test_periodic_task_entering_dst(hass, freezer): """Test periodic task behavior when entering dst.""" timezone = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(timezone) specific_runs = [] - # DST starts early morning March 27th 2022 - yy = 2022 - mm = 3 - dd = 27 + today = date.today().isoformat() + tomorrow = (date.today() + timedelta(days=1)).isoformat() - # There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30 - time_that_will_not_match_right_away = datetime( - yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0 - ) # Make sure we enter DST during the test - assert ( - time_that_will_not_match_right_away.utcoffset() - != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() + now_local = dt_util.now() + assert now_local.utcoffset() != (now_local + timedelta(hours=2)).utcoffset() + + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + hour=2, + minute=30, + second=0, ) - with patch( - "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away - ): - unsub = async_track_time_change( - hass, - callback(lambda x: specific_runs.append(x)), - hour=2, - minute=30, - second=0, - ) - - async_fire_time_changed( - hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{today} 01:50:00.999999+01:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed( - hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone) - ) + # There was no 02:30 today, the event should not fire until tomorrow + freezer.move_to(f"{today} 03:50:00.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed( - hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{tomorrow} 01:50:00.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed( - hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{tomorrow} 02:50:00.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 1 unsub() -async def test_periodic_task_entering_dst_2(hass): +# DST starts early morning March 28th 2021 +@pytest.mark.freeze_time("2021-03-28 01:59:59+01:00") +async def test_periodic_task_entering_dst_2(hass, freezer): """Test periodic task behavior when entering dst. This tests a task firing every second in the range 0..58 (not *:*:59) @@ -3461,220 +3451,182 @@ async def test_periodic_task_entering_dst_2(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - # DST starts early morning March 27th 2022 - yy = 2022 - mm = 3 - dd = 27 + today = date.today().isoformat() + tomorrow = (date.today() + timedelta(days=1)).isoformat() - # There's no 2022-03-27 02:00:00, the event should not fire until 2022-03-28 03:00:00 - time_that_will_not_match_right_away = datetime( - yy, mm, dd, 1, 59, 59, tzinfo=timezone, fold=0 - ) # Make sure we enter DST during the test - assert ( - time_that_will_not_match_right_away.utcoffset() - != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() + now_local = dt_util.now() + assert now_local.utcoffset() != (now_local + timedelta(hours=2)).utcoffset() + + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + second=list(range(59)), ) - with patch( - "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away - ): - unsub = async_track_time_change( - hass, - callback(lambda x: specific_runs.append(x)), - second=list(range(59)), - ) - - async_fire_time_changed( - hass, datetime(yy, mm, dd, 1, 59, 59, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{today} 01:59:59.999999+01:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed( - hass, datetime(yy, mm, dd, 3, 0, 0, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{today} 03:00:00.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed( - hass, datetime(yy, mm, dd, 3, 0, 1, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{today} 03:00:01.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 2 - async_fire_time_changed( - hass, datetime(yy, mm, dd + 1, 1, 59, 59, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{tomorrow} 01:59:59.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 3 - async_fire_time_changed( - hass, datetime(yy, mm, dd + 1, 2, 0, 0, 999999, tzinfo=timezone) - ) + freezer.move_to(f"{tomorrow} 02:00:00.999999+02:00") + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(specific_runs) == 4 unsub() -async def test_periodic_task_leaving_dst(hass): +# DST ends early morning October 31st 2021 +@pytest.mark.freeze_time("2021-10-31 02:28:00+02:00") +async def test_periodic_task_leaving_dst(hass, freezer): """Test periodic task behavior when leaving dst.""" timezone = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(timezone) specific_runs = [] - # DST ends early morning Ocotber 30th 2022 - yy = 2022 - mm = 10 - dd = 30 - - time_that_will_not_match_right_away = datetime( - yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 - ) + today = date.today().isoformat() + tomorrow = (date.today() + timedelta(days=1)).isoformat() # Make sure we leave DST during the test - assert ( - time_that_will_not_match_right_away.utcoffset() - != time_that_will_not_match_right_away.replace(fold=1).utcoffset() - ) + now_local = dt_util.now() + assert now_local.utcoffset() != (now_local + timedelta(hours=1)).utcoffset() - with patch( - "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away - ): - unsub = async_track_time_change( - hass, - callback(lambda x: specific_runs.append(x)), - hour=2, - minute=30, - second=0, - ) + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + hour=2, + minute=30, + second=0, + ) # The task should not fire yet - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 02:28:00.999999+02:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 0 # The task should fire - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 02:30:00.999999+02:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 1 # The task should not fire again - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 02:55:00.999999+02:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 1 # DST has ended, the task should not fire yet - async_fire_time_changed( - hass, - datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1), - ) + freezer.move_to(f"{today} 02:15:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 # DST has ended await hass.async_block_till_done() assert len(specific_runs) == 1 # The task should fire - async_fire_time_changed( - hass, - datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1), - ) + freezer.move_to(f"{today} 02:45:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 await hass.async_block_till_done() assert len(specific_runs) == 2 # The task should not fire again - async_fire_time_changed( - hass, - datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), - ) + freezer.move_to(f"{today} 02:55:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 await hass.async_block_till_done() assert len(specific_runs) == 2 # The task should fire again the next day - async_fire_time_changed( - hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1) - ) + freezer.move_to(f"{tomorrow} 02:55:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 3 unsub() -async def test_periodic_task_leaving_dst_2(hass): +# DST ends early morning October 31st 2021 +@pytest.mark.freeze_time("2021-10-31 02:28:00+02:00") +async def test_periodic_task_leaving_dst_2(hass, freezer): """Test periodic task behavior when leaving dst.""" timezone = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(timezone) specific_runs = [] - # DST ends early morning Ocotber 30th 2022 - yy = 2022 - mm = 10 - dd = 30 + today = date.today().isoformat() - time_that_will_not_match_right_away = datetime( - yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 - ) # Make sure we leave DST during the test - assert ( - time_that_will_not_match_right_away.utcoffset() - != time_that_will_not_match_right_away.replace(fold=1).utcoffset() - ) + now_local = dt_util.now() + assert now_local.utcoffset() != (now_local + timedelta(hours=1)).utcoffset() - with patch( - "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away - ): - unsub = async_track_time_change( - hass, - callback(lambda x: specific_runs.append(x)), - minute=30, - second=0, - ) + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + minute=30, + second=0, + ) # The task should not fire yet - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 02:28:00.999999+02:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 0 # The task should fire - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 02:55:00.999999+02:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 1 # DST has ended, the task should not fire yet - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1) - ) + freezer.move_to(f"{today} 02:15:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 await hass.async_block_till_done() assert len(specific_runs) == 1 # The task should fire - async_fire_time_changed( - hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1) - ) + freezer.move_to(f"{today} 02:45:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 await hass.async_block_till_done() assert len(specific_runs) == 2 # The task should not fire again - async_fire_time_changed( - hass, - datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), - ) + freezer.move_to(f"{today} 02:55:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 1 await hass.async_block_till_done() assert len(specific_runs) == 2 # The task should fire again the next hour - async_fire_time_changed( - hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0) - ) + freezer.move_to(f"{today} 03:55:00.999999+01:00") + async_fire_time_changed(hass) + assert dt_util.now().fold == 0 await hass.async_block_till_done() assert len(specific_runs) == 3 From 2df52a3bf6eafcf290e0878ba658f11d3cde8647 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 18:21:08 +0100 Subject: [PATCH 0199/1452] Fix incorrect entity category in Advantage Air (#58754) --- homeassistant/components/advantage_air/binary_sensor.py | 4 ++-- homeassistant/components/advantage_air/sensor.py | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index a7d7308d78c..eec0ce7dfa5 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -74,7 +74,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index ed2e3b78156..d879693fdb5 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -6,12 +6,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -55,7 +50,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" From 0c4863198e8c6f67f9e358f7cffc95e9fcfc63b2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 2 Nov 2021 18:21:49 +0100 Subject: [PATCH 0200/1452] Add command_template to MQTT number platform (#58949) --- homeassistant/components/mqtt/number.py | 24 ++++-- tests/components/mqtt/test_number.py | 107 ++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index f39ca870505..6d0163abe32 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -31,6 +31,8 @@ from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +CONF_COMMAND_TEMPLATE = "command_template" + _LOGGER = logging.getLogger(__name__) CONF_MIN = "min" @@ -61,6 +63,7 @@ def validate_config(config): _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -133,9 +136,16 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """(Re)Setup the entity.""" self._optimistic = config[CONF_OPTIMISTIC] - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._templates = { + CONF_COMMAND_TEMPLATE: config.get(CONF_COMMAND_TEMPLATE), + CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), + } + for key, tpl in self._templates.items(): + if tpl is None: + self._templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._templates[key] = tpl.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -144,10 +154,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) try: if payload == self._config[CONF_PAYLOAD_RESET]: num_value = None @@ -224,6 +231,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): if value.is_integer(): current_number = int(value) + payload = self._templates[CONF_COMMAND_TEMPLATE](current_number) if self._optimistic: self._current_number = current_number @@ -232,7 +240,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], - current_number, + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 3b8b370eff8..797a7b894fc 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -209,6 +209,76 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): assert state.state == "42.1" +async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mock): + """Test that set_value service works in optimistic mode and with a command_template.""" + topic = "test/number" + + fake_state = ha.State("switch.test", "3") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "3" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + # Integer + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, '{"number": 30 }', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "30" + + # Float with no decimal -> integer + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.0}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, '{"number": 42 }', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42" + + # Float with decimal -> float + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.1}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + topic, '{"number": 42.1 }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42.1" + + async def test_run_number_service(hass, mqtt_mock): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/number/set" @@ -243,6 +313,43 @@ async def test_run_number_service(hass, mqtt_mock): assert state.state == "32" +async def test_run_number_service_with_command_template(hass, mqtt_mock): + """Test that set_value service works in non optimistic mode and with a command_template.""" + cmd_topic = "test/number/set" + state_topic = "test/number" + + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, state_topic, "32") + state = hass.states.get("number.test_number") + assert state.state == "32" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + cmd_topic, '{"number": 30 }', 0, False + ) + state = hass.states.get("number.test_number") + assert state.state == "32" + + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( From 26055e1f1430369b30765044ee2fac54a2b5bc01 Mon Sep 17 00:00:00 2001 From: David Langerman | Onyx Zero Software Date: Tue, 2 Nov 2021 13:23:30 -0400 Subject: [PATCH 0201/1452] Add support for TP-Link KL400L5 (#58944) Co-authored-by: J. Nick Koston --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 4db98d680d3..1531f96c545 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,10 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + { + "hostname": "k[lp]*", + "macaddress": "60A4B7*" + }, { "hostname": "k[lp]*", "macaddress": "005F67*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 92222cf092b..d21fc8d63b0 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -361,6 +361,11 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "60A4B7*" + }, { "domain": "tplink", "hostname": "k[lp]*", From e983370c2792d79b8ec11f9ffdb1f62cf7372548 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Nov 2021 18:27:06 +0100 Subject: [PATCH 0202/1452] Use zeroconf attributes (A-D) (#58835) Co-authored-by: epenet --- .../components/apple_tv/config_flow.py | 22 +++++++-------- homeassistant/components/axis/config_flow.py | 17 +++++++---- homeassistant/components/bond/config_flow.py | 5 ++-- .../components/bosch_shc/config_flow.py | 28 +++++++++---------- .../components/brother/config_flow.py | 5 ++-- homeassistant/components/cast/config_flow.py | 6 +++- .../components/daikin/config_flow.py | 13 ++++++--- .../devolo_home_control/config_flow.py | 6 +++- .../devolo_home_network/config_flow.py | 10 +++---- .../components/doorbird/config_flow.py | 13 ++++++--- homeassistant/components/zeroconf/__init__.py | 3 ++ 11 files changed, 78 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 56ad1e83e23..f1a89688ad4 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -9,17 +9,13 @@ from pyatv.convert import protocol_str import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_ADDRESS, - CONF_NAME, - CONF_PIN, - CONF_PROTOCOL, - CONF_TYPE, -) +from homeassistant.components import zeroconf +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN, CONF_PROTOCOL from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN @@ -148,16 +144,18 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"devices": self._devices_str()}, ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle device found via zeroconf.""" - service_type = discovery_info[CONF_TYPE] - properties = discovery_info["properties"] + service_type = discovery_info[zeroconf.ATTR_TYPE] + properties = discovery_info[zeroconf.ATTR_PROPERTIES] if service_type == "_mediaremotetv._tcp.local.": identifier = properties["UniqueIdentifier"] name = properties["Name"] elif service_type == "_touch-able._tcp.local.": - identifier = discovery_info["name"].split(".")[0] + identifier = discovery_info[zeroconf.ATTR_NAME].split(".")[0] name = properties["CtlN"] else: return self.async_abort(reason="unknown") diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index d313e4b2745..e3994cedd39 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -6,6 +6,7 @@ from urllib.parse import urlsplit import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( @@ -17,7 +18,9 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.network import is_link_local from .const import ( @@ -174,14 +177,18 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): } ) - async def async_step_zeroconf(self, discovery_info: dict): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Prepare configuration for a Zeroconf discovered Axis device.""" return await self._process_discovered_device( { - CONF_HOST: discovery_info[CONF_HOST], - CONF_MAC: format_mac(discovery_info["properties"]["macaddress"]), - CONF_NAME: discovery_info["name"].split(".", 1)[0], - CONF_PORT: discovery_info[CONF_PORT], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_MAC: format_mac( + discovery_info[zeroconf.ATTR_PROPERTIES]["macaddress"] + ), + CONF_NAME: discovery_info[zeroconf.ATTR_NAME].split(".", 1)[0], + CONF_PORT: discovery_info[zeroconf.ATTR_PORT], } ) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 6f70d37e0a1..5996cd03bae 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -10,6 +10,7 @@ from bond_api import Bond import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant @@ -94,8 +95,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - name: str = discovery_info[CONF_NAME] - host: str = discovery_info[CONF_HOST] + name: str = discovery_info[zeroconf.ATTR_NAME] + host: str = discovery_info[zeroconf.ATTR_HOST] bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) for entry in self._async_current_entries(): diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 416dc6cf304..c86569d6a4d 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -12,7 +12,7 @@ from boschshcpy.exceptions import ( import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.components.zeroconf import async_get_instance +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN from .const import ( @@ -40,7 +40,7 @@ def write_tls_asset(hass: core.HomeAssistant, filename: str, asset: bytes) -> No file_handle.write(asset.decode("utf-8")) -def create_credentials_and_validate(hass, host, user_input, zeroconf): +def create_credentials_and_validate(hass, host, user_input, zeroconf_instance): """Create and store credentials and validate session.""" helper = SHCRegisterClient(host, user_input[CONF_PASSWORD]) result = helper.register(host, "HomeAssistant") @@ -54,21 +54,21 @@ def create_credentials_and_validate(hass, host, user_input, zeroconf): hass.config.path(DOMAIN, CONF_SHC_CERT), hass.config.path(DOMAIN, CONF_SHC_KEY), True, - zeroconf, + zeroconf_instance, ) session.authenticate() return result -def get_info_from_host(hass, host, zeroconf): +def get_info_from_host(hass, host, zeroconf_instance): """Get information from host.""" session = SHCSession( host, "", "", True, - zeroconf, + zeroconf_instance, ) information = session.mdns_info() return {"title": information.name, "unique_id": information.unique_id} @@ -123,14 +123,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the credentials step.""" errors = {} if user_input is not None: - zeroconf = await async_get_instance(self.hass) + zeroconf_instance = await zeroconf.async_get_instance(self.hass) try: result = await self.hass.async_add_executor_job( create_credentials_and_validate, self.hass, self.host, user_input, - zeroconf, + zeroconf_instance, ) except SHCAuthenticationError: errors["base"] = "invalid_auth" @@ -183,14 +183,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" - if not discovery_info.get("name", "").startswith("Bosch SHC"): + if not discovery_info.get(zeroconf.ATTR_NAME, "").startswith("Bosch SHC"): return self.async_abort(reason="not_bosch_shc") try: hosts = ( - discovery_info["host"] - if isinstance(discovery_info["host"], list) - else [discovery_info["host"]] + discovery_info[zeroconf.ATTR_HOST] + if isinstance(discovery_info[zeroconf.ATTR_HOST], list) + else [discovery_info[zeroconf.ATTR_HOST]] ) for host in hosts: if host.startswith("169."): # skip link local address @@ -202,7 +202,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except SHCConnectionError: return self.async_abort(reason="cannot_connect") - local_name = discovery_info["hostname"][:-1] + local_name = discovery_info[zeroconf.ATTR_HOSTNAME][:-1] node_name = local_name[: -len(".local")] await self.async_set_unique_id(self.info["unique_id"]) @@ -227,11 +227,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _get_info(self, host): """Get additional information.""" - zeroconf = await async_get_instance(self.hass) + zeroconf_instance = await zeroconf.async_get_instance(self.hass) return await self.hass.async_add_executor_job( get_info_from_host, self.hass, host, - zeroconf, + zeroconf_instance, ) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 20e5938884f..fa743e68d56 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -9,6 +9,7 @@ from brother import Brother, SnmpError, UnsupportedModel import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import DiscoveryInfoType @@ -84,13 +85,13 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: brother.local. - self.host = discovery_info["hostname"].rstrip(".") + self.host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) snmp_engine = get_snmp_engine(self.hass) - model = discovery_info.get("properties", {}).get("product") + model = discovery_info.get(zeroconf.ATTR_PROPERTIES, {}).get("product") try: self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index bb316f2b511..dbf52a8f238 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -2,7 +2,9 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, CONF_UUID, DOMAIN @@ -49,7 +51,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_config() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 18447c56d18..52e4fa255aa 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -10,7 +10,10 @@ from pydaikin.discovery import Discovery import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -123,18 +126,20 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input.get(CONF_PASSWORD), ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) - devices = Discovery().poll(ip=discovery_info[CONF_HOST]) + devices = Discovery().poll(ip=discovery_info[zeroconf.ATTR_HOST]) if not devices: _LOGGER.debug( "Could not find MAC-address for %s," " make sure the required UDP ports are open (see integration documentation)", - discovery_info[CONF_HOST], + discovery_info[zeroconf.ATTR_HOST], ) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(next(iter(devices))[KEY_MAC]) self._abort_if_unique_id_configured() - self.host = discovery_info[CONF_HOST] + self.host = discovery_info[zeroconf.ATTR_HOST] return await self.async_step_user() diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index e6b3dcbe329..0cff72a4321 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -6,6 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -49,7 +50,10 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Check if it is a gateway - if discovery_info.get("properties", {}).get("MT") in SUPPORTED_MODEL_TYPES: + if ( + discovery_info.get(zeroconf.ATTR_PROPERTIES, {}).get("MT") + in SUPPORTED_MODEL_TYPES + ): await self._async_handle_discovery_without_unique_id() return await self.async_step_zeroconf_confirm() return self.async_abort(reason="Not a devolo Home Control gateway.") diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index fa0ee983b69..20efe388407 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -77,17 +77,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle zerooconf discovery.""" - if discovery_info["properties"]["MT"] in ["2600", "2601"]: + if discovery_info[zeroconf.ATTR_PROPERTIES]["MT"] in ["2600", "2601"]: return self.async_abort(reason="home_control") - await self.async_set_unique_id(discovery_info["properties"]["SN"]) + await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["SN"]) self._abort_if_unique_id_configured() # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context[CONF_HOST] = discovery_info["host"] + self.context[CONF_HOST] = discovery_info[zeroconf.ATTR_HOST] self.context["title_placeholders"] = { - PRODUCT: discovery_info["properties"]["Product"], - CONF_NAME: discovery_info["hostname"].split(".")[0], + PRODUCT: discovery_info[zeroconf.ATTR_PROPERTIES]["Product"], + CONF_NAME: discovery_info[zeroconf.ATTR_HOSTNAME].split(".")[0], } return await self.async_step_zeroconf_confirm() diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 01fcc2b2c22..a2c8496277d 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -8,8 +8,11 @@ import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.network import is_link_local from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI @@ -90,10 +93,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data = self.discovery_schema or _schema_with_defaults() return self.async_show_form(step_id="user", data_schema=data, errors=errors) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Prepare configuration for a discovered doorbird device.""" - macaddress = discovery_info["properties"]["macaddress"] - host = discovery_info[CONF_HOST] + macaddress = discovery_info[zeroconf.ATTR_PROPERTIES]["macaddress"] + host = discovery_info[zeroconf.ATTR_HOST] if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") @@ -109,7 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") chop_ending = "._axis-video._tcp.local." - friendly_hostname = discovery_info["name"] + friendly_hostname = discovery_info[zeroconf.ATTR_NAME] if friendly_hostname.endswith(chop_ending): friendly_hostname = friendly_hostname[: -len(chop_ending)] diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index cd48b572577..f49fc708a1f 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -63,8 +63,11 @@ MAX_NAME_LEN = 63 # Attributes for HaServiceInfo ATTR_HOST: Final = "host" +ATTR_HOSTNAME: Final = "hostname" ATTR_NAME: Final = "name" +ATTR_PORT: Final = "port" ATTR_PROPERTIES: Final = "properties" +ATTR_TYPE: Final = "type" CONFIG_SCHEMA = vol.Schema( From 2b22d635d952da357dd3009bbe7198d506133d09 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Nov 2021 18:27:31 +0100 Subject: [PATCH 0203/1452] Use zeroconf HaServiceInfo in tests (A-D) (#58836) Co-authored-by: epenet --- tests/components/apple_tv/test_config_flow.py | 27 ++++---- tests/components/axis/test_config_flow.py | 65 ++++++++++--------- tests/components/bond/test_config_flow.py | 45 +++++++------ .../components/bosch_shc/test_config_flow.py | 31 ++++----- tests/components/brother/test_config_flow.py | 25 ++++--- tests/components/daikin/test_config_flow.py | 3 +- tests/components/devolo_home_control/const.py | 22 ++++--- tests/components/devolo_home_network/const.py | 20 +++--- tests/components/doorbird/test_config_flow.py | 41 ++++++------ 9 files changed, 151 insertions(+), 128 deletions(-) diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 45edaa36251..ff772723cd2 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -7,15 +7,16 @@ from pyatv.const import Protocol import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN from tests.common import MockConfigEntry -DMAP_SERVICE = { - "type": "_touch-able._tcp.local.", - "name": "dmapid.something", - "properties": {"CtlN": "Apple TV"}, -} +DMAP_SERVICE = zeroconf.HaServiceInfo( + type="_touch-able._tcp.local.", + name="dmapid.something", + properties={"CtlN": "Apple TV"}, +) @pytest.fixture(autouse=True) @@ -399,10 +400,10 @@ async def test_zeroconf_unsupported_service_aborts(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "type": "_dummy._tcp.local.", - "properties": {}, - }, + data=zeroconf.HaServiceInfo( + type="_dummy._tcp.local.", + properties={}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unknown" @@ -413,10 +414,10 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "type": "_mediaremotetv._tcp.local.", - "properties": {"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, - }, + data=zeroconf.HaServiceInfo( + type="_mediaremotetv._tcp.local.", + properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["description_placeholders"] == {"name": "MRP Device"} diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 7d62e999809..3ba1ceee906 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -5,6 +5,7 @@ import pytest import respx from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( CONF_EVENTS, @@ -292,17 +293,17 @@ async def test_reauth_flow_update_configuration(hass): ), ( SOURCE_ZEROCONF, - { - "host": DEFAULT_HOST, - "port": 80, - "hostname": f"axis-{MAC.lower()}.local.", - "type": "_axis-video._tcp.local.", - "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", - "properties": { + zeroconf.HaServiceInfo( + host=DEFAULT_HOST, + port=80, + hostname=f"axis-{MAC.lower()}.local.", + type="_axis-video._tcp.local.", + name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + properties={ "_raw": {"macaddress": MAC.encode()}, "macaddress": MAC, }, - }, + ), ), ], ) @@ -362,12 +363,12 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ), ( SOURCE_ZEROCONF, - { - CONF_HOST: DEFAULT_HOST, - CONF_PORT: 80, - "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", - "properties": {"macaddress": MAC}, - }, + zeroconf.HaServiceInfo( + host=DEFAULT_HOST, + port=80, + name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + properties={"macaddress": MAC}, + ), ), ], ) @@ -410,12 +411,12 @@ async def test_discovered_device_already_configured( ), ( SOURCE_ZEROCONF, - { - CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, - "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", - "properties": {"macaddress": MAC}, - }, + zeroconf.HaServiceInfo( + host="2.3.4.5", + port=8080, + name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + properties={"macaddress": MAC}, + ), 8080, ), ], @@ -478,12 +479,12 @@ async def test_discovery_flow_updated_configuration( ), ( SOURCE_ZEROCONF, - { - CONF_HOST: "", - CONF_PORT: 0, - "name": "", - "properties": {"macaddress": "01234567890"}, - }, + zeroconf.HaServiceInfo( + host="", + port=0, + name="", + properties={"macaddress": "01234567890"}, + ), ), ], ) @@ -516,12 +517,12 @@ async def test_discovery_flow_ignore_non_axis_device( ), ( SOURCE_ZEROCONF, - { - CONF_HOST: "169.254.3.4", - CONF_PORT: 80, - "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", - "properties": {"macaddress": MAC}, - }, + zeroconf.HaServiceInfo( + host="169.254.3.4", + port=80, + name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + properties={"macaddress": MAC}, + ), ), ], ) diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 27f0e69e79b..f815f0432c0 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, Mock, patch from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, core +from homeassistant.components import zeroconf from homeassistant.components.bond.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST @@ -196,7 +197,9 @@ async def test_zeroconf_form(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"}, + data=zeroconf.HaServiceInfo( + name="test-bond-id.some-other-tail-info", host="test-host" + ), ) assert result["type"] == "form" assert result["errors"] == {} @@ -226,7 +229,9 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"}, + data=zeroconf.HaServiceInfo( + name="test-bond-id.some-other-tail-info", host="test-host" + ), ) await hass.async_block_till_done() assert result["type"] == "form" @@ -259,7 +264,9 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"}, + data=zeroconf.HaServiceInfo( + name="test-bond-id.some-other-tail-info", host="test-host" + ), ) await hass.async_block_till_done() assert result["type"] == "form" @@ -295,10 +302,10 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "name": "already-registered-bond-id.some-other-tail-info", - "host": "updated-host", - }, + data=zeroconf.HaServiceInfo( + name="already-registered-bond-id.some-other-tail-info", + host="updated-host", + ), ) assert result["type"] == "abort" @@ -336,10 +343,10 @@ async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistan result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "name": "already-registered-bond-id.some-other-tail-info", - "host": "updated-host", - }, + data=zeroconf.HaServiceInfo( + name="already-registered-bond-id.some-other-tail-info", + host="updated-host", + ), ) await hass.async_block_till_done() @@ -369,10 +376,10 @@ async def test_zeroconf_already_configured_no_reload_same_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "name": "already-registered-bond-id.some-other-tail-info", - "host": "stored-host", - }, + data=zeroconf.HaServiceInfo( + name="already-registered-bond-id.some-other-tail-info", + host="stored-host", + ), ) await hass.async_block_till_done() @@ -386,10 +393,10 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): await _help_test_form_unexpected_error( hass, source=config_entries.SOURCE_ZEROCONF, - initial_input={ - "name": "test-bond-id.some-other-tail-info", - "host": "test-host", - }, + initial_input=zeroconf.HaServiceInfo( + name="test-bond-id.some-other-tail-info", + host="test-host", + ), user_input={CONF_ACCESS_TOKEN: "test-token"}, error=Exception(), ) diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index be5c3d76b53..21892aed94e 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -10,6 +10,7 @@ from boschshcpy.exceptions import ( from boschshcpy.information import SHCInformation from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.bosch_shc.config_flow import write_tls_asset from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN @@ -19,13 +20,13 @@ MOCK_SETTINGS = { "name": "Test name", "device": {"mac": "test-mac", "hostname": "test-host"}, } -DISCOVERY_INFO = { - "host": ["169.1.1.1", "1.1.1.1"], - "port": 0, - "hostname": "shc012345.local.", - "type": "_http._tcp.local.", - "name": "Bosch SHC [test-mac]._http._tcp.local.", -} +DISCOVERY_INFO = zeroconf.HaServiceInfo( + host=["169.1.1.1", "1.1.1.1"], + port=0, + hostname="shc012345.local.", + type="_http._tcp.local.", + name="Bosch SHC [test-mac]._http._tcp.local.", +) async def test_form_user(hass, mock_zeroconf): @@ -527,13 +528,13 @@ async def test_zeroconf_cannot_connect(hass, mock_zeroconf): async def test_zeroconf_link_local(hass, mock_zeroconf): """Test we get the form.""" - DISCOVERY_INFO_LINK_LOCAL = { - "host": ["169.1.1.1"], - "port": 0, - "hostname": "shc012345.local.", - "type": "_http._tcp.local.", - "name": "Bosch SHC [test-mac]._http._tcp.local.", - } + DISCOVERY_INFO_LINK_LOCAL = zeroconf.HaServiceInfo( + host=["169.1.1.1"], + port=0, + hostname="shc012345.local.", + type="_http._tcp.local.", + name="Bosch SHC [test-mac]._http._tcp.local.", + ) with patch( "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError @@ -551,7 +552,7 @@ async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data={"host": "1.1.1.1", "name": "notboschshc"}, + data=zeroconf.HaServiceInfo(host="1.1.1.1", name="notboschshc"), context={"source": config_entries.SOURCE_ZEROCONF}, ) assert result["type"] == "abort" diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 70c42884bf9..8a221a21c36 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from brother import SnmpError, UnsupportedModel from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE @@ -143,7 +144,9 @@ async def test_zeroconf_snmp_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "name": "Brother Printer"}, + data=zeroconf.HaServiceInfo( + hostname="example.local.", name="Brother Printer" + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -156,11 +159,11 @@ async def test_zeroconf_unsupported_model(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "hostname": "example.local.", - "name": "Brother Printer", - "properties": {"product": "MFC-8660DN"}, - }, + data=zeroconf.HaServiceInfo( + hostname="example.local.", + name="Brother Printer", + properties={"product": "MFC-8660DN"}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -181,7 +184,9 @@ async def test_zeroconf_device_exists_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "name": "Brother Printer"}, + data=zeroconf.HaServiceInfo( + hostname="example.local.", name="Brother Printer" + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -196,7 +201,7 @@ async def test_zeroconf_no_probe_existing_device(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "localhost", "name": "Brother Printer"}, + data=zeroconf.HaServiceInfo(hostname="localhost", name="Brother Printer"), ) await hass.async_block_till_done() @@ -215,7 +220,9 @@ async def test_zeroconf_confirm_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "name": "Brother Printer"}, + data=zeroconf.HaServiceInfo( + hostname="example.local.", name="Brother Printer" + ), ) assert result["step_id"] == "zeroconf_confirm" diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index f39d117ed39..d6ac4e12c56 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -7,6 +7,7 @@ from aiohttp import ClientError, web_exceptions from pydaikin.exceptions import DaikinException import pytest +from homeassistant.components import zeroconf from homeassistant.components.daikin.const import KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD @@ -119,7 +120,7 @@ async def test_api_password_abort(hass): @pytest.mark.parametrize( "source, data, unique_id", [ - (SOURCE_ZEROCONF, {CONF_HOST: HOST}, MAC), + (SOURCE_ZEROCONF, zeroconf.HaServiceInfo(host=HOST), MAC), ], ) async def test_discovery_zeroconf( diff --git a/tests/components/devolo_home_control/const.py b/tests/components/devolo_home_control/const.py index 33a98a15e2d..761a249745f 100644 --- a/tests/components/devolo_home_control/const.py +++ b/tests/components/devolo_home_control/const.py @@ -1,12 +1,14 @@ """Constants used for mocking data.""" -DISCOVERY_INFO = { - "host": "192.168.0.1", - "port": 14791, - "hostname": "test.local.", - "type": "_dvl-deviceapi._tcp.local.", - "name": "dvl-deviceapi", - "properties": { +from homeassistant.components import zeroconf + +DISCOVERY_INFO = zeroconf.HaServiceInfo( + host="192.168.0.1", + port=14791, + hostname="test.local.", + type="_dvl-deviceapi._tcp.local.", + name="dvl-deviceapi", + properties={ "Path": "/deviceapi", "Version": "v0", "Features": "", @@ -15,8 +17,8 @@ DISCOVERY_INFO = { "FirmwareVersion": "8.90.4", "PlcMacAddress": "AA:BB:CC:DD:EE:FF", }, -} +) -DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = {"properties": {"MT": "2700"}} +DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = zeroconf.HaServiceInfo(properties={"MT": "2700"}) -DISCOVERY_INFO_WRONG_DEVICE = {"properties": {"Features": ""}} +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.HaServiceInfo(properties={"Features": ""}) diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index e9b2113c4a6..a26704c3131 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -1,5 +1,7 @@ """Constants used for mocking data.""" +from homeassistant.components import zeroconf + IP = "1.1.1.1" CONNECTED_STATIONS = { @@ -14,13 +16,13 @@ CONNECTED_STATIONS = { ], } -DISCOVERY_INFO = { - "host": IP, - "port": 14791, - "hostname": "test.local.", - "type": "_dvl-deviceapi._tcp.local.", - "name": "dLAN pro 1200+ WiFi ac._dvl-deviceapi._tcp.local.", - "properties": { +DISCOVERY_INFO = zeroconf.HaServiceInfo( + host=IP, + port=14791, + hostname="test.local.", + type="_dvl-deviceapi._tcp.local.", + name="dLAN pro 1200+ WiFi ac._dvl-deviceapi._tcp.local.", + properties={ "Path": "abcdefghijkl/deviceapi", "Version": "v0", "Product": "dLAN pro 1200+ WiFi ac", @@ -32,9 +34,9 @@ DISCOVERY_INFO = { "PS": "", "PlcMacAddress": "AA:BB:CC:DD:EE:FF", }, -} +) -DISCOVERY_INFO_WRONG_DEVICE = {"properties": {"MT": "2600"}} +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.HaServiceInfo(properties={"MT": "2600"}) NEIGHBOR_ACCESS_POINTS = { "neighbor_aps": [ diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index e9f43bf7af2..2b5dc5e3346 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -5,6 +5,7 @@ import pytest import requests from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -81,11 +82,11 @@ async def test_form_zeroconf_wrong_oui(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "notdoorbirdoui"}, - "host": "192.168.1.8", - "name": "Doorstation - abc123._axis-video._tcp.local.", - }, + data=zeroconf.HaServiceInfo( + properties={"macaddress": "notdoorbirdoui"}, + host="192.168.1.8", + name="Doorstation - abc123._axis-video._tcp.local.", + ), ) assert result["type"] == "abort" assert result["reason"] == "not_doorbird_device" @@ -97,11 +98,11 @@ async def test_form_zeroconf_link_local_ignored(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "host": "169.254.103.61", - "name": "Doorstation - abc123._axis-video._tcp.local.", - }, + data=zeroconf.HaServiceInfo( + properties={"macaddress": "1CCAE3DOORBIRD"}, + host="169.254.103.61", + name="Doorstation - abc123._axis-video._tcp.local.", + ), ) assert result["type"] == "abort" assert result["reason"] == "link_local_address" @@ -120,11 +121,11 @@ async def test_form_zeroconf_correct_oui(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, + data=zeroconf.HaServiceInfo( + properties={"macaddress": "1CCAE3DOORBIRD"}, + name="Doorstation - abc123._axis-video._tcp.local.", + host="192.168.1.5", + ), ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -179,11 +180,11 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, + data=zeroconf.HaServiceInfo( + properties={"macaddress": "1CCAE3DOORBIRD"}, + name="Doorstation - abc123._axis-video._tcp.local.", + host="192.168.1.5", + ), ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT From 4a2fb0e7ab68c0868c5d25cde04e2a984938bcbd Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Tue, 2 Nov 2021 12:48:29 -0500 Subject: [PATCH 0204/1452] Fix color temp selection when brightness changed in Tuya light (#58341) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 47 +++++++++++++------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index dbae8e2e57f..75442159dea 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -443,7 +443,29 @@ class TuyaLightEntity(TuyaEntity, LightEntity): """Turn on or control the light.""" commands = [{"code": self.entity_description.key, "value": True}] - if self._color_data_type and ( + if self._color_temp_type and ATTR_COLOR_TEMP in kwargs: + if color_mode_dpcode := self.entity_description.color_mode: + commands += [ + { + "code": color_mode_dpcode, + "value": WorkMode.WHITE, + }, + ] + + commands += [ + { + "code": self._color_temp_dpcode, + "value": round( + self._color_temp_type.remap_value_from( + kwargs[ATTR_COLOR_TEMP], + self.min_mireds, + self.max_mireds, + reverse=True, + ) + ), + }, + ] + elif self._color_data_type and ( ATTR_HS_COLOR in kwargs or (ATTR_BRIGHTNESS in kwargs and self.color_mode == COLOR_MODE_HS) ): @@ -486,29 +508,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity): }, ] - elif ATTR_COLOR_TEMP in kwargs and self._color_temp_type: - if color_mode_dpcode := self.entity_description.color_mode: - commands += [ - { - "code": color_mode_dpcode, - "value": WorkMode.WHITE, - }, - ] - - commands += [ - { - "code": self._color_temp_dpcode, - "value": round( - self._color_temp_type.remap_value_from( - kwargs[ATTR_COLOR_TEMP], - self.min_mireds, - self.max_mireds, - reverse=True, - ) - ), - }, - ] - if ( ATTR_BRIGHTNESS in kwargs and self.color_mode != COLOR_MODE_HS From d1bb580dc31bcdc98ea9935fd3df784e7a236b10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 19:10:42 +0100 Subject: [PATCH 0205/1452] Extend Tuya Dimmer (tgq) support (#58951) --- homeassistant/components/tuya/light.py | 7 +++++++ homeassistant/components/tuya/select.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 75442159dea..581cd85647b 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -177,6 +177,13 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Dimmer # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 "tgq": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + name="Light", + brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), + brightness_max=DPCode.BRIGHTNESS_MAX_1, + brightness_min=DPCode.BRIGHTNESS_MIN_1, + ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_1, name="Light", diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 6df5b4e84dd..b4da7b8dfae 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -177,6 +177,22 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Dimmer + # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 + "tgq": ( + SelectEntityDescription( + key=DPCode.LED_TYPE_1, + name="Light Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LED_TYPE_2, + name="Light 2 Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } From 5315d7eb0a565e9df25294ef446896081a2824c9 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 2 Nov 2021 19:27:19 +0100 Subject: [PATCH 0206/1452] Add device configuration URL to Solar-Log (#58954) --- homeassistant/components/solarlog/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index d54845f8027..5d79efb94c9 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -34,6 +34,7 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, coordinator.unique_id)}, manufacturer="Solar-Log", name=coordinator.name, + configuration_url=coordinator.host, ) @property From ab20bf4e9ac6289903fc9ff25259f68bbf51da6e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 19:27:46 +0100 Subject: [PATCH 0207/1452] Add support for IoT Switches (tdq) in Tuya (#58952) --- homeassistant/components/tuya/select.py | 16 ++++++++++++++++ homeassistant/components/tuya/switch.py | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index b4da7b8dfae..921dc21eaaa 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -143,6 +143,22 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # IoT Switch? + # Note: Undocumented + "tdq": ( + SelectEntityDescription( + key=DPCode.RELAY_STATUS, + name="Power on Behavior", + device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LIGHT_MODE, + name="Indicator Light Mode", + device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 6f0f7b85b17..8395c02cf39 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -369,6 +369,31 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # IoT Switch? + # Note: Undocumented + "tdq": ( + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch 1", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Switch 2", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_3, + name="Switch 3", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Solar Light # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 "tyndj": ( From 2f4b7fe8095a8528f7297a69987ac7367730c9cc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 3 Nov 2021 00:11:55 +0000 Subject: [PATCH 0208/1452] [ci skip] Translation update --- .../amberelectric/translations/ja.json | 3 +- .../aurora_abb_powerone/translations/he.json | 7 ++++ .../aurora_abb_powerone/translations/ja.json | 9 ++++ .../binary_sensor/translations/ja.json | 6 +++ .../devolo_home_network/translations/he.json | 20 +++++++++ .../components/dlna_dmr/translations/he.json | 12 +++++- .../components/dlna_dmr/translations/ja.json | 13 +++++- .../components/efergy/translations/ja.json | 3 +- .../environment_canada/translations/ja.json | 5 +++ .../components/flux_led/translations/ja.json | 25 +++++++++++ .../components/lookin/translations/he.json | 28 +++++++++++++ .../motion_blinds/translations/ja.json | 3 +- .../components/netatmo/translations/he.json | 4 ++ .../components/octoprint/translations/he.json | 21 ++++++++++ .../components/octoprint/translations/ja.json | 6 +++ .../components/ridwell/translations/he.json | 26 ++++++++++++ .../simplisafe/translations/ja.json | 7 ++++ .../components/tradfri/translations/ja.json | 1 + .../components/tuya/translations/ja.json | 11 ++++- .../tuya/translations/select.he.json | 20 +++++++++ .../tuya/translations/select.ja.json | 42 +++++++++++++++++++ .../tuya/translations/sensor.ja.json | 4 ++ .../components/venstar/translations/he.json | 22 ++++++++++ .../components/venstar/translations/ja.json | 9 ++++ .../components/watttime/translations/ja.json | 10 +++++ .../components/yeelight/translations/he.json | 2 +- .../components/zwave_js/translations/ja.json | 23 +++++++++- 27 files changed, 334 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/aurora_abb_powerone/translations/he.json create mode 100644 homeassistant/components/devolo_home_network/translations/he.json create mode 100644 homeassistant/components/flux_led/translations/ja.json create mode 100644 homeassistant/components/lookin/translations/he.json create mode 100644 homeassistant/components/octoprint/translations/he.json create mode 100644 homeassistant/components/ridwell/translations/he.json create mode 100644 homeassistant/components/tuya/translations/select.he.json create mode 100644 homeassistant/components/tuya/translations/select.ja.json create mode 100644 homeassistant/components/venstar/translations/he.json create mode 100644 homeassistant/components/venstar/translations/ja.json diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json index e0a3590c8b4..03e4bef5c4b 100644 --- a/homeassistant/components/amberelectric/translations/ja.json +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" - } + }, + "title": "Amber Electric" } } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/he.json b/homeassistant/components/aurora_abb_powerone/translations/he.json new file mode 100644 index 00000000000..822dcf2be14 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/ja.json b/homeassistant/components/aurora_abb_powerone/translations/ja.json index 6df075f97e2..5977b1513b0 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ja.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_serial_ports": "COM\u30dd\u30fc\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u901a\u4fe1\u3059\u308b\u306b\u306f\u6709\u52b9\u306aRS485\u30c7\u30d0\u30a4\u30b9\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3001\u30a2\u30c9\u30ec\u30b9\u3001\u96fb\u6c17\u7684\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3001\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044(\u663c\u9593)", + "cannot_open_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u3051\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044", + "invalid_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u304c\u6709\u52b9\u306a\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3001\u3082\u3057\u304f\u306f\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 9a58a1b831f..a4be88f4b12 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -22,6 +22,8 @@ "is_not_plugged_in": "{entity_name} \u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u3066\u3044\u307e\u3059", "is_not_powered": "{entity_name} \u306f\u96fb\u529b\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "is_not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", + "is_not_running": "{entity_name} \u306f\u5b9f\u884c\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "is_not_tampered": "{entity_name} \u306f\u6539\u7ac4(tampering)\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", "is_not_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u3059", "is_occupied": "{entity_name} \u306f\u5360\u6709\u3055\u308c\u3066\u3044\u307e\u3059", "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", @@ -31,8 +33,10 @@ "is_powered": "{entity_name} \u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u3059", "is_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u3059", "is_problem": "{entity_name} \u304c\u554f\u984c\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_running": "{entity_name} \u304c\u5b9f\u884c\u3055\u308c\u3066\u3044\u307e\u3059", "is_smoke": "{entity_name} \u304c\u7159\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_sound": "{entity_name} \u304c\u97f3\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", + "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", "is_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "is_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u3059" }, @@ -42,6 +46,8 @@ "connected": "{entity_name} \u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", "gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", + "is_not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "no_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u306a\u304f\u306a\u3063\u305f", "not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", "not_hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u305b\u3093", diff --git a/homeassistant/components/devolo_home_network/translations/he.json b/homeassistant/components/devolo_home_network/translations/he.json new file mode 100644 index 00000000000..6bb4c9a7ed3 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/he.json b/homeassistant/components/dlna_dmr/translations/he.json index fbdaa0403f4..7025721fa43 100644 --- a/homeassistant/components/dlna_dmr/translations/he.json +++ b/homeassistant/components/dlna_dmr/translations/he.json @@ -1,15 +1,25 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "flow_title": "{name}", "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" }, + "manual": { + "data": { + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + }, "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" } } diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 681117d07d9..65a786b1368 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -15,13 +15,24 @@ "user": { "data": { "url": "URL" - } + }, + "description": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066URL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "\u767a\u898b\u3055\u308c\u305fDLNA DMR\u6a5f\u5668" } } }, "options": { "error": { "invalid_url": "\u7121\u52b9\u306aURL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u306e\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL", + "listen_port": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u30dd\u30fc\u30c8(\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u30e9\u30f3\u30c0\u30e0)", + "poll_availability": "\u30c7\u30d0\u30a4\u30b9\u306e\u53ef\u7528\u6027\u3092\u30dd\u30fc\u30ea\u30f3\u30b0" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json index c2ff6bbb145..664ab46bafe 100644 --- a/homeassistant/components/efergy/translations/ja.json +++ b/homeassistant/components/efergy/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "api_key": "API\u30ad\u30fc" - } + }, + "title": "Efergy" } } } diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json index 4268a928cbb..c363d19e784 100644 --- a/homeassistant/components/environment_canada/translations/ja.json +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "bad_station_id": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID\u304c\u7121\u52b9\u3001\u6b20\u843d\u3057\u3066\u3044\u308b\u3001\u307e\u305f\u306f\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID \u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u5185\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "error_response": "\u30ab\u30ca\u30c0\u74b0\u5883\u304b\u3089\u306e\u5fdc\u7b54\u30a8\u30e9\u30fc", + "too_many_attempts": "\u30ab\u30ca\u30c0\u74b0\u5883\u7701\u3078\u306e\u63a5\u7d9a\u306f\u30ec\u30fc\u30c8\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u300260\u79d2\u5f8c\u306b\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flux_led/translations/ja.json b/homeassistant/components/flux_led/translations/ja.json new file mode 100644 index 00000000000..4fa710d1331 --- /dev/null +++ b/homeassistant/components/flux_led/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "{model} {id} ({ipaddr}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u30ab\u30b9\u30bf\u30e0\u30a8\u30d5\u30a7\u30af\u30c8: 1\uff5e16\u8272[R,G,B]\u306e\u30ea\u30b9\u30c8\u3002\u4f8b: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u30ab\u30b9\u30bf\u30e0\u30a8\u30d5\u30a7\u30af\u30c8: \u8272\u3092\u5207\u308a\u66ff\u3048\u308b\u30a8\u30d5\u30a7\u30af\u30c8\u306e\u901f\u5ea6\u3092\u30d1\u30fc\u30bb\u30f3\u30c6\u30fc\u30b8\u3067\u8868\u793a\u3002", + "custom_effect_transition": "\u30ab\u30b9\u30bf\u30e0\u30a8\u30d5\u30a7\u30af\u30c8: \u8272\u3068\u8272\u306e\u9593\u3067\u306e\u9077\u79fb(\u30c8\u30e9\u30f3\u30b8\u30b7\u30e7\u30f3)\u306e\u7a2e\u985e\u3002", + "mode": "\u9078\u629e\u3057\u305f\u660e\u308b\u3055\u30e2\u30fc\u30c9\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/he.json b/homeassistant/components/lookin/translations/he.json new file mode 100644 index 00000000000..3110857a512 --- /dev/null +++ b/homeassistant/components/lookin/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "\u05e9\u05dd" + } + }, + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index 744606ae4e2..a290e97a70e 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -17,7 +17,8 @@ "data": { "wait_for_push": "\u66f4\u65b0\u6642\u306b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8 \u30d7\u30c3\u30b7\u30e5\u3092\u5f85\u6a5f\u3059\u308b" }, - "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a" + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", + "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" } } } diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json index 32d7ecac5f0..1d769516a08 100644 --- a/homeassistant/components/netatmo/translations/he.json +++ b/homeassistant/components/netatmo/translations/he.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "create_entry": { @@ -12,6 +13,9 @@ "step": { "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } }, diff --git a/homeassistant/components/octoprint/translations/he.json b/homeassistant/components/octoprint/translations/he.json new file mode 100644 index 00000000000..0d36726a3fd --- /dev/null +++ b/homeassistant/components/octoprint/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json index 17ec3918a5c..e3cb8eccc81 100644 --- a/homeassistant/components/octoprint/translations/ja.json +++ b/homeassistant/components/octoprint/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "progress": { + "get_api_key": "OctoPrint UI\u3092\u958b\u304d\u3001Home Assistant\u306e\u30a2\u30af\u30bb\u30b9\u30ea\u30af\u30a8\u30b9\u30c8\u3067\u3092 '\u8a31\u53ef' \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/he.json b/homeassistant/components/ridwell/translations/he.json new file mode 100644 index 00000000000..608c61a9687 --- /dev/null +++ b/homeassistant/components/ridwell/translations/he.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 70f15e85dd5..503f0b2520f 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" + }, "step": { "input_auth_code": { "data": { "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" }, + "description": "SimpliSafe Web\u30a2\u30d7\u30ea\u306eURL\u304b\u3089\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b:", "title": "\u627f\u8a8d\u7d42\u4e86" + }, + "user": { + "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066 Submit \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json index 7e02bd92743..40c0e02eca1 100644 --- a/homeassistant/components/tradfri/translations/ja.json +++ b/homeassistant/components/tradfri/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_authenticate": "\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306f\u3001Homekit\u306a\u3069\u306e\u4ed6\u306e\u30b5\u30fc\u30d0\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f", "invalid_key": "\u63d0\u4f9b\u3055\u308c\u305f\u30ad\u30fc\u3067\u306e\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u304c\u5f15\u304d\u7d9a\u304d\u767a\u751f\u3059\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3067\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" }, diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 8db8bd8d45e..5c33fb91659 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -1,19 +1,28 @@ { "config": { + "error": { + "login_error": "\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc ({code}): {msg}" + }, "step": { "login": { "data": { "access_id": "\u30a2\u30af\u30bb\u30b9ID", + "access_secret": "\u30a2\u30af\u30bb\u30b9\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", + "endpoint": "\u5229\u7528\u53ef\u80fd\u30be\u30fc\u30f3", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "tuya_app_type": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea", "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" }, + "description": "Tuya\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Tuya" }, "user": { "data": { - "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", + "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7" } } } diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json new file mode 100644 index 00000000000..cdf4f19e72d --- /dev/null +++ b/homeassistant/components/tuya/translations/select.he.json @@ -0,0 +1,20 @@ +{ + "state": { + "tuya__basic_nightvision": { + "1": "\u05db\u05d1\u05d5\u05d9", + "2": "\u05de\u05d5\u05e4\u05e2\u05dc" + }, + "tuya__led_type": { + "led": "\u05dc\u05d3" + }, + "tuya__light_mode": { + "none": "\u05db\u05d1\u05d5\u05d9" + }, + "tuya__relay_status": { + "off": "\u05db\u05d1\u05d5\u05d9", + "on": "\u05de\u05d5\u05e4\u05e2\u05dc", + "power_off": "\u05db\u05d1\u05d5\u05d9", + "power_on": "\u05de\u05d5\u05e4\u05e2\u05dc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json new file mode 100644 index 00000000000..9607d436a15 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -0,0 +1,42 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "0": "\u7121\u52b9", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "\u81ea\u52d5" + }, + "tuya__decibel_sensitivity": { + "0": "\u4f4e\u611f\u5ea6", + "1": "\u9ad8\u611f\u5ea6" + }, + "tuya__ipc_work_mode": { + "0": "\u4f4e\u96fb\u529b\u30e2\u30fc\u30c9", + "1": "\u9023\u7d9a\u4f5c\u696d\u30e2\u30fc\u30c9" + }, + "tuya__led_type": { + "halogen": "\u30cf\u30ed\u30b2\u30f3", + "incandescent": "\u767d\u71b1\u706f", + "led": "LED" + }, + "tuya__light_mode": { + "pos": "\u30b9\u30a4\u30c3\u30c1\u306e\u4f4d\u7f6e\u3092\u793a\u3059", + "relay": "\u30b9\u30a4\u30c3\u30c1\u306e\u30aa\u30f3/\u30aa\u30d5\u72b6\u614b\u3092\u793a\u3059" + }, + "tuya__motion_sensitivity": { + "0": "\u4f4e\u611f\u5ea6", + "1": "\u4e2d\u7a0b\u5ea6\u306e\u611f\u5ea6", + "2": "\u9ad8\u611f\u5ea6" + }, + "tuya__record_mode": { + "1": "\u30a4\u30d9\u30f3\u30c8\u306e\u307f\u3092\u8a18\u9332", + "2": "\u9023\u7d9a\u8a18\u9332" + }, + "tuya__relay_status": { + "last": "\u6700\u5f8c\u306e\u72b6\u614b\u3092\u8a18\u61b6", + "memory": "\u6700\u5f8c\u306e\u72b6\u614b\u3092\u8a18\u61b6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.ja.json b/homeassistant/components/tuya/translations/sensor.ja.json index 98142aa25cc..6c08faf52de 100644 --- a/homeassistant/components/tuya/translations/sensor.ja.json +++ b/homeassistant/components/tuya/translations/sensor.ja.json @@ -4,6 +4,10 @@ "boiling_temp": "\u6cb8\u70b9", "cooling": "\u51b7\u5374", "heating": "\u6696\u623f", + "heating_temp": "\u52a0\u71b1(\u6696\u623f)\u6e29\u5ea6", + "reserve_1": "Reserve 1", + "reserve_2": "Reserve 2", + "reserve_3": "Reserve 3", "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4", "warm": "\u4fdd\u6e29" } diff --git a/homeassistant/components/venstar/translations/he.json b/homeassistant/components/venstar/translations/he.json new file mode 100644 index 00000000000..88029de05dc --- /dev/null +++ b/homeassistant/components/venstar/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "pin": "\u05e7\u05d5\u05d3 PIN", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/ja.json b/homeassistant/components/venstar/translations/ja.json new file mode 100644 index 00000000000..2c30e8f5d24 --- /dev/null +++ b/homeassistant/components/venstar/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Venstar\u793e\u306e\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306b\u63a5\u7d9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 0acaaea625d..324ab6d1e3b 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -5,5 +5,15 @@ "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u76e3\u8996\u5bfe\u8c61\u306e\u5834\u6240\u3092\u5730\u56f3\u4e0a\u306b\u8868\u793a" + }, + "title": "WattTime\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json index 535aa833a7f..0fe8dd0f06e 100644 --- a/homeassistant/components/yeelight/translations/he.json +++ b/homeassistant/components/yeelight/translations/he.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "\u05d3\u05d2\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "model": "\u05d3\u05d2\u05dd", "nightlight_switch": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05de\u05ea\u05d2 \u05ea\u05d0\u05d5\u05e8\u05ea \u05dc\u05d9\u05dc\u05d4", "save_on_change": "\u05e9\u05de\u05d5\u05e8 \u05e1\u05d8\u05d8\u05d5\u05e1 \u05d1\u05e9\u05d9\u05e0\u05d5\u05d9", "transition": "\u05d6\u05de\u05df \u05de\u05e2\u05d1\u05e8 (\u05d0\u05dc\u05e4\u05d9\u05d5\u05ea \u05e9\u05e0\u05d9\u05d4)", diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 78714cd5a7c..793a886b00b 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -1,10 +1,31 @@ { + "config": { + "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", + "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", + "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", + "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" + } + } + } + }, + "device_automation": { + "action_type": { + "clear_lock_usercode": "{entity_name} \u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u30af\u30ea\u30a2" + } + }, "options": { "step": { "configure_addon": { "data": { "log_level": "\u30ed\u30b0\u30ec\u30d9\u30eb", - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af" + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af", + "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", + "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", + "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", + "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" }, "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" }, From 51be7d53d66ec1662c16444de77c94a96714a7ab Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 3 Nov 2021 00:30:29 +0000 Subject: [PATCH 0209/1452] Aurora abb defer unique_id assignment during yaml import (#58887) * Defer unique_id assignment during yaml import if dark * Back out variable name change to simplify. * Allow config flow yaml setup deferral. * Fix deferred yaml import * Code review: only wrap necessary lines in try blk * Code review: catch possible duplicate unique_id * Simplify assignment. * Code review: use timedelta to retry yaml import * Code review: if a different error occurs, raise it * Remove current config entry if duplicate unique_id * Code review: remove unnecessary line. * Code review: revert change, leave to other PR. * Code review: remove unnecessary patch & min->sec * Remove unnecessary else after raise. * Increase test coverage. * Check the number of config entries at each stage * Raise ConfigEntryNotReady when connection fails. * Log & return false for error on yaml import --- .../aurora_abb_powerone/__init__.py | 42 ++++- .../aurora_abb_powerone/aurora_device.py | 8 +- .../aurora_abb_powerone/config_flow.py | 6 - .../aurora_abb_powerone/test_config_flow.py | 175 +++++++++++++++++- .../aurora_abb_powerone/test_init.py | 12 ++ .../aurora_abb_powerone/test_sensor.py | 36 ++++ 6 files changed, 260 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index ff26c3770f0..2c3d0c546cd 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -10,13 +10,15 @@ import logging -from aurorapy.client import AuroraSerialClient +from aurorapy.client import AuroraError, AuroraSerialClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .config_flow import validate_and_connect +from .const import ATTR_SERIAL_NUMBER, DOMAIN PLATFORMS = ["sensor"] @@ -29,9 +31,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): comport = entry.data[CONF_PORT] address = entry.data[CONF_ADDRESS] serclient = AuroraSerialClient(address, comport, parity="N", timeout=1) + # To handle yaml import attempts in darkeness, (re)try connecting only if + # unique_id not yet assigned. + if entry.unique_id is None: + try: + res = await hass.async_add_executor_job( + validate_and_connect, hass, entry.data + ) + except AuroraError as error: + if "No response after" in str(error): + raise ConfigEntryNotReady("No response (could be dark)") from error + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + except OSError as error: + if error.errno == 19: # No such device. + _LOGGER.error("Failed to connect to inverter: no such COM port") + return False + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + else: + # If we got here, the device is now communicating (maybe after + # being in darkness). But there's a small risk that the user has + # configured via the UI since we last attempted the yaml setup, + # which means we'd get a duplicate unique ID. + new_id = res[ATTR_SERIAL_NUMBER] + # Check if this unique_id has already been used + for existing_entry in hass.config_entries.async_entries(DOMAIN): + if existing_entry.unique_id == new_id: + _LOGGER.debug( + "Remove already configured config entry for id %s", new_id + ) + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id) + ) + return False + hass.config_entries.async_update_entry(entry, unique_id=new_id) hass.data.setdefault(DOMAIN, {})[entry.unique_id] = serclient - hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index 3913515a9b9..d2aed5ec7a8 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -1,4 +1,6 @@ """Top level class for AuroraABBPowerOneSolarPV inverters and sensors.""" +from __future__ import annotations + import logging from aurorapy.client import AuroraSerialClient @@ -29,9 +31,11 @@ class AuroraDevice(Entity): self._available = True @property - def unique_id(self) -> str: + def unique_id(self) -> str | None: """Return the unique id for this device.""" - serial = self._data[ATTR_SERIAL_NUMBER] + serial = self._data.get(ATTR_SERIAL_NUMBER) + if serial is None: + return None return f"{serial}_{self.entity_description.key}" @property diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index c0c87e9e103..012fe7b14bb 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -81,16 +81,10 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_setup") conf = {} - conf[ATTR_SERIAL_NUMBER] = "sn_unknown_yaml" - conf[ATTR_MODEL] = "model_unknown_yaml" - conf[ATTR_FIRMWARE] = "fw_unknown_yaml" conf[CONF_PORT] = config["device"] conf[CONF_ADDRESS] = config["address"] # config["name"] from yaml is ignored. - await self.async_set_unique_id(self.flow_id) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) async def async_step_user(self, user_input=None): diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index d385d33ddd9..620d7e10e53 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aurora ABB PowerOne Solar PV config flow.""" +from datetime import timedelta from logging import INFO from unittest.mock import patch @@ -12,7 +13,20 @@ from homeassistant.components.aurora_abb_powerone.const import ( ATTR_SERIAL_NUMBER, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + + +def _simulated_returns(index, global_measure=None): + returns = { + 3: 45.678, # power + 21: 9.876, # temperature + 5: 12345, # energy + } + return returns[index] async def test_form(hass): @@ -150,16 +164,161 @@ async def test_form_invalid_com_ports(hass): # Tests below can be deleted after deprecation period is finished. -async def test_import(hass): - """Test configuration.yaml import used during migration.""" - TESTDATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} - with patch( - "homeassistant.components.generic.camera.GenericCamera.async_camera_image", - return_value=None, - ): +async def test_import_day(hass): + """Test .yaml import when the inverter is able to communicate.""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_PORT] == "/dev/ttyUSB7" assert result["data"][CONF_ADDRESS] == 3 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_night(hass): + """Test .yaml import when the inverter is inaccessible (e.g. darkness).""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + # First time round, no response. + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("No response after"), + ) as mock_connect: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert not entry.unique_id + assert entry.state == ConfigEntryState.SETUP_RETRY + + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == "/dev/ttyUSB7" + assert result["data"][CONF_ADDRESS] == 3 + + # Second time round, talking this time. + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ): + # Wait >5seconds for the config to auto retry. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert entry.unique_id + + assert len(mock_connect.mock_calls) == 1 + assert hass.states.get("sensor.power_output").state == "45.7" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_import_night_then_user(hass): + """Attempt yaml import and fail (dark), but user sets up manually before auto retry.""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + # First time round, no response. + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("No response after"), + ) as mock_connect: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert not entry.unique_id + assert entry.state == ConfigEntryState.SETUP_RETRY + + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == "/dev/ttyUSB7" + assert result["data"][CONF_ADDRESS] == 3 + + # Failed once, now simulate the user initiating config flow with valid settings. + fakecomports = [] + fakecomports.append(list_ports_common.ListPortInfo("/dev/ttyUSB7")) + with patch( + "serial.tools.list_ports.comports", + return_value=fakecomports, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + # Now retry yaml - it should fail with duplicate + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ): + # Wait >5seconds for the config to auto retry. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/aurora_abb_powerone/test_init.py b/tests/components/aurora_abb_powerone/test_init.py index bd0f1c727cd..3bef40a14d3 100644 --- a/tests/components/aurora_abb_powerone/test_init.py +++ b/tests/components/aurora_abb_powerone/test_init.py @@ -19,6 +19,18 @@ async def test_unload_entry(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "homeassistant.components.aurora_abb_powerone.sensor.AuroraSensor.update", return_value=None, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ): mock_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index ae9360498c7..c1d1d4ad5c8 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -64,6 +64,18 @@ async def test_setup_platform_valid_config(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ), patch( "aurorapy.client.AuroraSerialClient.cumulated_energy", side_effect=_simulated_returns, @@ -94,6 +106,18 @@ async def test_sensors(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ), patch( "aurorapy.client.AuroraSerialClient.cumulated_energy", side_effect=_simulated_returns, @@ -123,6 +147,18 @@ async def test_sensor_dark(hass): # sun is up with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) From 16371e657915bd26c5629677cb6be5abfe4bd69b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 2 Nov 2021 23:21:56 -0400 Subject: [PATCH 0210/1452] Add missing ZMW currency (#58959) --- homeassistant/helpers/config_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e6c2792304b..f2ac86239f8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1444,6 +1444,7 @@ currency = vol.In( "YER", "ZAR", "ZMK", + "ZMW", "ZWL", }, msg="invalid ISO 4217 formatted currency", From 8fda2e0a1d26a2974d5d186e63f11f4fd5a8cae6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 2 Nov 2021 22:37:52 -0700 Subject: [PATCH 0211/1452] Address late review of motionEye media browser (#58925) * Media-content type fixes post-codereview. * More f-string. * Use the 'video' media class not 'movie'. --- .../components/motioneye/media_source.py | 17 +++++++++----- .../components/motioneye/test_media_source.py | 22 +++++++++---------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 4cc3dd9f2f7..a24c9e7ab26 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -10,8 +10,9 @@ from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_IMAGE, - MEDIA_CLASS_MOVIE, MEDIA_CLASS_VIDEO, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, ) from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( @@ -239,7 +240,9 @@ class MotionEyeMediaSource(MediaSource): domain=DOMAIN, identifier=f"{config.entry_id}#{device.id}#{kind}", media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_CLASS_DIRECTORY, + media_content_type=( + MEDIA_TYPE_VIDEO if kind == "movies" else MEDIA_TYPE_IMAGE + ), title=( f"{config.title} {device.name} {kind.title()}" if full_title @@ -248,7 +251,7 @@ class MotionEyeMediaSource(MediaSource): can_play=False, can_expand=True, children_media_class=( - MEDIA_CLASS_MOVIE if kind == "movies" else MEDIA_CLASS_IMAGE + MEDIA_CLASS_VIDEO if kind == "movies" else MEDIA_CLASS_IMAGE ), ) @@ -274,7 +277,7 @@ class MotionEyeMediaSource(MediaSource): parsed_path = PurePath(path) if path != "/": - base.title += " " + str(PurePath(*parsed_path.parts[1:])) + base.title += f" {PurePath(*parsed_path.parts[1:])}" base.children = [] @@ -339,7 +342,11 @@ class MotionEyeMediaSource(MediaSource): f"#{kind}#{full_child_path}" ), media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_CLASS_DIRECTORY, + media_content_type=( + MEDIA_TYPE_VIDEO + if kind == "movies" + else MEDIA_TYPE_IMAGE + ), title=display_child_path, can_play=False, can_expand=True, diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 65c700ab2ee..f2c8db3879b 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -159,20 +159,20 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: { "title": "Movies", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "video", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#movies" ), "can_play": False, "can_expand": True, - "children_media_class": "movie", + "children_media_class": "video", "thumbnail": None, }, { "title": "Images", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "image", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#images" @@ -193,20 +193,20 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: assert media.as_dict() == { "title": "http://test:8766 Test Camera Movies", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "video", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#movies" ), "can_play": False, "can_expand": True, - "children_media_class": "movie", + "children_media_class": "video", "thumbnail": None, "children": [ { "title": "2021-04-25", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "video", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#movies#/2021-04-25" @@ -227,14 +227,14 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: assert media.as_dict() == { "title": "http://test:8766 Test Camera Movies 2021-04-25", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "video", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#movies" ), "can_play": False, "can_expand": True, - "children_media_class": "movie", + "children_media_class": "video", "thumbnail": None, "children": [ { @@ -305,7 +305,7 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: assert media.as_dict() == { "title": "http://test:8766 Test Camera Images 2021-04-12", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "image", "media_content_id": ( "media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#images" @@ -469,14 +469,14 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: assert media.as_dict() == { "title": "http://test:8766 Test Camera Movies 2021-04-25", "media_class": "directory", - "media_content_type": "directory", + "media_content_type": "video", "media_content_id": ( f"media-source://motioneye" f"/74565ad414754616000674c87bdc876c#{device.id}#movies" ), "can_play": False, "can_expand": True, - "children_media_class": "movie", + "children_media_class": "video", "thumbnail": None, "children": [], } From 58bee8a326977dc4c479537a415d115f90db3b0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 08:27:59 +0100 Subject: [PATCH 0212/1452] Bump actions/checkout from 2.3.5 to 2.4.0 (#58978) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++----- .github/workflows/ci.yaml | 38 ++++++++++++++--------------- .github/workflows/translations.yaml | 4 +-- .github/workflows/wheels.yml | 6 ++--- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 708e360d59b..72a57ea1421 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -23,7 +23,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -97,7 +97,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' @@ -170,7 +170,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Login to DockerHub uses: docker/login-action@v1.10.0 @@ -201,7 +201,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -233,7 +233,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Login to DockerHub uses: docker/login-action@v1.10.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aa5a6ddd3ca..890fed4694b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v2.2.2 @@ -84,7 +84,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -124,7 +124,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -164,7 +164,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -207,7 +207,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -226,7 +226,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -269,7 +269,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -312,7 +312,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -352,7 +352,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -395,7 +395,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -436,7 +436,7 @@ jobs: # needs: prepare-base # steps: # - name: Check out code from GitHub - # uses: actions/checkout@v2.3.5 + # uses: actions/checkout@v2.4.0 # - name: Run ShellCheck # uses: ludeeus/action-shellcheck@0.3.0 @@ -446,7 +446,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -493,7 +493,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -517,7 +517,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -551,7 +551,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -595,7 +595,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -626,7 +626,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -660,7 +660,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -718,7 +718,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 6e734528f0e..76e5ba6d719 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ebb4a65b80e..56b09886736 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -21,7 +21,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Get information id: info @@ -68,7 +68,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Download env_file uses: actions/download-artifact@v2 @@ -108,7 +108,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Download env_file uses: actions/download-artifact@v2 From a4fc808e49222a6d786add20dda16c14989a9000 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 3 Nov 2021 08:45:22 +0100 Subject: [PATCH 0213/1452] Fix broken ViCare burner & compressor sensors (#58962) --- homeassistant/components/vicare/binary_sensor.py | 4 ++-- homeassistant/components/vicare/sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index d025d2b1ba6..4484d5f5040 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -129,14 +129,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= all_devices.append(entity) try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, BURNER_SENSORS, api.burners ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors ) except PyViCareNotSupportedFeatureError: diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 044632b6244..2ff8ce4bf7d 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -393,14 +393,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= all_devices.append(entity) try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, BURNER_SENSORS, api.burners ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors ) except PyViCareNotSupportedFeatureError: From 1548877e3631a849b5f370eef5ec1371e9420ede Mon Sep 17 00:00:00 2001 From: kodsnutten Date: Wed, 3 Nov 2021 10:21:54 +0100 Subject: [PATCH 0214/1452] Fix unique_id of derived sent-sensors (#58298) --- homeassistant/components/upnp/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index f7cc242f6f1..54176715b84 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -84,7 +84,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( ), UpnpSensorEntityDescription( key=BYTES_SENT, - unique_id="KiB/sent", + unique_id="KiB/sec_sent", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, @@ -100,7 +100,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( ), UpnpSensorEntityDescription( key=PACKETS_SENT, - unique_id="packets/sent", + unique_id="packets/sec_sent", name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, From a7d958ae8a9d3c5b73c5e71d3b9d1e01f2b3e792 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Nov 2021 11:51:17 +0100 Subject: [PATCH 0215/1452] Update frontend to 20211103.0 (#58988) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9f15dc4fc5a..c913de3367f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211028.0" + "home-assistant-frontend==20211103.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 36ab9ad8c38..90e0a14dadc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index c84aac7e3ec..1104f60c9ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5b5ad5f9cd..544e75b5496 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -506,7 +506,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1af621ef13e9ea24b4e6444c10294e681fa3be36 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Alvarez Date: Wed, 3 Nov 2021 05:28:04 -0600 Subject: [PATCH 0216/1452] Fix battery_is_charging sensor on system bridge (#58980) --- homeassistant/components/system_bridge/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index a622a3a925a..7681a428cca 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -41,7 +41,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, .. key="battery_is_charging", name="Battery Is Charging", device_class=DEVICE_CLASS_BATTERY_CHARGING, - value=lambda bridge: bridge.information.updates.available, + value=lambda bridge: bridge.battery.isCharging, ), ) From 96c03aec06a24f8bb6f1957c19fb4bed9ca2175e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 3 Nov 2021 17:28:11 +0100 Subject: [PATCH 0217/1452] Fix timedelta-based sensors for xiaomi_miio (#58995) --- homeassistant/components/xiaomi_miio/device.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 488f4cc066f..5a186c23570 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -167,8 +167,7 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): return cls._parse_datetime_time(value) if isinstance(value, datetime.datetime): return cls._parse_datetime_datetime(value) - if isinstance(value, datetime.timedelta): - return cls._parse_time_delta(value) + if value is None: _LOGGER.debug("Attribute %s is None, this is unexpected", attribute) @@ -176,7 +175,7 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): @staticmethod def _parse_time_delta(timedelta: datetime.timedelta) -> int: - return timedelta.seconds + return int(timedelta.total_seconds()) @staticmethod def _parse_datetime_time(time: datetime.time) -> str: @@ -192,7 +191,3 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): @staticmethod def _parse_datetime_datetime(time: datetime.datetime) -> str: return time.isoformat() - - @staticmethod - def _parse_datetime_timedelta(time: datetime.timedelta) -> int: - return time.seconds From c9c95165e47dc7f98ff650ddfd91be8239c0bad8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 4 Nov 2021 00:12:21 +0000 Subject: [PATCH 0218/1452] [ci skip] Translation update --- .../components/aemet/translations/ru.json | 2 +- .../components/ambee/translations/ru.json | 2 +- .../components/arcam_fmj/translations/bg.json | 5 ++++ .../components/asuswrt/translations/ru.json | 4 +-- .../components/awair/translations/bg.json | 23 ++++++++++++++ .../azure_devops/translations/ru.json | 2 +- .../components/blebox/translations/ru.json | 2 +- .../components/bond/translations/bg.json | 16 ++++++++++ .../components/bosch_shc/translations/ru.json | 2 +- .../components/bsblan/translations/ru.json | 2 +- .../components/control4/translations/bg.json | 21 +++++++++++++ .../components/denonavr/translations/bg.json | 15 ++++++++++ .../components/dexcom/translations/bg.json | 12 +++++++- .../components/dunehd/translations/bg.json | 19 ++++++++++++ .../components/dunehd/translations/ru.json | 2 +- .../components/firmata/translations/bg.json | 7 +++++ .../forecast_solar/translations/ru.json | 2 +- .../components/gogogate2/translations/ru.json | 2 +- .../growatt_server/translations/ru.json | 2 +- .../components/guardian/translations/ru.json | 2 +- .../huawei_lte/translations/bg.json | 1 + .../components/hue/translations/bg.json | 14 +++++++++ .../humidifier/translations/bg.json | 9 +++++- .../hvv_departures/translations/bg.json | 30 +++++++++++++++++++ .../components/metoffice/translations/bg.json | 20 +++++++++++++ .../modern_forms/translations/ru.json | 2 +- .../components/mqtt/translations/bg.json | 7 ++++- .../components/mullvad/translations/ru.json | 2 +- .../components/nam/translations/ru.json | 2 +- .../components/netatmo/translations/ru.json | 2 +- .../openweathermap/translations/ru.json | 2 +- .../ovo_energy/translations/ru.json | 2 +- .../p1_monitor/translations/ru.json | 2 +- .../components/pi_hole/translations/bg.json | 1 + .../plum_lightpad/translations/bg.json | 18 +++++++++++ .../components/poolsense/translations/bg.json | 19 ++++++++++++ .../components/sma/translations/ru.json | 2 +- .../components/smappee/translations/bg.json | 12 ++++++++ .../components/smappee/translations/ru.json | 2 +- .../components/smarthab/translations/bg.json | 12 ++++++++ .../components/sms/translations/bg.json | 19 ++++++++++++ .../components/soma/translations/fr.json | 2 +- .../components/sonarr/translations/bg.json | 3 ++ .../speedtestdotnet/translations/bg.json | 14 +++++++++ .../squeezebox/translations/bg.json | 19 +++++++++++- .../components/syncthru/translations/bg.json | 23 ++++++++++++++ .../components/tasmota/translations/ru.json | 2 +- .../tellduslive/translations/bg.json | 1 + .../components/tile/translations/bg.json | 8 +++++ .../components/toon/translations/bg.json | 1 + .../components/tuya/translations/ru.json | 4 +-- .../components/withings/translations/bg.json | 3 ++ .../components/wolflink/translations/bg.json | 25 ++++++++++++++++ .../wolflink/translations/sensor.bg.json | 1 + .../xiaomi_aqara/translations/bg.json | 15 ++++++++++ .../xiaomi_aqara/translations/ru.json | 2 +- .../xiaomi_miio/translations/bg.json | 1 + .../yamaha_musiccast/translations/ru.json | 2 +- 58 files changed, 418 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/awair/translations/bg.json create mode 100644 homeassistant/components/bond/translations/bg.json create mode 100644 homeassistant/components/control4/translations/bg.json create mode 100644 homeassistant/components/denonavr/translations/bg.json create mode 100644 homeassistant/components/dunehd/translations/bg.json create mode 100644 homeassistant/components/firmata/translations/bg.json create mode 100644 homeassistant/components/hvv_departures/translations/bg.json create mode 100644 homeassistant/components/metoffice/translations/bg.json create mode 100644 homeassistant/components/plum_lightpad/translations/bg.json create mode 100644 homeassistant/components/poolsense/translations/bg.json create mode 100644 homeassistant/components/smappee/translations/bg.json create mode 100644 homeassistant/components/smarthab/translations/bg.json create mode 100644 homeassistant/components/sms/translations/bg.json create mode 100644 homeassistant/components/syncthru/translations/bg.json create mode 100644 homeassistant/components/wolflink/translations/bg.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/bg.json diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json index 1dc0e21b0df..f9278af712b 100644 --- a/homeassistant/components/aemet/translations/ru.json +++ b/homeassistant/components/aemet/translations/ru.json @@ -14,7 +14,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json index 02458c7609f..c229c2d6020 100644 --- a/homeassistant/components/ambee/translations/ru.json +++ b/homeassistant/components/ambee/translations/ru.json @@ -21,7 +21,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." } } } diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json index 4983c9a14b2..26b157c0e5f 100644 --- a/homeassistant/components/arcam_fmj/translations/bg.json +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{host}", "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index f77fcb4fb3a..35254821f23 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -20,10 +20,10 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438", - "ssh_key": "\u041f\u0443\u0442\u044c \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", + "ssh_key": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u043c AsusWRT.", "title": "AsusWRT" } } diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json new file mode 100644 index 00000000000..1d5233cabbf --- /dev/null +++ b/homeassistant/components/awair/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "email": "Email" + } + }, + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json index 5e629b6d558..5fe2aa58b5c 100644 --- a/homeassistant/components/azure_devops/translations/ru.json +++ b/homeassistant/components/azure_devops/translations/ru.json @@ -24,7 +24,7 @@ "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)", "project": "\u041f\u0440\u043e\u0435\u043a\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.", "title": "Azure DevOps" } } diff --git a/homeassistant/components/blebox/translations/ru.json b/homeassistant/components/blebox/translations/ru.json index b230c4e9974..4b3528ec4fe 100644 --- a/homeassistant/components/blebox/translations/ru.json +++ b/homeassistant/components/blebox/translations/ru.json @@ -16,7 +16,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BleBox.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BleBox.", "title": "BleBox" } } diff --git a/homeassistant/components/bond/translations/bg.json b/homeassistant/components/bond/translations/bg.json new file mode 100644 index 00000000000..6eb147e8ddd --- /dev/null +++ b/homeassistant/components/bond/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ru.json b/homeassistant/components/bosch_shc/translations/ru.json index ebbfd46812c..498003b2501 100644 --- a/homeassistant/components/bosch_shc/translations/ru.json +++ b/homeassistant/components/bosch_shc/translations/ru.json @@ -29,7 +29,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Bosch Smart Home Controller.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Bosch Smart Home Controller.", "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 SHC" } } diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json index e52a249a493..27297418983 100644 --- a/homeassistant/components/bsblan/translations/ru.json +++ b/homeassistant/components/bsblan/translations/ru.json @@ -16,7 +16,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } diff --git a/homeassistant/components/control4/translations/bg.json b/homeassistant/components/control4/translations/bg.json new file mode 100644 index 00000000000..cda77281219 --- /dev/null +++ b/homeassistant/components/control4/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/bg.json b/homeassistant/components/denonavr/translations/bg.json new file mode 100644 index 00000000000..6ec6215c6e1 --- /dev/null +++ b/homeassistant/components/denonavr/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json index 2ac8a444100..ec574a06c2f 100644 --- a/homeassistant/components/dexcom/translations/bg.json +++ b/homeassistant/components/dexcom/translations/bg.json @@ -1,7 +1,17 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "server": "\u0421\u044a\u0440\u0432\u044a\u0440", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/bg.json b/homeassistant/components/dunehd/translations/bg.json new file mode 100644 index 00000000000..56eb33213e4 --- /dev/null +++ b/homeassistant/components/dunehd/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index be35fe8b092..c1537de579f 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Dune HD. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://www.home-assistant.io/integrations/dunehd\n\n\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0435\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Dune HD. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://www.home-assistant.io/integrations/dunehd\n\n\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0435\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "title": "Dune HD" } } diff --git a/homeassistant/components/firmata/translations/bg.json b/homeassistant/components/firmata/translations/bg.json new file mode 100644 index 00000000000..c30e629d8ad --- /dev/null +++ b/homeassistant/components/firmata/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json index bd1e4ae70c0..9cf8e87a8e2 100644 --- a/homeassistant/components/forecast_solar/translations/ru.json +++ b/homeassistant/components/forecast_solar/translations/ru.json @@ -10,7 +10,7 @@ "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Forecast.Solar." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Forecast.Solar." } } }, diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 3c5af51d94e..0ff1fbd7662 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -15,7 +15,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Gogogate2 \u0438\u043b\u0438 ismartgate" } } diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index 0b98838dac8..c5eedf66ad3 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -20,7 +20,7 @@ "url": "URL-\u0430\u0434\u0440\u0435\u0441", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Growatt." + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Growatt." } } }, diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 1095a568d78..ca904d980af 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -14,7 +14,7 @@ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." }, "zeroconf_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elexa Guardian?" diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 997c3bc1456..0ec49564e62 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -13,6 +13,7 @@ "login_attempts_exceeded": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u0442\u0435 \u043e\u043f\u0438\u0442\u0438 \u0437\u0430 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 \u0441\u0430 \u043d\u0430\u0434\u0432\u0438\u0448\u0435\u043d\u0438. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e", "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 864963b3da5..f5091f34333 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -24,6 +24,11 @@ "link": { "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_philips_hue.jpg)", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" + }, + "manual": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } } } }, @@ -32,5 +37,14 @@ "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_unreachable": "{name}" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/bg.json b/homeassistant/components/humidifier/translations/bg.json index 21aa58a9e64..5bf60744a03 100644 --- a/homeassistant/components/humidifier/translations/bg.json +++ b/homeassistant/components/humidifier/translations/bg.json @@ -1,7 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + }, "state": { "_": { - "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/bg.json b/homeassistant/components/hvv_departures/translations/bg.json new file mode 100644 index 00000000000..fb5ab8f3eb5 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "offset": "\u041e\u0442\u043c\u0435\u0441\u0442\u0432\u0430\u043d\u0435 (\u043c\u0438\u043d\u0443\u0442\u0438)" + }, + "title": "\u041e\u043f\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/bg.json b/homeassistant/components/metoffice/translations/bg.json new file mode 100644 index 00000000000..a45f3015f93 --- /dev/null +++ b/homeassistant/components/metoffice/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ru.json b/homeassistant/components/modern_forms/translations/ru.json index 9bfba30bf33..68b46c57f47 100644 --- a/homeassistant/components/modern_forms/translations/ru.json +++ b/homeassistant/components/modern_forms/translations/ru.json @@ -16,7 +16,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u043c Modern Forms." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u043c Modern Forms." }, "zeroconf_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440 Modern Forms `{name}`?", diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 4790a02329d..f69eb1b4cc9 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -28,10 +28,15 @@ } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "broker": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json index cf22d6e4f49..3b8af4fe508 100644 --- a/homeassistant/components/mullvad/translations/ru.json +++ b/homeassistant/components/mullvad/translations/ru.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." } } } diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json index d475081285b..1e94cb848f3 100644 --- a/homeassistant/components/nam/translations/ru.json +++ b/homeassistant/components/nam/translations/ru.json @@ -17,7 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Nettigo Air Monitor." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Nettigo Air Monitor." } } } diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index e53e641b5ee..1bb004b5464 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -54,7 +54,7 @@ "mode": "\u0420\u0430\u0441\u0447\u0435\u0442", "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0438", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0438.", "title": "\u041e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b Netatmo" }, "public_weather_areas": { diff --git a/homeassistant/components/openweathermap/translations/ru.json b/homeassistant/components/openweathermap/translations/ru.json index f113d8205f7..1a22b38d546 100644 --- a/homeassistant/components/openweathermap/translations/ru.json +++ b/homeassistant/components/openweathermap/translations/ru.json @@ -17,7 +17,7 @@ "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OpenWeatherMap. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OpenWeatherMap. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index d0a2ffb798d..f44b0e27110 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -19,7 +19,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OVO Energy.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OVO Energy.", "title": "OVO Energy" } } diff --git a/homeassistant/components/p1_monitor/translations/ru.json b/homeassistant/components/p1_monitor/translations/ru.json index 661cc1c8968..78277de42c1 100644 --- a/homeassistant/components/p1_monitor/translations/ru.json +++ b/homeassistant/components/p1_monitor/translations/ru.json @@ -10,7 +10,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 P1 Monitor." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 P1 Monitor." } } } diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 4983c9a14b2..3493e514ef3 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/plum_lightpad/translations/bg.json b/homeassistant/components/plum_lightpad/translations/bg.json new file mode 100644 index 00000000000..597f4d36165 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/bg.json b/homeassistant/components/poolsense/translations/bg.json new file mode 100644 index 00000000000..0b38b9c4741 --- /dev/null +++ b/homeassistant/components/poolsense/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/ru.json b/homeassistant/components/sma/translations/ru.json index ab1b7635bc3..c91f79c0fb7 100644 --- a/homeassistant/components/sma/translations/ru.json +++ b/homeassistant/components/sma/translations/ru.json @@ -19,7 +19,7 @@ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c SMA.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c SMA.", "title": "SMA Solar" } } diff --git a/homeassistant/components/smappee/translations/bg.json b/homeassistant/components/smappee/translations/bg.json new file mode 100644 index 00000000000..83b32fc7c85 --- /dev/null +++ b/homeassistant/components/smappee/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index c6191daab0d..284b43cf4fc 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -15,7 +15,7 @@ "data": { "environment": "\u041e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Smappee." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Smappee." }, "local": { "data": { diff --git a/homeassistant/components/smarthab/translations/bg.json b/homeassistant/components/smarthab/translations/bg.json new file mode 100644 index 00000000000..75022ed3005 --- /dev/null +++ b/homeassistant/components/smarthab/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/bg.json b/homeassistant/components/sms/translations/bg.json new file mode 100644 index 00000000000..6834b67bdd3 --- /dev/null +++ b/homeassistant/components/sms/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/fr.json b/homeassistant/components/soma/translations/fr.json index b0a287b1708..8531f839df0 100644 --- a/homeassistant/components/soma/translations/fr.json +++ b/homeassistant/components/soma/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", "connection_error": "Impossible de se connecter \u00e0 SOMA Connect.", "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index f370ff8a2fd..4acf9d803a7 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -3,12 +3,15 @@ "abort": { "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, + "flow_title": "{name}", "step": { "reauth_confirm": { "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/speedtestdotnet/translations/bg.json b/homeassistant/components/speedtestdotnet/translations/bg.json index 1c6120581b0..0e175ee9902 100644 --- a/homeassistant/components/speedtestdotnet/translations/bg.json +++ b/homeassistant/components/speedtestdotnet/translations/bg.json @@ -2,6 +2,20 @@ "config": { "abort": { "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "server_name": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0442\u0435\u0441\u0442\u043e\u0432 \u0441\u044a\u0440\u0432\u044a\u0440" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/bg.json b/homeassistant/components/squeezebox/translations/bg.json index a9446bb8a17..a9b1dad60c8 100644 --- a/homeassistant/components/squeezebox/translations/bg.json +++ b/homeassistant/components/squeezebox/translations/bg.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{host}", "step": { "edit": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" } } } diff --git a/homeassistant/components/syncthru/translations/bg.json b/homeassistant/components/syncthru/translations/bg.json new file mode 100644 index 00000000000..bd8d6d4bf88 --- /dev/null +++ b/homeassistant/components/syncthru/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "\u0418\u043c\u0435" + } + }, + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/ru.json b/homeassistant/components/tasmota/translations/ru.json index 3f4b8996f2f..4f01d164030 100644 --- a/homeassistant/components/tasmota/translations/ru.json +++ b/homeassistant/components/tasmota/translations/ru.json @@ -11,7 +11,7 @@ "data": { "discovery_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tasmota.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tasmota.", "title": "Tasmota" }, "confirm": { diff --git a/homeassistant/components/tellduslive/translations/bg.json b/homeassistant/components/tellduslive/translations/bg.json index 6e770c58b30..6848917b501 100644 --- a/homeassistant/components/tellduslive/translations/bg.json +++ b/homeassistant/components/tellduslive/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index 946b62a8690..cd419ccf91c 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -2,6 +2,14 @@ "config": { "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/bg.json b/homeassistant/components/toon/translations/bg.json index 11793c5bb5d..fabff55d680 100644 --- a/homeassistant/components/toon/translations/bg.json +++ b/homeassistant/components/toon/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", "no_agreements": "\u0422\u043e\u0437\u0438 \u043f\u0440\u043e\u0444\u0438\u043b \u043d\u044f\u043c\u0430 Toon \u0434\u0438\u0441\u043f\u043b\u0435\u0438." } } diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index b02562813c6..e13914683f1 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -21,7 +21,7 @@ "tuya_app_type": "\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "username": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", "title": "Tuya" }, "user": { @@ -35,7 +35,7 @@ "tuya_project_type": "\u0422\u0438\u043f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 Tuya", "username": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", "title": "Tuya" } } diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index d0445aed41f..b258b9d548a 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -14,6 +14,9 @@ }, "description": "\u041a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b \u0441\u0442\u0435 \u0438\u0437\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u043d\u0430 Withings? \u0412\u0430\u0436\u043d\u043e \u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438\u0442\u0435 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442, \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u0435\u043d \u0441\u043b\u0443\u0447\u0430\u0439 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438.", "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + }, + "reauth": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/wolflink/translations/bg.json b/homeassistant/components/wolflink/translations/bg.json new file mode 100644 index 00000000000..3caea097ed7 --- /dev/null +++ b/homeassistant/components/wolflink/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "device": { + "data": { + "device_name": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index d9a9400e4d5..1889d3efb34 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", "urlaubsmodus": "\u0412\u0430\u043a\u0430\u043d\u0446\u0438\u043e\u043d\u0435\u043d \u0440\u0435\u0436\u0438\u043c", diff --git a/homeassistant/components/xiaomi_aqara/translations/bg.json b/homeassistant/components/xiaomi_aqara/translations/bg.json new file mode 100644 index 00000000000..0fbc7416296 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 46b46e2beec..aa18808d222 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -35,7 +35,7 @@ "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index b692bd4bb9f..a73df3c5609 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -8,6 +8,7 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "no_device_selected": "\u041d\u0435 \u0435 \u0438\u0437\u0431\u0440\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, + "flow_title": "{name}", "step": { "cloud": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json index 161a97ad15d..a09b1b4cf3c 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ru.json +++ b/homeassistant/components/yamaha_musiccast/translations/ru.json @@ -16,7 +16,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 MusicCast." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 MusicCast." } } } From 6419950283ec2d5a57bd0bc98a8266755b64ef33 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 4 Nov 2021 03:38:47 +0000 Subject: [PATCH 0219/1452] Aurora abb improvements (#58504) * Add type hints. * Refactor AuroraDevice to AuroraDeviceEntity * Refactor AuroraDevice to AuroraDeviceEntity * Connection class is defined in manifest. * Separate words with underscore in variable names * Remove duplicated code. * Remove unused "unknown" string * Test import yaml when integration already setup * Remove test already done in config_flow test * Convert variable names to snake case * Shorten AuroraDeviceEntity to AuroraEntity * Add typing * Remove unnecessary integration setup in test. * Refactor "already_setup" to "already_configured" * Use common string * Reduce the amount of code in the try block. * Fix merge * Allow yaml setup to be deferred if no comms * Properly setup all sensors for defered yaml setup. * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Add type hints. * Refactor AuroraDevice to AuroraDeviceEntity * Refactor AuroraDevice to AuroraDeviceEntity * Connection class is defined in manifest. * Separate words with underscore in variable names * Remove duplicated code. * Remove unused "unknown" string * Test import yaml when integration already setup * Remove test already done in config_flow test * Convert variable names to snake case * Shorten AuroraDeviceEntity to AuroraEntity * Add typing * Remove unnecessary integration setup in test. * Refactor "already_setup" to "already_configured" * Use common string * Reduce the amount of code in the try block. * Allow yaml setup to be deferred if no comms * Properly setup all sensors for defered yaml setup. * Code review: move line out of try block. * Improve test coverage Co-authored-by: Martin Hjelmare --- .../aurora_abb_powerone/__init__.py | 10 +-- .../aurora_abb_powerone/aurora_device.py | 10 ++- .../aurora_abb_powerone/config_flow.py | 61 +++++++------ .../components/aurora_abb_powerone/sensor.py | 12 +-- .../aurora_abb_powerone/strings.json | 5 +- .../aurora_abb_powerone/test_config_flow.py | 89 +++++++++++++++++-- .../aurora_abb_powerone/test_sensor.py | 49 +--------- 7 files changed, 139 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 2c3d0c546cd..21724acaff6 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -25,12 +25,12 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aurora ABB PowerOne from a config entry.""" comport = entry.data[CONF_PORT] address = entry.data[CONF_ADDRESS] - serclient = AuroraSerialClient(address, comport, parity="N", timeout=1) + ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) # To handle yaml import attempts in darkeness, (re)try connecting only if # unique_id not yet assigned. if entry.unique_id is None: @@ -67,19 +67,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False hass.config_entries.async_update_entry(entry, unique_id=new_id) - hass.data.setdefault(DOMAIN, {})[entry.unique_id] = serclient + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client hass.config_entries.async_setup_platforms(entry, PLATFORMS) 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) # It should not be necessary to close the serial port because we close # it after every use in sensor.py, i.e. no need to do entry["client"].close() if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index d2aed5ec7a8..d9cfb744231 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -1,11 +1,13 @@ """Top level class for AuroraABBPowerOneSolarPV inverters and sensors.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from aurorapy.client import AuroraSerialClient -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( ATTR_DEVICE_NAME, @@ -20,10 +22,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class AuroraDevice(Entity): +class AuroraEntity(Entity): """Representation of an Aurora ABB PowerOne device.""" - def __init__(self, client: AuroraSerialClient, data) -> None: + def __init__(self, client: AuroraSerialClient, data: Mapping[str, Any]) -> None: """Initialise the basic device.""" self._data = data self.type = "device" @@ -44,7 +46,7 @@ class AuroraDevice(Entity): return self._available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return { "identifiers": {(DOMAIN, self._data[ATTR_SERIAL_NUMBER])}, diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index 012fe7b14bb..3d292857266 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -1,5 +1,8 @@ """Config flow for Aurora ABB PowerOne integration.""" +from __future__ import annotations + import logging +from typing import Any from aurorapy.client import AuroraError, AuroraSerialClient import serial.tools.list_ports @@ -7,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from .const import ( ATTR_FIRMWARE, @@ -22,7 +26,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def validate_and_connect(hass: core.HomeAssistant, data): +def validate_and_connect( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -50,15 +56,15 @@ def validate_and_connect(hass: core.HomeAssistant, data): return ret -def scan_comports(): +def scan_comports() -> tuple[list[str] | None, str | None]: """Find and store available com ports for the GUI dropdown.""" - comports = serial.tools.list_ports.comports(include_links=True) - comportslist = [] - for port in comports: - comportslist.append(port.device) + com_ports = serial.tools.list_ports.comports(include_links=True) + com_ports_list = [] + for port in com_ports: + com_ports_list.append(port.device) _LOGGER.debug("COM port option: %s", port.device) - if len(comportslist) > 0: - return comportslist, comportslist[0] + if len(com_ports_list) > 0: + return com_ports_list, com_ports_list[0] _LOGGER.warning("No com ports found. Need a valid RS485 device to communicate") return None, None @@ -67,18 +73,17 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Aurora ABB PowerOne.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialise the config flow.""" self.config = None - self._comportslist = None - self._defaultcomport = None + self._com_ports_list = None + self._default_com_port = None - async def async_step_import(self, config: dict): + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import a configuration from config.yaml.""" if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") + return self.async_abort(reason="already_configured") conf = {} conf[CONF_PORT] = config["device"] @@ -87,14 +92,16 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialised by the user.""" errors = {} - if self._comportslist is None: + if self._com_ports_list is None: result = await self.hass.async_add_executor_job(scan_comports) - self._comportslist, self._defaultcomport = result - if self._defaultcomport is None: + self._com_ports_list, self._default_com_port = result + if self._default_com_port is None: return self.async_abort(reason="no_serial_ports") # Handle the initial step. @@ -103,14 +110,6 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await self.hass.async_add_executor_job( validate_and_connect, self.hass, user_input ) - info.update(user_input) - # Bomb out early if someone has already set up this device. - device_unique_id = info["serial_number"] - await self.async_set_unique_id(device_unique_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry(title=info["title"], data=info) - except OSError as error: if error.errno == 19: # No such device. errors["base"] = "invalid_serial_port" @@ -127,10 +126,18 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): error, ) errors["base"] = "cannot_connect" + else: + info.update(user_input) + # Bomb out early if someone has already set up this device. + device_unique_id = info["serial_number"] + await self.async_set_unique_id(device_unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=info) + # If no user input, must be first pass through the config. Show initial form. config_options = { - vol.Required(CONF_PORT, default=self._defaultcomport): vol.In( - self._comportslist + vol.Required(CONF_PORT, default=self._default_com_port): vol.In( + self._com_ports_list ), vol.Required(CONF_ADDRESS, default=DEFAULT_ADDRESS): vol.In( range(MIN_ADDRESS, MAX_ADDRESS + 1) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 4f196c39630..35be4e2b7d7 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from .aurora_device import AuroraDevice +from .aurora_device import AuroraEntity from .const import DEFAULT_ADDRESS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -84,7 +84,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: """Set up aurora_abb_powerone sensor based on a config entry.""" entities = [] - client = hass.data[DOMAIN][config_entry.unique_id] + client = hass.data[DOMAIN][config_entry.entry_id] data = config_entry.data for sens in SENSOR_TYPES: @@ -94,7 +94,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: async_add_entities(entities, True) -class AuroraSensor(AuroraDevice, SensorEntity): +class AuroraSensor(AuroraEntity, SensorEntity): """Representation of a Sensor on a Aurora ABB PowerOne Solar inverter.""" def __init__( @@ -106,7 +106,7 @@ class AuroraSensor(AuroraDevice, SensorEntity): """Initialize the sensor.""" super().__init__(client, data) self.entity_description = entity_description - self.availableprev = True + self.available_prev = True def update(self): """Fetch new state data for the sensor. @@ -114,7 +114,7 @@ class AuroraSensor(AuroraDevice, SensorEntity): This is the only method that should fetch new data for Home Assistant. """ try: - self.availableprev = self._attr_available + self.available_prev = self._attr_available self.client.connect() if self.entity_description.key == "instantaneouspower": # read ADC channel 3 (grid power output) @@ -145,7 +145,7 @@ class AuroraSensor(AuroraDevice, SensorEntity): else: raise error finally: - if self._attr_available != self.availableprev: + if self._attr_available != self.available_prev: if self._attr_available: _LOGGER.info("Communication with %s back online", self.name) else: diff --git a/homeassistant/components/aurora_abb_powerone/strings.json b/homeassistant/components/aurora_abb_powerone/strings.json index b705c5f69a5..bed403bd641 100644 --- a/homeassistant/components/aurora_abb_powerone/strings.json +++ b/homeassistant/components/aurora_abb_powerone/strings.json @@ -12,11 +12,10 @@ "error": { "cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)", "invalid_serial_port": "Serial port is not a valid device or could not be openned", - "cannot_open_serial_port": "Cannot open serial port, please check and try again", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_open_serial_port": "Cannot open serial port, please check and try again" }, "abort": { - "already_configured": "Device is already configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." } } diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index 620d7e10e53..e9a6a100f4a 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Aurora ABB PowerOne Solar PV config flow.""" from datetime import timedelta +import logging from logging import INFO from unittest.mock import patch @@ -17,7 +18,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ADDRESS, CONF_PORT from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed + +TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} def _simulated_returns(index, global_measure=None): @@ -163,10 +166,60 @@ async def test_form_invalid_com_ports(hass): assert len(mock_clientclose.mock_calls) == 1 +async def test_import_invalid_com_ports(hass, caplog): + """Test we display correct info when the comport is invalid..""" + + caplog.set_level(logging.ERROR) + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=OSError(19, "...no such device..."), + return_value=None, + ): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert entry.state == ConfigEntryState.SETUP_ERROR + assert "Failed to connect to inverter: " in caplog.text + + +async def test_import_com_port_wont_open(hass): + """Test we display correct info when comport won't open.""" + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("..could not open port..."), + ): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_import_other_oserror(hass): + """Test we display correct info when comport won't open.""" + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=OSError(18, "...another error..."), + ): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert entry.state == ConfigEntryState.SETUP_ERROR + + # Tests below can be deleted after deprecation period is finished. async def test_import_day(hass): """Test .yaml import when the inverter is able to communicate.""" - TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( "aurorapy.client.AuroraSerialClient.serial_number", @@ -195,7 +248,6 @@ async def test_import_day(hass): async def test_import_night(hass): """Test .yaml import when the inverter is inaccessible (e.g. darkness).""" - TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} # First time round, no response. with patch( @@ -241,13 +293,14 @@ async def test_import_night(hass): assert entry.unique_id assert len(mock_connect.mock_calls) == 1 - assert hass.states.get("sensor.power_output").state == "45.7" + power = hass.states.get("sensor.power_output") + assert power + assert power.state == "45.7" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 async def test_import_night_then_user(hass): """Attempt yaml import and fail (dark), but user sets up manually before auto retry.""" - TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} # First time round, no response. with patch( @@ -322,3 +375,29 @@ async def test_import_night_then_user(hass): await hass.async_block_till_done() assert entry.state == ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_import_already_existing(hass): + """Test configuration.yaml import when already configured.""" + TESTDATA = {"device": "/dev/ttyUSB7", "address": 7, "name": "MyAuroraPV"} + + entry = MockConfigEntry( + domain=DOMAIN, + title="MyAuroraPV", + unique_id="0123456", + data={ + CONF_PORT: "/dev/ttyUSB7", + CONF_ADDRESS: 7, + ATTR_FIRMWARE: "1.234", + ATTR_MODEL: "9.8.7.6 (A.B.C)", + ATTR_SERIAL_NUMBER: "9876543", + "title": "PhotoVoltaic Inverters", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index c1d1d4ad5c8..0a7b7e33302 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -12,16 +12,10 @@ from homeassistant.components.aurora_abb_powerone.const import ( DEFAULT_INTEGRATION_TITLE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_ADDRESS, CONF_PORT -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import ( - MockConfigEntry, - assert_setup_component, - async_fire_time_changed, -) +from tests.common import MockConfigEntry, async_fire_time_changed TEST_CONFIG = { "sensor": { @@ -56,49 +50,10 @@ def _mock_config_entry(): }, source="dummysource", entry_id="13579", + unique_id="654321", ) -async def test_setup_platform_valid_config(hass): - """Test that (deprecated) yaml import still works.""" - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=_simulated_returns, - ), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=_simulated_returns, - ), assert_setup_component( - 1, "sensor" - ): - assert await async_setup_component(hass, "sensor", TEST_CONFIG) - await hass.async_block_till_done() - power = hass.states.get("sensor.power_output") - assert power - assert power.state == "45.7" - - # try to set up a second time - should abort. - - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=TEST_CONFIG, - context={"source": SOURCE_IMPORT}, - ) - assert result["type"] == "abort" - assert result["reason"] == "already_setup" - - async def test_sensors(hass): """Test data coming back from inverter.""" mock_entry = _mock_config_entry() From a64cec6da1b3d762f902e738b99c05c74d371b31 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 4 Nov 2021 05:32:16 +0100 Subject: [PATCH 0220/1452] Bump aiopvpc to 2.2.1 (#59008) happening because some config change in the ESIOS API server, solved with a version patch in aiopvpc (details in https://github.com/azogue/aiopvpc/pull/28) --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 612376a7931..a30ae1e2732 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.2.0"], + "requirements": ["aiopvpc==2.2.1"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 1104f60c9ed..fd2ccfd4d04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.0 +aiopvpc==2.2.1 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 544e75b5496..3eb99039e9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.0 +aiopvpc==2.2.1 # homeassistant.components.webostv aiopylgtv==0.4.0 From 4345432d14defb3a299f770d2353a604261dc386 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Thu, 4 Nov 2021 09:58:58 +0100 Subject: [PATCH 0221/1452] Add state class to wallbox component (#58801) --- homeassistant/components/wallbox/sensor.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 25c0796d0bd..affc78d6210 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -4,7 +4,12 @@ from __future__ import annotations from dataclasses import dataclass import logging -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, @@ -54,6 +59,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { precision=2, native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAX_AVAILABLE_POWER_KEY: WallboxSensorEntityDescription( key=CONF_MAX_AVAILABLE_POWER_KEY, @@ -61,12 +67,14 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { precision=0, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( key=CONF_CHARGING_SPEED_KEY, icon="mdi:speedometer", name="Charging Speed", precision=0, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ADDED_RANGE_KEY: WallboxSensorEntityDescription( key=CONF_ADDED_RANGE_KEY, @@ -74,6 +82,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Added Range", precision=0, native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( key=CONF_ADDED_ENERGY_KEY, @@ -81,17 +90,20 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { precision=2, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_COST_KEY: WallboxSensorEntityDescription( key=CONF_COST_KEY, icon="mdi:ev-station", name="Cost", + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( key=CONF_STATE_OF_CHARGE_KEY, name="State of Charge", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT_MODE_KEY: WallboxSensorEntityDescription( key=CONF_CURRENT_MODE_KEY, @@ -114,6 +126,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Max. Charging Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), } From 2f3dea30e2c243c2822eca777fcf2d626cc2dec1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 10:29:10 +0100 Subject: [PATCH 0222/1452] Correct migration to recorder schema 22 (#59048) --- .../components/recorder/migration.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 508476e0c2b..fe3e1aeb84d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -12,6 +12,7 @@ from sqlalchemy.exc import ( SQLAlchemyError, ) from sqlalchemy.schema import AddConstraint, DropConstraint +from sqlalchemy.sql.expression import true from .models import ( SCHEMA_VERSION, @@ -24,7 +25,7 @@ from .models import ( StatisticsShortTerm, process_timestamp, ) -from .statistics import get_metadata_with_session, get_start_time +from .statistics import get_start_time from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -558,21 +559,25 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 session.add(StatisticsRuns(start=fake_start_time)) fake_start_time += timedelta(minutes=5) - # Copy last hourly statistic to the newly created 5-minute statistics table - sum_statistics = get_metadata_with_session( - instance.hass, session, statistic_type="sum" - ) - for metadata_id, _ in sum_statistics.values(): + # When querying the database, be careful to only explicitly query for columns + # which were present in schema version 21. If querying the table, SQLAlchemy + # will refer to future columns. + for sum_statistic in session.query(StatisticsMeta.id).filter_by(has_sum=true()): last_statistic = ( - session.query(Statistics) - .filter_by(metadata_id=metadata_id) + session.query( + Statistics.start, + Statistics.last_reset, + Statistics.state, + Statistics.sum, + ) + .filter_by(metadata_id=sum_statistic.id) .order_by(Statistics.start.desc()) .first() ) if last_statistic: session.add( StatisticsShortTerm( - metadata_id=last_statistic.metadata_id, + metadata_id=sum_statistic.id, start=last_statistic.start, last_reset=last_statistic.last_reset, state=last_statistic.state, From 50a1e908c4cb6e6686c5b52e97f438003e95fb92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:06:17 +0100 Subject: [PATCH 0223/1452] Increase time to authorize OctoPrint (#59051) --- homeassistant/components/octoprint/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index acc1449bd96..04efde16952 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -189,7 +189,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_input[CONF_API_KEY] = await octoprint.request_app_key( - "Home Assistant", user_input[CONF_USERNAME], 30 + "Home Assistant", user_input[CONF_USERNAME], 300 ) finally: # Continue the flow after show progress when the task is done. From 32784d1b83a97c22a881a8529f7e9a16dccae9ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:21:30 +0100 Subject: [PATCH 0224/1452] Constrain urllib3 to >=1.26.5 (#59043) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 90e0a14dadc..20b6a257213 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -36,8 +36,8 @@ zeroconf==0.36.11 pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 -urllib3>=1.24.3 +# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 +urllib3>=1.26.5 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3d2ace4c240..3deec512b4f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -63,8 +63,8 @@ CONSTRAINT_PATH = os.path.join( CONSTRAINT_BASE = """ pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 -urllib3>=1.24.3 +# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 +urllib3>=1.26.5 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 From 74beebc031260c693c20b3123fdc6a72010c7287 Mon Sep 17 00:00:00 2001 From: Thomas G <48775270+tomgie@users.noreply.github.com> Date: Thu, 4 Nov 2021 05:21:59 -0500 Subject: [PATCH 0225/1452] Swap sharkiq vacuum is_docked with is_charging (#58975) --- homeassistant/components/sharkiq/vacuum.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 3851dab7303..33412963cda 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -138,11 +138,6 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_SHARKIQ - @property - def is_docked(self) -> bool | None: - """Is vacuum docked.""" - return self.sharkiq.get_property_value(Properties.DOCKED_STATUS) - @property def error_code(self) -> int | None: """Return the last observed error code (or None).""" @@ -175,7 +170,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): In the app, these are (usually) handled by showing the robot as stopped and sending the user a notification. """ - if self.is_docked: + if self.sharkiq.get_property_value(Properties.CHARGING_STATUS): return STATE_DOCKED return self.operating_mode From a52466c33973865d371f107d8b2c517c5f27c07e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:27:20 +0100 Subject: [PATCH 0226/1452] Upgrade yamllint to 1.26.3 (#59047) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a95dfbd869..a0a9d9a07b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.1 + rev: v1.26.3 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a21931423b9..84ae9829164 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -13,4 +13,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pyupgrade==2.27.0 -yamllint==1.26.1 +yamllint==1.26.3 From da8b9cbe82a128cd956380eef1278f47f8716039 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:29:01 +0100 Subject: [PATCH 0227/1452] Upgrade isort to 5.10.0 (#59046) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0a9d9a07b2..57445820886 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.9.3 + rev: 5.10.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 84ae9829164..8cc41279c13 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,7 +7,7 @@ flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.0 flake8==4.0.1 -isort==5.9.3 +isort==5.10.0 mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 From 23a0f0b777b75f65415f77173d601f26a8889dee Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 4 Nov 2021 12:27:41 +0100 Subject: [PATCH 0228/1452] Bump velbus-aio to 2021.11.0 (#59040) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 4541b428a4a..454b9d24d07 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.10.7"], + "requirements": ["velbus-aio==2021.11.0"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index fd2ccfd4d04..c9669abc0e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.10.7 +velbus-aio==2021.11.0 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eb99039e9f..20f98cc448c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.10.7 +velbus-aio==2021.11.0 # homeassistant.components.venstar venstarcolortouch==0.15 From ea657e6656362d144ade9e683e3f5d6762ad08db Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 4 Nov 2021 14:14:31 +0100 Subject: [PATCH 0229/1452] Accept all roborock vacuum models for xiaomi_miio (#59018) --- homeassistant/components/xiaomi_miio/const.py | 2 + .../xiaomi_miio/test_config_flow.py | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c140ad526e2..124bdb77f5e 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -202,6 +202,7 @@ ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" +ROCKROBO_GENERIC = "roborock.vacuum" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, @@ -213,6 +214,7 @@ MODELS_VACUUM = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROCKROBO_GENERIC, ] MODELS_VACUUM_WITH_MOP = [ ROCKROBO_E2, diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 24aa5ac04e4..206da7ad4ae 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -695,6 +695,52 @@ async def config_flow_device_success(hass, model_to_test): } +async def config_flow_generic_roborock(hass): + """Test a successful config flow for a generic roborock vacuum.""" + DUMMY_MODEL = "roborock.vacuum.dummy" + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + assert result["errors"] == {} + + mock_info = get_mock_info(model=DUMMY_MODEL) + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DUMMY_MODEL + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: DUMMY_MODEL, + const.CONF_MAC: TEST_MAC, + } + + async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): """Test a successful zeroconf discovery of a device (base class).""" result = await hass.config_entries.flow.async_init( From 22248f891dae54906676960535c08bc7f1fd8e6b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 4 Nov 2021 14:22:58 +0100 Subject: [PATCH 0230/1452] Refactor velbus light code to make it more clear and readable (#58483) Co-authored-by: Franck Nijhof --- homeassistant/components/velbus/light.py | 118 +++++++++++++---------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index a252930b49d..2c6cd8d8776 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -21,33 +21,21 @@ async def async_setup_entry(hass, entry, async_add_entities): cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] entities = [] for channel in cntrl.get_all("light"): - entities.append(VelbusLight(channel, False)) + entities.append(VelbusLight(channel)) for channel in cntrl.get_all("led"): - entities.append(VelbusLight(channel, True)) + entities.append(VelbusButtonLight(channel)) async_add_entities(entities) class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" - def __init__(self, channel, led): - """Initialize a light Velbus entity.""" + _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + def __init__(self, channel): + """Initialize the dimmer.""" super().__init__(channel) - self._is_led = led - - @property - def name(self): - """Return the display name of this entity.""" - if self._is_led: - return f"LED {self._channel.get_name()}" - return self._channel.get_name() - - @property - def supported_features(self): - """Flag supported features.""" - if self._is_led: - return SUPPORT_FLASH - return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + self._attr_name = self._channel.get_name() @property def is_on(self): @@ -61,43 +49,69 @@ class VelbusLight(VelbusEntity, LightEntity): async def async_turn_on(self, **kwargs): """Instruct the Velbus light to turn on.""" - if self._is_led: - if ATTR_FLASH in kwargs: - if kwargs[ATTR_FLASH] == FLASH_LONG: - attr, *args = "set_led_state", "slow" - elif kwargs[ATTR_FLASH] == FLASH_SHORT: - attr, *args = "set_led_state", "fast" - else: - attr, *args = "set_led_state", "on" + if ATTR_BRIGHTNESS in kwargs: + # Make sure a low but non-zero value is not rounded down to zero + if kwargs[ATTR_BRIGHTNESS] == 0: + brightness = 0 else: - attr, *args = "set_led_state", "on" + brightness = max(int((kwargs[ATTR_BRIGHTNESS] * 100) / 255), 1) + attr, *args = ( + "set_dimmer_state", + brightness, + kwargs.get(ATTR_TRANSITION, 0), + ) else: - if ATTR_BRIGHTNESS in kwargs: - # Make sure a low but non-zero value is not rounded down to zero - if kwargs[ATTR_BRIGHTNESS] == 0: - brightness = 0 - else: - brightness = max(int((kwargs[ATTR_BRIGHTNESS] * 100) / 255), 1) - attr, *args = ( - "set_dimmer_state", - brightness, - kwargs.get(ATTR_TRANSITION, 0), - ) - else: - attr, *args = ( - "restore_dimmer_state", - kwargs.get(ATTR_TRANSITION, 0), - ) + attr, *args = ( + "restore_dimmer_state", + kwargs.get(ATTR_TRANSITION, 0), + ) await getattr(self._channel, attr)(*args) async def async_turn_off(self, **kwargs): """Instruct the velbus light to turn off.""" - if self._is_led: - attr, *args = "set_led_state", "off" - else: - attr, *args = ( - "set_dimmer_state", - 0, - kwargs.get(ATTR_TRANSITION, 0), - ) + attr, *args = ( + "set_dimmer_state", + 0, + kwargs.get(ATTR_TRANSITION, 0), + ) + await getattr(self._channel, attr)(*args) + + +class VelbusButtonLight(VelbusEntity, LightEntity): + """Representation of a Velbus light.""" + + _attr_entity_registry_enabled_default = False + _attr_supported_feature = SUPPORT_FLASH + + def __init__(self, channel): + """Initialize the button light (led).""" + super().__init__(channel) + self._attr_name = f"LED {self._channel.get_name()}" + + @property + def is_on(self): + """Return true if the light is on.""" + return self._channel.is_on() + + @property + def brightness(self): + """Return the brightness of the light.""" + return int((self._channel.get_dimmer_state() * 255) / 100) + + async def async_turn_on(self, **kwargs): + """Instruct the Velbus light to turn on.""" + if ATTR_FLASH in kwargs: + if kwargs[ATTR_FLASH] == FLASH_LONG: + attr, *args = "set_led_state", "slow" + elif kwargs[ATTR_FLASH] == FLASH_SHORT: + attr, *args = "set_led_state", "fast" + else: + attr, *args = "set_led_state", "on" + else: + attr, *args = "set_led_state", "on" + await getattr(self._channel, attr)(*args) + + async def async_turn_off(self, **kwargs): + """Instruct the velbus light to turn off.""" + attr, *args = "set_led_state", "off" await getattr(self._channel, attr)(*args) From f578eee81d460727e5272ea0400ec323bee3bf80 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 4 Nov 2021 14:25:07 +0100 Subject: [PATCH 0231/1452] Remove use_time sensor from mjjsq humidifers (#59066) --- homeassistant/components/xiaomi_miio/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index f818a809a5c..aefeea7c20a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -299,7 +299,7 @@ HUMIDIFIER_MIOT_SENSORS = ( ATTR_USE_TIME, ATTR_WATER_LEVEL, ) -HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_USE_TIME) +HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) PURIFIER_MIIO_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, From ea6504dfa2bf798a6e5d25518ce197002dc8b589 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 4 Nov 2021 14:26:17 +0100 Subject: [PATCH 0232/1452] Fix Nut resources option migration (#59020) Co-authored-by: Martin Hjelmare --- homeassistant/components/nut/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 81dd5f91f59..f0f2777373a 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -39,8 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # strip out the stale options CONF_RESOURCES if CONF_RESOURCES in entry.options: + new_data = {**entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES]} new_options = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} - hass.config_entries.async_update_entry(entry, options=new_options) + hass.config_entries.async_update_entry( + entry, data=new_data, options=new_options + ) config = entry.data host = config[CONF_HOST] From a852b6df6679e66d905f87eac40b4032d725f4f1 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 4 Nov 2021 14:37:44 +0100 Subject: [PATCH 0233/1452] Add typing info to velbus (part 1) (#59041) * Add typing info to velbus (part 1) * Fix pylint * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/velbus/cover.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/velbus/binary_sensor.py | 9 +++- homeassistant/components/velbus/climate.py | 43 ++++++------------- homeassistant/components/velbus/cover.py | 40 +++++++++++------ homeassistant/components/velbus/switch.py | 9 +++- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index be5d8d24698..014b1410fdc 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -1,11 +1,18 @@ """Support for Velbus Binary Sensors.""" from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 6bc848a92ab..a065679c4e5 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -7,13 +7,20 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN, PRESET_MODES -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] @@ -26,41 +33,17 @@ async def async_setup_entry(hass, entry, async_add_entities): class VelbusClimate(VelbusEntity, ClimateEntity): """Representation of a Velbus thermostat.""" - @property - def supported_features(self) -> int: - """Return the list off supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - - @property - def temperature_unit(self) -> str: - """Return the unit.""" - return TEMP_CELSIUS - - @property - def current_temperature(self) -> int | None: - """Return the current temperature.""" - return self._channel.get_state() - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode.""" - return HVAC_MODE_HEAT - - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT] + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + _attr_temperature_unit = TEMP_CELSIUS + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_hvac_modes = [HVAC_MODE_HEAT] + _attr_preset_modes = list(PRESET_MODES) @property def target_temperature(self) -> int | None: """Return the temperature we try to reach.""" return self._channel.get_climate_target() - @property - def preset_modes(self) -> list[str] | None: - """Return a list of all possible presets.""" - return list(PRESET_MODES) - @property def preset_mode(self) -> str | None: """Return the current Preset for this channel.""" diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 53fd32fad34..43cec0adb24 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -1,4 +1,10 @@ """Support for Velbus covers.""" +from __future__ import annotations + +from typing import Any + +from velbusaio.channels import Channel as VelbusChannel + from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -7,12 +13,19 @@ from homeassistant.components.cover import ( SUPPORT_STOP, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] @@ -25,20 +38,23 @@ async def async_setup_entry(hass, entry, async_add_entities): class VelbusCover(VelbusEntity, CoverEntity): """Representation a Velbus cover.""" - @property - def supported_features(self): - """Flag supported features.""" + def __init__(self, channel: VelbusChannel) -> None: + """Initialize the dimmer.""" + super().__init__(channel) if self._channel.support_position(): - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + self._attr_supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + else: + self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" return self._channel.is_closed() @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open @@ -47,18 +63,18 @@ class VelbusCover(VelbusEntity, CoverEntity): pos = self._channel.get_position() return 100 - pos - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._channel.open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._channel.close() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._channel.stop() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self._channel.set_position(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index 70c7e1eb457..c7b4a80f196 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -2,12 +2,19 @@ from typing import Any from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] From 23cb396aada1a3b2638af1e5f1ca8083ef4d8401 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 14:52:57 +0100 Subject: [PATCH 0234/1452] Upgrade restrictedpython to 5.2a1.dev0 (#59049) --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 0b5af0ae432..f3c25ad493b 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.1"], + "requirements": ["restrictedpython==5.2a1.dev0"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index c9669abc0e6..fbf75e63c32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2049,7 +2049,7 @@ regenmaschine==2021.10.0 renault-api==0.1.4 # homeassistant.components.python_script -restrictedpython==5.1 +restrictedpython==5.2a1.dev0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20f98cc448c..46d4e4c26ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ regenmaschine==2021.10.0 renault-api==0.1.4 # homeassistant.components.python_script -restrictedpython==5.1 +restrictedpython==5.2a1.dev0 # homeassistant.components.rflink rflink==0.0.58 From 10d6247fee181cfb832452f511bcb45bf0475d02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Nov 2021 10:07:50 -0500 Subject: [PATCH 0235/1452] Bump to aiohttp 3.8.0 (#58974) --- homeassistant/async_timeout_backcompat.py | 67 +++++++++++++++++++ homeassistant/components/ads/__init__.py | 2 +- .../aemet/weather_update_coordinator.py | 2 +- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/airly/config_flow.py | 2 +- homeassistant/components/alexa/auth.py | 2 +- .../components/alexa/state_report.py | 4 +- homeassistant/components/almond/__init__.py | 2 +- .../components/almond/config_flow.py | 2 +- .../components/analytics/analytics.py | 2 +- homeassistant/components/api/__init__.py | 2 +- .../components/arcam_fmj/__init__.py | 2 +- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/awair/__init__.py | 2 +- homeassistant/components/axis/device.py | 2 +- .../components/bluesound/media_player.py | 4 +- homeassistant/components/buienradar/util.py | 2 +- homeassistant/components/citybikes/sensor.py | 2 +- .../components/cloud/alexa_config.py | 2 +- homeassistant/components/cloud/http_api.py | 16 ++--- .../components/color_extractor/__init__.py | 2 +- .../components/comed_hourly_pricing/sensor.py | 2 +- .../components/coronavirus/__init__.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- .../components/daikin/config_flow.py | 2 +- .../components/deconz/config_flow.py | 6 +- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- homeassistant/components/elkm1/__init__.py | 2 +- .../components/faa_delays/__init__.py | 2 +- .../components/flick_electric/config_flow.py | 2 +- .../components/flick_electric/sensor.py | 2 +- homeassistant/components/flock/notify.py | 2 +- homeassistant/components/freedns/__init__.py | 2 +- .../components/garages_amsterdam/__init__.py | 2 +- homeassistant/components/gios/__init__.py | 2 +- homeassistant/components/gios/config_flow.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- .../components/google_domains/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/hue/config_flow.py | 2 +- homeassistant/components/hue/light.py | 2 +- homeassistant/components/hue/sensor_base.py | 2 +- homeassistant/components/iammeter/sensor.py | 4 +- homeassistant/components/imap/sensor.py | 2 +- homeassistant/components/ipma/weather.py | 4 +- .../components/isy994/config_flow.py | 2 +- homeassistant/components/kaiterra/api_data.py | 2 +- homeassistant/components/lifx_cloud/scene.py | 4 +- .../components/logi_circle/__init__.py | 2 +- .../components/logi_circle/config_flow.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/melcloud/__init__.py | 2 +- .../components/melcloud/config_flow.py | 2 +- .../components/microsoft_face/__init__.py | 2 +- homeassistant/components/mjpeg/camera.py | 2 +- homeassistant/components/mobile_app/notify.py | 2 +- homeassistant/components/mullvad/__init__.py | 2 +- homeassistant/components/mysensors/gateway.py | 4 +- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nam/config_flow.py | 2 +- homeassistant/components/nest/config_flow.py | 4 +- homeassistant/components/no_ip/__init__.py | 2 +- .../openalpr_cloud/image_processing.py | 2 +- .../weather_update_coordinator.py | 2 +- homeassistant/components/point/config_flow.py | 2 +- .../components/poolsense/__init__.py | 2 +- homeassistant/components/prowl/notify.py | 2 +- homeassistant/components/push/camera.py | 2 +- .../components/rainforest_eagle/data.py | 2 +- homeassistant/components/rest/switch.py | 4 +- homeassistant/components/rflink/__init__.py | 2 +- homeassistant/components/roomba/__init__.py | 4 +- homeassistant/components/sensibo/climate.py | 6 +- homeassistant/components/sharkiq/__init__.py | 11 +-- .../components/sharkiq/config_flow.py | 2 +- .../components/sharkiq/update_coordinator.py | 2 +- homeassistant/components/smhi/weather.py | 2 +- homeassistant/components/sonos/speaker.py | 2 +- homeassistant/components/srp_energy/sensor.py | 2 +- homeassistant/components/startca/sensor.py | 2 +- .../components/system_health/__init__.py | 2 +- .../components/tado/device_tracker.py | 2 +- .../components/tellduslive/config_flow.py | 2 +- .../components/thethingsnetwork/sensor.py | 2 +- .../components/tradfri/config_flow.py | 2 +- homeassistant/components/unifi/controller.py | 4 +- homeassistant/components/updater/__init__.py | 2 +- .../components/viaggiatreno/sensor.py | 2 +- homeassistant/components/voicerss/tts.py | 2 +- .../components/websocket_api/http.py | 2 +- .../components/worxlandroid/sensor.py | 2 +- homeassistant/components/yandextts/tts.py | 2 +- homeassistant/core.py | 4 +- homeassistant/helpers/aiohttp_client.py | 4 +- .../helpers/config_entry_oauth2_flow.py | 2 +- homeassistant/helpers/script.py | 10 ++- homeassistant/package_constraints.txt | 4 +- requirements.txt | 4 +- setup.py | 4 +- tests/components/cloud/test_http_api.py | 2 +- tests/components/generic/test_camera.py | 11 ++- tests/components/hyperion/test_camera.py | 2 +- tests/components/stream/conftest.py | 4 +- .../components/websocket_api/test_commands.py | 6 +- tests/helpers/test_script.py | 4 +- 106 files changed, 221 insertions(+), 142 deletions(-) create mode 100644 homeassistant/async_timeout_backcompat.py diff --git a/homeassistant/async_timeout_backcompat.py b/homeassistant/async_timeout_backcompat.py new file mode 100644 index 00000000000..0a59b0d900e --- /dev/null +++ b/homeassistant/async_timeout_backcompat.py @@ -0,0 +1,67 @@ +"""Provide backwards compat for async_timeout.""" +from __future__ import annotations + +import asyncio +import contextlib +import logging +from typing import Any + +import async_timeout + +from homeassistant.helpers.frame import ( + MissingIntegrationFrame, + get_integration_frame, + report_integration, +) + +_LOGGER = logging.getLogger(__name__) + + +def timeout( + delay: float | None, loop: asyncio.AbstractEventLoop | None = None +) -> async_timeout.Timeout: + """Backwards compatible timeout context manager that warns with loop usage.""" + if loop is None: + loop = asyncio.get_running_loop() + else: + _report( + "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2" + ) + if delay is not None: + deadline: float | None = loop.time() + delay + else: + deadline = None + return async_timeout.Timeout(deadline, loop) + + +def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None: + """Backwards compatible current_task.""" + _report( + "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead" + ) + return asyncio.current_task() + + +def enable() -> None: + """Enable backwards compat transitions.""" + async_timeout.timeout = timeout + async_timeout.current_task = current_task # type: ignore[attr-defined] + + +def _report(what: str) -> None: + """Report incorrect usage. + + Async friendly. + """ + integration_frame = None + + with contextlib.suppress(MissingIntegrationFrame): + integration_frame = get_integration_frame() + + if not integration_frame: + _LOGGER.warning( + "Detected code that %s; Please report this issue", what, stack_info=True + ) + return + + report_integration(what, integration_frame) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index d59d1e5aa0c..53687564cd2 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -307,7 +307,7 @@ class AdsEntity(Entity): self._ads_hub.add_device_notification, ads_var, plctype, update ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await self._event.wait() except asyncio.TimeoutError: _LOGGER.debug("Variable %s: Timeout during first update", ads_var) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 68c979ee27c..d791158b9de 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -140,7 +140,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): data = {} - with async_timeout.timeout(120): + async with async_timeout.timeout(120): weather_response = await self._get_aemet_weather() data = self._convert_weather_response(weather_response) return data diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 0304945e6d2..14a58abc248 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) - with async_timeout.timeout(20): + async with async_timeout.timeout(20): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index a6fa9f2d1d6..10e4990ee05 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -103,7 +103,7 @@ async def test_location( measurements = airly.create_measurements_session_point( latitude=latitude, longitude=longitude ) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await measurements.update() current = measurements.current diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 91729763804..d888b91a39e 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -105,7 +105,7 @@ class Auth: try: session = aiohttp_client.async_get_clientsession(self.hass) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await session.post( LWA_TOKEN_URI, headers=LWA_HEADERS, diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index e611960b9d9..1ab12041e32 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -132,7 +132,7 @@ async def async_send_changereport_message( session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with async_timeout.timeout(DEFAULT_TIMEOUT): + async with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post( config.endpoint, headers=headers, @@ -263,7 +263,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with async_timeout.timeout(DEFAULT_TIMEOUT): + async with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post( config.endpoint, headers=headers, diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 6a5449e3d51..03fc1f26011 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -192,7 +192,7 @@ async def _configure_almond_for_ha( # Store token in Almond try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): await api.async_create_device( { "kind": "io.home-assistant", diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index b7b56f93864..32d90f7b03b 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -24,7 +24,7 @@ async def async_verify_local_connection(hass: core.HomeAssistant, host: str): api = WebAlmondAPI(AlmondLocalAuth(host, websession)) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await api.async_list_apps() return True diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d7fa781945d..d1b8879bf7c 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -255,7 +255,7 @@ class Analytics: ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): response = await self.session.post(self.endpoint, json=payload) if response.status == 200: LOGGER.info( diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 229311ff6d9..d2201ccace0 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -131,7 +131,7 @@ class APIEventStream(HomeAssistantView): while True: try: - with async_timeout.timeout(STREAM_PING_INTERVAL): + async with async_timeout.timeout(STREAM_PING_INTERVAL): payload = await to_write.get() if payload is stop_obj: diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index d28de3b92aa..8784e0f13ae 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -85,7 +85,7 @@ async def _run_client(hass, client, interval): while True: try: - with async_timeout.timeout(interval): + async with async_timeout.timeout(interval): await client.start() _LOGGER.debug("Client connected %s", client.host) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 69880da5a39..920b910269d 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_data(): """Update data via library.""" - with async_timeout.timeout(20): + async with async_timeout.timeout(20): try: await atag.update() except AtagException as err: diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 39853dab9de..d78931e5d2f 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -58,7 +58,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> Any | None: """Update data via Awair client library.""" - with timeout(API_TIMEOUT): + async with timeout(API_TIMEOUT): try: LOGGER.debug("Fetching users and devices") user = await self._awair.user() diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 823593ecacb..a2eceff6870 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -280,7 +280,7 @@ async def get_device(hass, host, port, username, password): ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): await device.vapix.initialize() return device diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 6c90a511a05..c91a2dedca3 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -358,7 +358,7 @@ class BluesoundPlayer(MediaPlayerEntity): try: websession = async_get_clientsession(self._hass) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await websession.get(url) if response.status == HTTPStatus.OK: @@ -400,7 +400,7 @@ class BluesoundPlayer(MediaPlayerEntity): try: - with async_timeout.timeout(125): + async with async_timeout.timeout(125): response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE} ) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 63c585f8c2f..3686e2bd3c9 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -88,7 +88,7 @@ class BrData: resp = None try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): resp = await websession.get(url) result[STATUS_CODE] = resp.status diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index fd0c96c6fbe..937e2582fbb 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -135,7 +135,7 @@ async def async_citybikes_request(hass, uri, schema): try: session = async_get_clientsession(hass) - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) json_response = await req.json() diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a6c30a5a79b..0d1bdf66c12 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -313,7 +313,7 @@ class AlexaConfig(alexa_config.AbstractConfig): ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) return True diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 5bfebec40a3..64d3943dda7 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -204,7 +204,7 @@ class CloudLogoutView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.logout() return self.json_message("ok") @@ -230,7 +230,7 @@ class CloudRegisterView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.auth.async_register(data["email"], data["password"]) return self.json_message("ok") @@ -249,7 +249,7 @@ class CloudResendConfirmView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.auth.async_resend_email_confirm(data["email"]) return self.json_message("ok") @@ -268,7 +268,7 @@ class CloudForgotPasswordView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.auth.async_forgot_password(data["email"]) return self.json_message("ok") @@ -314,7 +314,7 @@ async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] try: - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): data = await cloud_api.async_subscription_info(cloud) except aiohttp.ClientError: connection.send_error( @@ -353,7 +353,7 @@ async def websocket_update_prefs(hass, connection, msg): if changes.get(PREF_ALEXA_REPORT_STATE): alexa_config = await cloud.client.get_alexa_config() try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await alexa_config.async_get_access_token() except asyncio.TimeoutError: connection.send_error( @@ -574,7 +574,7 @@ async def alexa_sync(hass, connection, msg): cloud = hass.data[DOMAIN] alexa_config = await cloud.client.get_alexa_config() - with async_timeout.timeout(10): + async with async_timeout.timeout(10): try: success = await alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: @@ -597,7 +597,7 @@ async def thingtalk_convert(hass, connection, msg): """Convert a query.""" cloud = hass.data[DOMAIN] - with async_timeout.timeout(10): + async with async_timeout.timeout(10): try: connection.send_result( msg["id"], await thingtalk.async_convert(cloud, msg["query"]) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index b0ab5c2aba7..4d8118483b6 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -113,7 +113,7 @@ async def async_setup(hass, hass_config): try: session = aiohttp_client.async_get_clientsession(hass) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await session.get(url) except (asyncio.TimeoutError, aiohttp.ClientError) as err: diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index fc038adc568..080b31036d9 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -104,7 +104,7 @@ class ComedHourlyPricingSensor(SensorEntity): else: url_string += "?type=currenthouraverage" - with async_timeout.timeout(60): + async with async_timeout.timeout(60): response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' text = await response.text() diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index d130e131c8b..5deceb5cddc 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -66,7 +66,7 @@ async def get_coordinator( return hass.data[DOMAIN] async def async_get_cases(): - with async_timeout.timeout(10): + async with async_timeout.timeout(10): return { case.country: case for case in await coronavirus.get_cases( diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 185537cc7d0..507ec2f5d79 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -65,7 +65,7 @@ async def daikin_api_setup(hass, host, key, uuid, password): session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with timeout(TIMEOUT): + async with timeout(TIMEOUT): device = await Appliance.factory( host, session, key=key, uuid=uuid, password=password ) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 52e4fa255aa..89b27b68c81 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -70,7 +70,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password = None try: - with timeout(TIMEOUT): + async with timeout(TIMEOUT): device = await Appliance.factory( host, self.hass.helpers.aiohttp_client.async_get_clientsession(), diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 3a6c5aecfb5..42541123b4f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -85,7 +85,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session = aiohttp_client.async_get_clientsession(self.hass) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): self.bridges = await deconz_discovery(session) except (asyncio.TimeoutError, ResponseError): @@ -141,7 +141,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): api_key = await deconz_session.get_api_key() except (ResponseError, RequestError, asyncio.TimeoutError): @@ -159,7 +159,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session = aiohttp_client.async_get_clientsession(self.hass) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): self.bridge_id = await deconz_get_bridge_id( session, **self.deconz_config ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index ddb0d47190c..fd18be76011 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -276,7 +276,7 @@ async def get_gateway( connection_status=async_connection_status_callback, ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await deconz.refresh_state() return deconz diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 16606156314..8331570fd2f 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -125,7 +125,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(_TIMEOUT): + async with async_timeout.timeout(_TIMEOUT): response = await websession.get(self._url) self._last_image = await response.read() diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 3b59fffe553..07111282c3d 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -319,7 +319,7 @@ async def async_wait_for_elk_to_sync(elk, timeout, conf_host): elk.add_handler("login", login_status) elk.add_handler("sync_complete", sync_complete) try: - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): await event.wait() except asyncio.TimeoutError: _LOGGER.error( diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index e27916ec6c1..205fa016130 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -56,7 +56,7 @@ class FAADataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): try: - with timeout(10): + async with timeout(10): await self.data.update() except ClientConnectionError as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index c76b44396f5..7f21397d5a7 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -45,7 +45,7 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: - with async_timeout.timeout(60): + async with async_timeout.timeout(60): token = await auth.async_get_access_token() except asyncio.TimeoutError as err: raise CannotConnect() from err diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 938507e4b0c..7ca3b99f928 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -67,7 +67,7 @@ class FlickPricingSensor(SensorEntity): if self._price and self._price.end_at >= utcnow(): return # Power price data is still valid - with async_timeout.timeout(60): + async with async_timeout.timeout(60): self._price = await self._api.getPricing() self._attributes[ATTR_START_AT] = self._price.start_at diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index de5c078f714..ee89937599a 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -41,7 +41,7 @@ class FlockNotificationService(BaseNotificationService): _LOGGER.debug("Attempting to call Flock at %s", self._url) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await self._session.post(self._url, json=payload) result = await response.json() diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 7aa34c8780e..754e6cb8818 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -72,7 +72,7 @@ async def _update_freedns(hass, session, url, auth_token): params[auth_token] = "" try: - with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(TIMEOUT): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index be228e2f3a0..2077dec741f 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -39,7 +39,7 @@ async def get_coordinator( return hass.data[DOMAIN] async def async_get_garages(): - with async_timeout.timeout(10): + async with async_timeout.timeout(10): return { garage.garage_name: garage for garage in await garages_amsterdam.get_garages( diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index c3227254075..8457f62fd3f 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -87,7 +87,7 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: - with timeout(API_TIMEOUT): + async with timeout(API_TIMEOUT): return cast(Dict[str, Any], await self.gios.async_update()) except ( ApiError, diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index ff3f33408a5..0fa5052e129 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -37,7 +37,7 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession = async_get_clientsession(self.hass) - with timeout(API_TIMEOUT): + async with timeout(API_TIMEOUT): gios = Gios(user_input[CONF_STATION_ID], websession) await gios.async_update() diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index f402e2a7a06..3d65f4eb297 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -281,7 +281,7 @@ class GoogleCloudTTSProvider(Provider): ) # pylint: enable=no-member - with async_timeout.timeout(10, loop=self.hass.loop): + async with async_timeout.timeout(10): response = await self.hass.async_add_executor_job( self._client.synthesize_speech, synthesis_input, voice, audio_config ) diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index ae6cb5c70d5..59386eb378a 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -65,7 +65,7 @@ async def _update_google_domains(hass, session, domain, user, password, timeout) params = {"hostname": domain} try: - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 19ab2128d62..e669cf7b031 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -280,7 +280,7 @@ class HueBridge: async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): """Create a bridge object and verify authentication.""" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): # Create username if we don't have one if not bridge.username: device_name = unicode_slug.slugify( diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 7149b4d9442..409f88cbe04 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -84,7 +84,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Find / discover bridges try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): bridges = await discover_nupnp( websession=aiohttp_client.async_get_clientsession(self.hass) ) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 13a3a70ae53..12fbf77aa8b 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: - with async_timeout.timeout(4): + async with async_timeout.timeout(4): return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 957565c54e9..a99d92c0393 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -61,7 +61,7 @@ class SensorManager: async def async_update_data(self): """Update sensor data.""" try: - with async_timeout.timeout(4): + async with async_timeout.timeout(4): return await self.bridge.async_request_call( self.bridge.api.sensors.update ) diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index de0e76fc3aa..3d7336fea5b 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -42,7 +42,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= config_port = config[CONF_PORT] config_name = config[CONF_NAME] try: - with async_timeout.timeout(PLATFORM_TIMEOUT): + async with async_timeout.timeout(PLATFORM_TIMEOUT): api = await real_time_api(config_host, config_port) except (IamMeterError, asyncio.TimeoutError) as err: _LOGGER.error("Device is not ready") @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_update_data(): try: - with async_timeout.timeout(PLATFORM_TIMEOUT): + async with async_timeout.timeout(PLATFORM_TIMEOUT): return await api.get_data() except (IamMeterError, asyncio.TimeoutError) as err: raise UpdateFailed from err diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index c3d6b2198ce..89a1348e90e 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -134,7 +134,7 @@ class ImapSensor(SensorEntity): idle = await self._connection.idle_start() await self._connection.wait_server_push() self._connection.idle_done() - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await idle else: self.async_write_ha_state() diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 32a5967f8b4..f831bac2eef 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -142,7 +142,7 @@ async def async_get_api(hass): async def async_get_location(hass, api, latitude, longitude): """Retrieve pyipma location, location name to be used as the entity name.""" - with async_timeout.timeout(30): + async with async_timeout.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( @@ -172,7 +172,7 @@ class IPMAWeather(WeatherEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Condition and Forecast.""" - with async_timeout.timeout(10): + async with async_timeout.timeout(10): new_observation = await self._location.observation(self._api) new_forecast = await self._location.forecast(self._api) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 34c7a40cfc0..413d0689b6e 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -89,7 +89,7 @@ async def validate_input(hass: core.HomeAssistant, data): ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): isy_conf_xml = await isy_conn.test_connection() except ISYInvalidAuthError as error: raise InvalidAuth from error diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index b426a298ddb..f34ae161c6d 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -53,7 +53,7 @@ class KaiterraApiData: """Get the data from Kaiterra API.""" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) except (ClientResponseError, asyncio.TimeoutError): _LOGGER.debug("Couldn't fetch data from Kaiterra API") diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index ec2aca00aa9..c91feeaef82 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: httpsession = async_get_clientsession(hass) - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): scenes_resp = await httpsession.get(url, headers=headers) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -81,7 +81,7 @@ class LifxCloudScene(Scene): try: httpsession = async_get_clientsession(self.hass) - with async_timeout.timeout(self._timeout): + async with async_timeout.timeout(self._timeout): await httpsession.put(url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index d9060b10080..45b34928a30 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -147,7 +147,7 @@ async def async_setup_entry(hass, entry): return False try: - with async_timeout.timeout(_TIMEOUT): + async with async_timeout.timeout(_TIMEOUT): # Ensure the cameras property returns the same Camera objects for # all devices. Performs implicit login and session validation. await logi_circle.synchronize_cameras() diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 9054b476332..7453fe27e18 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -158,7 +158,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: - with async_timeout.timeout(_TIMEOUT): + async with async_timeout.timeout(_TIMEOUT): await logi_session.authorize(code) except AuthorizationFailed: (self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS]) = "invalid_auth" diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 0c473367fe9..98dd0f45d2b 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -250,7 +250,7 @@ class MailboxMediaView(MailboxView): mailbox = self.get_mailbox(platform) with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(10): + async with async_timeout.timeout(10): try: stream = await mailbox.async_get_media(msgid) except StreamError as err: diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 518d902a7e1..af34498aba2 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -145,7 +145,7 @@ async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: """Query connected devices from MELCloud.""" session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with timeout(10): + async with timeout(10): all_devices = await get_devices( token, session, diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 9c15f5ec242..139a4e8e44d 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -42,7 +42,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: - with timeout(10): + async with timeout(10): if (acquired_token := token) is None: acquired_token = await pymelcloud.login( username, diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 9f7131d1935..d0f08427b51 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -299,7 +299,7 @@ class MicrosoftFace: payload = None try: - with async_timeout.timeout(self.timeout): + async with async_timeout.timeout(self.timeout): response = await getattr(self.websession, method)( url, data=payload, headers=headers, params=params ) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index d486f78d334..85f0c21f90c 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -122,7 +122,7 @@ class MjpegCamera(Camera): websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await websession.get(self._still_image_url, auth=self._auth) image = await response.read() diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index bd5f1354ad3..e1dada100f9 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -149,7 +149,7 @@ class MobileAppNotificationService(BaseNotificationService): target_data["registration_info"] = reg_info try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await async_get_clientsession(self._hass).post( push_url, json=target_data ) diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index eeab5abed2f..fe02983633b 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -18,7 +18,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict) -> bool: """Set up Mullvad VPN integration.""" async def async_get_mullvad_api_data(): - with async_timeout.timeout(10): + async with async_timeout.timeout(10): api = await hass.async_add_executor_job(MullvadAPI) return api.data diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 41311f45b03..fa7bf1ca88d 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -107,7 +107,7 @@ async def try_connect( connect_task = None try: connect_task = asyncio.create_task(gateway.start()) - with async_timeout.timeout(GATEWAY_READY_TIMEOUT): + async with async_timeout.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() return True except asyncio.TimeoutError: @@ -319,7 +319,7 @@ async def _gw_start( # Gatways connected via mqtt doesn't send gateway ready message. return try: - with async_timeout.timeout(GATEWAY_READY_TIMEOUT): + async with async_timeout.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() except asyncio.TimeoutError: _LOGGER.warning( diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 4843e96b5a8..5052ffbaf1f 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -100,7 +100,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): # Device firmware uses synchronous code and doesn't respond to http queries # when reading data from sensors. The nettigo-air-quality library tries to # get the data 4 times, so we use a longer than usual timeout here. - with async_timeout.timeout(30): + async with async_timeout.timeout(30): data = await self.nam.async_update() except (ApiError, ClientConnectorError, InvalidSensorData) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 458895e69c5..0fe2c8f9c65 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -120,5 +120,5 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Device firmware uses synchronous code and doesn't respond to http queries # when reading data from sensors. The nettigo-air-monitor library tries to get # the data 4 times, so we use a longer than usual timeout here. - with async_timeout.timeout(30): + async with async_timeout.timeout(30): return await nam.async_get_mac_address() diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 1ec3e421a0d..189a8189e8a 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -211,7 +211,7 @@ class NestFlowHandler( if user_input is not None: try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): tokens = await flow["convert_code"](user_input["code"]) return self._entry_from_tokens( f"Nest (via {flow['name']})", flow, tokens @@ -228,7 +228,7 @@ class NestFlowHandler( _LOGGER.exception("Unexpected error resolving code") try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): url = await flow["gen_authorize_url"](self.flow_id) except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 97015eab38a..1c4bcb40819 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -96,7 +96,7 @@ async def _update_no_ip( } try: - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): resp = await session.get(url, params=params, headers=headers) body = await resp.text() diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index dedf242e0c7..c4734d4c168 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -113,7 +113,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): body = {"image_bytes": str(b64encode(image), "utf-8")} try: - with async_timeout.timeout(self.timeout): + async with async_timeout.timeout(self.timeout): request = await websession.post( OPENALPR_API_URL, params=params, data=body ) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 5c2633a7a33..f4814e64d9a 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -73,7 +73,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): data = {} - with async_timeout.timeout(20): + async with async_timeout.timeout(20): try: weather_response = await self._get_owm_weather() data = self._convert_weather_response(weather_response) diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index fbcbcd02a2b..8520e53654f 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -93,7 +93,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "follow_link" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): url = await self._get_authorization_url() except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index fb7927de970..7ca7751b6f0 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -90,7 +90,7 @@ class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" data = {} - with async_timeout.timeout(10): + async with async_timeout.timeout(10): try: data = await self.poolsense.get_poolsense_data() except (PoolSenseError) as error: diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 91dd8eca5ca..837bad930f4 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -56,7 +56,7 @@ class ProwlNotificationService(BaseNotificationService): session = async_get_clientsession(self._hass) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await session.post(url, data=payload) result = await response.text() diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 8f4d1d04dcf..b5ce846d52f 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -72,7 +72,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): data = dict(await request.post()) except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error: _LOGGER.error("Could not get information from POST <%s>", error) diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index 91447392ea8..52f40e81d40 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -50,7 +50,7 @@ async def async_get_type(hass, cloud_id, install_code, host): ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): meters = await hub.get_device_list() except aioeagle.BadAuth as err: raise InvalidAuth from err diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 83bd5ae27ae..3448b79979c 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -210,7 +210,7 @@ class RestSwitch(SwitchEntity): rendered_headers = render_templates(self._headers) rendered_params = render_templates(self._params) - with async_timeout.timeout(self._timeout): + async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( self._resource, auth=self._auth, @@ -236,7 +236,7 @@ class RestSwitch(SwitchEntity): rendered_headers = render_templates(self._headers) rendered_params = render_templates(self._params) - with async_timeout.timeout(self._timeout): + async with async_timeout.timeout(self._timeout): req = await websession.get( self._state_resource, auth=self._auth, diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index e0ce94cb723..6a59212d6c1 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -270,7 +270,7 @@ async def async_setup(hass, config): ) try: - with async_timeout.timeout(CONNECTION_TIMEOUT): + async with async_timeout.timeout(CONNECTION_TIMEOUT): transport, protocol = await connection except ( diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 3936d3f6d1d..2911f46d55d 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -80,7 +80,7 @@ async def async_connect_or_timeout(hass, roomba): """Connect to vacuum.""" try: name = None - with async_timeout.timeout(10): + async with async_timeout.timeout(10): _LOGGER.debug("Initialize connection to vacuum") await hass.async_add_executor_job(roomba.connect) while not roomba.roomba_connected or name is None: @@ -104,7 +104,7 @@ async def async_connect_or_timeout(hass, roomba): async def async_disconnect_or_timeout(hass, roomba): """Disconnect to vacuum.""" _LOGGER.debug("Disconnect vacuum") - with async_timeout.timeout(3): + async with async_timeout.timeout(3): await hass.async_add_executor_job(roomba.disconnect) return True diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index b0b211e4a7d..7104d9ebff7 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) devices = [] try: - with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(TIMEOUT): for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: devices.append( @@ -363,7 +363,7 @@ class SensiboClimate(ClimateEntity): async def async_update(self): """Retrieve latest state.""" try: - with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(TIMEOUT): data = await self._client.async_get_device(self._id, _FETCH_FIELDS) except ( aiohttp.client_exceptions.ClientError, @@ -389,7 +389,7 @@ class SensiboClimate(ClimateEntity): async def _async_set_ac_state_property(self, name, value, assumed_state=False): """Set AC state.""" try: - with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(TIMEOUT): await self._client.async_set_ac_state_property( self._id, name, value, self._ac_states, assumed_state ) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index ed5c7ae1b54..d3246b5ac9e 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -26,7 +26,7 @@ class CannotConnect(exceptions.HomeAssistantError): async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: """Connect to vacuum.""" try: - with async_timeout.timeout(API_TIMEOUT): + async with async_timeout.timeout(API_TIMEOUT): _LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() except SharkIqAuthError: @@ -71,10 +71,11 @@ async def async_setup_entry(hass, config_entry): async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): """Disconnect to vacuum.""" _LOGGER.debug("Disconnecting from Ayla Api") - with async_timeout.timeout(5), suppress( - SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError - ): - await coordinator.ayla_api.async_sign_out() + async with async_timeout.timeout(5): + with suppress( + SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError + ): + await coordinator.ayla_api.async_sign_out() async def async_update_options(hass, config_entry): diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 8fef217a609..53306e15d12 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -27,7 +27,7 @@ async def validate_input(hass: core.HomeAssistant, data): ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): _LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() except (asyncio.TimeoutError, aiohttp.ClientError) as errors: diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 16ed0e14d9a..f8559bcc31f 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -54,7 +54,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): """Asynchronously update the data for a single vacuum.""" dsn = sharkiq.serial_number _LOGGER.debug("Updating sharkiq data for device DSN %s", dsn) - with timeout(API_TIMEOUT): + async with timeout(API_TIMEOUT): await sharkiq.async_update() async def _async_update_data(self) -> bool: diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index ec99f2a12ae..ac1ec53f67c 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -125,7 +125,7 @@ class SmhiWeather(WeatherEntity): async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): self._forecasts = await self.get_weather_forecast() self._fail_count = 0 diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 66a2b46eb12..593bd8f034a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -972,7 +972,7 @@ class SonosSpeaker: return True try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): while not _test_groups(groups): await hass.data[DATA_SONOS].topology_condition.wait() except asyncio.TimeoutError: diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 4a5e3c33748..747b25faa50 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass, entry, async_add_entities): # Fetch srp_energy data start_date = datetime.now() + timedelta(days=-1) end_date = datetime.now() - with async_timeout.timeout(10): + async with async_timeout.timeout(10): hourly_usage = await hass.async_add_executor_job( api.usage, start_date, diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 931e9eabfc0..135c3eeebda 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -193,7 +193,7 @@ class StartcaData: """Get the Start.ca bandwidth data from the web service.""" _LOGGER.debug("Updating Start.ca usage data") url = f"https://www.start.ca/support/usage/api?key={self.api_key}" - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) if req.status != HTTPStatus.OK: _LOGGER.error("Request failed with status: %u", req.status) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 2683f6a2f3a..1d4adbfca64 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -64,7 +64,7 @@ async def get_integration_info( ): """Get integration system health.""" try: - with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): + async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) except asyncio.TimeoutError: data = {"error": {"type": "failed", "error": "timeout"}} diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index e49a5be5a71..977be7d226a 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -106,7 +106,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 712f25560cd..18ae324a572 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -92,7 +92,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): auth_url = await self.hass.async_add_executor_job(self._get_auth_url) if not auth_url: return self.async_abort(reason="unknown_authorize_url_generation") diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 792eaa0170c..2431d1e2022 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -124,7 +124,7 @@ class TtnDataStorage: """Get the current state from The Things Network Data Storage.""" try: session = async_get_clientsession(self._hass) - with async_timeout.timeout(DEFAULT_TIMEOUT): + async with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.get(self._url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 11a56200eda..4de2aa302f0 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -174,7 +174,7 @@ async def authenticate( api_factory = await APIFactory.init(host, psk_id=identity) try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): key = await api_factory.generate_psk(security_code) except RequestError as err: raise AuthError("invalid_security_code") from err diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index b1ebbbe3475..14df45e3aeb 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -419,7 +419,7 @@ class UniFiController: async def async_reconnect(self) -> None: """Try to reconnect UniFi session.""" try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): await self.api.login() self.api.start_websocket() @@ -488,7 +488,7 @@ async def get_controller( ) try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await controller.check_unifi_os() await controller.login() return controller diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index f624cf87eda..1d86dbaea8e 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -125,7 +125,7 @@ async def get_newest_version(hass): """Get the newest Home Assistant version.""" session = async_get_clientsession(hass) - with async_timeout.timeout(30): + async with async_timeout.timeout(30): req = await session.get(UPDATER_URL) try: diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 0457572e066..c80f4ef42c3 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -70,7 +70,7 @@ async def async_http_request(hass, uri): """Perform actual request.""" try: session = hass.helpers.aiohttp_client.async_get_clientsession(hass) - with async_timeout.timeout(REQUEST_TIMEOUT): + async with async_timeout.timeout(REQUEST_TIMEOUT): req = await session.get(uri) if req.status != HTTPStatus.OK: return {"error": req.status} diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 3558179c4d1..2525393739b 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -196,7 +196,7 @@ class VoiceRSSProvider(Provider): form_data["hl"] = language try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): request = await websession.post(VOICERSS_API_URL, data=form_data) if request.status != HTTPStatus.OK: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 22fa9816f4e..b657b5f5d94 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -176,7 +176,7 @@ class WebSocketHandler: # Auth Phase try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): msg = await wsock.receive() except asyncio.TimeoutError as err: disconnect_warn = "Did not receive auth message within 10 seconds" diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 464892e9126..98b7470b7a8 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -85,7 +85,7 @@ class WorxLandroidSensor(SensorEntity): try: session = async_get_clientsession(self.hass) - with async_timeout.timeout(self.timeout): + async with async_timeout.timeout(self.timeout): auth = aiohttp.helpers.BasicAuth("admin", self.pin) mower_response = await session.get(self.url, auth=auth) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index ec0868b2443..4cbbc679e42 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -121,7 +121,7 @@ class YandexSpeechKitProvider(Provider): options = options or {} try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): url_param = { "text": message, "lang": actual_language, diff --git a/homeassistant/core.py b/homeassistant/core.py index 34b48e66953..891d718a81f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -25,7 +25,7 @@ import attr import voluptuous as vol import yarl -from homeassistant import block_async_io, loader, util +from homeassistant import async_timeout_backcompat, block_async_io, loader, util from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -82,7 +82,7 @@ STAGE_1_SHUTDOWN_TIMEOUT = 100 STAGE_2_SHUTDOWN_TIMEOUT = 60 STAGE_3_SHUTDOWN_TIMEOUT = 30 - +async_timeout_backcompat.enable() block_async_io.enable() T = TypeVar("T") diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 3e5496b4179..908a9d68ddf 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -123,7 +123,7 @@ async def async_aiohttp_proxy_web( ) -> web.StreamResponse | None: """Stream websession request to aiohttp web response.""" try: - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): req = await web_coro except asyncio.CancelledError: @@ -164,7 +164,7 @@ async def async_aiohttp_proxy_stream( # Suppressing something went wrong fetching data, closed connection with suppress(asyncio.TimeoutError, aiohttp.ClientError): while hass.is_running: - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): data = await stream.read(buffer_size) if not data: diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 42478e67bb9..3c987b1ea9e 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -270,7 +270,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step_done(next_step_id="creation") try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): url = await self.flow_impl.async_generate_authorize_url(self.flow_id) except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a79caad74b0..6a727aefb4d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -476,7 +476,10 @@ class _ScriptRun: def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" wait_var = self._variables["wait"] - wait_var["remaining"] = to_context.remaining if to_context else timeout + if to_context and to_context.deadline: + wait_var["remaining"] = to_context.deadline - self._hass.loop.time() + else: + wait_var["remaining"] = timeout wait_var["completed"] = True done.set() @@ -777,7 +780,10 @@ class _ScriptRun: async def async_done(variables, context=None): wait_var = self._variables["wait"] - wait_var["remaining"] = to_context.remaining if to_context else timeout + if to_context and to_context.deadline: + wait_var["remaining"] = to_context.deadline - self._hass.loop.time() + else: + wait_var["remaining"] = timeout wait_var["trigger"] = variables["trigger"] done.set() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 20b6a257213..32a75ef29d0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,11 +1,11 @@ PyJWT==2.1.0 PyNaCl==1.4.0 aiodiscover==1.4.5 -aiohttp==3.7.4.post0 +aiohttp==3.8.0 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.22.11 -async_timeout==3.0.1 +async_timeout==4.0.0 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/requirements.txt b/requirements.txt index 84f3e342435..d89db253e06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.7.4.post0 +aiohttp==3.8.0 astral==2.2 -async_timeout==3.0.1 +async_timeout==4.0.0 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/setup.py b/setup.py index 2834a0b973d..647d4c50190 100755 --- a/setup.py +++ b/setup.py @@ -32,9 +32,9 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.7.4.post0", + "aiohttp==3.8.0", "astral==2.2", - "async_timeout==3.0.1", + "async_timeout==4.0.0", "attrs==21.2.0", "awesomeversion==21.10.1", 'backports.zoneinfo;python_version<"3.9"', diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 566f2041fdd..947a8e0125c 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -119,7 +119,7 @@ async def test_login_view(hass, cloud_client): async def test_login_view_random_exception(cloud_client): """Try logging in with invalid JSON.""" - with patch("async_timeout.timeout", side_effect=ValueError("Boom")): + with patch("hass_nabucasa.Cloud.login", side_effect=ValueError("Boom")): req = await cloud_client.post( "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 9f72520105e..c52b4bf6e8f 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -3,7 +3,9 @@ import asyncio from http import HTTPStatus from unittest.mock import patch +import aiohttp import httpx +import pytest import respx from homeassistant import config as hass_config @@ -132,10 +134,13 @@ async def test_limit_refetch(hass, hass_client): hass.states.async_set("sensor.temp", "5") - with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError()): + with pytest.raises(aiohttp.ServerTimeoutError), patch( + "async_timeout.timeout", side_effect=asyncio.TimeoutError() + ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 0 - assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + + assert respx.calls.call_count == 0 + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR hass.states.async_set("sensor.temp", "10") diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index a32663d7725..2ab16fb1301 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -121,7 +121,7 @@ async def test_camera_image_failed_start_stream_call(hass: HomeAssistant) -> Non await setup_test_config_entry(hass, hyperion_client=client) with pytest.raises(HomeAssistantError): - await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0) + await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0.01) assert client.async_send_image_stream_start.called assert not client.async_send_image_stream_stop.called diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index f5f66258f70..58c69218f14 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -95,13 +95,13 @@ class SaveRecordWorkerSync: async def get_segments(self): """Return the recorded video segments.""" - with async_timeout.timeout(TEST_TIMEOUT): + async with async_timeout.timeout(TEST_TIMEOUT): await self._save_event.wait() return self._segments async def join(self): """Verify save worker was invoked and block on shutdown.""" - with async_timeout.timeout(TEST_TIMEOUT): + async with async_timeout.timeout(TEST_TIMEOUT): await self._save_event.wait() self._save_thread.join(timeout=TEST_TIMEOUT) assert not self._save_thread.is_alive() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 447f38f9a9c..1099519a2a0 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -369,7 +369,7 @@ async def test_subscribe_unsubscribe_events(hass, websocket_client): hass.bus.async_fire("test_event", {"hello": "world"}) hass.bus.async_fire("ignore_event") - with timeout(3): + async with timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 5 @@ -566,7 +566,7 @@ async def test_subscribe_unsubscribe_events_whitelist( hass.bus.async_fire("themes_updated") - with timeout(3): + async with timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 6 @@ -1051,7 +1051,7 @@ async def test_subscribe_trigger(hass, websocket_client): hass.bus.async_fire("test_event", {"hello": "world"}, context=context) hass.bus.async_fire("ignore_event") - with timeout(3): + async with timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 5 diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fd98145cab2..a3bf128d3c4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -858,7 +858,7 @@ async def test_wait_basic_times_out(hass, action_type): assert script_obj.last_action == wait_alias hass.states.async_set("switch.test", "not_on") - with timeout(0.1): + async with timeout(0.1): await hass.async_block_till_done() except asyncio.TimeoutError: timed_out = True @@ -1238,7 +1238,7 @@ async def test_wait_template_with_utcnow_no_match(hass): ): async_fire_time_changed(hass, second_non_matching_time) - with timeout(0.1): + async with timeout(0.1): await hass.async_block_till_done() except asyncio.TimeoutError: timed_out = True From 620db191b1e765ff983f60065b79ac53e441abdb Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 4 Nov 2021 16:17:20 +0100 Subject: [PATCH 0236/1452] Use entity category and state class in devolo Home Network (#59071) * Use entity category and state class * Add tests --- .../components/devolo_home_network/sensor.py | 10 +++++++++- tests/components/devolo_home_network/test_sensor.py | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 3b0175d8c31..66f686a603e 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -7,8 +7,13 @@ from typing import Any from devolo_plc_api.device import Device -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -39,6 +44,7 @@ class DevoloSensorEntityDescription( SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription( key=CONNECTED_PLC_DEVICES, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lan", name="Connected PLC devices", @@ -51,10 +57,12 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { entity_registry_enabled_default=True, icon="mdi:wifi", name="Connected Wifi clients", + state_class=STATE_CLASS_MEASUREMENT, value_func=lambda data: len(data["connected_stations"]), ), NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( key=NEIGHBORING_WIFI_NETWORKS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:wifi-marker", name="Neighboring Wifi networks", diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 100db9005aa..6ce4c36cdb5 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -8,9 +8,10 @@ from homeassistant.components.devolo_home_network.const import ( LONG_UPDATE_INTERVAL, SHORT_UPDATE_INTERVAL, ) -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.components.sensor import DOMAIN, STATE_CLASS_MEASUREMENT +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.util import dt from . import configure_integration @@ -46,6 +47,7 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT # Emulate device failure with patch( @@ -86,6 +88,9 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): assert state is not None assert state.state == "1" + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + # Emulate device failure with patch( "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", @@ -125,6 +130,9 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): assert state is not None assert state.state == "1" + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + # Emulate device failure with patch( "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", From be4e9f91b62b4f64e207b389796cd864428bb26b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 16:34:35 +0100 Subject: [PATCH 0237/1452] Change minimum supported SQLite version to 3.31.0 (#59073) --- homeassistant/components/recorder/util.py | 4 ++-- tests/components/recorder/test_util.py | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b4bbce27130..6c53347a536 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -49,7 +49,7 @@ MIN_VERSION_MARIA_DB_ROWNUM = AwesomeVersion("10.2.0", AwesomeVersionStrategy.SI MIN_VERSION_MYSQL = AwesomeVersion("8.0.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_MYSQL_ROWNUM = AwesomeVersion("5.8.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_PGSQL = AwesomeVersion("12.0", AwesomeVersionStrategy.SIMPLEVER) -MIN_VERSION_SQLITE = AwesomeVersion("3.32.1", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_SQLITE = AwesomeVersion("3.31.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_SQLITE_ROWNUM = AwesomeVersion("3.25.0", AwesomeVersionStrategy.SIMPLEVER) # This is the maximum time after the recorder ends the session @@ -295,7 +295,7 @@ def _warn_unsupported_dialect(dialect): "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please migrate your database to a supported software before then", dialect, - "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.32.1", + "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index da88d17f02a..940925c48ca 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -364,20 +364,16 @@ def test_supported_pgsql(caplog, pgsql_version): "sqlite_version,message", [ ( - "3.32.0", - "Version 3.32.0 of SQLite is not supported; minimum supported version is 3.32.1.", - ), - ( - "3.31.0", - "Version 3.31.0 of SQLite is not supported; minimum supported version is 3.32.1.", + "3.30.0", + "Version 3.30.0 of SQLite is not supported; minimum supported version is 3.31.0.", ), ( "2.0.0", - "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.32.1.", + "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.31.0.", ), ( "dogs", - "Version dogs of SQLite is not supported; minimum supported version is 3.32.1.", + "Version dogs of SQLite is not supported; minimum supported version is 3.31.0.", ), ], ) @@ -410,7 +406,7 @@ def test_warn_outdated_sqlite(caplog, sqlite_version, message): @pytest.mark.parametrize( "sqlite_version", [ - ("3.32.1"), + ("3.31.0"), ("3.33.0"), ], ) From 4c5aca93df3827d14893335e73ab5226944f3df0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 16:46:45 +0100 Subject: [PATCH 0238/1452] Add recorder status WS API (#58989) * Add recorder status WS API * Rename recorder/status to recorder/info * Silence pylint * Improve tests * Address review comments * Tweak * Try to fix tests * Try to debug flaky tests * Try to fix tests * Revert changes to async_migration_in_progress * Try to fix tests * Remove debug prints * Apply suggestions from code review --- homeassistant/components/recorder/__init__.py | 15 ++- homeassistant/components/recorder/const.py | 2 + .../components/recorder/websocket_api.py | 35 ++++- tests/components/recorder/common.py | 13 ++ tests/components/recorder/test_migrate.py | 13 +- .../components/recorder/test_websocket_api.py | 125 +++++++++++++++++- 6 files changed, 185 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 21acc183e50..103435fa519 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -52,7 +52,13 @@ from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util from . import history, migration, purge, statistics, websocket_api -from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX +from .const import ( + CONF_DB_INTEGRITY_CHECK, + DATA_INSTANCE, + DOMAIN, + MAX_QUEUE_BACKLOG, + SQLITE_URL_PREFIX, +) from .models import ( Base, Events, @@ -83,8 +89,6 @@ ATTR_KEEP_DAYS = "keep_days" ATTR_REPACK = "repack" ATTR_APPLY_FILTER = "apply_filter" -MAX_QUEUE_BACKLOG = 30000 - SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, @@ -1089,3 +1093,8 @@ class Recorder(threading.Thread): self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) self._end_session() self._close_connection() + + @property + def recording(self): + """Return if the recorder is recording.""" + return self._event_listener is not None diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index eab3c30e99e..a04218264ee 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -6,6 +6,8 @@ DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" +MAX_QUEUE_BACKLOG = 30000 + # The maximum number of rows (events) we purge in one delete statement # sqlite3 has a limit of 999 until version 3.32.0 diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index ba77692fe8e..b06d2d07f1e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,14 +1,19 @@ """The Energy websocket API.""" from __future__ import annotations +from typing import TYPE_CHECKING + import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback -from .const import DATA_INSTANCE +from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG from .statistics import validate_statistics +if TYPE_CHECKING: + from . import Recorder + @callback def async_setup(hass: HomeAssistant) -> None: @@ -16,6 +21,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_validate_statistics) websocket_api.async_register_command(hass, ws_clear_statistics) websocket_api.async_register_command(hass, ws_update_statistics_metadata) + websocket_api.async_register_command(hass, ws_info) @websocket_api.websocket_command( @@ -72,3 +78,30 @@ def ws_update_statistics_metadata( msg["statistic_id"], msg["unit_of_measurement"] ) connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/info", + } +) +@callback +def ws_info( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return status of the recorder.""" + instance: Recorder = hass.data[DATA_INSTANCE] + + backlog = instance.queue.qsize() if instance and instance.queue else None + migration_in_progress = instance.migration_in_progress if instance else False + recording = instance.recording if instance else False + thread_alive = instance.is_alive() if instance else False + + recorder_info = { + "backlog": backlog, + "max_backlog": MAX_QUEUE_BACKLOG, + "migration_in_progress": migration_in_progress, + "recording": recording, + "thread_running": thread_alive, + } + connection.send_result(msg["id"], recorder_info) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 7414548c864..b21d671560c 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -1,12 +1,15 @@ """Common test utils for working with recorder.""" from datetime import timedelta +from sqlalchemy import create_engine + from homeassistant import core as ha from homeassistant.components import recorder from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, fire_time_changed +from tests.components.recorder import models_original DEFAULT_PURGE_TASKS = 3 @@ -80,3 +83,13 @@ def corrupt_db_file(test_db_file): with open(test_db_file, "w+") as fhandle: fhandle.seek(200) fhandle.write("I am a corrupt db" * 100) + + +def create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + engine = create_engine(*args, **kwargs) + models_original.Base.metadata.create_all(engine) + return engine diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 5586e06d337..562935e78a1 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -23,10 +23,9 @@ from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import session_scope import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done_without_instance +from .common import async_wait_recording_done_without_instance, create_engine_test from tests.common import async_fire_time_changed -from tests.components.recorder import models_original def _get_native_states(hass, entity_id): @@ -37,16 +36,6 @@ def _get_native_states(hass, entity_id): ] -def create_engine_test(*args, **kwargs): - """Test version of create_engine that initializes with old schema. - - This simulates an existing db with the old schema. - """ - engine = create_engine(*args, **kwargs) - models_original.Base.metadata.create_all(engine) - return engine - - async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" assert await recorder.async_migration_in_progress(hass) is False diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index d52393fb693..52a9424b4ac 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,18 +1,29 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +import threading +from unittest.mock import patch import pytest from pytest import approx +from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM -from .common import trigger_db_commit +from .common import ( + async_wait_recording_done_without_instance, + create_engine_test, + trigger_db_commit, +) -from tests.common import init_recorder_component +from tests.common import ( + async_fire_time_changed, + async_init_recorder_component, + init_recorder_component, +) POWER_SENSOR_ATTRIBUTES = { "device_class": "power", @@ -237,3 +248,113 @@ async def test_update_statistics_metadata(hass, hass_ws_client, new_unit): "unit_of_measurement": new_unit, } ] + + +async def test_recorder_info(hass, hass_ws_client): + """Test getting recorder status.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "backlog": 0, + "max_backlog": 30000, + "migration_in_progress": False, + "recording": True, + "thread_running": True, + } + + +async def test_recorder_info_no_recorder(hass, hass_ws_client): + """Test getting recorder status when recorder is not present.""" + client = await hass_ws_client() + + await client.send_json({"id": 1, "type": "recorder/info"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_command" + + +async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): + """Test getting recorder status when recorder is not started.""" + config = {recorder.CONF_DB_URL: "sqlite://no_file", recorder.CONF_DB_RETRY_WAIT: 0} + + client = await hass_ws_client() + + with patch("homeassistant.components.recorder.migration.migrate_schema"): + assert not await async_setup_component( + hass, recorder.DOMAIN, {recorder.DOMAIN: config} + ) + assert recorder.DOMAIN not in hass.config.components + await hass.async_block_till_done() + + # Wait for recorder to shut down + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + + await client.send_json({"id": 1, "type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["recording"] is False + assert response["result"]["thread_running"] is False + + +async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): + """Test getting recorder status when recorder queue is exhausted.""" + assert await recorder.async_migration_in_progress(hass) is False + + migration_done = threading.Event() + + real_migration = recorder.migration.migrate_schema + + def stalled_migration(*args): + """Make migration stall.""" + nonlocal migration_done + migration_done.wait() + return real_migration(*args) + + with patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics" + ), patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch.object( + recorder, "MAX_QUEUE_BACKLOG", 1 + ), patch( + "homeassistant.components.recorder.migration.migrate_schema", + wraps=stalled_migration, + ): + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + await hass.async_block_till_done() + + # Detect queue full + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + client = await hass_ws_client() + + # Check the status + await client.send_json({"id": 1, "type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["migration_in_progress"] is True + assert response["result"]["recording"] is False + assert response["result"]["thread_running"] is True + + # Let migration finish + migration_done.set() + await async_wait_recording_done_without_instance(hass) + + # Check the status after migration finished + await client.send_json({"id": 2, "type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["migration_in_progress"] is False + assert response["result"]["recording"] is True + assert response["result"]["thread_running"] is True From d126d88977ce59c4ccf1c3c2537cc321b38a6f63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 16:50:43 +0100 Subject: [PATCH 0239/1452] Add Button entity component platform (#57642) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/button/__init__.py | 104 +++++++++++++++++ homeassistant/components/button/const.py | 4 + .../components/button/device_action.py | 58 ++++++++++ .../components/button/device_trigger.py | 73 ++++++++++++ homeassistant/components/button/manifest.json | 7 ++ homeassistant/components/button/services.yaml | 6 + homeassistant/components/button/strings.json | 11 ++ .../components/button/translations/en.json | 11 ++ homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/button.py | 65 +++++++++++ mypy.ini | 11 ++ script/hassfest/manifest.py | 1 + tests/components/button/__init__.py | 1 + tests/components/button/test_device_action.py | 87 ++++++++++++++ .../components/button/test_device_trigger.py | 108 ++++++++++++++++++ tests/components/button/test_init.py | 64 +++++++++++ tests/components/demo/test_button.py | 47 ++++++++ .../custom_components/test/button.py | 47 ++++++++ 20 files changed, 708 insertions(+) create mode 100644 homeassistant/components/button/__init__.py create mode 100644 homeassistant/components/button/const.py create mode 100644 homeassistant/components/button/device_action.py create mode 100644 homeassistant/components/button/device_trigger.py create mode 100644 homeassistant/components/button/manifest.json create mode 100644 homeassistant/components/button/services.yaml create mode 100644 homeassistant/components/button/strings.json create mode 100644 homeassistant/components/button/translations/en.json create mode 100644 homeassistant/components/demo/button.py create mode 100644 tests/components/button/__init__.py create mode 100644 tests/components/button/test_device_action.py create mode 100644 tests/components/button/test_device_trigger.py create mode 100644 tests/components/button/test_init.py create mode 100644 tests/components/demo/test_button.py create mode 100644 tests/testing_config/custom_components/test/button.py diff --git a/.strict-typing b/.strict-typing index 059ef850ab7..95d03544661 100644 --- a/.strict-typing +++ b/.strict-typing @@ -24,6 +24,7 @@ homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* homeassistant.components.braviatv.* homeassistant.components.brother.* +homeassistant.components.button.* homeassistant.components.calendar.* homeassistant.components.camera.* homeassistant.components.canary.* diff --git a/CODEOWNERS b/CODEOWNERS index 3534b73676d..21e6526b03f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -85,6 +85,7 @@ homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bsblan/* @liudger homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 +homeassistant/components/button/* @home-assistant/core homeassistant/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/circuit/* @braam diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py new file mode 100644 index 00000000000..fe2d1cb6435 --- /dev/null +++ b/homeassistant/components/button/__init__.py @@ -0,0 +1,104 @@ +"""Component to pressing a button as platforms.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging +from typing import final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import DOMAIN, SERVICE_PRESS + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Button entities.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_PRESS, + {}, + "_async_press_action", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class ButtonEntityDescription(EntityDescription): + """A class that describes button entities.""" + + +class ButtonEntity(RestoreEntity): + """Representation of a Button entity.""" + + entity_description: ButtonEntityDescription + _attr_should_poll = False + _attr_device_class: None = None + _attr_state: None = None + __last_pressed: datetime | None = None + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if self.__last_pressed is None: + return None + return self.__last_pressed.isoformat() + + @final + async def _async_press_action(self) -> None: + """Press the button (from e.g., service call). + + Should not be overridden, handle setting last press timestamp. + """ + self.__last_pressed = dt_util.utcnow() + self.async_write_ha_state() + await self.async_press() + + async def async_added_to_hass(self) -> None: + """Call when the button is added to hass.""" + state = await self.async_get_last_state() + if state is not None and state.state is not None: + self.__last_pressed = dt_util.parse_datetime(state.state) + + def press(self) -> None: + """Press the button.""" + raise NotImplementedError() + + async def async_press(self) -> None: + """Press the button.""" + await self.hass.async_add_executor_job(self.press) diff --git a/homeassistant/components/button/const.py b/homeassistant/components/button/const.py new file mode 100644 index 00000000000..273314b2e97 --- /dev/null +++ b/homeassistant/components/button/const.py @@ -0,0 +1,4 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "button" +SERVICE_PRESS = "press" diff --git a/homeassistant/components/button/device_action.py b/homeassistant/components/button/device_action.py new file mode 100644 index 00000000000..2dffd9c600f --- /dev/null +++ b/homeassistant/components/button/device_action.py @@ -0,0 +1,58 @@ +"""Provides device actions for Button.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, SERVICE_PRESS + +ACTION_TYPES = {"press"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device actions for button devices.""" + registry = entity_registry.async_get(hass) + return [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "press", + } + for entry in entity_registry.async_entries_for_device(registry, device_id) + if entry.domain == DOMAIN + ] + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Context | None +) -> None: + """Execute a device action.""" + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: config[CONF_ENTITY_ID], + }, + blocking=True, + context=context, + ) diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py new file mode 100644 index 00000000000..2005bc82194 --- /dev/null +++ b/homeassistant/components/button/device_trigger.py @@ -0,0 +1,73 @@ +"""Provides device triggers for Button.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers.state import ( + TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA, + async_attach_trigger as async_attach_state_trigger, +) +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = {"pressed"} + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List device triggers for button devices.""" + registry = entity_registry.async_get(hass) + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "pressed", + } + for entry in entity_registry.async_entries_for_device(registry, device_id) + if entry.domain == DOMAIN + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + state_config = { + CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + state_config = STATE_TRIGGER_SCHEMA(state_config) + return await async_attach_state_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/button/manifest.json b/homeassistant/components/button/manifest.json new file mode 100644 index 00000000000..beeaca487a6 --- /dev/null +++ b/homeassistant/components/button/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "button", + "name": "Button", + "documentation": "https://www.home-assistant.io/integrations/button", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/button/services.yaml b/homeassistant/components/button/services.yaml new file mode 100644 index 00000000000..245368f9d5b --- /dev/null +++ b/homeassistant/components/button/services.yaml @@ -0,0 +1,6 @@ +press: + name: Press + description: Press the button entity. + target: + entity: + domain: button diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json new file mode 100644 index 00000000000..ca774c57d77 --- /dev/null +++ b/homeassistant/components/button/strings.json @@ -0,0 +1,11 @@ +{ + "title": "Button", + "device_automation": { + "trigger_type": { + "pressed": "{entity_name} has been pressed" + }, + "action_type": { + "press": "Press {entity_name} button" + } + } +} diff --git a/homeassistant/components/button/translations/en.json b/homeassistant/components/button/translations/en.json new file mode 100644 index 00000000000..8b19cf25774 --- /dev/null +++ b/homeassistant/components/button/translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Press {entity_name} button" + }, + "trigger_type": { + "pressed": "{entity_name} has been pressed" + } + }, + "title": "Button" +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index db53c1c528f..952b7cbd9b2 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -15,6 +15,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "air_quality", "alarm_control_panel", "binary_sensor", + "button", "camera", "climate", "cover", diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py new file mode 100644 index 00000000000..9ef54f30db3 --- /dev/null +++ b/homeassistant/components/demo/button.py @@ -0,0 +1,65 @@ +"""Demo platform that offers a fake button entity.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: + """Set up the demo Button entity.""" + async_add_entities( + [ + DemoButton( + unique_id="push", + name="Push", + icon="mdi:gesture-tap-button", + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoButton(ButtonEntity): + """Representation of a demo button entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + icon: str, + ) -> None: + """Initialize the Demo button entity.""" + self._attr_unique_id = unique_id + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_icon = icon + self._attr_device_info = { + "identifiers": {(DOMAIN, unique_id)}, + "name": name, + } + + async def async_press(self) -> None: + """Send out a persistent notification.""" + self.hass.components.persistent_notification.async_create( + "Button pressed", title="Button" + ) diff --git a/mypy.ini b/mypy.ini index f52ee13b689..086db083892 100644 --- a/mypy.ini +++ b/mypy.ini @@ -275,6 +275,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.button.*] +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.calendar.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index abade24dbf9..0bec9e51436 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -38,6 +38,7 @@ NO_IOT_CLASS = [ "automation", "binary_sensor", "blueprint", + "button", "calendar", "camera", "climate", diff --git a/tests/components/button/__init__.py b/tests/components/button/__init__.py new file mode 100644 index 00000000000..77489053189 --- /dev/null +++ b/tests/components/button/__init__.py @@ -0,0 +1 @@ +"""The tests for the Button integration.""" diff --git a/tests/components/button/test_device_action.py b/tests/components/button/test_device_action.py new file mode 100644 index 00000000000..984be163d42 --- /dev/null +++ b/tests/components/button/test_device_action.py @@ -0,0 +1,87 @@ +"""The tests for Button device actions.""" +import pytest + +from homeassistant.components import automation +from homeassistant.components.button import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry, entity_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry: + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry: + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions( + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: entity_registry.EntityRegistry, +) -> None: + """Test we get the expected actions from a button.""" + 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(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "press", + "device_id": device_entry.id, + "entity_id": "button.test_5678", + } + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass: HomeAssistant) -> None: + """Test for press action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "button.entity", + "type": "press", + }, + }, + ] + }, + ) + + press_calls = async_mock_service(hass, DOMAIN, "press") + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(press_calls) == 1 + assert press_calls[0].domain == DOMAIN + assert press_calls[0].service == "press" + assert press_calls[0].data == {"entity_id": "button.entity"} diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py new file mode 100644 index 00000000000..913aec9088e --- /dev/null +++ b/tests/components/button/test_device_trigger.py @@ -0,0 +1,108 @@ +"""The tests for Button device triggers.""" +from __future__ import annotations + +import pytest + +from homeassistant.components import automation +from homeassistant.components.button import DOMAIN +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry: + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant) -> EntityRegistry: + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass: HomeAssistant) -> list[ServiceCall]: + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers( + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: EntityRegistry, +) -> None: + """Test we get the expected triggers from a button.""" + 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(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "pressed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("button.entity", "unknown") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "button.entity", + "type": "pressed", + }, + "action": { + "service": "test.automation", + "data": { + "some": ( + "to - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }} - " + "{{ trigger.id}}" + ) + }, + }, + } + ] + }, + ) + + # Test triggering device trigger with a to state + hass.states.async_set("button.entity", "2021-01-01T23:59:59+00:00") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "to - device - {} - unknown - 2021-01-01T23:59:59+00:00 - None - 0".format( + "button.entity" + ) diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py new file mode 100644 index 00000000000..2846a6f3a8d --- /dev/null +++ b/tests/components/button/test_init.py @@ -0,0 +1,64 @@ +"""The tests for the Button component.""" +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonEntity +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import mock_restore_cache + + +async def test_button(hass: HomeAssistant) -> None: + """Test getting data from the mocked button entity.""" + button = ButtonEntity() + assert button.state is None + + button.hass = hass + + with pytest.raises(NotImplementedError): + await button.async_press() + + button.press = MagicMock() + await button.async_press() + + assert button.press.called + + +async def test_custom_integration(hass, caplog, enable_custom_integrations): + """Test we integration.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + assert hass.states.get("button.button_1").state == STATE_UNKNOWN + + now = dt_util.utcnow() + with patch("homeassistant.core.dt_util.utcnow", return_value=now): + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.button_1"}, + blocking=True, + ) + + assert hass.states.get("button.button_1").state == now.isoformat() + assert "The button has been pressed" in caplog.text + + +async def test_restore_state(hass, enable_custom_integrations): + """Test we restore state integration.""" + mock_restore_cache(hass, (State("button.button_1", "2021-01-01T23:59:59+00:00"),)) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + assert hass.states.get("button.button_1").state == "2021-01-01T23:59:59+00:00" diff --git a/tests/components/demo/test_button.py b/tests/components/demo/test_button.py new file mode 100644 index 00000000000..d4f6f97cb7a --- /dev/null +++ b/tests/components/demo/test_button.py @@ -0,0 +1,47 @@ +"""The tests for the demo button component.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.button.const import DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +ENTITY_PUSH = "button.push" + + +@pytest.fixture(autouse=True) +async def setup_demo_button(hass: HomeAssistant) -> None: + """Initialize setup demo button entity.""" + assert await async_setup_component(hass, DOMAIN, {"button": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_PUSH) + assert state + assert state.state == STATE_UNKNOWN + + +async def test_press(hass: HomeAssistant) -> None: + """Test pressing the button.""" + state = hass.states.get(ENTITY_PUSH) + assert state + assert state.state == STATE_UNKNOWN + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ENTITY_PUSH}, + blocking=True, + ) + + state = hass.states.get(ENTITY_PUSH) + assert state + assert state.state == now.isoformat() diff --git a/tests/testing_config/custom_components/test/button.py b/tests/testing_config/custom_components/test/button.py new file mode 100644 index 00000000000..471e0f42a39 --- /dev/null +++ b/tests/testing_config/custom_components/test/button.py @@ -0,0 +1,47 @@ +""" +Provide a mock button platform. + +Call init before using it in your tests to ensure clean test data. +""" +import logging + +from homeassistant.components.button import ButtonEntity + +from tests.common import MockEntity + +UNIQUE_BUTTON_1 = "unique_button_1" + +ENTITIES = [] + +_LOGGER = logging.getLogger(__name__) + + +class MockButtonEntity(MockEntity, ButtonEntity): + """Mock Button class.""" + + def press(self) -> None: + """Press the button.""" + _LOGGER.info("The button has been pressed") + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockButtonEntity( + name="button 1", + unique_id="unique_button_1", + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From 7945facf1e812e01718f0cfdc14f09853c3353f5 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 4 Nov 2021 17:00:25 +0100 Subject: [PATCH 0240/1452] Fix mop attribute for unified mop and water box in Xiaomi Miio (#58990) Co-authored-by: Teemu R. Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/binary_sensor.py | 22 +++++++++++++++++-- homeassistant/components/xiaomi_miio/const.py | 3 +++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 1bdd647da79..ef172bbbbc5 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -29,6 +29,7 @@ from .const import ( MODELS_HUMIDIFIER_MJJSQ, MODELS_VACUUM, MODELS_VACUUM_WITH_MOP, + MODELS_VACUUM_WITH_SEPARATE_MOP, ) from .device import XiaomiCoordinatedMiioEntity @@ -77,7 +78,7 @@ FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( - key=ATTR_MOP_ATTACHED, + key=ATTR_WATER_BOX_ATTACHED, name="Mop Attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, @@ -105,6 +106,19 @@ VACUUM_SENSORS = { ), } +VACUUM_SENSORS_SEPARATE_MOP = { + **VACUUM_SENSORS, + ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( + key=ATTR_MOP_ATTACHED, + name="Mop Attached", + icon="mdi:square-rounded", + parent_key=VacuumCoordinatorDataAttributes.status, + entity_registry_enabled_default=True, + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + 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) @@ -118,8 +132,12 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] + sensors = VACUUM_SENSORS - for sensor, description in VACUUM_SENSORS.items(): + if config_entry.data[CONF_MODEL] in MODELS_VACUUM_WITH_SEPARATE_MOP: + sensors = VACUUM_SENSORS_SEPARATE_MOP + + for sensor, description in sensors.items(): parent_key_data = getattr(coordinator.data, description.parent_key) if getattr(parent_key_data, description.key, None) is None: _LOGGER.debug( diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 124bdb77f5e..60c16a0717a 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -225,6 +225,9 @@ MODELS_VACUUM_WITH_MOP = [ ROCKROBO_S6_PURE, ROCKROBO_S7, ] +MODELS_VACUUM_WITH_SEPARATE_MOP = [ + ROCKROBO_S7, +] MODELS_AIR_MONITOR = [ MODEL_AIRQUALITYMONITOR_V1, From c3fc19915e919f26a072fd1ec4cfcfe7f12611d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 4 Nov 2021 17:54:27 +0100 Subject: [PATCH 0241/1452] Mqtt fan fail deprecated options for classic speeds (#58992) * Fail deprecated options * new removed validator * correct module_name - add tests * Add test cant find module cv.removed * module name from stack+1 * Remove error from log. Just throw. * assert on thrown exception text * cleanup formatting remove KeyStyleAdapter * format the replacement_key and update test * deprecated vs removed - add raise_if_present opt * doc string update * is deprecated --- homeassistant/components/mqtt/fan.py | 28 +++---- homeassistant/helpers/config_validation.py | 90 +++++++++++++++++----- tests/helpers/test_config_validation.py | 72 ++++++++++++++++- 3 files changed, 152 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 34e5d66a5e7..f1040825cdf 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -175,13 +175,13 @@ PLATFORM_SCHEMA = vol.All( # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, # are deprecated, support will be removed with release 2021.9 - cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), - cv.deprecated(CONF_PAYLOAD_LOW_SPEED), - cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), - cv.deprecated(CONF_SPEED_COMMAND_TOPIC), - cv.deprecated(CONF_SPEED_LIST), - cv.deprecated(CONF_SPEED_STATE_TOPIC), - cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), + cv.removed(CONF_PAYLOAD_HIGH_SPEED), + cv.removed(CONF_PAYLOAD_LOW_SPEED), + cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), + cv.removed(CONF_SPEED_COMMAND_TOPIC), + cv.removed(CONF_SPEED_LIST), + cv.removed(CONF_SPEED_STATE_TOPIC), + cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, @@ -191,13 +191,13 @@ DISCOVERY_SCHEMA = vol.All( # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, # are deprecated, support will be removed with release 2021.9 - cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), - cv.deprecated(CONF_PAYLOAD_LOW_SPEED), - cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), - cv.deprecated(CONF_SPEED_COMMAND_TOPIC), - cv.deprecated(CONF_SPEED_LIST), - cv.deprecated(CONF_SPEED_STATE_TOPIC), - cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), + cv.removed(CONF_PAYLOAD_HIGH_SPEED), + cv.removed(CONF_PAYLOAD_LOW_SPEED), + cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), + cv.removed(CONF_SPEED_COMMAND_TOPIC), + cv.removed(CONF_SPEED_LIST), + cv.removed(CONF_SPEED_STATE_TOPIC), + cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_speed_range_configuration, valid_preset_mode_configuration, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f2ac86239f8..c9cd3c66e92 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -78,7 +78,6 @@ from homeassistant.helpers import ( script_variables as script_variables_helper, template as template_helper, ) -from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import raise_if_invalid_path, slugify as util_slugify import homeassistant.util.dt as dt_util @@ -709,23 +708,26 @@ class multi_select: return selected -def deprecated( +def _deprecated_or_removed( key: str, replacement_key: str | None = None, default: Any | None = None, + raise_if_present: bool | None = False, + option_status: str | None = "is deprecated", ) -> Callable[[dict], dict]: """ - Log key as deprecated and provide a replacement (if exists). + Log key as deprecated and provide a replacement (if exists) or fail. Expected behavior: - - Outputs the appropriate deprecation warning if key is detected + - Outputs or throws the appropriate deprecation warning if key is detected + - Outputs or throws the appropriate error if key is detected and removed from support - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case """ - module = inspect.getmodule(inspect.stack(context=0)[1].frame) + module = inspect.getmodule(inspect.stack(context=0)[2].frame) if module is not None: module_name = module.__name__ else: @@ -733,36 +735,36 @@ def deprecated( # will be missing information, so let's guard. # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key: warning = ( - "The '{key}' option is deprecated," + "The '{key}' option {option_status}," " please replace it with '{replacement_key}'" ) else: warning = ( - "The '{key}' option is deprecated," + "The '{key}' option {option_status}," " please remove it from your configuration" ) def validator(config: dict) -> dict: - """Check if key is in config and log warning.""" + """Check if key is in config and log warning or error.""" if key in config: try: - KeywordStyleAdapter(logging.getLogger(module_name)).warning( - warning.replace( - "'{key}' option", - f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore - ), - key=key, - replacement_key=replacement_key, + warning_local = warning.replace( + "'{key}' option", + f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore ) except AttributeError: - KeywordStyleAdapter(logging.getLogger(module_name)).warning( - warning, - key=key, - replacement_key=replacement_key, - ) + warning_local = warning + warning_local = warning_local.format( + key=key, + replacement_key=replacement_key, + option_status=option_status, + ) + if raise_if_present: + raise vol.Invalid(warning_local) + + logging.getLogger(module_name).warning(warning_local) value = config[key] if replacement_key: config.pop(key) @@ -782,6 +784,52 @@ def deprecated( return validator +def deprecated( + key: str, + replacement_key: str | None = None, + default: Any | None = None, + raise_if_present: bool | None = False, +) -> Callable[[dict], dict]: + """ + Log key as deprecated and provide a replacement (if exists). + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected or raises an exception + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning if neither key nor replacement_key are provided + - Adds replacement_key with default value in this case + """ + return _deprecated_or_removed( + key, + replacement_key=replacement_key, + default=default, + raise_if_present=raise_if_present, + option_status="is deprecated", + ) + + +def removed( + key: str, + default: Any | None = None, + raise_if_present: bool | None = True, +) -> Callable[[dict], dict]: + """ + Log key as deprecated and fail the config validation. + + Expected behavior: + - Outputs the appropriate error if key is detected and removed from support or raises an exception + """ + return _deprecated_or_removed( + key, + replacement_key=None, + default=default, + raise_if_present=raise_if_present, + option_status="was removed", + ) + + def key_value_schemas( key: str, value_schemas: dict[Hashable, vol.Schema] ) -> Callable[[Any], dict[Hashable, Any]]: diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cf832dfde50..79fa5538417 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -712,6 +712,46 @@ def test_deprecated_with_no_optionals(caplog, schema): assert test_data == output +def test_deprecated_or_removed_param_and_raise(caplog, schema): + """ + Test removed or deprecation options and fail the config validation by raising an exception. + + Expected behavior: + - Outputs the appropriate deprecation or removed from support error if key is detected + """ + removed_schema = vol.All(cv.deprecated("mars", raise_if_present=True), schema) + + test_data = {"mars": True} + with pytest.raises(vol.Invalid) as excinfo: + removed_schema(test_data) + assert ( + "The 'mars' option is deprecated, please remove it from your configuration" + in str(excinfo.value) + ) + assert len(caplog.records) == 0 + + test_data = {"venus": True} + output = removed_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + deprecated_schema = vol.All(cv.removed("mars"), schema) + + test_data = {"mars": True} + with pytest.raises(vol.Invalid) as excinfo: + deprecated_schema(test_data) + assert ( + "The 'mars' option was removed, please remove it from your configuration" + in str(excinfo.value) + ) + assert len(caplog.records) == 0 + + test_data = {"venus": True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + def test_deprecated_with_replacement_key(caplog, schema): """ Test deprecation behaves correctly when only a replacement key is provided. @@ -846,17 +886,43 @@ def test_deprecated_cant_find_module(): default=False, ) + with patch("inspect.getmodule", return_value=None): + # This used to raise. + cv.removed( + "mars", + default=False, + ) -def test_deprecated_logger_with_config_attributes(caplog): + +def test_deprecated_or_removed_logger_with_config_attributes(caplog): """Test if the logger outputs the correct message if the line and file attribute is available in config.""" file: str = "configuration.yaml" line: int = 54 - replacement = f"'mars' option near {file}:{line} is deprecated" + + # test as deprecated option + replacement_key = "jupiter" + option_status = "is deprecated" + replacement = f"'mars' option near {file}:{line} {option_status}, please replace it with '{replacement_key}'" config = OrderedDict([("mars", "blah")]) setattr(config, "__config_file__", file) setattr(config, "__line__", line) - cv.deprecated("mars", replacement_key="jupiter", default=False)(config) + cv.deprecated("mars", replacement_key=replacement_key, default=False)(config) + + assert len(caplog.records) == 1 + assert replacement in caplog.text + + caplog.clear() + assert len(caplog.records) == 0 + + # test as removed option + option_status = "was removed" + replacement = f"'mars' option near {file}:{line} {option_status}, please remove it from your configuration" + config = OrderedDict([("mars", "blah")]) + setattr(config, "__config_file__", file) + setattr(config, "__line__", line) + + cv.removed("mars", default=False, raise_if_present=False)(config) assert len(caplog.records) == 1 assert replacement in caplog.text From 491e62792bec2a4125a0de26bfe8c88c98f1a1d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 18:35:43 +0100 Subject: [PATCH 0242/1452] Correct rescheduling of ExternalStatisticsTask (#59076) --- homeassistant/components/recorder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 103435fa519..6a9c53b96ad 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -798,7 +798,7 @@ class Recorder(threading.Thread): if statistics.add_external_statistics(self, metadata, stats): return # Schedule a new statistics task if this one didn't finish - self.queue.put(StatisticsTask(metadata, stats)) + self.queue.put(ExternalStatisticsTask(metadata, stats)) def _process_one_event(self, event): """Process one event.""" From a62bc6b3b9d5bf3163cea50739265726546d73c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 19:12:21 +0100 Subject: [PATCH 0243/1452] Minor tweak of cv.deprecated + cv.removed (#59095) * Minor tweak of cv.deprecated + cv.removed * Satisfy pylint --- homeassistant/helpers/config_validation.py | 58 +++++++++++----------- tests/helpers/test_config_validation.py | 4 +- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index c9cd3c66e92..2d38acafadf 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -710,10 +710,10 @@ class multi_select: def _deprecated_or_removed( key: str, - replacement_key: str | None = None, - default: Any | None = None, - raise_if_present: bool | None = False, - option_status: str | None = "is deprecated", + replacement_key: str | None, + default: Any | None, + raise_if_present: bool, + option_removed: bool, ) -> Callable[[dict], dict]: """ Log key as deprecated and provide a replacement (if exists) or fail. @@ -735,36 +735,34 @@ def _deprecated_or_removed( # will be missing information, so let's guard. # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key: - warning = ( - "The '{key}' option {option_status}," - " please replace it with '{replacement_key}'" - ) + if option_removed: + logger_func = logging.getLogger(module_name).error + option_status = "has been removed" else: - warning = ( - "The '{key}' option {option_status}," - " please remove it from your configuration" - ) + logger_func = logging.getLogger(module_name).warning + option_status = "is deprecated" def validator(config: dict) -> dict: """Check if key is in config and log warning or error.""" if key in config: try: - warning_local = warning.replace( - "'{key}' option", - f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore - ) + near = f"near {config.__config_file__}:{config.__line__} " # type: ignore except AttributeError: - warning_local = warning - warning_local = warning_local.format( - key=key, - replacement_key=replacement_key, - option_status=option_status, - ) - if raise_if_present: - raise vol.Invalid(warning_local) + near = "" + arguments: tuple[str, ...] + if replacement_key: + warning = "The '%s' option %s%s, please replace it with '%s'" + arguments = (key, near, option_status, replacement_key) + else: + warning = ( + "The '%s' option %s%s, please remove it from your configuration" + ) + arguments = (key, near, option_status) - logging.getLogger(module_name).warning(warning_local) + if raise_if_present: + raise vol.Invalid(warning % arguments) + + logger_func(warning, *arguments) value = config[key] if replacement_key: config.pop(key) @@ -805,8 +803,8 @@ def deprecated( key, replacement_key=replacement_key, default=default, - raise_if_present=raise_if_present, - option_status="is deprecated", + raise_if_present=raise_if_present or False, + option_removed=False, ) @@ -825,8 +823,8 @@ def removed( key, replacement_key=None, default=default, - raise_if_present=raise_if_present, - option_status="was removed", + raise_if_present=raise_if_present or False, + option_removed=True, ) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 79fa5538417..4b8e4fe9e49 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -741,7 +741,7 @@ def test_deprecated_or_removed_param_and_raise(caplog, schema): with pytest.raises(vol.Invalid) as excinfo: deprecated_schema(test_data) assert ( - "The 'mars' option was removed, please remove it from your configuration" + "The 'mars' option has been removed, please remove it from your configuration" in str(excinfo.value) ) assert len(caplog.records) == 0 @@ -916,7 +916,7 @@ def test_deprecated_or_removed_logger_with_config_attributes(caplog): assert len(caplog.records) == 0 # test as removed option - option_status = "was removed" + option_status = "has been removed" replacement = f"'mars' option near {file}:{line} {option_status}, please remove it from your configuration" config = OrderedDict([("mars", "blah")]) setattr(config, "__config_file__", file) From 38b61f3ff91b9abba6794e893f6049a4266b9c77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 20:22:11 +0100 Subject: [PATCH 0244/1452] Upgrade mitemp_bt to 0.0.5 (#59054) * Upgrade mitemp_bt to 0.0.4 * Upgrade mitemp_bt to 0.0.5 --- homeassistant/components/mitemp_bt/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 8c5906ae439..f0465315cef 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -2,7 +2,7 @@ "domain": "mitemp_bt", "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "requirements": ["mitemp_bt==0.0.3"], + "requirements": ["mitemp_bt==0.0.5"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index fbf75e63c32..5990fff3346 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,7 +1017,7 @@ millheater==0.8.0 minio==4.0.9 # homeassistant.components.mitemp_bt -mitemp_bt==0.0.3 +mitemp_bt==0.0.5 # homeassistant.components.motion_blinds motionblinds==0.5.7 From ea4009fd81ad6aeb25c09e5e7fa7c2a7efcb96b2 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 4 Nov 2021 15:34:54 -0600 Subject: [PATCH 0245/1452] Use entity_category in litterrobot (#59074) --- homeassistant/components/litterrobot/entity.py | 7 +++++++ homeassistant/components/litterrobot/select.py | 4 ++-- homeassistant/components/litterrobot/switch.py | 8 ++++---- tests/components/litterrobot/test_select.py | 8 +++++++- tests/components/litterrobot/test_switch.py | 8 +++++++- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 5a6590f9360..064c9bcf8f1 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -9,6 +9,7 @@ from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_call_later @@ -111,3 +112,9 @@ class LitterRobotControlEntity(LitterRobotEntity): ) .timetz() ) + + +class LitterRobotConfigEntity(LitterRobotControlEntity): + """A Litter-Robot entity that can control configuration of the unit.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index ceb20b52d40..c1a0718510d 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotControlEntity +from .entity import LitterRobotConfigEntity from .hub import LitterRobotHub TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES = "Clean Cycle Wait Time Minutes" @@ -33,7 +33,7 @@ async def async_setup_entry( ) -class LitterRobotSelect(LitterRobotControlEntity, SelectEntity): +class LitterRobotSelect(LitterRobotConfigEntity, SelectEntity): """Litter-Robot Select.""" _attr_icon = "mdi:timer-outline" diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 25385ecf650..9a08a008d96 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -9,11 +9,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotControlEntity +from .entity import LitterRobotConfigEntity from .hub import LitterRobotHub -class LitterRobotNightLightModeSwitch(LitterRobotControlEntity, SwitchEntity): +class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property @@ -35,7 +35,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotControlEntity, SwitchEntity): await self.perform_action_and_refresh(self.robot.set_night_light, False) -class LitterRobotPanelLockoutSwitch(LitterRobotControlEntity, SwitchEntity): +class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property @@ -57,7 +57,7 @@ class LitterRobotPanelLockoutSwitch(LitterRobotControlEntity, SwitchEntity): await self.perform_action_and_refresh(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES: list[tuple[type[LitterRobotControlEntity], str]] = [ +ROBOT_SWITCHES: list[tuple[type[LitterRobotConfigEntity], str]] = [ (LitterRobotNightLightModeSwitch, "Night Light Mode"), (LitterRobotPanelLockoutSwitch, "Panel Lockout"), ] diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index c0c436764af..dfb1b0e639e 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -10,8 +10,9 @@ from homeassistant.components.select import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.util.dt import utcnow from .conftest import setup_integration @@ -28,6 +29,11 @@ async def test_wait_time_select(hass: HomeAssistant, mock_account): select = hass.states.get(SELECT_ENTITY_ID) assert select + ent_reg = entity_registry.async_get(hass) + entity_entry = ent_reg.async_get(SELECT_ENTITY_ID) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} count = 0 diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index 2659b1cc049..d651b09fadc 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -9,7 +9,8 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_CATEGORY_CONFIG, STATE_ON +from homeassistant.helpers import entity_registry from homeassistant.util.dt import utcnow from .conftest import setup_integration @@ -28,6 +29,11 @@ async def test_switch(hass, mock_account): assert switch assert switch.state == STATE_ON + ent_reg = entity_registry.async_get(hass) + entity_entry = ent_reg.async_get(NIGHT_LIGHT_MODE_ENTITY_ID) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + @pytest.mark.parametrize( "entity_id,robot_command", From 4d4d778598d406bd7d2be430ae06ab23502f2177 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 4 Nov 2021 22:43:38 +0100 Subject: [PATCH 0246/1452] Add tests for KNX light (#58912) * test lights: simple, brightness, color_temp * hs, xyy, rgb individual * test rgb, rgbw --- tests/components/knx/conftest.py | 7 + tests/components/knx/test_light.py | 1042 ++++++++++++++++++++++++++++ 2 files changed, 1049 insertions(+) create mode 100644 tests/components/knx/test_light.py diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 47e7b94c32d..ca6f81057e1 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -30,6 +30,13 @@ class KNXTestKit: # telegrams to an InternalGroupAddress won't be queued here self._outgoing_telegrams: asyncio.Queue = asyncio.Queue() + def assert_state(self, entity_id: str, state: str, **attributes) -> None: + """Assert the state of an entity.""" + test_state = self.hass.states.get(entity_id) + assert test_state.state == state + for attribute, value in attributes.items(): + assert test_state.attributes.get(attribute) == value + async def setup_integration(self, config): """Create the KNX integration.""" diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py new file mode 100644 index 00000000000..bb7a67d6bb4 --- /dev/null +++ b/tests/components/knx/test_light.py @@ -0,0 +1,1042 @@ +"""Test KNX light.""" +from __future__ import annotations + +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS +from homeassistant.components.knx.schema import LightSchema +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_XY, +) +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + + +async def test_light_simple(hass: HomeAssistant, knx: KNXTestKit): + """Test simple KNX light.""" + test_address = "1/1/1" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + } + } + ) + assert len(hass.states.async_all()) == 1 + + knx.assert_state("light.test", STATE_OFF) + # turn on light + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_address, True) + knx.assert_state( + "light.test", + STATE_ON, + color_mode=COLOR_MODE_ONOFF, + ) + # turn off light + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_address, False) + knx.assert_state("light.test", STATE_OFF) + # receive ON telegram + await knx.receive_write(test_address, True) + knx.assert_state("light.test", STATE_ON) + + # receive OFF telegram + await knx.receive_write(test_address, False) + knx.assert_state("light.test", STATE_OFF) + + # switch does not respond to read by default + await knx.receive_read(test_address) + await knx.assert_telegram_count(0) + + +async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit): + """Test dimmable KNX light.""" + test_address = "1/1/1" + test_brightness = "1/1/2" + test_brightness_state = "1/1/3" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + } + } + ) + # StateUpdater initialize state + await knx.assert_read(test_brightness_state) + # turn on light via brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 80}, + blocking=True, + ) + await knx.assert_write(test_brightness, (80,)) + # state is still OFF until controller reports otherwise + knx.assert_state("light.test", STATE_OFF) + await knx.receive_write(test_address, True) + knx.assert_state( + "light.test", + STATE_ON, + brightness=80, + color_mode=COLOR_MODE_BRIGHTNESS, + ) + # receive brightness changes from KNX + await knx.receive_write(test_brightness_state, (255,)) + knx.assert_state("light.test", STATE_ON, brightness=255) + await knx.receive_write(test_brightness, (128,)) + knx.assert_state("light.test", STATE_ON, brightness=128) + # turn off light via brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 0}, + blocking=True, + ) + await knx.assert_write(test_address, False) + knx.assert_state("light.test", STATE_OFF) + + +async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light color temperature adjustable in Kelvin.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_brightness = "1/1/3" + test_brightness_state = "1/1/4" + test_ct = "1/1/5" + test_ct_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + LightSchema.CONF_COLOR_TEMP_ADDRESS: test_ct, + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS: test_ct_state, + LightSchema.CONF_COLOR_TEMP_MODE: "absolute", + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_brightness_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_brightness_state, (255,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_ct_state) + await knx.receive_response(test_ct_state, (0x0A, 0x8C)) # 2700 Kelvin - 370 Mired + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_COLOR_TEMP, + color_temp=370, + ) + # change color temperature from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_TEMP: 250}, # 4000 Kelvin - 0x0FA0 + blocking=True, + ) + await knx.assert_write(test_ct, (0x0F, 0xA0)) + knx.assert_state("light.test", STATE_ON, color_temp=250) + # change color temperature from KNX + await knx.receive_write(test_ct_state, (0x17, 0x70)) # 6000 Kelvin - 166 Mired + knx.assert_state("light.test", STATE_ON, color_temp=166) + + +async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light color temperature adjustable in percent.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_brightness = "1/1/3" + test_brightness_state = "1/1/4" + test_ct = "1/1/5" + test_ct_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + LightSchema.CONF_COLOR_TEMP_ADDRESS: test_ct, + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS: test_ct_state, + LightSchema.CONF_COLOR_TEMP_MODE: "relative", + LightSchema.CONF_MIN_KELVIN: 3000, + LightSchema.CONF_MAX_KELVIN: 4000, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_brightness_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_brightness_state, (255,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_ct_state) + await knx.receive_response(test_ct_state, (0xFF,)) # 100 % - 4000 K - 250 Mired + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_COLOR_TEMP, + color_temp=250, + ) + # change color temperature from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_TEMP: 300}, # 3333 Kelvin - 33 % - 0x54 + blocking=True, + ) + await knx.assert_write(test_ct, (0x54,)) + knx.assert_state("light.test", STATE_ON, color_temp=300) + # change color temperature from KNX + await knx.receive_write(test_ct_state, (0xE6,)) # 3900 Kelvin - 90 % - 256 Mired + knx.assert_state("light.test", STATE_ON, color_temp=256) + + +async def test_light_hs_color(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with hs color.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_brightness = "1/1/3" + test_brightness_state = "1/1/4" + test_hue = "1/1/5" + test_hue_state = "1/1/6" + test_sat = "1/1/7" + test_sat_state = "1/1/8" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + LightSchema.CONF_HUE_ADDRESS: test_hue, + LightSchema.CONF_HUE_STATE_ADDRESS: test_hue_state, + LightSchema.CONF_SATURATION_ADDRESS: test_sat, + LightSchema.CONF_SATURATION_STATE_ADDRESS: test_sat_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_brightness_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_brightness_state, (255,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_hue_state) + await knx.assert_read(test_sat_state) + await knx.receive_response(test_hue_state, (0xFF,)) + await knx.receive_response(test_sat_state, (0xFF,)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_HS, + hs_color=(360, 100), + ) + # change color from HA - only hue + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "blue"}, # hue: 240, sat: 100 + blocking=True, + ) + await knx.assert_write(test_hue, (0xAA,)) + knx.assert_state("light.test", STATE_ON, brightness=255, hs_color=(240, 100)) + + # change color from HA - only saturation + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.test", + ATTR_HS_COLOR: (240, 50), + }, # hue: 60, sat: 12.157 + blocking=True, + ) + await knx.assert_write(test_sat, (0x80,)) + knx.assert_state("light.test", STATE_ON, brightness=255, hs_color=(240, 50)) + + # change color from HA - hue and sat + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, # hue: 330, sat: 59 + blocking=True, + ) + await knx.assert_write(test_hue, (0xEA,)) + await knx.assert_write(test_sat, (0x96,)) + knx.assert_state("light.test", STATE_ON, brightness=255, hs_color=(330, 59)) + + # change color and brightness from KNX + await knx.receive_write(test_brightness, (0xB2,)) + knx.assert_state("light.test", STATE_ON, brightness=178, hs_color=(330, 59)) + await knx.receive_write(test_hue, (0x7D,)) + knx.assert_state("light.test", STATE_ON, brightness=178, hs_color=(176, 59)) + await knx.receive_write(test_sat, (0xD1,)) + knx.assert_state("light.test", STATE_ON, brightness=178, hs_color=(176, 82)) + + +async def test_light_xyy_color(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with xyy color.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_xyy = "1/1/5" + test_xyy_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_XYY_ADDRESS: test_xyy, + LightSchema.CONF_XYY_STATE_ADDRESS: test_xyy_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_xyy_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_xyy_state, (0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x03)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=204, + color_mode=COLOR_MODE_XY, + xy_color=(0.8, 0.8), + ) + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 139, ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_xyy, (179, 116, 76, 139, 139, 3)) + knx.assert_state("light.test", STATE_ON, brightness=139, xy_color=(0.701, 0.299)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + await knx.assert_write(test_xyy, (0, 0, 0, 0, 255, 1)) + knx.assert_state("light.test", STATE_ON, brightness=255, xy_color=(0.701, 0.299)) + + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_xyy, (120, 16, 63, 59, 0, 2)) + knx.assert_state("light.test", STATE_ON, brightness=255, xy_color=(0.469, 0.247)) + + # change color and brightness from KNX + await knx.receive_write(test_xyy, (0x85, 0x1E, 0x4F, 0x5C, 0x19, 0x03)) + knx.assert_state("light.test", STATE_ON, brightness=25, xy_color=(0.52, 0.31)) + # change brightness from KNX + await knx.receive_write(test_xyy, (0x00, 0x00, 0x00, 0x00, 0x80, 0x01)) + knx.assert_state("light.test", STATE_ON, brightness=128, xy_color=(0.52, 0.31)) + # change color from KNX + await knx.receive_write(test_xyy, (0x2E, 0x14, 0x40, 0x00, 0x00, 0x02)) + knx.assert_state("light.test", STATE_ON, brightness=128, xy_color=(0.18, 0.25)) + + +async def test_light_xyy_color_with_brightness(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with xyy color and explicit brightness address.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_brightness = "1/1/3" + test_brightness_state = "1/1/4" + test_xyy = "1/1/5" + test_xyy_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + LightSchema.CONF_XYY_ADDRESS: test_xyy, + LightSchema.CONF_XYY_STATE_ADDRESS: test_xyy_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_brightness_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_brightness_state, (255,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_xyy_state) + await knx.receive_response(test_xyy_state, (0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x03)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, # brightness form xyy_color ignored when extra brightness GA is used + color_mode=COLOR_MODE_XY, + xy_color=(0.8, 0.8), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_xyy, (179, 116, 76, 139, 0, 2)) + knx.assert_state("light.test", STATE_ON, brightness=255, xy_color=(0.701, 0.299)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 139}, + blocking=True, + ) + await knx.assert_write(test_brightness, (0x8B,)) + knx.assert_state("light.test", STATE_ON, brightness=139, xy_color=(0.701, 0.299)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 255, ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_xyy, (120, 16, 63, 59, 255, 3)) + # brightness relies on brightness_state GA + await knx.receive_write(test_brightness_state, (255,)) + knx.assert_state("light.test", STATE_ON, brightness=255, xy_color=(0.469, 0.247)) + + # change color and brightness from KNX + await knx.receive_write(test_xyy, (0x85, 0x1E, 0x4F, 0x5C, 0x00, 0x02)) + knx.assert_state("light.test", STATE_ON, brightness=255, xy_color=(0.52, 0.31)) + await knx.receive_write(test_brightness, (21,)) + knx.assert_state("light.test", STATE_ON, brightness=21, xy_color=(0.52, 0.31)) + + +async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with rgb color in individual GAs.""" + test_red = "1/1/3" + test_red_state = "1/1/4" + test_green = "1/1/5" + test_green_state = "1/1/6" + test_blue = "1/1/7" + test_blue_state = "1/1/8" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + LightSchema.CONF_INDIVIDUAL_COLORS: { + LightSchema.CONF_RED: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_red, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_red_state, + }, + LightSchema.CONF_GREEN: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_green, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_green_state, + }, + LightSchema.CONF_BLUE: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_blue, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_blue_state, + }, + }, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_red_state) + await knx.assert_read(test_green_state) + await knx.receive_response(test_red_state, (255,)) + await knx.receive_response(test_green_state, (255,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_blue_state) + await knx.receive_response(test_blue_state, (255,)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_RGB, + rgb_color=(255, 255, 255), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 0, 0)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await knx.assert_write(test_red, (200,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + # + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (105,)) + await knx.assert_write(test_blue, (180,)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 105, 180)) + + # turn OFF from KNX + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (0,)) + await knx.receive_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_OFF) + # turn ON from KNX + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (180,)) + await knx.receive_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + + # turn OFF from HA + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_red, (0,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_OFF) + + # turn ON from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test"}, + blocking=True, + ) + # color will not be restored - defaults to white + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (255,)) + await knx.assert_write(test_blue, (255,)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 255, 255)) + + # turn ON with brightness only from HA - defaults to white + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (0,)) + await knx.receive_write(test_blue, (0,)) + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 45}, + blocking=True, + ) + await knx.assert_write(test_red, (45,)) + await knx.assert_write(test_green, (45,)) + await knx.assert_write(test_blue, (45,)) + + +async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with rgbw color in individual GAs.""" + test_red = "1/1/3" + test_red_state = "1/1/4" + test_green = "1/1/5" + test_green_state = "1/1/6" + test_blue = "1/1/7" + test_blue_state = "1/1/8" + test_white = "1/1/9" + test_white_state = "1/1/10" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + LightSchema.CONF_INDIVIDUAL_COLORS: { + LightSchema.CONF_RED: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_red, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_red_state, + }, + LightSchema.CONF_GREEN: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_green, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_green_state, + }, + LightSchema.CONF_BLUE: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_blue, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_blue_state, + }, + LightSchema.CONF_WHITE: { + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_white, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_white_state, + }, + }, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_red_state) + await knx.assert_read(test_green_state) + await knx.receive_response(test_red_state, (0,)) + await knx.receive_response(test_green_state, (0,)) + # # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read(test_blue_state) + await knx.assert_read(test_white_state) + await knx.receive_response(test_blue_state, (0,)) + await knx.receive_response(test_white_state, (255,)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_RGBW, + rgbw_color=(0, 0, 0, 255), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgbw_color=(255, 0, 0, 0)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await knx.assert_write(test_red, (200,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (128,)) + await knx.assert_write(test_white, (178,)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + rgb_color=(255, 105, 180), + rgbw_color=(255, 0, 128, 178), + ) + + # turn OFF from KNX + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (0,)) + await knx.receive_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON) + await knx.receive_write(test_white, (0,)) + knx.assert_state("light.test", STATE_OFF) + # turn ON from KNX + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (180,)) + await knx.receive_write(test_blue, (0,)) + await knx.receive_write(test_white, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + + # turn OFF from HA + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_red, (0,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (0,)) + knx.assert_state("light.test", STATE_OFF) + + # turn ON from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test"}, + blocking=True, + ) + # color will not be restored - defaults to 100% on all channels + await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_green, (255,)) + await knx.assert_write(test_blue, (255,)) + await knx.assert_write(test_white, (255,)) + knx.assert_state( + "light.test", STATE_ON, brightness=255, rgbw_color=(255, 255, 255, 255) + ) + + # turn ON with brightness only from HA - defaults to white + await knx.receive_write(test_red, (0,)) + await knx.receive_write(test_green, (0,)) + await knx.receive_write(test_blue, (0,)) + await knx.receive_write(test_white, (0,)) + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 45}, + blocking=True, + ) + await knx.assert_write(test_red, (0,)) + await knx.assert_write(test_green, (0,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (45,)) + + +async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with rgb color.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_rgb = "1/1/5" + test_rgb_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_COLOR_ADDRESS: test_rgb, + LightSchema.CONF_COLOR_STATE_ADDRESS: test_rgb_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_rgb_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_rgb_state, (0xFF, 0xFF, 0xFF)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_mode=COLOR_MODE_RGB, + rgb_color=(255, 255, 255), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_rgb, (255, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 0, 0)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await knx.assert_write(test_rgb, (200, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + # + await knx.assert_write(test_rgb, (128, 52, 90)) + knx.assert_state("light.test", STATE_ON, brightness=128, rgb_color=(128, 52, 90)) + + # turn OFF from KNX + await knx.receive_write(test_address_state, False) + knx.assert_state("light.test", STATE_OFF) + # receive color update from KNX - still OFF + await knx.receive_write(test_rgb, (0, 180, 0)) + knx.assert_state("light.test", STATE_OFF) + # turn ON from KNX - include color update + await knx.receive_write(test_address_state, True) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + + # turn OFF from HA + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_address, False) + knx.assert_state("light.test", STATE_OFF) + + # turn ON from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test"}, + blocking=True, + ) + # color will be restored in no other state was received + await knx.assert_write(test_address, True) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + + +async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with rgbw color.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_rgbw = "1/1/5" + test_rgbw_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_RGBW_ADDRESS: test_rgbw, + LightSchema.CONF_RGBW_STATE_ADDRESS: test_rgbw_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_rgbw_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=103, + color_mode=COLOR_MODE_RGBW, + rgbw_color=(100, 101, 102, 103), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgbw_color=(255, 0, 0, 0)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (128, 0, 64, 89, 0x00, 0x0F)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=128, + rgb_color=(128, 52, 90), + rgbw_color=(128, 0, 64, 89), + ) + + # turn OFF from KNX + await knx.receive_write(test_address_state, False) + knx.assert_state("light.test", STATE_OFF) + # receive color update from KNX - still OFF + await knx.receive_write(test_rgbw, (0, 180, 0, 0, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_OFF) + # turn ON from KNX - include color update + await knx.receive_write(test_address_state, True) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + + # turn OFF from HA + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.test"}, + blocking=True, + ) + await knx.assert_write(test_address, False) + knx.assert_state("light.test", STATE_OFF) + + # turn ON from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test"}, + blocking=True, + ) + # color will be restored in no other state was received + await knx.assert_write(test_address, True) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + + +async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX light with rgbw color with dedicated brightness.""" + test_address = "1/1/1" + test_address_state = "1/1/2" + test_brightness = "1/1/3" + test_brightness_state = "1/1/4" + test_rgbw = "1/1/5" + test_rgbw_state = "1/1/6" + await knx.setup_integration( + { + LightSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_STATE_ADDRESS: test_address_state, + LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS: test_brightness_state, + LightSchema.CONF_RGBW_ADDRESS: test_rgbw, + LightSchema.CONF_RGBW_STATE_ADDRESS: test_rgbw_state, + }, + ] + } + ) + # StateUpdater initialize state + await knx.assert_read(test_address_state) + await knx.assert_read(test_brightness_state) + await knx.receive_response(test_address_state, True) + await knx.receive_response(test_brightness_state, (0x67,)) + await knx.assert_read(test_rgbw_state) + await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + + knx.assert_state( + "light.test", + STATE_ON, + brightness=103, + color_mode=COLOR_MODE_RGBW, + rgbw_color=(100, 101, 102, 103), + ) + # change color from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "red"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=103, rgbw_color=(255, 0, 0, 0)) + # # relies on dedicated brightness state + await knx.receive_write(test_brightness_state, (0xFF,)) + knx.assert_state("light.test", STATE_ON, brightness=255, rgbw_color=(255, 0, 0, 0)) + + # change brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await knx.assert_write(test_brightness, (200,)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) + # # relies on dedicated rgbw state + await knx.receive_write(test_rgbw_state, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (255, 0, 128, 178, 0x00, 0x0F)) + await knx.assert_write(test_brightness, (128,)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=128, + rgb_color=(255, 105, 180), + rgbw_color=(255, 0, 128, 178), + ) From 56b7f94bbcaf1f2c3d9edee92ffc3e00225ee603 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 4 Nov 2021 22:51:42 +0100 Subject: [PATCH 0247/1452] Add tests for KNX scene (#58900) --- tests/components/knx/test_scene.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/components/knx/test_scene.py diff --git a/tests/components/knx/test_scene.py b/tests/components/knx/test_scene.py new file mode 100644 index 00000000000..452e5347932 --- /dev/null +++ b/tests/components/knx/test_scene.py @@ -0,0 +1,44 @@ +"""Test KNX scene.""" + +from homeassistant.components.knx.const import KNX_ADDRESS +from homeassistant.components.knx.schema import SceneSchema +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) + +from .conftest import KNXTestKit + + +async def test_activate_knx_scene(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX scene.""" + await knx.setup_integration( + { + SceneSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + SceneSchema.CONF_SCENE_NUMBER: 24, + KNX_ADDRESS: "1/1/1", + CONF_ENTITY_CATEGORY: ENTITY_CATEGORY_DIAGNOSTIC, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + + registry = await async_get_entity_registry(hass) + entity = registry.async_get("scene.test") + assert entity.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entity.unique_id == "1/1/1_24" + + await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.test"}, blocking=True + ) + + # assert scene was called on bus + await knx.assert_write("1/1/1", (0x17,)) From 54e7ef08e34b03f08079f92d48e93e039380d9a0 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 4 Nov 2021 22:52:39 +0100 Subject: [PATCH 0248/1452] Add test for KNX weather entity (#58898) --- tests/components/knx/test_weather.py | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/components/knx/test_weather.py diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py new file mode 100644 index 00000000000..0fc1255bf26 --- /dev/null +++ b/tests/components/knx/test_weather.py @@ -0,0 +1,101 @@ +"""Test KNX weather.""" +from homeassistant.components.knx.schema import WeatherSchema +from homeassistant.components.weather import ( + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + + +async def test_weather(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX weather.""" + + await knx.setup_integration( + { + WeatherSchema.PLATFORM_NAME: { + CONF_NAME: "test", + WeatherSchema.CONF_KNX_WIND_ALARM_ADDRESS: "1/1/1", + WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS: "1/1/2", + WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS: "1/1/3", + WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS: "1/1/4", + WeatherSchema.CONF_KNX_BRIGHTNESS_EAST_ADDRESS: "1/1/5", + WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS: "1/1/6", + WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS: "1/1/7", + WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS: "1/1/8", + WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS: "1/1/9", + WeatherSchema.CONF_KNX_WIND_BEARING_ADDRESS: "1/1/10", + WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS: "1/1/11", + WeatherSchema.CONF_KNX_DAY_NIGHT_ADDRESS: "1/1/12", + WeatherSchema.CONF_KNX_AIR_PRESSURE_ADDRESS: "1/1/13", + } + } + ) + assert len(hass.states.async_all()) == 1 + state = hass.states.get("weather.test") + assert state.state is ATTR_CONDITION_EXCEPTIONAL + + # StateUpdater initialize states + await knx.assert_read("1/1/11") + await knx.receive_response("1/1/11", (0, 40)) + + # brightness + await knx.assert_read("1/1/6") + await knx.receive_response("1/1/6", (0x7C, 0x5E)) + await knx.assert_read("1/1/8") + await knx.receive_response("1/1/8", (0x7C, 0x5E)) + await knx.assert_read("1/1/7") + await knx.receive_response("1/1/7", (0x7C, 0x5E)) + await knx.assert_read("1/1/5") + await knx.receive_response("1/1/5", (0x7C, 0x5E)) + + # wind speed + await knx.assert_read("1/1/9") + await knx.receive_response("1/1/9", (0, 40)) + + # wind bearing + await knx.assert_read("1/1/10") + await knx.receive_response("1/1/10", (0xBF,)) + + # alarms + await knx.assert_read("1/1/2") + await knx.receive_response("1/1/2", False) + await knx.assert_read("1/1/3") + await knx.receive_response("1/1/3", False) + await knx.assert_read("1/1/1") + await knx.receive_response("1/1/1", False) + + # day night + await knx.assert_read("1/1/12") + await knx.receive_response("1/1/12", False) + + # air pressure + await knx.assert_read("1/1/13") + await knx.receive_response("1/1/13", (0x6C, 0xAD)) + + # humidity + await knx.assert_read("1/1/4") + await knx.receive_response("1/1/4", (0, 40)) + + # verify state + state = hass.states.get("weather.test") + assert state.attributes["temperature"] == 0.4 + assert state.attributes["wind_bearing"] == 270 + assert state.attributes["wind_speed"] == 1.4400000000000002 + assert state.attributes["pressure"] == 980.5824 + assert state.state is ATTR_CONDITION_SUNNY + + # update from KNX - set rain alarm + await knx.receive_write("1/1/2", True) + state = hass.states.get("weather.test") + assert state.state is ATTR_CONDITION_RAINY + + # update from KNX - set wind alarm + await knx.receive_write("1/1/2", False) + await knx.receive_write("1/1/1", True) + state = hass.states.get("weather.test") + assert state.state is ATTR_CONDITION_WINDY From fa4e8906961948936fbf10e94864bb9637d4b9ea Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 4 Nov 2021 15:56:16 -0700 Subject: [PATCH 0249/1452] Revamp nest authentication config flows and remove need for redirect urls (#59033) * Add support for Installed Auth authentication flows. Add support for additional credential types to make configuration simpler for end users. The existing Web App auth flow requires users to configure redirect urls with Google that has a very high security bar: requires ssl, and a publicly resolvable dns name. The new Installed App flow requires the user to copy/paste an access code and is the same flow used by the `google` calendar integration. This also allows us to let users create one authentication credential to use with multiple google integrations. * Remove hard migration for nest config entries, using soft migration * Add comment explaining soft migration * Revet changes to common.py made obsolete by removing migration * Reduce unnecessary diffs in nest common.py * Update config entries using library method * Run `python3 -m script.translations develop` * Revert nest auth domain * Remove compat function which is no longer needed * Remove stale nest comment * Adjust typing for python3.8 * Address PR feedback for nest auth revamp --- homeassistant/components/nest/__init__.py | 69 ++++++- homeassistant/components/nest/config_flow.py | 50 ++++- homeassistant/components/nest/const.py | 1 + homeassistant/components/nest/strings.json | 7 + .../components/nest/translations/en.json | 7 + tests/components/nest/test_config_flow_sdm.py | 179 +++++++++++++++--- 6 files changed, 269 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fce475e1788..1cbe97dc3b7 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -30,7 +30,14 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from . import api, config_flow -from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import ( + DATA_SDM, + DATA_SUBSCRIBER, + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + OOB_REDIRECT_URI, +) from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry @@ -68,6 +75,51 @@ CONFIG_SCHEMA = vol.Schema( # Platforms for SDM API PLATFORMS = ["sensor", "camera", "climate"] +WEB_AUTH_DOMAIN = DOMAIN +INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" + + +class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): + """OAuth implementation using OAuth for web applications.""" + + name = "OAuth for Web" + + def __init__( + self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str + ) -> None: + """Initialize WebAuth.""" + super().__init__( + hass, + WEB_AUTH_DOMAIN, + client_id, + client_secret, + OAUTH2_AUTHORIZE.format(project_id=project_id), + OAUTH2_TOKEN, + ) + + +class InstalledAppAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): + """OAuth implementation using OAuth for installed applications.""" + + name = "OAuth for Apps" + + def __init__( + self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str + ) -> None: + """Initialize InstalledAppAuth.""" + super().__init__( + hass, + INSTALLED_AUTH_DOMAIN, + client_id, + client_secret, + OAUTH2_AUTHORIZE.format(project_id=project_id), + OAUTH2_TOKEN, + ) + + @property + def redirect_uri(self) -> str: + """Return the redirect uri.""" + return OOB_REDIRECT_URI async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -90,13 +142,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config_flow.NestFlowHandler.register_sdm_api(hass) config_flow.NestFlowHandler.async_register_implementation( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( + InstalledAppAuth( hass, - DOMAIN, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - OAUTH2_AUTHORIZE.format(project_id=project_id), - OAUTH2_TOKEN, + project_id, + ), + ) + config_flow.NestFlowHandler.async_register_implementation( + hass, + WebAuth( + hass, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + project_id, ), ) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 189a8189e8a..ec567aaa14e 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -1,15 +1,12 @@ """Config flow to configure Nest. -This configuration flow supports two APIs: - - The new Device Access program and the Smart Device Management API - - The legacy nest API +This configuration flow supports the following: + - SDM API with Installed app flow where user enters an auth code manually + - SDM API with Web OAuth flow with redirect back to Home Assistant + - Legacy Nest API auth flow with where user enters an auth code manually NestFlowHandler is an implementation of AbstractOAuth2FlowHandler with -some overrides to support the old APIs auth flow. That is, for the new -API this class has hardly any special config other than url parameters, -and everything else custom is for the old api. When configured with the -new api via NestFlowHandler.register_sdm_api, the custom methods just -invoke the AbstractOAuth2FlowHandler methods. +some overrides to support installed app and old APIs auth flow. """ from __future__ import annotations @@ -28,7 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.json import load_json -from .const import DATA_SDM, DOMAIN, SDM_SCOPES +from .const import DATA_SDM, DOMAIN, OOB_REDIRECT_URI, SDM_SCOPES DATA_FLOW_IMPL = "nest_flow_implementation" _LOGGER = logging.getLogger(__name__) @@ -154,6 +151,14 @@ class NestFlowHandler( step_id="reauth_confirm", data_schema=vol.Schema({}), ) + existing_entries = self._async_current_entries() + if existing_entries: + # Pick an existing auth implementation for Reauth if present. Note + # only one ConfigEntry is allowed so its safe to pick the first. + entry = next(iter(existing_entries)) + if "auth_implementation" in entry.data: + data = {"implementation": entry.data["auth_implementation"]} + return await self.async_step_user(data) return await self.async_step_user() async def async_step_user( @@ -167,6 +172,33 @@ class NestFlowHandler( return await super().async_step_user(user_input) return await self.async_step_init(user_input) + async def async_step_auth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create an entry for auth.""" + if self.flow_impl.domain == "nest.installed": + # The default behavior from the parent class is to redirect the + # user with an external step. When using installed app auth, we + # instead prompt the user to sign in and copy/paste and + # authentication code back into this form. + # Note: This is similar to the Legacy API flow below, but it is + # simpler to reuse the OAuth logic in the parent class than to + # reuse SDM code with Legacy API code. + if user_input is not None: + self.external_data = { + "code": user_input["code"], + "state": {"redirect_uri": OOB_REDIRECT_URI}, + } + return await super().async_step_creation(user_input) + + result = await super().async_step_auth() + return self.async_show_form( + step_id="auth", + description_placeholders={"url": result["url"]}, + data_schema=vol.Schema({vol.Required("code"): str}), + ) + return await super().async_step_auth(user_input) + async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 3aba9ef5a7e..25b43de1032 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -16,3 +16,4 @@ SDM_SCOPES = [ "https://www.googleapis.com/auth/pubsub", ] API_URL = "https://smartdevicemanagement.googleapis.com/v1" +OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 26ec49c0d75..84cfc3435a6 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -4,6 +4,13 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "auth": { + "title": "Link Google Account", + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", + "data": { + "code": "[%key:common::config_flow::data::access_token%]" + } + }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nest integration needs to re-authenticate your account" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 4487beb0f43..be35cf1b54e 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -18,6 +18,13 @@ "unknown": "Unexpected error" }, "step": { + "auth": { + "data": { + "code": "Access Token" + }, + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", + "title": "Link Google Account" + }, "init": { "data": { "flow_impl": "Provider" diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index a8f892045f5..2b7ac71d44c 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries, setup from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -26,6 +27,12 @@ CONFIG = { "http": {"base_url": "https://example.com"}, } +ORIG_AUTH_DOMAIN = DOMAIN +WEB_AUTH_DOMAIN = DOMAIN +APP_AUTH_DOMAIN = f"{DOMAIN}.installed" +WEB_REDIRECT_URL = "https://example.com/auth/external/callback" +APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" + def get_config_entry(hass): """Return a single config entry.""" @@ -43,31 +50,65 @@ class OAuthFixture: self.hass_client = hass_client_no_auth self.aioclient_mock = aioclient_mock - async def async_oauth_flow(self, result): - """Invoke the oauth flow with fake responses.""" - state = config_entry_oauth2_flow._encode_jwt( - self.hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, + async def async_pick_flow(self, result: dict, auth_domain: str) -> dict: + """Invoke flow to puth the auth type to use for this flow.""" + assert result["type"] == "form" + assert result["step_id"] == "pick_implementation" + + return await self.hass.config_entries.flow.async_configure( + result["flow_id"], {"implementation": auth_domain} ) - oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) - assert result["type"] == "external" - assert result["url"] == ( - f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" - "+https://www.googleapis.com/auth/pubsub" - "&access_type=offline&prompt=consent" - ) + async def async_oauth_web_flow(self, result: dict) -> ConfigEntry: + """Invoke the oauth flow for Web Auth with fake responses.""" + state = self.create_state(result, WEB_REDIRECT_URL) + assert result["url"] == self.authorize_url(state, WEB_REDIRECT_URL) + # Simulate user redirect back with auth code client = await self.hass_client() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" + return await self.async_finish_flow(result) + + async def async_oauth_app_flow(self, result: dict) -> ConfigEntry: + """Invoke the oauth flow for Installed Auth with fake responses.""" + # Render form with a link to get an auth token + assert result["type"] == "form" + assert result["step_id"] == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + state = self.create_state(result, APP_REDIRECT_URL) + assert result["description_placeholders"]["url"] == self.authorize_url( + state, APP_REDIRECT_URL + ) + # Simulate user entering auth token in form + return await self.async_finish_flow(result, {"code": "abcd"}) + + def create_state(self, result: dict, redirect_url: str) -> str: + """Create state object based on redirect url.""" + return config_entry_oauth2_flow._encode_jwt( + self.hass, + { + "flow_id": result["flow_id"], + "redirect_uri": redirect_url, + }, + ) + + def authorize_url(self, state: str, redirect_url: str) -> str: + """Generate the expected authorization url.""" + oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) + return ( + f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={redirect_url}" + f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" + "+https://www.googleapis.com/auth/pubsub" + "&access_type=offline&prompt=consent" + ) + + async def async_finish_flow(self, result, user_input: dict = None) -> ConfigEntry: + """Finish the OAuth flow exchanging auth token for refresh token.""" self.aioclient_mock.post( OAUTH2_TOKEN, json={ @@ -81,8 +122,13 @@ class OAuthFixture: with patch( "homeassistant.components.nest.async_setup_entry", return_value=True ) as mock_setup: - await self.hass.config_entries.flow.async_configure(result["flow_id"]) + await self.hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) assert len(mock_setup.mock_calls) == 1 + await self.hass.async_block_till_done() + + return get_config_entry(self.hass) @pytest.fixture @@ -91,17 +137,18 @@ async def oauth(hass, hass_client_no_auth, aioclient_mock, current_request_with_ return OAuthFixture(hass, hass_client_no_auth, aioclient_mock) -async def test_full_flow(hass, oauth): +async def test_web_full_flow(hass, oauth): """Check full flow.""" assert await setup.async_setup_component(hass, DOMAIN, CONFIG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await oauth.async_oauth_flow(result) - entry = get_config_entry(hass) - assert entry.title == "Configuration.yaml" + result = await oauth.async_pick_flow(result, WEB_AUTH_DOMAIN) + + entry = await oauth.async_oauth_web_flow(result) + assert entry.title == "OAuth for Web" assert "token" in entry.data entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN @@ -113,7 +160,7 @@ async def test_full_flow(hass, oauth): } -async def test_reauth(hass, oauth): +async def test_web_reauth(hass, oauth): """Test Nest reauthentication.""" assert await setup.async_setup_component(hass, DOMAIN, CONFIG) @@ -121,7 +168,7 @@ async def test_reauth(hass, oauth): old_entry = MockConfigEntry( domain=DOMAIN, data={ - "auth_implementation": DOMAIN, + "auth_implementation": WEB_AUTH_DOMAIN, "token": { # Verify this is replaced at end of the test "access_token": "some-revoked-token", @@ -148,10 +195,9 @@ async def test_reauth(hass, oauth): # Run the oauth flow result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - await oauth.async_oauth_flow(result) + entry = await oauth.async_oauth_web_flow(result) # Verify existing tokens are replaced - entry = get_config_entry(hass) entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN assert entry.data["token"] == { @@ -160,12 +206,13 @@ async def test_reauth(hass, oauth): "type": "Bearer", "expires_in": 60, } + assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN async def test_single_config_entry(hass): """Test that only a single config entry is allowed.""" old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} ) old_entry.add_to_hass(hass) @@ -187,12 +234,12 @@ async def test_unexpected_existing_config_entries(hass, oauth): assert await setup.async_setup_component(hass, DOMAIN, CONFIG) old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} ) old_entry.add_to_hass(hass) old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} ) old_entry.add_to_hass(hass) @@ -209,7 +256,7 @@ async def test_unexpected_existing_config_entries(hass, oauth): flows = hass.config_entries.flow.async_progress() result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - await oauth.async_oauth_flow(result) + await oauth.async_oauth_web_flow(result) # Only a single entry now exists, and the other was cleaned up entries = hass.config_entries.async_entries(DOMAIN) @@ -223,3 +270,75 @@ async def test_unexpected_existing_config_entries(hass, oauth): "type": "Bearer", "expires_in": 60, } + + +async def test_app_full_flow(hass, oauth, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + + entry = await oauth.async_oauth_app_flow(result) + assert entry.title == "OAuth for Apps" + assert "token" in entry.data + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_app_reauth(hass, oauth): + """Test Nest reauthentication for Installed App Auth.""" + + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + old_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": APP_AUTH_DOMAIN, + "token": { + # Verify this is replaced at end of the test + "access_token": "some-revoked-token", + }, + "sdm": {}, + }, + unique_id=DOMAIN, + ) + old_entry.add_to_hass(hass) + + entry = get_config_entry(hass) + assert entry.data["token"] == { + "access_token": "some-revoked-token", + } + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data + ) + + # Advance through the reauth flow + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + # Run the oauth flow + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + await oauth.async_oauth_app_flow(result) + + # Verify existing tokens are replaced + entry = get_config_entry(hass) + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN From c8d8513a1a44268ed34f68df099e5019360206fc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 5 Nov 2021 00:13:45 +0000 Subject: [PATCH 0250/1452] [ci skip] Translation update --- .../amberelectric/translations/ja.json | 12 ++- .../aurora_abb_powerone/translations/he.json | 3 + .../binary_sensor/translations/pt-BR.json | 13 +++ .../components/button/translations/ca.json | 11 +++ .../components/button/translations/de.json | 11 +++ .../components/button/translations/et.json | 11 +++ .../components/button/translations/hu.json | 11 +++ .../components/button/translations/it.json | 11 +++ .../components/button/translations/pt-BR.json | 11 +++ .../components/button/translations/ru.json | 11 +++ .../button/translations/zh-Hant.json | 11 +++ .../components/cloud/translations/ja.json | 7 ++ .../crownstone/translations/ja.json | 79 +++++++++++++++++++ .../devolo_home_network/translations/ja.json | 5 ++ .../translations/pt-BR.json | 25 ++++++ .../components/dlna_dmr/translations/ja.json | 18 ++++- .../components/esphome/translations/ja.json | 3 + .../components/flux_led/translations/ja.json | 3 + .../homeassistant/translations/ja.json | 7 ++ .../components/homekit/translations/ja.json | 11 +++ .../components/iotawatt/translations/ja.json | 11 +++ .../components/lookin/translations/ja.json | 10 +++ .../modem_callerid/translations/ja.json | 10 +++ .../motioneye/translations/pt-BR.json | 9 +++ .../netatmo/translations/pt-BR.json | 9 +++ .../components/netgear/translations/ja.json | 30 +++++++ .../components/octoprint/translations/ja.json | 5 +- .../opengarage/translations/ja.json | 4 +- .../components/renault/translations/ja.json | 19 +++++ .../components/ridwell/translations/ja.json | 7 ++ .../ridwell/translations/pt-BR.json | 28 +++++++ .../components/sense/translations/pt-BR.json | 7 ++ .../components/shelly/translations/ja.json | 14 ++++ .../components/switchbot/translations/ja.json | 7 ++ .../synology_dsm/translations/ja.json | 12 +++ .../components/tplink/translations/ja.json | 14 ++++ .../tuya/translations/select.ja.json | 11 ++- .../uptimerobot/translations/ja.json | 18 +++++ .../components/venstar/translations/ja.json | 6 ++ .../components/watttime/translations/ja.json | 12 +++ .../xiaomi_miio/translations/select.ja.json | 9 +++ .../components/zwave_js/translations/ja.json | 8 +- 42 files changed, 526 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/button/translations/ca.json create mode 100644 homeassistant/components/button/translations/de.json create mode 100644 homeassistant/components/button/translations/et.json create mode 100644 homeassistant/components/button/translations/hu.json create mode 100644 homeassistant/components/button/translations/it.json create mode 100644 homeassistant/components/button/translations/pt-BR.json create mode 100644 homeassistant/components/button/translations/ru.json create mode 100644 homeassistant/components/button/translations/zh-Hant.json create mode 100644 homeassistant/components/cloud/translations/ja.json create mode 100644 homeassistant/components/crownstone/translations/ja.json create mode 100644 homeassistant/components/devolo_home_network/translations/pt-BR.json create mode 100644 homeassistant/components/homeassistant/translations/ja.json create mode 100644 homeassistant/components/homekit/translations/ja.json create mode 100644 homeassistant/components/iotawatt/translations/ja.json create mode 100644 homeassistant/components/modem_callerid/translations/ja.json create mode 100644 homeassistant/components/netatmo/translations/pt-BR.json create mode 100644 homeassistant/components/netgear/translations/ja.json create mode 100644 homeassistant/components/renault/translations/ja.json create mode 100644 homeassistant/components/ridwell/translations/pt-BR.json create mode 100644 homeassistant/components/shelly/translations/ja.json create mode 100644 homeassistant/components/switchbot/translations/ja.json create mode 100644 homeassistant/components/synology_dsm/translations/ja.json create mode 100644 homeassistant/components/uptimerobot/translations/ja.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.ja.json diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json index 03e4bef5c4b..1fc5f6c58c1 100644 --- a/homeassistant/components/amberelectric/translations/ja.json +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -1,10 +1,20 @@ { "config": { "step": { + "site": { + "data": { + "site_name": "\u30b5\u30a4\u30c8\u540d", + "site_nmi": "\u30b5\u30a4\u30c8NMI" + }, + "description": "\u8ffd\u52a0\u3057\u305f\u3044\u30b5\u30a4\u30c8\u306eNMI\u3092\u9078\u629e", + "title": "Amber Electric" + }, "user": { "data": { - "api_token": "API\u30c8\u30fc\u30af\u30f3" + "api_token": "API\u30c8\u30fc\u30af\u30f3", + "site_id": "\u30b5\u30a4\u30c8ID" }, + "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u305f\u3081\u306b {api_url} \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059", "title": "Amber Electric" } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/he.json b/homeassistant/components/aurora_abb_powerone/translations/he.json index 822dcf2be14..ea40181bd9a 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/he.json +++ b/homeassistant/components/aurora_abb_powerone/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" } diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 385d8620d76..711ac35f9f6 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -9,6 +9,19 @@ "no_motion": "{entity_name} parou de detectar movimento" } }, + "device_class": { + "cold": "frio", + "gas": "g\u00e1s", + "heat": "calor", + "moisture": "umidade", + "motion": "movimento", + "occupancy": "presen\u00e7a", + "power": "energia", + "problem": "problema", + "smoke": "fuma\u00e7a", + "sound": "som", + "vibration": "vibra\u00e7\u00e3o" + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/button/translations/ca.json b/homeassistant/components/button/translations/ca.json new file mode 100644 index 00000000000..376570dd7be --- /dev/null +++ b/homeassistant/components/button/translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Prem el bot\u00f3 {entity_name}" + }, + "trigger_type": { + "pressed": "S'ha premut {entity_name}" + } + }, + "title": "Bot\u00f3" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/de.json b/homeassistant/components/button/translations/de.json new file mode 100644 index 00000000000..26d651ea1f0 --- /dev/null +++ b/homeassistant/components/button/translations/de.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Dr\u00fccke die {entity_name} Taste" + }, + "trigger_type": { + "pressed": "{entity_name} wurde gedr\u00fcckt" + } + }, + "title": "Taste" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/et.json b/homeassistant/components/button/translations/et.json new file mode 100644 index 00000000000..b5d5cec992b --- /dev/null +++ b/homeassistant/components/button/translations/et.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Vajuta nuppu {entity_name}" + }, + "trigger_type": { + "pressed": "Vajutati nuppu {entity_name}" + } + }, + "title": "Nupp" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/hu.json b/homeassistant/components/button/translations/hu.json new file mode 100644 index 00000000000..4a3e9274623 --- /dev/null +++ b/homeassistant/components/button/translations/hu.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Nyomja meg: {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} megnyomva" + } + }, + "title": "Gomb" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/it.json b/homeassistant/components/button/translations/it.json new file mode 100644 index 00000000000..9cfc538a5ce --- /dev/null +++ b/homeassistant/components/button/translations/it.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Premi il pulsante {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} \u00e8 stato premuto" + } + }, + "title": "Pulsante" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/pt-BR.json b/homeassistant/components/button/translations/pt-BR.json new file mode 100644 index 00000000000..840bc6ddcc2 --- /dev/null +++ b/homeassistant/components/button/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Pressione o bot\u00e3o {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} foi pressionado" + } + }, + "title": "Bot\u00e3o" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/ru.json b/homeassistant/components/button/translations/ru.json new file mode 100644 index 00000000000..187f3846442 --- /dev/null +++ b/homeassistant/components/button/translations/ru.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u041d\u0430\u0436\u0430\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 {entity_name}" + }, + "trigger_type": { + "pressed": "\u041d\u0430\u0436\u0430\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 {entity_name}" + } + }, + "title": "\u041a\u043d\u043e\u043f\u043a\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/zh-Hant.json b/homeassistant/components/button/translations/zh-Hant.json new file mode 100644 index 00000000000..e91b342dd44 --- /dev/null +++ b/homeassistant/components/button/translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u6309\u4e0b {entity_name} \u6309\u9215" + }, + "trigger_type": { + "pressed": "{entity_name} \u5df2\u6309\u4e0b" + } + }, + "title": "\u6309\u9215" +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json new file mode 100644 index 00000000000..5cd3600a13a --- /dev/null +++ b/homeassistant/components/cloud/translations/ja.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "remote_server": "\u30ea\u30e2\u30fc\u30c8\u30b5\u30fc\u30d0\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json new file mode 100644 index 00000000000..848514c7f7d --- /dev/null +++ b/homeassistant/components/crownstone/translations/ja.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "usb_setup_complete": "Crownstone USB\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u304c\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002", + "usb_setup_unsuccessful": "Crownstone USB\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "account_not_verified": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002Crownstone\u304b\u3089\u306e\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30b7\u30e7\u30f3\u30e1\u30fc\u30eb\u3092\u901a\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "usb_config": { + "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + }, + "usb_manual_config": { + "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "Email", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "Crownstone\u30a2\u30ab\u30a6\u30f3\u30c8" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere", + "use_usb_option": "\u30ed\u30fc\u30ab\u30eb\u30c7\u30fc\u30bf\u306e\u9001\u4fe1\u306b\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3092\u4f7f\u7528\u3059\u308b" + } + }, + "usb_config": { + "data": { + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", + "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + }, + "usb_config_option": { + "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", + "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" + }, + "usb_manual_config_option": { + "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index c3eb278d18f..c95621b3cc7 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -5,6 +5,11 @@ }, "flow_title": "{product} ({name})", "step": { + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "zeroconf_confirm": { "description": "\u30db\u30b9\u30c8\u540d\u304c `{host_name}` \u306e devolo\u793e\u306e\u30db\u30fc\u30e0\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "devolo\u793e\u306e\u30db\u30fc\u30e0\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u6a5f\u5668\u3092\u767a\u898b" diff --git a/homeassistant/components/devolo_home_network/translations/pt-BR.json b/homeassistant/components/devolo_home_network/translations/pt-BR.json new file mode 100644 index 00000000000..edffd23f3af --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado", + "home_control": "A Unidade Central de Home Control Devolo n\u00e3o funciona com esta integra\u00e7\u00e3o." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "zeroconf_confirm": { + "description": "Deseja adicionar o dispositivo de rede dom\u00e9stica Devolo com o nome \"{host_name}\" ao Home Assistant?", + "title": "Dispositivo de rede dom\u00e9stica Devolo descoberto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 65a786b1368..6c041c865da 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -1,7 +1,16 @@ { "config": { "abort": { - "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059" + "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "discovery_error": "\u4e00\u81f4\u3059\u308b DLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", + "incomplete_config": "\u69cb\u6210\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", + "non_unique_id": "\u540c\u4e00\u306eID\u3067\u8907\u6570\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", + "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" + }, + "error": { + "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, "flow_title": "{name}", "step": { @@ -9,11 +18,15 @@ "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u3092\u5165\u308c\u3001\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u79fb\u884c\u3092\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044" }, "manual": { + "data": { + "url": "URL" + }, "description": "\u30c7\u30d0\u30a4\u30b9\u8a18\u8ff0XML\u30d5\u30a1\u30a4\u30eb\u3078\u306eURL", "title": "\u624b\u52d5\u3067DLNA DMR\u6a5f\u5668\u306b\u63a5\u7d9a" }, "user": { "data": { + "host": "\u30db\u30b9\u30c8", "url": "URL" }, "description": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066URL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", @@ -31,7 +44,8 @@ "callback_url_override": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u306e\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL", "listen_port": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u30dd\u30fc\u30c8(\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u30e9\u30f3\u30c0\u30e0)", "poll_availability": "\u30c7\u30d0\u30a4\u30b9\u306e\u53ef\u7528\u6027\u3092\u30dd\u30fc\u30ea\u30f3\u30b0" - } + }, + "title": "DLNA Digital Media Renderer\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 1205652aff2..4e980989720 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -13,6 +13,9 @@ "description": "ESPHome\u306e\u30ce\u30fc\u30c9 `{name}` \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "\u691c\u51fa\u3055\u308c\u305fESPHome\u306e\u30ce\u30fc\u30c9" }, + "reauth_confirm": { + "description": "ESPHome\u30c7\u30d0\u30a4\u30b9 {name} \u3001\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u3092\u6709\u52b9\u306b\u3057\u305f\u304b\u3001\u6697\u53f7\u5316\u306e\u30ad\u30fc\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f\u3002\u66f4\u65b0\u3055\u308c\u305f\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "description": "\u3042\u306a\u305f\u306e[ESPHome](https://esphomelib.com/)\u306e\u30ce\u30fc\u30c9\u306e\u63a5\u7d9a\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/flux_led/translations/ja.json b/homeassistant/components/flux_led/translations/ja.json index 4fa710d1331..45e7d8a606b 100644 --- a/homeassistant/components/flux_led/translations/ja.json +++ b/homeassistant/components/flux_led/translations/ja.json @@ -6,6 +6,9 @@ "description": "{model} {id} ({ipaddr}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json new file mode 100644 index 00000000000..0504d2f2ad5 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "user": "\u30e6\u30fc\u30b6\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json new file mode 100644 index 00000000000..35ada78aa74 --- /dev/null +++ b/homeassistant/components/homekit/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "cameras": { + "data": { + "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json new file mode 100644 index 00000000000..1c43df55349 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/ja.json b/homeassistant/components/lookin/translations/ja.json index 8282b91ccd7..e4ac6a11f08 100644 --- a/homeassistant/components/lookin/translations/ja.json +++ b/homeassistant/components/lookin/translations/ja.json @@ -2,8 +2,18 @@ "config": { "flow_title": "{name} ({host})", "step": { + "device_name": { + "data": { + "name": "\u540d\u524d" + } + }, "discovery_confirm": { "description": "{name} ({host}) \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" + } } } } diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json new file mode 100644 index 00000000000..c928c7e76c3 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", + "title": "Phone\u30e2\u30c7\u30e0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pt-BR.json b/homeassistant/components/motioneye/translations/pt-BR.json index cbc33e7c1c4..ec20df02074 100644 --- a/homeassistant/components/motioneye/translations/pt-BR.json +++ b/homeassistant/components/motioneye/translations/pt-BR.json @@ -8,5 +8,14 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" } + }, + "options": { + "step": { + "init": { + "data": { + "stream_url_template": "Modelo de URL de fluxo" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pt-BR.json b/homeassistant/components/netatmo/translations/pt-BR.json new file mode 100644 index 00000000000..77e55a889c4 --- /dev/null +++ b/homeassistant/components/netatmo/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json new file mode 100644 index 00000000000..65132c94377 --- /dev/null +++ b/homeassistant/components/netgear/translations/ja.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "config": "\u63a5\u7d9a\u307e\u305f\u306f\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc : \u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d (\u30aa\u30d7\u30b7\u30e7\u30f3)" + }, + "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u30db\u30fc\u30e0\u30bf\u30a4\u30e0\u3092\u8003\u616e\u3059\u308b(\u79d2)" + }, + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json index e3cb8eccc81..f42ab595ab6 100644 --- a/homeassistant/components/octoprint/translations/ja.json +++ b/homeassistant/components/octoprint/translations/ja.json @@ -3,15 +3,18 @@ "abort": { "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, + "flow_title": "OctoPrint Printer: {host}", "progress": { "get_api_key": "OctoPrint UI\u3092\u958b\u304d\u3001Home Assistant\u306e\u30a2\u30af\u30bb\u30b9\u30ea\u30af\u30a8\u30b9\u30c8\u3067\u3092 '\u8a31\u53ef' \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, "step": { "user": { "data": { + "host": "\u30db\u30b9\u30c8", "path": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30d1\u30b9", "port": "\u30dd\u30fc\u30c8\u756a\u53f7", - "ssl": "SSL\u3092\u4f7f\u7528" + "ssl": "SSL\u3092\u4f7f\u7528", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/opengarage/translations/ja.json b/homeassistant/components/opengarage/translations/ja.json index 83f7d96e874..91393016e27 100644 --- a/homeassistant/components/opengarage/translations/ja.json +++ b/homeassistant/components/opengarage/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "device_key": "\u30c7\u30d0\u30a4\u30b9\u30ad\u30fc" + "device_key": "\u30c7\u30d0\u30a4\u30b9\u30ad\u30fc", + "host": "\u30dd\u30fc\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json new file mode 100644 index 00000000000..3415f92f0c9 --- /dev/null +++ b/homeassistant/components/renault/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user": { + "data": { + "locale": "\u30ed\u30b1\u30fc\u30eb", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "title": "\u30eb\u30ce\u30fc\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json index f802ddc0ab0..c12ffee267d 100644 --- a/homeassistant/components/ridwell/translations/ja.json +++ b/homeassistant/components/ridwell/translations/ja.json @@ -2,9 +2,16 @@ "config": { "step": { "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } } diff --git a/homeassistant/components/ridwell/translations/pt-BR.json b/homeassistant/components/ridwell/translations/pt-BR.json new file mode 100644 index 00000000000..befa822057f --- /dev/null +++ b/homeassistant/components/ridwell/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi feita com sucesso" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Por favor, digite novamente a senha para {username}:", + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + }, + "description": "Digite seu nome de usu\u00e1rio e senha:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pt-BR.json b/homeassistant/components/sense/translations/pt-BR.json index d04d91c034b..b61651bf441 100644 --- a/homeassistant/components/sense/translations/pt-BR.json +++ b/homeassistant/components/sense/translations/pt-BR.json @@ -7,6 +7,13 @@ "cannot_connect": "Falha ao conectar, tente novamente", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "timeout": "Tempo limite" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json new file mode 100644 index 00000000000..c6021ac5b23 --- /dev/null +++ b/homeassistant/components/shelly/translations/ja.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "trigger_subtype": { + "button4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + }, + "trigger_type": { + "btn_down": "{subtype} button down", + "btn_up": "{subtype} button up", + "double_push": "{subtype} double push", + "long_push": "{subtype} long push", + "single_push": "{subtype} single push" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json new file mode 100644 index 00000000000..76788c484fe --- /dev/null +++ b/homeassistant/components/switchbot/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json new file mode 100644 index 00000000000..1b69cbdf7c8 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 28f8763eea2..58fbc868d85 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -3,6 +3,20 @@ "step": { "confirm": { "description": "TP-Link\u30b9\u30de\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "discovery_confirm": { + "description": "{name} {model} ({host}) \u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u884c\u3044\u307e\u3059\u304b\uff1f" + }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index 9607d436a15..fcc3466bf62 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -6,7 +6,9 @@ "2": "60 Hz" }, "tuya__basic_nightvision": { - "0": "\u81ea\u52d5" + "0": "\u81ea\u52d5", + "1": "\u30aa\u30d5", + "2": "\u30aa\u30f3" }, "tuya__decibel_sensitivity": { "0": "\u4f4e\u611f\u5ea6", @@ -22,6 +24,7 @@ "led": "LED" }, "tuya__light_mode": { + "none": "\u30aa\u30d5", "pos": "\u30b9\u30a4\u30c3\u30c1\u306e\u4f4d\u7f6e\u3092\u793a\u3059", "relay": "\u30b9\u30a4\u30c3\u30c1\u306e\u30aa\u30f3/\u30aa\u30d5\u72b6\u614b\u3092\u793a\u3059" }, @@ -36,7 +39,11 @@ }, "tuya__relay_status": { "last": "\u6700\u5f8c\u306e\u72b6\u614b\u3092\u8a18\u61b6", - "memory": "\u6700\u5f8c\u306e\u72b6\u614b\u3092\u8a18\u61b6" + "memory": "\u6700\u5f8c\u306e\u72b6\u614b\u3092\u8a18\u61b6", + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3", + "power_off": "\u30aa\u30d5", + "power_on": "\u30aa\u30f3" } } } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json new file mode 100644 index 00000000000..210be855fc5 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "error": { + "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u65e2\u5b58\u306e\u69cb\u6210\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" + }, + "step": { + "reauth_confirm": { + "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, + "user": { + "description": "UptimeRobot\u304b\u3089\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/ja.json b/homeassistant/components/venstar/translations/ja.json index 2c30e8f5d24..4417972a338 100644 --- a/homeassistant/components/venstar/translations/ja.json +++ b/homeassistant/components/venstar/translations/ja.json @@ -2,6 +2,12 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "pin": "PIN\u30b3\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, "title": "Venstar\u793e\u306e\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 324ab6d1e3b..d0b14d727aa 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -1,8 +1,20 @@ { "config": { + "error": { + "unknown_coordinates": "\u7def\u5ea6/\u7d4c\u5ea6\u306e\u30c7\u30fc\u30bf\u306a\u3057" + }, "step": { + "coordinates": { + "description": "\u76e3\u8996\u3059\u308b\u7def\u5ea6(latitude)\u3068\u7d4c\u5ea6(longitude )\u3092\u5165\u529b:" + }, + "location": { + "description": "\u76e3\u8996\u3059\u308b\u5834\u6240\u3092\u9078\u629e:" + }, "reauth_confirm": { "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + }, + "user": { + "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/select.ja.json b/homeassistant/components/xiaomi_miio/translations/select.ja.json new file mode 100644 index 00000000000..22a7a4ea058 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "\u660e\u308b\u3044", + "dim": "\u8584\u6697\u3044", + "off": "\u30aa\u30d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 793a886b00b..42579491c85 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -13,7 +13,13 @@ }, "device_automation": { "action_type": { - "clear_lock_usercode": "{entity_name} \u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u30af\u30ea\u30a2" + "clear_lock_usercode": "{entity_name} \u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u30af\u30ea\u30a2", + "ping": "ping\u30c7\u30d0\u30a4\u30b9", + "refresh_value": "{entity_name} \u306e\u5024\u3092\u66f4\u65b0", + "reset_meter": "{subtype} \u306e\u30e1\u30fc\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8", + "set_config_parameter": "\u8a2d\u5b9a\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u3092\u8a2d\u5b9a", + "set_lock_usercode": "{entity_name} \u306b\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u8a2d\u5b9a", + "set_value": "Z-Wave\u5024\u306e\u8a2d\u5b9a\u5024" } }, "options": { From e57c60bb2fb36d79580f48906e0bb0f833566254 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Nov 2021 22:10:47 -0500 Subject: [PATCH 0251/1452] Bump flux_led to 0.24.14 (#59121) --- homeassistant/components/flux_led/__init__.py | 2 +- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 248ce7261e9..717a3c9e2b0 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -182,7 +182,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=self.device.ipaddr, - update_interval=timedelta(seconds=5), + update_interval=timedelta(seconds=10), # We don't want an immediate refresh since the device # takes a moment to reflect the state change request_refresh_debouncer=Debouncer( diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4b5a63542c9..d0134d07f79 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.13"], + "requirements": ["flux_led==0.24.14"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5990fff3346..5be24b23093 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.13 +flux_led==0.24.14 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46d4e4c26ba..cb32a187e92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.13 +flux_led==0.24.14 # homeassistant.components.homekit fnvhash==0.1.0 From ca00551e4f7e086e93536377f50e40246958d39e Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 4 Nov 2021 23:11:22 -0400 Subject: [PATCH 0252/1452] Environment Canada config_flow fix (#59029) --- .../environment_canada/config_flow.py | 11 +++++----- .../environment_canada/test_config_flow.py | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index e1eda36c345..683f312d9d5 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -20,13 +20,12 @@ async def validate_input(data): lat = data.get(CONF_LATITUDE) lon = data.get(CONF_LONGITUDE) station = data.get(CONF_STATION) - lang = data.get(CONF_LANGUAGE) + lang = data.get(CONF_LANGUAGE).lower() - weather_data = ECWeather( - station_id=station, - coordinates=(lat, lon), - language=lang.lower(), - ) + if station: + weather_data = ECWeather(station_id=station, language=lang) + else: + weather_data = ECWeather(coordinates=(lat, lon), language=lang) await weather_data.update() if lat is None or lon is None: diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index f2ebb48346c..2614778f9b4 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -123,7 +123,24 @@ async def test_exception_handling(hass, error): assert result["errors"] == {"base": base_error} -async def test_lat_or_lon_not_specified(hass): +async def test_import_station_not_specified(hass): + """Test that the import step works.""" + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + fake_config = dict(FAKE_CONFIG) + del fake_config[CONF_STATION] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE + + +async def test_import_lat_lon_not_specified(hass): """Test that the import step works.""" with mocked_ec(), patch( "homeassistant.components.environment_canada.async_setup_entry", @@ -131,6 +148,7 @@ async def test_lat_or_lon_not_specified(hass): ): fake_config = dict(FAKE_CONFIG) del fake_config[CONF_LATITUDE] + del fake_config[CONF_LONGITUDE] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config ) From dc1edc98fc009e9097c7ce4beab4a0afb5565443 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Nov 2021 04:12:21 +0100 Subject: [PATCH 0253/1452] Remove unused KeywordStyleAdapter (#59091) --- homeassistant/helpers/logging.py | 58 -------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 homeassistant/helpers/logging.py diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py deleted file mode 100644 index e996e7cca10..00000000000 --- a/homeassistant/helpers/logging.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Helpers for logging allowing more advanced logging styles to be used.""" -from __future__ import annotations - -from collections.abc import Mapping, MutableMapping -import inspect -import logging -from typing import Any - - -class KeywordMessage: - """ - Represents a logging message with keyword arguments. - - Adapted from: https://stackoverflow.com/a/24683360/2267718 - """ - - def __init__(self, fmt: Any, args: Any, kwargs: Mapping[str, Any]) -> None: - """Initialize a new KeywordMessage object.""" - self._fmt = fmt - self._args = args - self._kwargs = kwargs - - def __str__(self) -> str: - """Convert the object to a string for logging.""" - return str(self._fmt).format(*self._args, **self._kwargs) - - -class KeywordStyleAdapter(logging.LoggerAdapter): - """Represents an adapter wrapping the logger allowing KeywordMessages.""" - - def __init__( - self, logger: logging.Logger, extra: Mapping[str, Any] | None = None - ) -> None: - """Initialize a new StyleAdapter for the provided logger.""" - super().__init__(logger, extra or {}) - - def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: - """Log the message provided at the appropriate level.""" - if self.isEnabledFor(level): - msg, log_kwargs = self.process(msg, kwargs) - self.logger._log( # pylint: disable=protected-access - level, KeywordMessage(msg, args, kwargs), (), **log_kwargs - ) - - def process( - self, msg: Any, kwargs: MutableMapping[str, Any] - ) -> tuple[Any, MutableMapping[str, Any]]: - """Process the keyword args in preparation for logging.""" - return ( - msg, - { - k: kwargs[k] - for k in inspect.getfullargspec( - self.logger._log # pylint: disable=protected-access - ).args[1:] - if k in kwargs - }, - ) From 185f7beafc05fc355109fd417350591459650366 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Nov 2021 04:21:38 +0100 Subject: [PATCH 0254/1452] Improve recorder migration tests (#59075) --- tests/components/recorder/common.py | 4 +- ...{models_original.py => models_schema_0.py} | 0 tests/components/recorder/models_schema_16.py | 457 ++++++++++++++ tests/components/recorder/models_schema_18.py | 471 ++++++++++++++ tests/components/recorder/models_schema_22.py | 593 ++++++++++++++++++ tests/components/recorder/test_migrate.py | 65 +- 6 files changed, 1585 insertions(+), 5 deletions(-) rename tests/components/recorder/{models_original.py => models_schema_0.py} (100%) create mode 100644 tests/components/recorder/models_schema_16.py create mode 100644 tests/components/recorder/models_schema_18.py create mode 100644 tests/components/recorder/models_schema_22.py diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index b21d671560c..d5c9c4f8762 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, fire_time_changed -from tests.components.recorder import models_original +from tests.components.recorder import models_schema_0 DEFAULT_PURGE_TASKS = 3 @@ -91,5 +91,5 @@ def create_engine_test(*args, **kwargs): This simulates an existing db with the old schema. """ engine = create_engine(*args, **kwargs) - models_original.Base.metadata.create_all(engine) + models_schema_0.Base.metadata.create_all(engine) return engine diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_schema_0.py similarity index 100% rename from tests/components/recorder/models_original.py rename to tests/components/recorder/models_schema_0.py diff --git a/tests/components/recorder/models_schema_16.py b/tests/components/recorder/models_schema_16.py new file mode 100644 index 00000000000..23b0ec1f921 --- /dev/null +++ b/tests/components/recorder/models_schema_16.py @@ -0,0 +1,457 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 16, +used by Home Assistant Core 2021.6.0, which was the initial version +to include long term statistics. +It is used to test the schema migration logic. +""" + +import json +import logging + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 16 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, +] + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) + + +class Events(Base): # type: ignore + """Event history data.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event, event_data=None): + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=event_data or json.dumps(event.data, cls=JSONEncoder), + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id=True): + """Convert to a natve HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore + """State change history.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state = event.data.get("new_state") + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = "" + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = "{}" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps(dict(state.attributes), cls=JSONEncoder) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id=True): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + json.loads(self.attributes), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class Statistics(Base): # type: ignore + """Statistics.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_STATISTICS + id = Column(Integer, primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + source = Column(String(32)) + statistic_id = Column(String(255)) + start = Column(DATETIME_TYPE, index=True) + mean = Column(Float()) + min = Column(Float()) + max = Column(Float()) + last_reset = Column(DATETIME_TYPE) + state = Column(Float()) + sum = Column(Float()) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "statistic_id", "start"), + ) + + @staticmethod + def from_stats(source, statistic_id, start, stats): + """Create object from a statistics.""" + return Statistics( + source=source, + statistic_id=statistic_id, + start=start, + **stats, + ) + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id=True): + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +def process_timestamp(ts): + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +def process_timestamp_to_utc_isoformat(ts): + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/models_schema_18.py b/tests/components/recorder/models_schema_18.py new file mode 100644 index 00000000000..3eeebc8e649 --- /dev/null +++ b/tests/components/recorder/models_schema_18.py @@ -0,0 +1,471 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 18, +used by Home Assistant Core 2021.7.0, which did a major refactoring +of long term statistics database models. +It is used to test the schema migration logic. +""" + +import json +import logging + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 18 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, +] + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) + + +class Events(Base): # type: ignore + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event, event_data=None): + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=event_data or json.dumps(event.data, cls=JSONEncoder), + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id=True): + """Convert to a natve HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state = event.data.get("new_state") + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = "" + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = "{}" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps(dict(state.attributes), cls=JSONEncoder) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id=True): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + json.loads(self.attributes), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class Statistics(Base): # type: ignore + """Statistics.""" + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS + id = Column(Integer, primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + metadata_id = Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + start = Column(DATETIME_TYPE, index=True) + mean = Column(Float()) + min = Column(Float()) + max = Column(Float()) + last_reset = Column(DATETIME_TYPE) + state = Column(Float()) + sum = Column(Float()) + + @staticmethod + def from_stats(metadata_id, start, stats): + """Create object from a statistics.""" + return Statistics( + metadata_id=metadata_id, + start=start, + **stats, + ) + + +class StatisticsMeta(Base): # type: ignore + """Statistics meta data.""" + + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + + @staticmethod + def from_meta(source, statistic_id, unit_of_measurement, has_mean, has_sum): + """Create object from meta data.""" + return StatisticsMeta( + source=source, + statistic_id=statistic_id, + unit_of_measurement=unit_of_measurement, + has_mean=has_mean, + has_sum=has_sum, + ) + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id=True): + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +def process_timestamp(ts): + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +def process_timestamp_to_utc_isoformat(ts): + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/models_schema_22.py b/tests/components/recorder/models_schema_22.py new file mode 100644 index 00000000000..3bcef248e0f --- /dev/null +++ b/tests/components/recorder/models_schema_22.py @@ -0,0 +1,593 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 22, +used by Home Assistant Core 2021.10.0, which adds a table for +5-minute statistics. +It is used to test the schema migration logic. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime, timedelta +import json +import logging +from typing import TypedDict, overload + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 22 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event, event_data=None): + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=event_data + or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id=True): + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state = event.data.get("new_state") + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = "" + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = "{}" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps( + dict(state.attributes), cls=JSONEncoder, separators=(",", ":") + ) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id=True): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + json.loads(self.attributes), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: Iterable[StatisticData] + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr + def metadata_id(self): + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData): + """Create object from a statistics.""" + return cls( # type: ignore + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_short_term_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + statistic_id: str + unit_of_measurement: str | None + has_mean: bool + has_sum: bool + + +class StatisticsMeta(Base): # type: ignore + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + + @staticmethod + def from_meta( + source: str, + statistic_id: str, + unit_of_measurement: str | None, + has_mean: bool, + has_sum: bool, + ) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta( + source=source, + statistic_id=statistic_id, + unit_of_measurement=unit_of_measurement, + has_mean=has_mean, + has_sum=has_sum, + ) + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id=True): + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 562935e78a1..e3cf8fc3527 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,7 +1,10 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access import datetime +import importlib import sqlite3 +import sys +import threading from unittest.mock import ANY, Mock, PropertyMock, call, patch import pytest @@ -222,7 +225,8 @@ async def test_events_during_migration_queue_exhausted(hass): assert len(db_states) == 2 -async def test_schema_migrate(hass): +@pytest.mark.parametrize("start_version", [0, 16, 18, 22]) +async def test_schema_migrate(hass, start_version): """Test the full schema migration logic. We're just testing that the logic can execute successfully here without @@ -230,21 +234,76 @@ async def test_schema_migrate(hass): inspection could quickly become quite cumbersome. """ + migration_done = threading.Event() + migration_stall = threading.Event() + migration_version = None + real_migration = recorder.migration.migrate_schema + + def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + module = f"tests.components.recorder.models_schema_{str(start_version)}" + importlib.import_module(module) + old_models = sys.modules[module] + engine = create_engine(*args, **kwargs) + old_models.Base.metadata.create_all(engine) + if start_version > 0: + with Session(engine) as session: + session.add(recorder.models.SchemaChanges(schema_version=start_version)) + session.commit() + return engine + def _mock_setup_run(self): self.run_info = RecorderRuns( start=self.recording_start, created=dt_util.utcnow() ) - with patch("sqlalchemy.create_engine", new=create_engine_test), patch( + def _instrument_migration(*args): + """Control migration progress and check results.""" + nonlocal migration_done + nonlocal migration_version + nonlocal migration_stall + migration_stall.wait() + try: + real_migration(*args) + except Exception: + migration_done.set() + raise + + # Check and report the outcome of the migration; if migration fails + # the recorder will silently create a new database. + with session_scope(hass=hass) as session: + res = ( + session.query(models.SchemaChanges) + .order_by(models.SchemaChanges.change_id.desc()) + .first() + ) + migration_version = res.schema_version + migration_done.set() + + with patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ), patch( "homeassistant.components.recorder.Recorder._setup_run", side_effect=_mock_setup_run, autospec=True, - ) as setup_run: + ) as setup_run, patch( + "homeassistant.components.recorder.migration.migrate_schema", + wraps=_instrument_migration, + ): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) + assert await recorder.async_migration_in_progress(hass) is True + migration_stall.set() await hass.async_block_till_done() + migration_done.wait() + await async_wait_recording_done_without_instance(hass) + assert migration_version == models.SCHEMA_VERSION assert setup_run.called + assert await recorder.async_migration_in_progress(hass) is not True def test_invalid_update(): From 8cc2f3b7a481340fa2328b36904966dd619c88a4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Nov 2021 06:34:10 +0100 Subject: [PATCH 0255/1452] Use zeroconf attributes in enphase-envoy (#58961) --- .../components/enphase_envoy/config_flow.py | 10 +++++--- .../enphase_envoy/test_config_flow.py | 25 ++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 8a4b4b19e58..fa253ca442c 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -9,6 +9,7 @@ import httpx import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -20,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -99,11 +101,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if CONF_HOST in entry.data } - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info["properties"]["serialnum"] + self.serial = discovery_info[zeroconf.ATTR_PROPERTIES]["serialnum"] await self.async_set_unique_id(self.serial) - self.ip_address = discovery_info[CONF_HOST] + self.ip_address = discovery_info[zeroconf.ATTR_HOST] self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): if ( diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 6e32cf88975..bbe8b014d13 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch import httpx from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.enphase_envoy.const import DOMAIN from homeassistant.core import HomeAssistant @@ -157,10 +158,10 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"serialnum": "1234"}, - "host": "1.1.1.1", - }, + data=zeroconf.HaServiceInfo( + properties={"serialnum": "1234"}, + host="1.1.1.1", + ), ) await hass.async_block_till_done() @@ -253,10 +254,10 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"serialnum": "1234"}, - "host": "1.1.1.1", - }, + data=zeroconf.HaServiceInfo( + properties={"serialnum": "1234"}, + host="1.1.1.1", + ), ) assert result["type"] == "abort" @@ -288,10 +289,10 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"serialnum": "1234"}, - "host": "1.1.1.1", - }, + data=zeroconf.HaServiceInfo( + properties={"serialnum": "1234"}, + host="1.1.1.1", + ), ) await hass.async_block_till_done() From 470b01e4ce224015843ac391429cb192bbd44208 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Nov 2021 10:40:56 +0100 Subject: [PATCH 0256/1452] Move async_migration_in_progress (#59087) --- homeassistant/components/hassio/__init__.py | 5 ++-- .../components/homeassistant/__init__.py | 5 ++-- homeassistant/components/recorder/__init__.py | 12 -------- homeassistant/components/recorder/util.py | 11 +++++++ .../components/recorder/websocket_api.py | 3 +- homeassistant/helpers/recorder.py | 9 +++--- tests/components/recorder/test_migrate.py | 30 +++++++++---------- .../components/recorder/test_websocket_api.py | 2 +- tests/helpers/test_recorder.py | 12 ++++---- 9 files changed, 43 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index ef273b388ca..e05491b5ae1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -563,9 +563,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_handle_core_service(call): """Service handler for handling core services.""" - if ( - call.service in SHUTDOWN_SERVICES - and await recorder.async_migration_in_progress(hass) + if call.service in SHUTDOWN_SERVICES and recorder.async_migration_in_progress( + hass ): _LOGGER.error( "The system cannot %s while a database upgrade is in progress", diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 8c31859b8e0..d75358fe62e 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -138,9 +138,8 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no async def async_handle_core_service(call): """Service handler for handling core services.""" - if ( - call.service in SHUTDOWN_SERVICES - and await recorder.async_migration_in_progress(hass) + if call.service in SHUTDOWN_SERVICES and recorder.async_migration_in_progress( + hass ): _LOGGER.error( "The system cannot %s while a database upgrade is in progress", diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 6a9c53b96ad..da3955cb9b8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -176,18 +176,6 @@ CONFIG_SCHEMA = vol.Schema( ) -@bind_hass -async def async_migration_in_progress(hass: HomeAssistant) -> bool: - """Determine is a migration is in progress. - - This is a thin wrapper that allows us to change - out the implementation later. - """ - if DATA_INSTANCE not in hass.data: - return False - 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. diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 6c53347a536..c63f6abee3a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -455,3 +455,14 @@ def perodic_db_cleanups(instance: Recorder): _LOGGER.debug("WAL checkpoint") with instance.engine.connect() as connection: connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) + + +def async_migration_in_progress(hass: HomeAssistant) -> bool: + """Determine is a migration is in progress. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].migration_in_progress diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index b06d2d07f1e..5a4f0425919 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG from .statistics import validate_statistics +from .util import async_migration_in_progress if TYPE_CHECKING: from . import Recorder @@ -93,7 +94,7 @@ def ws_info( instance: Recorder = hass.data[DATA_INSTANCE] backlog = instance.queue.qsize() if instance and instance.queue else None - migration_in_progress = instance.migration_in_progress if instance else False + migration_in_progress = async_migration_in_progress(hass) recording = instance.recording if instance else False thread_alive = instance.is_alive() if instance else False diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index e3ed3428a2a..a51d9de59e2 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -4,12 +4,11 @@ from homeassistant.core import HomeAssistant -async def async_migration_in_progress(hass: HomeAssistant) -> bool: +def async_migration_in_progress(hass: HomeAssistant) -> bool: """Check to see if a recorder migration is in progress.""" if "recorder" not in hass.config.components: return False - from homeassistant.components import ( # pylint: disable=import-outside-toplevel - recorder, - ) + # pylint: disable-next=import-outside-toplevel + from homeassistant.components import recorder - return await recorder.async_migration_in_progress(hass) + return recorder.util.async_migration_in_progress(hass) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index e3cf8fc3527..7cdf69f1282 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -41,7 +41,7 @@ def _get_native_states(hass, entity_id): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -54,7 +54,7 @@ async def test_schema_update_calls(hass): ) await async_wait_recording_done_without_instance(hass) - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False update.assert_has_calls( [ call(hass.data[DATA_INSTANCE], ANY, version + 1, 0) @@ -65,7 +65,7 @@ async def test_schema_update_calls(hass): async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -74,15 +74,15 @@ async def test_migration_in_progress(hass): hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) await hass.data[DATA_INSTANCE].async_migration_event.wait() - assert await recorder.async_migration_in_progress(hass) is True + assert recorder.util.async_migration_in_progress(hass) is True await async_wait_recording_done_without_instance(hass) - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False async def test_database_migration_failed(hass): """Test we notify if the migration fails.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -104,7 +104,7 @@ async def test_database_migration_failed(hass): await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) await hass.async_block_till_done() - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False assert len(mock_create.mock_calls) == 2 assert len(mock_dismiss.mock_calls) == 1 @@ -112,7 +112,7 @@ async def test_database_migration_failed(hass): async def test_database_migration_encounters_corruption(hass): """Test we move away the database if its corrupt.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() @@ -133,13 +133,13 @@ async def test_database_migration_encounters_corruption(hass): hass.states.async_set("my.entity", "off", {}) await async_wait_recording_done_without_instance(hass) - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False assert move_away.called async def test_database_migration_encounters_corruption_not_sqlite(hass): """Test we fail on database error when we cannot recover.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.migration.schema_is_current", @@ -164,7 +164,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) await hass.async_block_till_done() - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False assert not move_away.called assert len(mock_create.mock_calls) == 2 assert len(mock_dismiss.mock_calls) == 1 @@ -173,7 +173,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): async def test_events_during_migration_are_queued(hass): """Test that events during migration are queued.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -190,7 +190,7 @@ async def test_events_during_migration_are_queued(hass): await hass.data[DATA_INSTANCE].async_recorder_ready.wait() await async_wait_recording_done_without_instance(hass) - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") assert len(db_states) == 2 @@ -198,7 +198,7 @@ async def test_events_during_migration_are_queued(hass): async def test_events_during_migration_queue_exhausted(hass): """Test that events during migration takes so long the queue is exhausted.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -216,7 +216,7 @@ async def test_events_during_migration_queue_exhausted(hass): await hass.data[DATA_INSTANCE].async_recorder_ready.wait() await async_wait_recording_done_without_instance(hass) - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") assert len(db_states) == 1 hass.states.async_set("my.entity", "on", {}) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 52a9424b4ac..7a45dea0379 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -305,7 +305,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): """Test getting recorder status when recorder queue is exhausted.""" - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.util.async_migration_in_progress(hass) is False migration_done = threading.Event() diff --git a/tests/helpers/test_recorder.py b/tests/helpers/test_recorder.py index 60d60a2335e..c0663cba165 100644 --- a/tests/helpers/test_recorder.py +++ b/tests/helpers/test_recorder.py @@ -10,23 +10,23 @@ from tests.common import async_init_recorder_component async def test_async_migration_in_progress(hass): """Test async_migration_in_progress wraps the recorder.""" with patch( - "homeassistant.components.recorder.async_migration_in_progress", + "homeassistant.components.recorder.util.async_migration_in_progress", return_value=False, ): - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.async_migration_in_progress(hass) is False # The recorder is not loaded with patch( - "homeassistant.components.recorder.async_migration_in_progress", + "homeassistant.components.recorder.util.async_migration_in_progress", return_value=True, ): - assert await recorder.async_migration_in_progress(hass) is False + assert recorder.async_migration_in_progress(hass) is False await async_init_recorder_component(hass) # The recorder is now loaded with patch( - "homeassistant.components.recorder.async_migration_in_progress", + "homeassistant.components.recorder.util.async_migration_in_progress", return_value=True, ): - assert await recorder.async_migration_in_progress(hass) is True + assert recorder.async_migration_in_progress(hass) is True From e5ee38d3b0f204eec47358e1220f7564a22e850a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Nov 2021 10:49:58 +0100 Subject: [PATCH 0257/1452] Adjust recorder tests according to #59087 (#59143) --- tests/components/recorder/test_migrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 7cdf69f1282..5c8a1c556c9 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -296,14 +296,14 @@ async def test_schema_migrate(hass, start_version): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) - assert await recorder.async_migration_in_progress(hass) is True + assert recorder.util.async_migration_in_progress(hass) is True migration_stall.set() await hass.async_block_till_done() migration_done.wait() await async_wait_recording_done_without_instance(hass) assert migration_version == models.SCHEMA_VERSION assert setup_run.called - assert await recorder.async_migration_in_progress(hass) is not True + assert recorder.util.async_migration_in_progress(hass) is not True def test_invalid_update(): From 8b25bd0cea1bec8125900737b59f9964da2b057e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Nov 2021 05:22:59 -0500 Subject: [PATCH 0258/1452] Bump zeroconf to 0.36.12 (#59133) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.36.11...0.36.12 Bugfix: Prevent service lookups from deadlocking if time abruptly moves backwards --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 3f4dfb4929e..0c25a4c9860 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.36.11"], + "requirements": ["zeroconf==0.36.12"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 32a75ef29d0..b8cb9d075a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.11 +zeroconf==0.36.12 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 5be24b23093..f88fa091ea9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.11 +zeroconf==0.36.12 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb32a187e92..a2109bb10fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1436,7 +1436,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.11 +zeroconf==0.36.12 # homeassistant.components.zha zha-quirks==0.0.63 From 5ac55b3443b2bca4620eacafc4ac38a69b8db142 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Fri, 5 Nov 2021 04:00:57 -0700 Subject: [PATCH 0259/1452] Add binary_sensor to venstar to track alerts (#58831) * Add binary_sensor to venstar to track alerts * Add binary_sensor.py to coveragerc * Apply suggestions from code review by alengwenus Co-authored-by: Andre Lengwenus * Fixup black any mypy complaints * Yank the typing, it makes everything complain Co-authored-by: Andre Lengwenus --- .coveragerc | 1 + homeassistant/components/venstar/__init__.py | 12 ++++- .../components/venstar/binary_sensor.py | 53 +++++++++++++++++++ tests/components/venstar/test_init.py | 6 +++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/venstar/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 4f161e7b5e1..6987e985467 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1171,6 +1171,7 @@ omit = homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/__init__.py + homeassistant/components/venstar/binary_sensor.py homeassistant/components/venstar/climate.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 69bc1bf188c..8c46df66a91 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -PLATFORMS = ["climate"] +PLATFORMS = ["climate", "binary_sensor"] async def async_setup_entry(hass, config): @@ -96,6 +96,16 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): raise update_coordinator.UpdateFailed( f"Exception during Venstar sensor update: {ex}" ) from ex + + # older venstars sometimes cannot handle rapid sequential connections + await asyncio.sleep(3) + + try: + await self.hass.async_add_executor_job(self.client.update_alerts) + except (OSError, RequestException) as ex: + raise update_coordinator.UpdateFailed( + f"Exception during Venstar alert update: {ex}" + ) from ex return None diff --git a/homeassistant/components/venstar/binary_sensor.py b/homeassistant/components/venstar/binary_sensor.py new file mode 100644 index 00000000000..d7b1a69c4cb --- /dev/null +++ b/homeassistant/components/venstar/binary_sensor.py @@ -0,0 +1,53 @@ +"""Alarm sensors for the Venstar Thermostat.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) + +from . import VenstarEntity +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up Vensar device binary_sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + if coordinator.client.alerts is None: + return + sensors = [ + VenstarBinarySensor(coordinator, config_entry, alert["name"]) + for alert in coordinator.client.alerts + ] + + async_add_entities(sensors, True) + + +class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): + """Represent a Venstar alert.""" + + _attr_device_class = DEVICE_CLASS_PROBLEM + + def __init__(self, coordinator, config, alert): + """Initialize the alert.""" + super().__init__(coordinator, config) + self.alert = alert + self._config = config + self._unique_id = f"{self._config.entry_id}_{self.alert.replace(' ', '_')}" + self._name = f"{self._client.name} {self.alert}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + for alert in self._client.alerts: + if alert["name"] == self.alert: + return alert["active"] diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 03739f19616..08dae732466 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -33,6 +33,9 @@ async def test_setup_entry(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_info", new=VenstarColorTouchMock.update_info, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_alerts", + new=VenstarColorTouchMock.update_alerts, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -64,6 +67,9 @@ async def test_setup_entry_exception(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_info", new=VenstarColorTouchMock.broken_update_info, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_alerts", + new=VenstarColorTouchMock.update_alerts, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From d709fcdd30aa32edd5359a6aef1b2edaff750c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 5 Nov 2021 12:03:00 +0100 Subject: [PATCH 0260/1452] Bump pyhaversion from 21.10.0 to 21.11.1 (#59147) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index aa8a2659dcd..f5dec053399 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ - "pyhaversion==21.10.0" + "pyhaversion==21.11.1" ], "codeowners": [ "@fabaff", diff --git a/requirements_all.txt b/requirements_all.txt index f88fa091ea9..9392ca91ee2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1517,7 +1517,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.10.0 +pyhaversion==21.11.1 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2109bb10fe..f60897db31d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -902,7 +902,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.10.0 +pyhaversion==21.11.1 # homeassistant.components.heos pyheos==0.7.2 From 442d65e8da3fd3c9e486520c0fe6c436f732633a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 5 Nov 2021 13:11:48 +0100 Subject: [PATCH 0261/1452] Use entity category in devolo Home Control (#59104) * Use entity category * Add tests --- .../devolo_home_control/binary_sensor.py | 5 +++++ .../components/devolo_home_control/sensor.py | 3 ++- tests/components/devolo_home_control/mocks.py | 17 +++++++++++++++-- .../devolo_home_control/test_binary_sensor.py | 17 +++++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index c19d74b4c33..4fadc8b5f46 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -94,8 +95,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._value = self._binary_sensor_property.state + if self._attr_device_class == DEVICE_CLASS_SAFETY: + self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + if element_uid.startswith("devolo.WarningBinaryFI:"): self._attr_device_class = DEVICE_CLASS_PROBLEM + self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC self._attr_entity_registry_enabled_default = False @property diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 61c3e9a5c19..c0ce78dfd72 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -132,6 +132,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): ) self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") + self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC self._attr_native_unit_of_measurement = PERCENTAGE self._value = device_instance.battery_level diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 6651215251a..693b4e7351d 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -57,6 +57,16 @@ class BinarySensorMock(DeviceMock): self.binary_sensor_property = {"Test": BinarySensorPropertyMock()} +class BinarySensorMockOverload(DeviceMock): + """devolo Home Control disabled binary sensor device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_sensor_property = {"Overload": BinarySensorPropertyMock()} + self.binary_sensor_property["Overload"].sensor_type = "overload" + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -90,12 +100,15 @@ class HomeControlMock(HomeControl): class HomeControlMockBinarySensor(HomeControlMock): - """devolo Home Control gateway mock with binary sensor device.""" + """devolo Home Control gateway mock with binary sensor devices.""" def __init__(self, **kwargs: Any) -> None: """Initialize the mock.""" super().__init__() - self.devices = {"Test": BinarySensorMock()} + self.devices = { + "Test": BinarySensorMock(), + "Overload": BinarySensorMockOverload(), + } self.publisher = Publisher(self.devices.keys()) self.publisher.unregister = MagicMock() diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py index 32c2e97e7c9..9b13220ad7c 100644 --- a/tests/components/devolo_home_control/test_binary_sensor.py +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -4,8 +4,14 @@ from unittest.mock import patch import pytest from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from . import configure_integration from .mocks import ( @@ -33,6 +39,13 @@ async def test_binary_sensor(hass: HomeAssistant): assert state is not None assert state.state == STATE_OFF + state = hass.states.get(f"{DOMAIN}.test_2") + assert state is not None + er = entity_registry.async_get(hass) + assert ( + er.async_get(f"{DOMAIN}.test_2").entity_category == ENTITY_CATEGORY_DIAGNOSTIC + ) + # Emulate websocket message: sensor turned on test_gateway.publisher.dispatch("Test", ("Test", True)) await hass.async_block_till_done() @@ -111,4 +124,4 @@ async def test_remove_from_hass(hass: HomeAssistant): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - test_gateway.publisher.unregister.assert_called_once() + assert test_gateway.publisher.unregister.call_count == 2 From c0801c12337ff25b9a18cea51138e5dafa5f9ae8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 5 Nov 2021 14:42:08 +0100 Subject: [PATCH 0262/1452] Type vlc_telnet strictly (#59154) --- .strict-typing | 1 + .../components/vlc_telnet/media_player.py | 22 +++++++++---------- mypy.ini | 11 ++++++++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.strict-typing b/.strict-typing index 95d03544661..f574aeb79d6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -135,6 +135,7 @@ homeassistant.components.uptime.* homeassistant.components.uptimerobot.* homeassistant.components.vacuum.* homeassistant.components.vallox.* +homeassistant.components.vlc_telnet.* homeassistant.components.water_heater.* homeassistant.components.watttime.* homeassistant.components.weather.* diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index cb231f62861..d83b3f39f40 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -104,7 +104,7 @@ def catch_vlc_errors(func: Func) -> Func: """Catch VLC errors.""" @wraps(func) - async def wrapper(self, *args: Any, **kwargs: Any) -> Any: + async def wrapper(self: VlcDevice, *args: Any, **kwargs: Any) -> Any: """Catch VLC errors and modify availability.""" try: await func(self, *args, **kwargs) @@ -206,12 +206,12 @@ class VlcDevice(MediaPlayerEntity): self._media_title = data_info["filename"] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self._state @@ -226,42 +226,42 @@ class VlcDevice(MediaPlayerEntity): return self._volume @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" return self._muted @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return SUPPORT_VLC @property - def media_content_type(self): + def media_content_type(self) -> str: """Content type of current playing media.""" return MEDIA_TYPE_MUSIC @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" return self._media_duration @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" return self._media_position @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime | None: """When was the position of the current playing media valid.""" return self._media_position_updated_at @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" return self._media_title @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" return self._media_artist diff --git a/mypy.ini b/mypy.ini index 086db083892..425cfd1aa57 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1496,6 +1496,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.vlc_telnet.*] +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.water_heater.*] check_untyped_defs = true disallow_incomplete_defs = true From 6a0c1a78aa218a694aa7eea01b20a8095e613509 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 5 Nov 2021 15:31:32 +0100 Subject: [PATCH 0263/1452] Address venstar review comments (#59151) * Address venstar review comments * Apply review suggestions * Address review suggestions --- homeassistant/components/venstar/__init__.py | 2 +- .../components/venstar/binary_sensor.py | 23 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 8c46df66a91..72e8b3e32d9 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -PLATFORMS = ["climate", "binary_sensor"] +PLATFORMS = ["binary_sensor", "climate"] async def async_setup_entry(hass, config): diff --git a/homeassistant/components/venstar/binary_sensor.py b/homeassistant/components/venstar/binary_sensor.py index d7b1a69c4cb..1d6c8f49bd8 100644 --- a/homeassistant/components/venstar/binary_sensor.py +++ b/homeassistant/components/venstar/binary_sensor.py @@ -14,12 +14,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: if coordinator.client.alerts is None: return - sensors = [ + async_add_entities( VenstarBinarySensor(coordinator, config_entry, alert["name"]) for alert in coordinator.client.alerts - ] - - async_add_entities(sensors, True) + ) class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): @@ -31,19 +29,8 @@ class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): """Initialize the alert.""" super().__init__(coordinator, config) self.alert = alert - self._config = config - self._unique_id = f"{self._config.entry_id}_{self.alert.replace(' ', '_')}" - self._name = f"{self._client.name} {self.alert}" - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device.""" - return self._name + self._attr_unique_id = f"{config.entry_id}_{alert.replace(' ', '_')}" + self._attr_name = f"{self._client.name} {alert}" @property def is_on(self): @@ -51,3 +38,5 @@ class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): for alert in self._client.alerts: if alert["name"] == self.alert: return alert["active"] + + return None From 30cba6ee8b4f17ad4d19665e2520de847889bb00 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 5 Nov 2021 15:31:51 +0100 Subject: [PATCH 0264/1452] Add zwave_js select entity category (#59157) * Set zwave_js select entity as category config * Update tests --- homeassistant/components/zwave_js/select.py | 5 +++ tests/components/zwave_js/test_select.py | 39 ++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 15223419ced..08f9059e125 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -9,6 +9,7 @@ from zwave_js_server.const.command_class.sound_switch import ToneID from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -52,6 +53,8 @@ async def async_setup_entry( class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave select entity.""" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo ) -> None: @@ -86,6 +89,8 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave default tone select entity.""" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo ) -> None: diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index 5ed0804723c..82f84372ee0 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -1,14 +1,25 @@ """Test the Z-Wave JS number platform.""" -from zwave_js_server.event import Event +from unittest.mock import MagicMock -from homeassistant.const import STATE_UNKNOWN +from zwave_js_server.event import Event +from zwave_js_server.model.node import Node + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2" PROTECTION_SELECT_ENTITY = "select.family_room_combo_local_protection_state" MULTILEVEL_SWITCH_SELECT_ENTITY = "select.front_door_siren" -async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration): +async def test_default_tone_select( + hass: HomeAssistant, + client: MagicMock, + aeotec_zw164_siren: Node, + integration: ConfigEntry, +) -> None: """Test the default tone select entity.""" node = aeotec_zw164_siren state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) @@ -48,6 +59,12 @@ async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration "30DOOR~1 (27 sec)", ] + entity_registry = er.async_get(hass) + entity_entry = entity_registry.async_get(DEFAULT_TONE_SELECT_ENTITY) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + # Test select option with string value await hass.services.async_call( "select", @@ -102,10 +119,16 @@ async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration node.receive_event(event) state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) + assert state assert state.state == "30DOOR~1 (27 sec)" -async def test_protection_select(hass, client, inovelli_lzw36, integration): +async def test_protection_select( + hass: HomeAssistant, + client: MagicMock, + inovelli_lzw36: Node, + integration: ConfigEntry, +) -> None: """Test the default tone select entity.""" node = inovelli_lzw36 state = hass.states.get(PROTECTION_SELECT_ENTITY) @@ -119,6 +142,12 @@ async def test_protection_select(hass, client, inovelli_lzw36, integration): "NoOperationPossible", ] + entity_registry = er.async_get(hass) + entity_entry = entity_registry.async_get(PROTECTION_SELECT_ENTITY) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + # Test select option with string value await hass.services.async_call( "select", @@ -176,6 +205,7 @@ async def test_protection_select(hass, client, inovelli_lzw36, integration): node.receive_event(event) state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state assert state.state == "ProtectedBySequence" # Test null value @@ -199,6 +229,7 @@ async def test_protection_select(hass, client, inovelli_lzw36, integration): node.receive_event(event) state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state assert state.state == STATE_UNKNOWN From dd0537054a7c4834e4efefc1ed321b278cfbf8d3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 5 Nov 2021 09:27:35 -0600 Subject: [PATCH 0265/1452] Bump aioguardian to 2021.11.0 (#59161) --- homeassistant/components/guardian/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index cbdd8fe3ba8..90e33a82452 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==1.0.8"], + "requirements": ["aioguardian==2021.11.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 9392ca91ee2..a840604ddd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aioftp==0.12.0 aiogithubapi==21.8.0 # homeassistant.components.guardian -aioguardian==1.0.8 +aioguardian==2021.11.0 # homeassistant.components.harmony aioharmony==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f60897db31d..937e73c5769 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioesphomeapi==10.2.0 aioflo==0.4.1 # homeassistant.components.guardian -aioguardian==1.0.8 +aioguardian==2021.11.0 # homeassistant.components.harmony aioharmony==0.2.8 From 2d48f4b65e7a16cbe3de52482861b8f2efd12eb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Nov 2021 10:27:52 -0500 Subject: [PATCH 0266/1452] Bump flux_led to 0.24.15 (#59159) - Changes: https://github.com/Danielhiversen/flux_led/compare/0.24.14...0.24.15 - Fixes color reporting for addressable devices --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d0134d07f79..3a3b7b7b572 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.14"], + "requirements": ["flux_led==0.24.15"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a840604ddd0..46c766ee2bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.14 +flux_led==0.24.15 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 937e73c5769..6b5441bc599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.14 +flux_led==0.24.15 # homeassistant.components.homekit fnvhash==0.1.0 From d384feb87feaab98ff347e7de5633153a79878a3 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Fri, 5 Nov 2021 19:11:30 +0100 Subject: [PATCH 0267/1452] Add configuration URL to Spider (#59171) --- homeassistant/components/spider/climate.py | 1 + homeassistant/components/spider/switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 72ae67c7600..4d3b24466f9 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -50,6 +50,7 @@ class SpiderThermostat(ClimateEntity): def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return DeviceInfo( + configuration_url="https://mijn.ithodaalderop.nl/", identifiers={(DOMAIN, self.thermostat.id)}, manufacturer=self.thermostat.manufacturer, model=self.thermostat.model, diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 4569105b8f4..089421ba447 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -28,6 +28,7 @@ class SpiderPowerPlug(SwitchEntity): def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return DeviceInfo( + configuration_url="https://mijn.ithodaalderop.nl/", identifiers={(DOMAIN, self.power_plug.id)}, manufacturer=self.power_plug.manufacturer, model=self.power_plug.model, From d2ffecbca422b4f33afc96f517f3d540d892583f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 5 Nov 2021 19:27:17 +0100 Subject: [PATCH 0268/1452] Tests for the Fronius integration (#57269) * tests for a Symo inverter system * update testing requirement * add tests for energy meter data * move response JSONs to fixture directory * add storage system response * review suggestion --- requirements_test_all.txt | 3 + tests/components/fronius/__init__.py | 23 ++ tests/components/fronius/const.py | 4 + tests/components/fronius/test_sensor.py | 305 ++++++++++++++++++ .../fixtures/fronius/symo/GetAPIVersion.json | 5 + .../fronius/symo/GetInverterInfo.json | 24 ++ .../GetInverterRealtimeDate_Device_1_day.json | 64 ++++ ...etInverterRealtimeDate_Device_1_night.json | 48 +++ .../fixtures/fronius/symo/GetLoggerInfo.json | 29 ++ .../symo/GetMeterRealtimeData_Device_0.json | 60 ++++ .../symo/GetMeterRealtimeData_System.json | 61 ++++ .../symo/GetPowerFlowRealtimeData_day.json | 38 +++ .../symo/GetPowerFlowRealtimeData_night.json | 38 +++ .../symo/GetStorageRealtimeData_System.json | 14 + 14 files changed, 716 insertions(+) create mode 100644 tests/components/fronius/__init__.py create mode 100644 tests/components/fronius/const.py create mode 100644 tests/components/fronius/test_sensor.py create mode 100644 tests/fixtures/fronius/symo/GetAPIVersion.json create mode 100644 tests/fixtures/fronius/symo/GetInverterInfo.json create mode 100644 tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json create mode 100644 tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json create mode 100644 tests/fixtures/fronius/symo/GetLoggerInfo.json create mode 100644 tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json create mode 100644 tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json create mode 100644 tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json create mode 100644 tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json create mode 100644 tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b5441bc599..e7fd0019df7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -891,6 +891,9 @@ pyfreedompro==1.1.0 # homeassistant.components.fritzbox pyfritzhome==0.6.2 +# homeassistant.components.fronius +pyfronius==0.7.0 + # homeassistant.components.ifttt pyfttt==0.3 diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py new file mode 100644 index 00000000000..41413166157 --- /dev/null +++ b/tests/components/fronius/__init__.py @@ -0,0 +1,23 @@ +"""Tests for the Fronius integration.""" + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_RESOURCE +from homeassistant.setup import async_setup_component + +from .const import DOMAIN, MOCK_HOST + + +async def setup_fronius_integration(hass, devices): + """Create the Fronius integration.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_RESOURCE: MOCK_HOST, + CONF_MONITORED_CONDITIONS: devices, + } + }, + ) + await hass.async_block_till_done() diff --git a/tests/components/fronius/const.py b/tests/components/fronius/const.py new file mode 100644 index 00000000000..7b7635e4d92 --- /dev/null +++ b/tests/components/fronius/const.py @@ -0,0 +1,4 @@ +"""Constants for Fronius tests.""" + +DOMAIN = "fronius" +MOCK_HOST = "http://fronius" diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py new file mode 100644 index 00000000000..b5617fad03c --- /dev/null +++ b/tests/components/fronius/test_sensor.py @@ -0,0 +1,305 @@ +"""Tests for the Fronius sensor platform.""" + +from homeassistant.components.fronius.sensor import ( + CONF_SCOPE, + DEFAULT_SCAN_INTERVAL, + SCOPE_DEVICE, + TYPE_INVERTER, + TYPE_LOGGER_INFO, + TYPE_METER, + TYPE_POWER_FLOW, +) +from homeassistant.const import CONF_DEVICE, CONF_SENSOR_TYPE, STATE_UNKNOWN +from homeassistant.util import dt + +from . import setup_fronius_integration +from .const import MOCK_HOST + +from tests.common import async_fire_time_changed, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def mock_responses(aioclient_mock: AiohttpClientMocker, night: bool = False) -> None: + """Mock responses for Fronius Symo inverter with meter.""" + aioclient_mock.clear_requests() + _day_or_night = "night" if night else "day" + + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/GetAPIVersion.cgi", + text=load_fixture("fronius/symo/GetAPIVersion.json"), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + "DeviceId=1&DataCollection=CommonInverterData", + text=load_fixture( + f"fronius/symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json" + ), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetInverterInfo.cgi", + text=load_fixture("fronius/symo/GetInverterInfo.json"), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetLoggerInfo.cgi", + text=load_fixture("fronius/symo/GetLoggerInfo.json"), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0", + text=load_fixture("fronius/symo/GetMeterRealtimeData_Device_0.json"), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System", + text=load_fixture("fronius/symo/GetMeterRealtimeData_System.json"), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetPowerFlowRealtimeData.fcgi", + text=load_fixture( + f"fronius/symo/GetPowerFlowRealtimeData_{_day_or_night}.json" + ), + ) + aioclient_mock.get( + f"{MOCK_HOST}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", + text=load_fixture("fronius/symo/GetStorageRealtimeData_System.json"), + ) + + +async def test_symo_inverter(hass, aioclient_mock): + """Test Fronius Symo inverter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + # Init at night + mock_responses(aioclient_mock, night=True) + config = { + CONF_SENSOR_TYPE: TYPE_INVERTER, + CONF_SCOPE: SCOPE_DEVICE, + CONF_DEVICE: 1, + } + await setup_fronius_integration(hass, [config]) + + assert len(hass.states.async_all()) == 10 + # 5 ignored from DeviceStatus + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25507686) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 16) + + # Second test at daytime when inverter is producing + mock_responses(aioclient_mock, night=False) + async_fire_time_changed(hass, dt.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 14 + # 4 additional AC entities + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44188000) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25508798) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 518) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + + +async def test_symo_logger(hass, aioclient_mock): + """Test Fronius Symo logger entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock) + config = { + CONF_SENSOR_TYPE: TYPE_LOGGER_INFO, + } + await setup_fronius_integration(hass, [config]) + + assert len(hass.states.async_all()) == 12 + # ignored constant entities: + # hardware_platform, hardware_version, product_type + # software_version, time_zone, time_zone_location + # time_stamp, unique_identifier, utc_offset + # + # states are rounded to 2 decimals + assert_state( + "sensor.cash_factor_fronius_logger_info_0_http_fronius", + 0.08, + ) + assert_state( + "sensor.co2_factor_fronius_logger_info_0_http_fronius", + 0.53, + ) + assert_state( + "sensor.delivery_factor_fronius_logger_info_0_http_fronius", + 0.15, + ) + + +async def test_symo_meter(hass, aioclient_mock): + """Test Fronius Symo meter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock) + config = { + CONF_SENSOR_TYPE: TYPE_METER, + CONF_SCOPE: SCOPE_DEVICE, + CONF_DEVICE: 0, + } + await setup_fronius_integration(hass, [config]) + + assert len(hass.states.async_all()) == 39 + # ignored entities: + # manufacturer, model, serial, enable, timestamp, visible, meter_location + # + # states are rounded to 2 decimals + assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.75) + assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) + assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.1) + assert_state( + "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 59960790 + ) + assert_state( + "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 723160 + ) + assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 35623065) + assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 15303334) + assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 15303334) + assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 35623065) + assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 50) + assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.79) + assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.05) + assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.56) + assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 5592.57) + assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", -0.99) + assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", -0.99) + assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.99) + assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 1) + assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", 51.48) + assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", 115.63) + assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -164.24) + assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", 2.87) + assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 1765.55) + assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 1515.8) + assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 2311.22) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", 5592.57) + assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 228.6) + assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 228.6) + assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 231) + assert_state( + "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 395.9 + ) + assert_state( + "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 398 + ) + assert_state( + "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 398 + ) + + +async def test_symo_power_flow(hass, aioclient_mock): + """Test Fronius Symo power flow entities.""" + async_fire_time_changed(hass, dt.utcnow()) + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + # First test at night + mock_responses(aioclient_mock, night=True) + config = { + CONF_SENSOR_TYPE: TYPE_POWER_FLOW, + } + await setup_fronius_integration(hass, [config]) + + assert len(hass.states.async_all()) == 12 + # ignored: location, mode, timestamp + # + # states are rounded to 2 decimals + assert_state( + "sensor.energy_day_fronius_power_flow_0_http_fronius", + 10828, + ) + assert_state( + "sensor.energy_total_fronius_power_flow_0_http_fronius", + 44186900, + ) + assert_state( + "sensor.energy_year_fronius_power_flow_0_http_fronius", + 25507686, + ) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", + STATE_UNKNOWN, + ) + assert_state( + "sensor.power_grid_fronius_power_flow_0_http_fronius", + 975.31, + ) + assert_state( + "sensor.power_load_fronius_power_flow_0_http_fronius", + -975.31, + ) + assert_state( + "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", + STATE_UNKNOWN, + ) + assert_state( + "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", + 0, + ) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", + STATE_UNKNOWN, + ) + + # Second test at daytime when inverter is producing + mock_responses(aioclient_mock, night=False) + async_fire_time_changed(hass, dt.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 12 + assert_state( + "sensor.energy_day_fronius_power_flow_0_http_fronius", + 1101.70, + ) + assert_state( + "sensor.energy_total_fronius_power_flow_0_http_fronius", + 44188000, + ) + assert_state( + "sensor.energy_year_fronius_power_flow_0_http_fronius", + 25508788, + ) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", + STATE_UNKNOWN, + ) + assert_state( + "sensor.power_grid_fronius_power_flow_0_http_fronius", + 1703.74, + ) + assert_state( + "sensor.power_load_fronius_power_flow_0_http_fronius", + -2814.74, + ) + assert_state( + "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", + 1111, + ) + assert_state( + "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", + 39.47, + ) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", + 100, + ) diff --git a/tests/fixtures/fronius/symo/GetAPIVersion.json b/tests/fixtures/fronius/symo/GetAPIVersion.json new file mode 100644 index 00000000000..59c1dbb1c5c --- /dev/null +++ b/tests/fixtures/fronius/symo/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.6-3" +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetInverterInfo.json b/tests/fixtures/fronius/symo/GetInverterInfo.json new file mode 100644 index 00000000000..8bbf01a6cb4 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetInverterInfo.json @@ -0,0 +1,24 @@ +{ + "Body": { + "Data": { + "1": { + "CustomName": "Symo 20", + "DT": 121, + "ErrorCode": 0, + "PVPower": 23100, + "Show": 1, + "StatusCode": 7, + "UniqueID": "1234567" + } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T13:41:00+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json b/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json new file mode 100644 index 00000000000..d504b125a62 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json @@ -0,0 +1,64 @@ +{ + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 1113 + }, + "DeviceStatus": { + "ErrorCode": 0, + "LEDColor": 2, + "LEDState": 0, + "MgmtTimerRemainingTime": -1, + "StateToReset": false, + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 49.939999999999998 + }, + "IAC": { + "Unit": "A", + "Value": 5.1900000000000004 + }, + "IDC": { + "Unit": "A", + "Value": 2.1899999999999999 + }, + "PAC": { + "Unit": "W", + "Value": 1190 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 44188000 + }, + "UAC": { + "Unit": "V", + "Value": 227.90000000000001 + }, + "UDC": { + "Unit": "V", + "Value": 518 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 25508798 + } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T10:01:17+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json b/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json new file mode 100644 index 00000000000..d3c5091fc00 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json @@ -0,0 +1,48 @@ +{ + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 10828 + }, + "DeviceStatus": { + "ErrorCode": 307, + "LEDColor": 1, + "LEDState": 0, + "MgmtTimerRemainingTime": 17, + "StateToReset": false, + "StatusCode": 3 + }, + "IDC": { + "Unit": "A", + "Value": 0 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 44186900 + }, + "UDC": { + "Unit": "V", + "Value": 16 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 25507686 + } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T21:16:59+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetLoggerInfo.json b/tests/fixtures/fronius/symo/GetLoggerInfo.json new file mode 100644 index 00000000000..f977823674e --- /dev/null +++ b/tests/fixtures/fronius/symo/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body": { + "LoggerInfo": { + "CO2Factor": 0.52999997138977051, + "CO2Unit": "kg", + "CashCurrency": "EUR", + "CashFactor": 0.078000001609325409, + "DefaultLanguage": "en", + "DeliveryFactor": 0.15000000596046448, + "HWVersion": "2.4E", + "PlatformID": "wilma", + "ProductID": "fronius-datamanager-card", + "SWVersion": "3.18.7-1", + "TimezoneLocation": "Vienna", + "TimezoneName": "CEST", + "UTCOffset": 7200, + "UniqueID": "123.4567890" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T23:56:32+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json b/tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json new file mode 100644 index 00000000000..762ec968dbc --- /dev/null +++ b/tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json @@ -0,0 +1,60 @@ +{ + "Body": { + "Data": { + "Current_AC_Phase_1": 7.7549999999999999, + "Current_AC_Phase_2": 6.6799999999999997, + "Current_AC_Phase_3": 10.102, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter 63A", + "Serial": "12345678" + }, + "Enable": 1, + "EnergyReactive_VArAC_Sum_Consumed": 59960790, + "EnergyReactive_VArAC_Sum_Produced": 723160, + "EnergyReal_WAC_Minus_Absolute": 35623065, + "EnergyReal_WAC_Plus_Absolute": 15303334, + "EnergyReal_WAC_Sum_Consumed": 15303334, + "EnergyReal_WAC_Sum_Produced": 35623065, + "Frequency_Phase_Average": 50, + "Meter_Location_Current": 0, + "PowerApparent_S_Phase_1": 1772.7929999999999, + "PowerApparent_S_Phase_2": 1527.048, + "PowerApparent_S_Phase_3": 2333.5619999999999, + "PowerApparent_S_Sum": 5592.5699999999997, + "PowerFactor_Phase_1": -0.98999999999999999, + "PowerFactor_Phase_2": -0.98999999999999999, + "PowerFactor_Phase_3": 0.98999999999999999, + "PowerFactor_Sum": 1, + "PowerReactive_Q_Phase_1": 51.479999999999997, + "PowerReactive_Q_Phase_2": 115.63, + "PowerReactive_Q_Phase_3": -164.24000000000001, + "PowerReactive_Q_Sum": 2.8700000000000001, + "PowerReal_P_Phase_1": 1765.55, + "PowerReal_P_Phase_2": 1515.8, + "PowerReal_P_Phase_3": 2311.2199999999998, + "PowerReal_P_Sum": 5592.5699999999997, + "TimeStamp": 1633977078, + "Visible": 1, + "Voltage_AC_PhaseToPhase_12": 395.89999999999998, + "Voltage_AC_PhaseToPhase_23": 398, + "Voltage_AC_PhaseToPhase_31": 398, + "Voltage_AC_Phase_1": 228.59999999999999, + "Voltage_AC_Phase_2": 228.59999999999999, + "Voltage_AC_Phase_3": 231 + } + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "DeviceId": "0", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-11T20:31:18+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json b/tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json new file mode 100644 index 00000000000..6c13116f351 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json @@ -0,0 +1,61 @@ +{ + "Body": { + "Data": { + "0": { + "Current_AC_Phase_1": 7.7549999999999999, + "Current_AC_Phase_2": 6.6799999999999997, + "Current_AC_Phase_3": 10.102, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter 63A", + "Serial": "12345678" + }, + "Enable": 1, + "EnergyReactive_VArAC_Sum_Consumed": 59960790, + "EnergyReactive_VArAC_Sum_Produced": 723160, + "EnergyReal_WAC_Minus_Absolute": 35623065, + "EnergyReal_WAC_Plus_Absolute": 15303334, + "EnergyReal_WAC_Sum_Consumed": 15303334, + "EnergyReal_WAC_Sum_Produced": 35623065, + "Frequency_Phase_Average": 50, + "Meter_Location_Current": 0, + "PowerApparent_S_Phase_1": 1772.7929999999999, + "PowerApparent_S_Phase_2": 1527.048, + "PowerApparent_S_Phase_3": 2333.5619999999999, + "PowerApparent_S_Sum": 5592.5699999999997, + "PowerFactor_Phase_1": -0.98999999999999999, + "PowerFactor_Phase_2": -0.98999999999999999, + "PowerFactor_Phase_3": 0.98999999999999999, + "PowerFactor_Sum": 1, + "PowerReactive_Q_Phase_1": 51.479999999999997, + "PowerReactive_Q_Phase_2": 115.63, + "PowerReactive_Q_Phase_3": -164.24000000000001, + "PowerReactive_Q_Sum": 2.8700000000000001, + "PowerReal_P_Phase_1": 1765.55, + "PowerReal_P_Phase_2": 1515.8, + "PowerReal_P_Phase_3": 2311.2199999999998, + "PowerReal_P_Sum": 5592.5699999999997, + "TimeStamp": 1633977078, + "Visible": 1, + "Voltage_AC_PhaseToPhase_12": 395.89999999999998, + "Voltage_AC_PhaseToPhase_23": 398, + "Voltage_AC_PhaseToPhase_31": 398, + "Voltage_AC_Phase_1": 228.59999999999999, + "Voltage_AC_Phase_2": 228.59999999999999, + "Voltage_AC_Phase_3": 231 + } + } + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-11T20:31:18+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json b/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json new file mode 100644 index 00000000000..03e6a74ecf1 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json @@ -0,0 +1,38 @@ +{ + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 121, + "E_Day": 1101.7000732421875, + "E_Total": 44188000, + "E_Year": 25508788, + "P": 1111 + } + }, + "Site": { + "E_Day": 1101.7000732421875, + "E_Total": 44188000, + "E_Year": 25508788, + "Meter_Location": "grid", + "Mode": "meter", + "P_Akku": null, + "P_Grid": 1703.74, + "P_Load": -2814.7399999999998, + "P_PV": 1111, + "rel_Autonomy": 39.4707859340472, + "rel_SelfConsumption": 100 + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T10:00:43+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json b/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json new file mode 100644 index 00000000000..141033fe4f2 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json @@ -0,0 +1,38 @@ +{ + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 121, + "E_Day": 10828, + "E_Total": 44186900, + "E_Year": 25507686, + "P": 0 + } + }, + "Site": { + "E_Day": 10828, + "E_Total": 44186900, + "E_Year": 25507686, + "Meter_Location": "grid", + "Mode": "meter", + "P_Akku": null, + "P_Grid": 975.30999999999995, + "P_Load": -975.30999999999995, + "P_PV": null, + "rel_Autonomy": 0, + "rel_SelfConsumption": null + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T23:49:54+02:00" + } +} \ No newline at end of file diff --git a/tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json b/tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json new file mode 100644 index 00000000000..db4ab288683 --- /dev/null +++ b/tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json @@ -0,0 +1,14 @@ +{ + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 255, + "Reason": "GetStorageRealtimeData request is not supported by this device.", + "UserMessage": "" + }, + "Timestamp": "2021-10-22T06:50:22+02:00" + } +} \ No newline at end of file From 6145ee97cb9aa54eb518c14d6f6596d6f1ddf8e6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 5 Nov 2021 13:29:12 -0600 Subject: [PATCH 0269/1452] Change ReCollect Waste device class to date (#59180) --- .../components/recollect_waste/sensor.py | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 06a96a3bb74..74a023fed94 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,17 +1,11 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from datetime import date, datetime, time - from aiorecollect.client import PickupType from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_FRIENDLY_NAME, - DEVICE_CLASS_TIMESTAMP, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,7 +13,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util.dt import as_utc from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN @@ -47,12 +40,6 @@ def async_get_pickup_type_names( ] -@callback -def async_get_utc_midnight(target_date: date) -> datetime: - """Get UTC midnight for a given date.""" - return as_utc(datetime.combine(target_date, time(0))) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -64,7 +51,7 @@ async def async_setup_entry( class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = DEVICE_CLASS_DATE def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" @@ -103,9 +90,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( self._entry, next_pickup_event.pickup_types ), - ATTR_NEXT_PICKUP_DATE: async_get_utc_midnight( - next_pickup_event.date - ).isoformat(), + ATTR_NEXT_PICKUP_DATE: next_pickup_event.date.isoformat(), } ) - self._attr_native_value = async_get_utc_midnight(pickup_event.date).isoformat() + self._attr_native_value = pickup_event.date.isoformat() From 8a1d80c60933f07c8325bcc37ac51a3c6dd37d5b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 5 Nov 2021 21:10:55 +0100 Subject: [PATCH 0270/1452] Fix regression after merging fixtures with old path (#59187) * Fix regression after merging old fixtures * Move to symo --- .../fronius/fixtures}/symo/GetAPIVersion.json | 0 .../fronius/fixtures}/symo/GetInverterInfo.json | 0 .../GetInverterRealtimeDate_Device_1_day.json | 0 .../GetInverterRealtimeDate_Device_1_night.json | 0 .../fronius/fixtures}/symo/GetLoggerInfo.json | 0 .../symo/GetMeterRealtimeData_Device_0.json | 0 .../symo/GetMeterRealtimeData_System.json | 0 .../symo/GetPowerFlowRealtimeData_day.json | 0 .../symo/GetPowerFlowRealtimeData_night.json | 0 .../symo/GetStorageRealtimeData_System.json | 0 tests/components/fronius/test_sensor.py | 16 ++++++++-------- 11 files changed, 8 insertions(+), 8 deletions(-) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetAPIVersion.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetInverterInfo.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetInverterRealtimeDate_Device_1_day.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetInverterRealtimeDate_Device_1_night.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetLoggerInfo.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetMeterRealtimeData_Device_0.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetMeterRealtimeData_System.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetPowerFlowRealtimeData_day.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetPowerFlowRealtimeData_night.json (100%) rename tests/{fixtures/fronius => components/fronius/fixtures}/symo/GetStorageRealtimeData_System.json (100%) diff --git a/tests/fixtures/fronius/symo/GetAPIVersion.json b/tests/components/fronius/fixtures/symo/GetAPIVersion.json similarity index 100% rename from tests/fixtures/fronius/symo/GetAPIVersion.json rename to tests/components/fronius/fixtures/symo/GetAPIVersion.json diff --git a/tests/fixtures/fronius/symo/GetInverterInfo.json b/tests/components/fronius/fixtures/symo/GetInverterInfo.json similarity index 100% rename from tests/fixtures/fronius/symo/GetInverterInfo.json rename to tests/components/fronius/fixtures/symo/GetInverterInfo.json diff --git a/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_day.json similarity index 100% rename from tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_day.json rename to tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_day.json diff --git a/tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_night.json similarity index 100% rename from tests/fixtures/fronius/symo/GetInverterRealtimeDate_Device_1_night.json rename to tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_night.json diff --git a/tests/fixtures/fronius/symo/GetLoggerInfo.json b/tests/components/fronius/fixtures/symo/GetLoggerInfo.json similarity index 100% rename from tests/fixtures/fronius/symo/GetLoggerInfo.json rename to tests/components/fronius/fixtures/symo/GetLoggerInfo.json diff --git a/tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json similarity index 100% rename from tests/fixtures/fronius/symo/GetMeterRealtimeData_Device_0.json rename to tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json diff --git a/tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_System.json similarity index 100% rename from tests/fixtures/fronius/symo/GetMeterRealtimeData_System.json rename to tests/components/fronius/fixtures/symo/GetMeterRealtimeData_System.json diff --git a/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_day.json similarity index 100% rename from tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_day.json rename to tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_day.json diff --git a/tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json similarity index 100% rename from tests/fixtures/fronius/symo/GetPowerFlowRealtimeData_night.json rename to tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json diff --git a/tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json b/tests/components/fronius/fixtures/symo/GetStorageRealtimeData_System.json similarity index 100% rename from tests/fixtures/fronius/symo/GetStorageRealtimeData_System.json rename to tests/components/fronius/fixtures/symo/GetStorageRealtimeData_System.json diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index b5617fad03c..4564d26fa9f 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -26,40 +26,40 @@ def mock_responses(aioclient_mock: AiohttpClientMocker, night: bool = False) -> aioclient_mock.get( f"{MOCK_HOST}/solar_api/GetAPIVersion.cgi", - text=load_fixture("fronius/symo/GetAPIVersion.json"), + text=load_fixture("symo/GetAPIVersion.json", "fronius"), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" "DeviceId=1&DataCollection=CommonInverterData", text=load_fixture( - f"fronius/symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json" + f"symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json", "fronius" ), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetInverterInfo.cgi", - text=load_fixture("fronius/symo/GetInverterInfo.json"), + text=load_fixture("symo/GetInverterInfo.json", "fronius"), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetLoggerInfo.cgi", - text=load_fixture("fronius/symo/GetLoggerInfo.json"), + text=load_fixture("symo/GetLoggerInfo.json", "fronius"), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0", - text=load_fixture("fronius/symo/GetMeterRealtimeData_Device_0.json"), + text=load_fixture("symo/GetMeterRealtimeData_Device_0.json", "fronius"), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System", - text=load_fixture("fronius/symo/GetMeterRealtimeData_System.json"), + text=load_fixture("symo/GetMeterRealtimeData_System.json", "fronius"), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetPowerFlowRealtimeData.fcgi", text=load_fixture( - f"fronius/symo/GetPowerFlowRealtimeData_{_day_or_night}.json" + f"symo/GetPowerFlowRealtimeData_{_day_or_night}.json", "fronius" ), ) aioclient_mock.get( f"{MOCK_HOST}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", - text=load_fixture("fronius/symo/GetStorageRealtimeData_System.json"), + text=load_fixture("symo/GetStorageRealtimeData_System.json", "fronius"), ) From 7b59dea67ecb099a8d7950d52993241685dbd2b8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 6 Nov 2021 00:11:41 +0000 Subject: [PATCH 0271/1452] [ci skip] Translation update --- .../components/airthings/translations/pl.json | 1 + .../amberelectric/translations/pl.json | 13 +++++ .../components/button/translations/ja.json | 11 ++++ .../components/button/translations/pl.json | 8 +++ .../crownstone/translations/pl.json | 56 ++++++++++++++++--- .../devolo_home_network/translations/pl.json | 14 +++++ .../components/dlna_dmr/translations/pl.json | 24 +++++++- .../components/esphome/translations/pl.json | 13 +++++ .../components/homekit/translations/pl.json | 2 +- .../modem_callerid/translations/pl.json | 6 +- .../components/motioneye/translations/ja.json | 2 +- .../components/nest/translations/ca.json | 7 +++ .../components/nest/translations/de.json | 7 +++ .../components/nest/translations/et.json | 7 +++ .../components/nest/translations/hu.json | 7 +++ .../components/nest/translations/ja.json | 4 ++ .../components/nest/translations/ru.json | 7 +++ .../components/nest/translations/zh-Hant.json | 7 +++ .../components/netgear/translations/pl.json | 7 +++ .../nmap_tracker/translations/pl.json | 1 + .../opengarage/translations/pl.json | 1 + .../components/rfxtrx/translations/pl.json | 7 ++- .../components/ridwell/translations/pl.json | 12 ++++ .../components/samsungtv/translations/pl.json | 1 + .../components/select/translations/pl.json | 2 +- .../components/shelly/translations/pl.json | 4 ++ .../components/switchbot/translations/pl.json | 6 +- .../components/tplink/translations/pl.json | 7 ++- .../components/tuya/translations/pl.json | 9 ++- .../components/zwave_js/translations/pl.json | 9 +++ 30 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/button/translations/ja.json create mode 100644 homeassistant/components/button/translations/pl.json create mode 100644 homeassistant/components/devolo_home_network/translations/pl.json create mode 100644 homeassistant/components/ridwell/translations/pl.json diff --git a/homeassistant/components/airthings/translations/pl.json b/homeassistant/components/airthings/translations/pl.json index 08b4f80938a..6c9cb9b5678 100644 --- a/homeassistant/components/airthings/translations/pl.json +++ b/homeassistant/components/airthings/translations/pl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "description": "Zaloguj si\u0119 pod {url}, aby znale\u017a\u0107 swoje dane uwierzytelniaj\u0105ce", "id": "ID", "secret": "Sekret" } diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json index 1054014ea49..1e9b66e3c3c 100644 --- a/homeassistant/components/amberelectric/translations/pl.json +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -1,7 +1,20 @@ { "config": { "step": { + "site": { + "data": { + "site_name": "Nazwa obiektu", + "site_nmi": "Numer identyfikacyjny (NMI) obiektu" + }, + "description": "Wybierz NMI obiektu, kt\u00f3ry chcesz doda\u0107", + "title": "Amber Electric" + }, "user": { + "data": { + "api_token": "Token API", + "site_id": "Identyfikator obiektu" + }, + "description": "Przejd\u017a do {api_url}, aby wygenerowa\u0107 klucz API", "title": "Amber Electric" } } diff --git a/homeassistant/components/button/translations/ja.json b/homeassistant/components/button/translations/ja.json new file mode 100644 index 00000000000..9d2f1615dc1 --- /dev/null +++ b/homeassistant/components/button/translations/ja.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "{entity_name} \u30dc\u30bf\u30f3\u3092\u62bc\u3059" + }, + "trigger_type": { + "pressed": "{entity_name} \u304c\u62bc\u3055\u308c\u307e\u3057\u305f" + } + }, + "title": "\u30dc\u30bf\u30f3" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/pl.json b/homeassistant/components/button/translations/pl.json new file mode 100644 index 00000000000..0c570aa6b46 --- /dev/null +++ b/homeassistant/components/button/translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "press": "naci\u015bnij przycisk {entity_name}" + } + }, + "title": "Przycisk" +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json index c71c27c4601..e3299767932 100644 --- a/homeassistant/components/crownstone/translations/pl.json +++ b/homeassistant/components/crownstone/translations/pl.json @@ -1,9 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "usb_setup_complete": "Konfiguracja USB Crownstone zako\u0144czona.", + "usb_setup_unsuccessful": "Konfiguracja USB Crownstone nie powiod\u0142a si\u0119." }, "error": { + "account_not_verified": "Konto niezweryfikowane. Aktywuj swoje konto za pomoc\u0105 e-maila aktywacyjnego od Crownstone.", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -11,12 +14,23 @@ "usb_config": { "data": { "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wybierz port szeregowy urz\u0105dzenia USB Crownstone lub wybierz opcj\u0119 \"Nie u\u017cywaj USB\", je\u015bli nie chcesz konfigurowa\u0107 urz\u0105dzenia USB. \n\nPoszukaj urz\u0105dzenia z VID 10C4 i PID EA60.", + "title": "Konfiguracja urz\u0105dzenia USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wprowad\u017a r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone.", + "title": "Wpisz r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Wybierz USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", + "title": "USB Crownstone Sphere" }, "user": { "data": { @@ -29,25 +43,53 @@ }, "options": { "step": { + "init": { + "data": { + "usb_sphere_option": "USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", + "use_usb_option": "U\u017cyj urz\u0105dzenia USB Crownstone do lokalnej transmisji danych" + } + }, "usb_config": { "data": { "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wybierz port szeregowy urz\u0105dzenia USB Crownstone. \n\nPoszukaj urz\u0105dzenia z VID 10C4 i PID EA60.", + "title": "Konfiguracja urz\u0105dzenia USB Crownstone" }, "usb_config_option": { "data": { "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wybierz port szeregowy urz\u0105dzenia USB Crownstone. \n\nPoszukaj urz\u0105dzenia z VID 10C4 i PID EA60.", + "title": "Konfiguracja urz\u0105dzenia USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wprowad\u017a r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone.", + "title": "Wpisz r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone" }, "usb_manual_config_option": { "data": { "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" - } + }, + "description": "Wprowad\u017a r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone.", + "title": "Wpisz r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Wybierz USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", + "title": "USB Crownstone Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Wybierz USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", + "title": "USB Crownstone Sphere" } } } diff --git a/homeassistant/components/devolo_home_network/translations/pl.json b/homeassistant/components/devolo_home_network/translations/pl.json new file mode 100644 index 00000000000..bcdb8956f1d --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json index 09fb3dce509..280a63be7fe 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -2,10 +2,15 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA", + "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 pasuj\u0105cego urz\u0105dzenia DLNA", + "incomplete_config": "W konfiguracji brakuje wymaganej zmiennej", + "non_unique_id": "Znaleziono wiele urz\u0105dze\u0144 z tym samym unikalnym identyfikatorem" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA" }, "flow_title": "{name}", "step": { @@ -24,5 +29,20 @@ } } } + }, + "options": { + "error": { + "invalid_url": "Nieprawid\u0142owy adres URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "Adres Callback URL dla detektora zdarze\u0144", + "listen_port": "Port detektora zdarze\u0144 (losowy, je\u015bli nie jest ustawiony)", + "poll_availability": "Sondowanie na dost\u0119pno\u015b\u0107 urz\u0105dze\u0144" + }, + "title": "Konfiguracja rendera multimedi\u00f3w cyfrowych (DMR) dla DLNA" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 2fa5f37ff18..da6154e9fb6 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -8,6 +8,7 @@ "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_psk": "Klucz szyfruj\u0105cy transport jest nieprawid\u0142owy. Upewnij si\u0119, \u017ce pasuje do tego, kt\u00f3ry masz w swojej konfiguracji.", "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -22,6 +23,18 @@ "description": "Czy chcesz doda\u0107 w\u0119ze\u0142 ESPHome `{name}` do Home Assistanta?", "title": "Znaleziono w\u0119ze\u0142 ESPHome" }, + "encryption_key": { + "data": { + "noise_psk": "Klucz szyfruj\u0105cy" + }, + "description": "Wprowad\u017a klucz szyfruj\u0105cy ustawiony w konfiguracji dla {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Klucz szyfruj\u0105cy" + }, + "description": "Urz\u0105dzenie ESPHome {name} w\u0142\u0105czy\u0142o szyfrowanie transportu lub zmieni\u0142o klucz szyfruj\u0105cy. Wprowad\u017a zaktualizowany klucz." + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 4b25a482cfd..5cc9ae00f0b 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -33,7 +33,7 @@ "camera_copy": "Kamery obs\u0142uguj\u0105ce kodek H.264" }, "description": "Sprawd\u017a, czy wszystkie kamery obs\u0142uguj\u0105 kodek H.264. Je\u015bli kamera nie wysy\u0142a strumienia skompresowanego kodekiem H.264, system b\u0119dzie transkodowa\u0142 wideo do H.264 dla HomeKit. Transkodowanie wymaga wydajnego procesora i jest ma\u0142o prawdopodobne, aby dzia\u0142a\u0142o na komputerach jednop\u0142ytkowych.", - "title": "Wyb\u00f3r kodeka wideo kamery" + "title": "Konfiguracja kamery" }, "include_exclude": { "data": { diff --git a/homeassistant/components/modem_callerid/translations/pl.json b/homeassistant/components/modem_callerid/translations/pl.json index b608a467395..5101fc574a0 100644 --- a/homeassistant/components/modem_callerid/translations/pl.json +++ b/homeassistant/components/modem_callerid/translations/pl.json @@ -10,14 +10,16 @@ }, "step": { "usb_confirm": { - "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego." + "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego.", + "title": "Modem telefoniczny" }, "user": { "data": { "name": "Nazwa", "port": "Port" }, - "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego." + "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego.", + "title": "Modem telefoniczny" } } } diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 5bded845e3f..4c52169a7af 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -3,7 +3,7 @@ "step": { "init": { "data": { - "stream_url_template": "\u30b9\u30c8\u30ea\u30fc\u30e0URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" + "stream_url_template": "Stream URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" } } } diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 35cc6eff946..c17e5ca4bf7 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -18,6 +18,13 @@ "unknown": "Error inesperat" }, "step": { + "auth": { + "data": { + "code": "Token d'acc\u00e9s" + }, + "description": "Per enlla\u00e7ar un compte de Google, [autoritza el compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa a continuaci\u00f3 el codi 'token' d'autenticaci\u00f3 proporcionat.", + "title": "Vinculaci\u00f3 amb compte de Google" + }, "init": { "data": { "flow_impl": "Prove\u00efdor" diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index dde725681d8..ba561ebc371 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -18,6 +18,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "auth": { + "data": { + "code": "Zugangstoken" + }, + "description": "Um dein Google-Konto zu verkn\u00fcpfen, w\u00e4hle [Konto autorisieren]({url}).\n\nKopiere nach der Autorisierung den unten angegebenen Auth Token Code.", + "title": "Google-Konto verkn\u00fcpfen" + }, "init": { "data": { "flow_impl": "Anbieter" diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 773835ea29b..f0e5c3d2359 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -18,6 +18,13 @@ "unknown": "Tundmatu viga tuvastuskoodi kinnitamisel" }, "step": { + "auth": { + "data": { + "code": "Juurdep\u00e4\u00e4sut\u00f5end" + }, + "description": "Oma Google'i konto sidumiseks vali [autoriseeri oma konto]({url}).\n\nP\u00e4rast autoriseerimist kopeeri ja aseta allpool esitatud Auth Token'i kood.", + "title": "Google'i konto linkimine" + }, "init": { "data": { "flow_impl": "Pakkuja" diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 95f284b9a81..15ad2d79a9f 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -18,6 +18,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "auth": { + "data": { + "code": "Hozz\u00e1f\u00e9r\u00e9si token" + }, + "description": "[Enged\u00e9lyezze] Google-fi\u00f3kj\u00e1t az \u00f6sszekapcsol\u00e1hoz ({url}).\n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja \u00e1t a kapott token k\u00f3dot.", + "title": "\u00d6sszekapcsol\u00e1s Google-al" + }, "init": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 4a1f8502d5a..2bbd3b1b9fa 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -5,6 +5,10 @@ "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059" }, "step": { + "auth": { + "description": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001 [authorize your account]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u8a8d\u8a3c\u5f8c\u3001\u63d0\u4f9b\u3055\u308c\u305f\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u306e\u30b3\u30fc\u30c9\u3092\u4ee5\u4e0b\u306b\u30b3\u30d4\u30fc\u30da\u30fc\u30b9\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, "init": { "data": { "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 1919f8db89e..8e090bd5ce2 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -18,6 +18,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "auth": { + "data": { + "code": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" + }, "init": { "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index d9b29c7fa7a..dbd8ad3b3df 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -18,6 +18,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "auth": { + "data": { + "code": "\u5b58\u53d6\u6b0a\u6756" + }, + "description": "\u6b32\u9023\u7d50 Google \u5e33\u865f\u3001\u8acb\u5148 [\u8a8d\u8b49\u5e33\u865f]({url})\u3002\n\n\u65bc\u8a8d\u8b49\u5f8c\u3001\u65bc\u4e0b\u65b9\u8cbc\u4e0a\u8a8d\u8b49\u6b0a\u6756\u4ee3\u78bc\u3002", + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, "init": { "data": { "flow_impl": "\u8a8d\u8b49\u63d0\u4f9b\u8005" diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json index 3cd56289fbd..f5feb67cfe1 100644 --- a/homeassistant/components/netgear/translations/pl.json +++ b/homeassistant/components/netgear/translations/pl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, + "error": { + "config": "B\u0142\u0105d po\u0142\u0105czenia lub logowania: sprawd\u017a konfiguracj\u0119" + }, "step": { "user": { "data": { @@ -12,6 +15,7 @@ "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika (opcjonalnie)" }, + "description": "Domy\u015blne IP lub nazwa hosta: {host}\nDomy\u015blny port: {port}\nDomy\u015blna nazwa u\u017cytkownika: {username}", "title": "Netgear" } } @@ -19,6 +23,9 @@ "options": { "step": { "init": { + "data": { + "consider_home": "Czas przed oznaczeniem \"w domu\" (w sekundach)" + }, "description": "Okre\u015bl opcjonalne ustawienia", "title": "Netgear" } diff --git a/homeassistant/components/nmap_tracker/translations/pl.json b/homeassistant/components/nmap_tracker/translations/pl.json index dc16816609c..98f27dcdd4b 100644 --- a/homeassistant/components/nmap_tracker/translations/pl.json +++ b/homeassistant/components/nmap_tracker/translations/pl.json @@ -25,6 +25,7 @@ "step": { "init": { "data": { + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"poza domem\" od utracenia z nim po\u0142\u0105czenia.", "exclude": "Adresy sieciowe (rozdzielone przecinkami) do wykluczenia ze skanowania", "home_interval": "Minimalna liczba minut mi\u0119dzy skanami aktywnych urz\u0105dze\u0144 (oszcz\u0119dzanie baterii)", "hosts": "Adresy sieciowe (oddzielone przecinkami) do skanowania", diff --git a/homeassistant/components/opengarage/translations/pl.json b/homeassistant/components/opengarage/translations/pl.json index d0905f8ed3d..14de1ee3a8e 100644 --- a/homeassistant/components/opengarage/translations/pl.json +++ b/homeassistant/components/opengarage/translations/pl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "device_key": "Klucz urz\u0105dzenia", "host": "Nazwa hosta lub adres IP", "port": "Port", "verify_ssl": "Weryfikacja certyfikatu SSL" diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index 0334ca217f8..0ac33ed40fc 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -44,8 +44,13 @@ } }, "device_automation": { + "action_type": { + "send_command": "wy\u015blij polecenie: {subtype}", + "send_status": "wy\u015blij aktualizacj\u0119 statusu: {subtype}" + }, "trigger_type": { - "command": "Otrzymane polecenie: {subtype}" + "command": "Otrzymane polecenie: {subtype}", + "status": "otrzymany status: {subtype}" } }, "few": "kilka", diff --git a/homeassistant/components/ridwell/translations/pl.json b/homeassistant/components/ridwell/translations/pl.json new file mode 100644 index 00000000000..b8b737c37a3 --- /dev/null +++ b/homeassistant/components/ridwell/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index ed117c60d21..00039239597 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "id_missing": "To urz\u0105dzenie Samsung nie ma numeru seryjnego.", + "missing_config_entry": "To urz\u0105dzenie Samsung nie ma wpisu konfiguracyjnego.", "not_supported": "To urz\u0105dzenie Samsung nie jest obecnie obs\u0142ugiwane", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/select/translations/pl.json b/homeassistant/components/select/translations/pl.json index 3d19c05a80a..26c82097ead 100644 --- a/homeassistant/components/select/translations/pl.json +++ b/homeassistant/components/select/translations/pl.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "select_option": "Zmie\u0144 opcj\u0119 {entity_name}" + "select_option": "zmie\u0144 opcj\u0119 {entity_name}" }, "condition_type": { "selected_option": "aktualnie wybrana opcja dla {entity_name}" diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 25d00cf1e53..3af4583d71c 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -37,11 +37,15 @@ "button4": "Czwarty przycisk" }, "trigger_type": { + "btn_down": "zostanie wci\u015bni\u0119ty przycisk \"w d\u00f3\u0142\" {subtype}", + "btn_up": "zostanie wci\u015bni\u0119ty przycisk \"do g\u00f3ry\" {subtype}", "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", + "double_push": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty", "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", + "single_push": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 2fdc90d59cf..23262ea8ed4 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -3,6 +3,8 @@ "abort": { "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", + "switchbot_unsupported_type": "Nieobs\u0142ugiwany typ Switchbota.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { @@ -15,7 +17,8 @@ "mac": "Adres MAC urz\u0105dzenia", "name": "Nazwa", "password": "Has\u0142o" - } + }, + "title": "Konfiguracja urz\u0105dzenia Switchbot" } } }, @@ -25,6 +28,7 @@ "data": { "retry_count": "Liczba ponownych pr\u00f3b", "retry_timeout": "Limit czasu mi\u0119dzy kolejnymi pr\u00f3bami", + "scan_timeout": "Jak d\u0142ugo skanowa\u0107 w poszukiwaniu danych reklamowych", "update_time": "Czas mi\u0119dzy aktualizacjami (sekundy)" } } diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index da91b12ea7c..b7fff06d4b7 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -8,10 +8,14 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} {model} ({host})?" + }, "pick_device": { "data": { "device": "Urz\u0105dzenie" @@ -20,7 +24,8 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP" - } + }, + "description": "Je\u015bli nie podasz IP lub nazwy hosta, wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." } } } diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 28993f5b659..9f40008135a 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -13,9 +13,15 @@ "step": { "login": { "data": { + "access_id": "Identyfikator dost\u0119pu", + "access_secret": "Has\u0142o dost\u0119pu", + "country_code": "Kod kraju", + "endpoint": "Strefa dost\u0119pno\u015bci", "password": "Has\u0142o", + "tuya_app_type": "Aplikacja mobilna", "username": "Konto" }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce Tuya", "title": "Tuya" }, "user": { @@ -24,10 +30,11 @@ "password": "Has\u0142o", "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", "region": "Region", + "tuya_project_type": "Typ projektu chmury Tuya", "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", - "title": "Tuya" + "title": "Integracja Tuya" } } }, diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 0ae905a0854..2cd8144789a 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -58,6 +58,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "wyczy\u015b\u0107 kod u\u017cytkownika w {entity_name}", + "ping": "pinguj urz\u0105dzenie", + "refresh_value": "od\u015bwie\u017c warto\u015bci dla {entity_name}", + "reset_meter": "zresetuj liczniki na {subtype}", + "set_config_parameter": "ustaw warto\u015b\u0107 parametru konfiguracji {subtype}", + "set_lock_usercode": "ustaw kod u\u017cytkownika w {entity_name}", + "set_value": "ustaw warto\u015b\u0107 Z-Wave" + }, "condition_type": { "config_parameter": "warto\u015b\u0107 parametru jest {subtype}", "node_status": "stan w\u0119z\u0142a", From 3693b9bd040c98ea3476c1042cca8c653183c1f5 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 6 Nov 2021 09:54:51 +0800 Subject: [PATCH 0272/1452] Adjust frag_duration setting in stream (#59135) --- homeassistant/components/stream/worker.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 881614b04a3..e4be3168393 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -102,18 +102,18 @@ class SegmentBuffer: # The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x] # of the part target duration. We use the frag_duration option to tell ffmpeg to try to # cut the fragments when they reach frag_duration. However, the resulting fragments can - # have variability in their durations and can end up being too short or too long. If - # there are two tracks, as in the case of a video feed with audio, the fragment cut seems - # to be done on the first track that crosses the desired threshold, and cutting on the - # audio track may result in a shorter video fragment than desired. Conversely, with a + # have variability in their durations and can end up being too short or too long. With a # video track with no audio, the discrete nature of frames means that the frame at the # end of a fragment will sometimes extend slightly beyond the desired frag_duration. - # Given this, our approach is to use a frag_duration near the upper end of the range for - # outputs with audio using a frag_duration at the lower end of the range for outputs with - # only video. + # If there are two tracks, as in the case of a video feed with audio, there is an added + # wrinkle as the fragment cut seems to be done on the first track that crosses the desired + # threshold, and cutting on the audio track may also result in a shorter video fragment + # than desired. + # Given this, our approach is to give ffmpeg a frag_duration somewhere in the middle + # of the range, hoping that the parts stay pretty well bounded, and we adjust the part + # durations a bit in the hls metadata so that everything "looks" ok. "frag_duration": str( - self._stream_settings.part_target_duration - * (98e4 if add_audio else 9e5) + self._stream_settings.part_target_duration * 9e5 ), } if self._stream_settings.ll_hls From aaaae6abcab458544d0325177e047f618cb308e3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 6 Nov 2021 06:53:34 -0400 Subject: [PATCH 0273/1452] Bump pyefergy to 0.1.4 (#59219) --- homeassistant/components/efergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index 17f104c561f..3c2d26f9941 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -3,7 +3,7 @@ "name": "Efergy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.1.3"], + "requirements": ["pyefergy==0.1.4"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 46c766ee2bc..93a3b8c8c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.1.3 +pyefergy==0.1.4 # homeassistant.components.eight_sleep pyeight==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7fd0019df7..c0a2e62109d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,7 +862,7 @@ pydispatcher==2.0.5 pyeconet==0.1.14 # homeassistant.components.efergy -pyefergy==0.1.3 +pyefergy==0.1.4 # homeassistant.components.everlights pyeverlights==0.1.0 From 2a05697c9171a2d96d3f52cc2089b8c341b32ac7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 6 Nov 2021 12:50:53 +0100 Subject: [PATCH 0274/1452] bump aioshelly to 1.0.4 (#59209) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 09a046ee78d..389d31ac195 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.2"], + "requirements": ["aioshelly==1.0.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 93a3b8c8c5b..a5cc8dbd81d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aiorecollect==1.0.8 aioridwell==0.2.0 # homeassistant.components.shelly -aioshelly==1.0.2 +aioshelly==1.0.4 # homeassistant.components.switcher_kis aioswitcher==2.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0a2e62109d..2f11230d4ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aiorecollect==1.0.8 aioridwell==0.2.0 # homeassistant.components.shelly -aioshelly==1.0.2 +aioshelly==1.0.4 # homeassistant.components.switcher_kis aioswitcher==2.0.6 From 66bdbbff45be3bbbfea3cbf4d7d09c7e82131e76 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 6 Nov 2021 12:31:06 +0000 Subject: [PATCH 0275/1452] Bump aiolyric to v1.0.8 (#59228) --- homeassistant/components/lyric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index fbcc9567c3a..c45d7fb38e9 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", "dependencies": ["http"], - "requirements": ["aiolyric==1.0.7"], + "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a5cc8dbd81d..7f33af38bff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -210,7 +210,7 @@ aiolip==1.1.6 aiolookin==0.0.3 # homeassistant.components.lyric -aiolyric==1.0.7 +aiolyric==1.0.8 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f11230d4ba..d927f865eea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiolip==1.1.6 aiolookin==0.0.3 # homeassistant.components.lyric -aiolyric==1.0.7 +aiolyric==1.0.8 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From 15636a4fe4b76514956ce7537de286f4b9eb3f6c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 6 Nov 2021 12:32:20 +0000 Subject: [PATCH 0276/1452] Bump systembridge to v2.2.1 (#59229) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 368262a767b..0a6bca604e7 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==2.1.3"], + "requirements": ["systembridge==2.2.1"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 7f33af38bff..118d0a9003f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2266,7 +2266,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==2.1.3 +systembridge==2.2.1 # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d927f865eea..e6066c1f127 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1327,7 +1327,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridge==2.1.3 +systembridge==2.2.1 # homeassistant.components.tellduslive tellduslive==0.10.11 From 23f0afa789cdd98566d4f796a9f2d8d0a2067144 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Nov 2021 07:34:10 -0500 Subject: [PATCH 0277/1452] Bump flux_led to 0.24.17 (#59211) * Bump flux_led to 0.24.16 - Changes: https://github.com/Danielhiversen/flux_led/compare/0.24.15...0.24.16 - Fixes turning on/off when device is out of sync internally (seen on 0x33 firmware 8) - Fixes #59190 * Bump to .17 to fix typing --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 3a3b7b7b572..429465a0f3c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.15"], + "requirements": ["flux_led==0.24.17"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 118d0a9003f..4a5bc0313c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.15 +flux_led==0.24.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6066c1f127..705f0224341 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.15 +flux_led==0.24.17 # homeassistant.components.homekit fnvhash==0.1.0 From 59ae35892c1299cbd96c83b03fb7416132c57cb2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 6 Nov 2021 13:40:45 +0100 Subject: [PATCH 0278/1452] Bump RMVtransport to v0.3.3 (#59210) --- homeassistant/components/rmvtransport/manifest.json | 10 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index a8e2b253324..bcbb96c7034 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -2,11 +2,7 @@ "domain": "rmvtransport", "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": [ - "PyRMVtransport==0.3.2" - ], - "codeowners": [ - "@cgtobi" - ], + "requirements": ["PyRMVtransport==0.3.3"], + "codeowners": ["@cgtobi"], "iot_class": "cloud_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 4a5bc0313c9..a04c413d3bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyNaCl==1.4.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.2 +PyRMVtransport==0.3.3 # homeassistant.components.telegram_bot PySocks==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 705f0224341..ea45b0abe2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,7 +21,7 @@ PyNaCl==1.4.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.2 +PyRMVtransport==0.3.3 # homeassistant.components.switchbot # PySwitchbot==0.12.0 From a9c5f68d646676cbe130d4af785b9866589bc624 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Nov 2021 08:13:48 -0500 Subject: [PATCH 0279/1452] Reduce code duplication in gogogate2 (#59165) --- homeassistant/components/gogogate2/common.py | 23 ++++-- homeassistant/components/gogogate2/cover.py | 42 +++-------- homeassistant/components/gogogate2/sensor.py | 73 +++++++------------- 3 files changed, 51 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index a70a1b6bf81..5d0392e6db2 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -80,18 +80,24 @@ class GoGoGate2Entity(CoordinatorEntity): super().__init__(data_update_coordinator) self._config_entry = config_entry self._door = door - self._unique_id = unique_id + self._door_id = door.door_id + self._api = data_update_coordinator.api + self._attr_unique_id = unique_id @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - def _get_door(self) -> AbstractDoor: + def door(self) -> AbstractDoor: + """Return the door object.""" door = get_door_by_id(self._door.door_id, self.coordinator.data) self._door = door or self._door return self._door + @property + def door_status(self) -> AbstractDoor: + """Return the door with status.""" + data = self.coordinator.data + door_with_statuses = self._api.async_get_door_statuses_from_info(data) + return door_with_statuses[self._door_id] + @property def device_info(self) -> DeviceInfo: """Device info for the controller.""" @@ -108,6 +114,11 @@ class GoGoGate2Entity(CoordinatorEntity): sw_version=data.firmwareversion, ) + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {"door_id": self._door_id} + def get_data_update_coordinator( hass: HomeAssistant, config_entry: ConfigEntry diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index fb5871e8636..16191304bb9 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -55,64 +55,42 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): """Initialize the object.""" unique_id = cover_unique_id(config_entry, door) super().__init__(config_entry, data_update_coordinator, door, unique_id) - self._api = data_update_coordinator.api - self._is_available = True + self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + self._attr_device_class = ( + DEVICE_CLASS_GATE if self.door.gate else DEVICE_CLASS_GARAGE + ) @property def name(self): """Return the name of the door.""" - return self._get_door().name + return self.door.name @property def is_closed(self): """Return true if cover is closed, else False.""" - door_status = self._get_door_status() + door_status = self.door_status if door_status == DoorStatus.OPENED: return False if door_status == DoorStatus.CLOSED: return True - return None - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - if self._get_door().gate: - return DEVICE_CLASS_GATE - - return DEVICE_CLASS_GARAGE - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE - @property def is_closing(self): """Return if the cover is closing or not.""" - return self._get_door_status() == TransitionDoorStatus.CLOSING + return self.door_status == TransitionDoorStatus.CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return self._get_door_status() == TransitionDoorStatus.OPENING + return self.door_status == TransitionDoorStatus.OPENING async def async_open_cover(self, **kwargs): """Open the door.""" - await self._api.async_open_door(self._get_door().door_id) + await self._api.async_open_door(self._door_id) await self.coordinator.async_refresh() async def async_close_cover(self, **kwargs): """Close the door.""" - await self._api.async_close_door(self._get_door().door_id) + await self._api.async_close_door(self._door_id) await self.coordinator.async_refresh() - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {"door_id": self._get_door().door_id} - - def _get_door_status(self) -> AbstractDoor: - return self._api.async_get_door_statuses_from_info(self.coordinator.data)[ - self._door.door_id - ] diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 6eb3d823c22..9721f9b8722 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -49,7 +49,20 @@ async def async_setup_entry( async_add_entities(sensors) -class DoorSensorBattery(GoGoGate2Entity, SensorEntity): +class DoorSensorEntity(GoGoGate2Entity, SensorEntity): + """Base class for door sensor entities.""" + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + attrs = super().extra_state_attributes + door = self.door + if door.sensorid is not None: + attrs["sensor_id"] = door.sensorid + return attrs + + +class DoorSensorBattery(DoorSensorEntity): """Battery sensor entity for gogogate2 door sensor.""" _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @@ -63,38 +76,21 @@ class DoorSensorBattery(GoGoGate2Entity, SensorEntity): """Initialize the object.""" unique_id = sensor_unique_id(config_entry, door, "battery") super().__init__(config_entry, data_update_coordinator, door, unique_id) + self._attr_device_class = DEVICE_CLASS_BATTERY + self._attr_state_class = STATE_CLASS_MEASUREMENT @property def name(self): """Return the name of the door.""" - return f"{self._get_door().name} battery" - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_BATTERY + return f"{self.door.name} battery" @property 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 - - @property - def state_class(self) -> str: - """Return the Measurement State Class.""" - return STATE_CLASS_MEASUREMENT - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - door = self._get_door() - if door.sensorid is not None: - return {"door_id": door.door_id, "sensor_id": door.sensorid} - return None + return self.door.voltage # This is a percentage, not an absolute voltage -class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): +class DoorSensorTemperature(DoorSensorEntity): """Temperature sensor entity for gogogate2 door sensor.""" def __init__( @@ -106,37 +102,16 @@ class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): """Initialize the object.""" unique_id = sensor_unique_id(config_entry, door, "temperature") super().__init__(config_entry, data_update_coordinator, door, unique_id) + self._attr_device_class = DEVICE_CLASS_TEMPERATURE + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_native_unit_of_measurement = TEMP_CELSIUS @property def name(self): """Return the name of the door.""" - return f"{self._get_door().name} temperature" - - @property - def state_class(self) -> str: - """Return the Measurement State Class.""" - return STATE_CLASS_MEASUREMENT - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TEMPERATURE + return f"{self.door.name} temperature" @property def native_value(self): """Return the state of the entity.""" - door = self._get_door() - return door.temperature - - @property - def native_unit_of_measurement(self): - """Return the unit_of_measurement.""" - return TEMP_CELSIUS - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - door = self._get_door() - if door.sensorid is not None: - return {"door_id": door.door_id, "sensor_id": door.sensorid} - return None + return self.door.temperature From 6a149706abfa4cad3a3ea35e37d7f4182f025a5e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 6 Nov 2021 16:32:58 +0100 Subject: [PATCH 0280/1452] add constant CONF_SLEEP_PERIOD (#59195) --- homeassistant/components/shelly/__init__.py | 13 +++++++------ homeassistant/components/shelly/binary_sensor.py | 3 ++- homeassistant/components/shelly/config_flow.py | 12 ++++++------ homeassistant/components/shelly/const.py | 1 + homeassistant/components/shelly/sensor.py | 4 ++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6b15e4e730d..6a981ab2b86 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -36,6 +36,7 @@ from .const import ( BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, BLOCK, CONF_COAP_PORT, + CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DEFAULT_COAP_PORT, DEVICE, @@ -143,7 +144,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo if device_entry and entry.entry_id not in device_entry.config_entries: device_entry = None - sleep_period = entry.data.get("sleep_period") + sleep_period = entry.data.get(CONF_SLEEP_PERIOD) @callback def _async_device_online(_: Any) -> None: @@ -152,7 +153,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo if sleep_period is None: data = {**entry.data} - data["sleep_period"] = get_block_device_sleep_period(device.settings) + data[CONF_SLEEP_PERIOD] = get_block_device_sleep_period(device.settings) data["model"] = device.settings["device"]["type"] hass.config_entries.async_update_entry(entry, data=data) @@ -194,7 +195,7 @@ async def async_block_device_setup( platforms = BLOCK_SLEEPING_PLATFORMS - if not entry.data.get("sleep_period"): + if not entry.data.get(CONF_SLEEP_PERIOD): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ REST ] = ShellyDeviceRestWrapper(hass, device) @@ -239,7 +240,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Initialize the Shelly device wrapper.""" self.device_id: str | None = None - if sleep_period := entry.data["sleep_period"]: + if sleep_period := entry.data[CONF_SLEEP_PERIOD]: update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period else: update_interval = ( @@ -369,7 +370,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data.""" - if sleep_period := self.entry.data.get("sleep_period"): + if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): # Sleeping device, no point polling it, just mark it unavailable raise update_coordinator.UpdateFailed( f"Sleeping device did not update within {sleep_period} seconds interval" @@ -477,7 +478,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platforms = BLOCK_SLEEPING_PLATFORMS - if not entry.data.get("sleep_period"): + if not entry.data.get(CONF_SLEEP_PERIOD): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None platforms = BLOCK_PLATFORMS diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index d7e4983df77..69f5f0e4440 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.binary_sensor import ( STATE_ON, BinarySensorEntity, ) +from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant @@ -174,7 +175,7 @@ async def async_setup_entry( hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor ) - if config_entry.data["sleep_period"]: + if config_entry.data[CONF_SLEEP_PERIOD]: await async_setup_entry_attribute_entities( hass, config_entry, diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b77868296bd..580221d376f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from homeassistant.helpers.typing import DiscoveryInfoType -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -63,7 +63,7 @@ async def validate_input( await rpc_device.shutdown() return { "title": get_rpc_device_name(rpc_device), - "sleep_period": 0, + CONF_SLEEP_PERIOD: 0, "model": rpc_device.model, "gen": 2, } @@ -78,7 +78,7 @@ async def validate_input( block_device.shutdown() return { "title": get_block_device_name(block_device), - "sleep_period": get_block_device_sleep_period(block_device.settings), + CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), "model": block_device.model, "gen": 1, } @@ -130,7 +130,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=device_info["title"], data={ **user_input, - "sleep_period": device_info["sleep_period"], + CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], "gen": device_info["gen"], }, @@ -166,7 +166,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={ **user_input, CONF_HOST: self.host, - "sleep_period": device_info["sleep_period"], + CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], "gen": device_info["gen"], }, @@ -224,7 +224,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self.device_info["title"], data={ "host": self.host, - "sleep_period": self.device_info["sleep_period"], + CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], "model": self.device_info["model"], "gen": self.device_info["gen"], }, diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 50f81511062..5fceb64e65c 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -54,6 +54,7 @@ AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10 # Multiplier used to calculate the "update_interval" for sleeping devices. SLEEP_PERIOD_MULTIPLIER: Final = 1.2 +CONF_SLEEP_PERIOD: Final = "sleep_period" # Multiplier used to calculate the "update_interval" for non-sleeping devices. UPDATE_PERIOD_MULTIPLIER: Final = 2.2 diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index f61df56eaa7..7fcf456b658 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import SHAIR_MAX_WORK_HOURS +from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -317,7 +317,7 @@ async def async_setup_entry( hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor ) - if config_entry.data["sleep_period"]: + if config_entry.data[CONF_SLEEP_PERIOD]: await async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, SENSORS, BlockSleepingSensor ) From af521c0a359af6c2e1fc0eef2d844b1cfa331354 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 6 Nov 2021 16:43:29 +0100 Subject: [PATCH 0281/1452] Upgrade numpy to 1.21.4 (#59188) Co-authored-by: Marvin Wichmann --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index e68541973b1..315d1b705df 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.2"], + "requirements": ["numpy==1.21.4"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7c0194a4896..3ac1ffd31f9 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.2", "pyiqvia==1.1.0"], + "requirements": ["numpy==1.21.4", "pyiqvia==1.1.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 047acf66bb8..9c1f51c4933 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.2", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.4", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 391eeb1cf87..8d5ea0acaa2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.21.2", + "numpy==1.21.4", "pillow==8.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 340edcb6626..831e97aed3d 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.2"], + "requirements": ["numpy==1.21.4"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a04c413d3bd..cb99f907ace 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.2 +numpy==1.21.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea45b0abe2c..a32bc0d5ea8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -664,7 +664,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.2 +numpy==1.21.4 # homeassistant.components.google oauth2client==4.0.0 From 933ad5ae49b752c1147cd2e25563920d32cfc978 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 6 Nov 2021 16:46:51 +0100 Subject: [PATCH 0282/1452] Fix tradfri group reachable access (#59217) --- .../components/tradfri/base_class.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index a54c9f7d500..8a7cc6a2f4a 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -60,7 +60,6 @@ class TradfriBaseClass(Entity): """Initialize a device.""" self._api = handle_error(api) self._attr_name = device.name - self._attr_available = device.reachable self._device: Device = device self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( None @@ -105,7 +104,6 @@ class TradfriBaseClass(Entity): """Refresh the device data.""" self._device = device self._attr_name = device.name - self._attr_available = device.reachable if write_ha: self.async_write_ha_state() @@ -116,6 +114,16 @@ class TradfriBaseDevice(TradfriBaseClass): All devices should inherit from this class. """ + def __init__( + self, + device: Device, + api: Callable[[Command | list[Command]], Any], + gateway_id: str, + ) -> None: + """Initialize a device.""" + self._attr_available = device.reachable + super().__init__(device, api, gateway_id) + @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -128,3 +136,11 @@ class TradfriBaseDevice(TradfriBaseClass): sw_version=info.firmware_version, via_device=(DOMAIN, self._gateway_id), ) + + def _refresh(self, device: Device, write_ha: bool = True) -> None: + """Refresh the device data.""" + # The base class _refresh cannot be used, because + # there are devices (group) that do not have .reachable + # so set _attr_available here and let the base class do the rest. + self._attr_available = device.reachable + super()._refresh(device, write_ha) From 7abf79d1f991d58051fea0afe56e714ce60d7fdb Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 6 Nov 2021 16:49:51 +0100 Subject: [PATCH 0283/1452] Fix typing in devolo Home Network (#59238) --- homeassistant/components/devolo_home_network/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 66f686a603e..b0f68ae280e 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -14,8 +14,8 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -72,7 +72,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all devices and sensors and setup them via config entry.""" device: Device = hass.data[DOMAIN][entry.entry_id]["device"] From 2e4ee487c1637ff97a0043e3bc77fb9844188be6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 6 Nov 2021 10:11:00 -0600 Subject: [PATCH 0284/1452] Address post-merge Ridwell code review (#58857) --- homeassistant/components/ridwell/__init__.py | 10 ++-- .../components/ridwell/config_flow.py | 49 +++++++------------ homeassistant/components/ridwell/sensor.py | 25 +++------- homeassistant/components/ridwell/strings.json | 2 +- .../components/ridwell/translations/en.json | 2 +- tests/components/ridwell/test_config_flow.py | 29 ++++------- 6 files changed, 42 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 419c74456aa..4aa5ea3e162 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -24,9 +24,6 @@ PLATFORMS: list[str] = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - session = aiohttp_client.async_get_clientsession(hass) try: @@ -67,8 +64,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] = accounts - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_ACCOUNT: accounts, + DATA_COORDINATOR: coordinator, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index 80c07e5bf99..1ca5a9b5941 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -38,25 +37,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" self._password: str | None = None - self._reauthing: bool = False self._username: str | None = None - @callback - def _async_show_errors( - self, errors: dict, error_step_id: str, error_schema: vol.Schema - ) -> FlowResult: - """Show an error on the correct form.""" - return self.async_show_form( - step_id=error_step_id, - data_schema=error_schema, - errors=errors, - description_placeholders={CONF_USERNAME: self._username}, - ) - async def _async_validate( self, error_step_id: str, error_schema: vol.Schema ) -> FlowResult: """Validate input credentials and proceed accordingly.""" + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) if TYPE_CHECKING: @@ -66,25 +53,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await async_get_client(self._username, self._password, session=session) except InvalidCredentialsError: - return self._async_show_errors( - {"base": "invalid_auth"}, error_step_id, error_schema - ) + errors["base"] = "invalid_auth" except RidwellError as err: LOGGER.error("Unknown Ridwell error: %s", err) - return self._async_show_errors( - {"base": "unknown"}, error_step_id, error_schema + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors=errors, + description_placeholders={CONF_USERNAME: self._username}, ) - if self._reauthing: - if existing_entry := await self.async_set_unique_id(self._username): - self.hass.config_entries.async_update_entry( - existing_entry, - data={**existing_entry.data, CONF_PASSWORD: self._password}, - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") + if existing_entry := await self.async_set_unique_id(self._username): + self.hass.config_entries.async_update_entry( + existing_entry, + data={**existing_entry.data, CONF_PASSWORD: self._password}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") return self.async_create_entry( title=self._username, @@ -93,7 +83,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, config: ConfigType) -> FlowResult: """Handle configuration by re-auth.""" - self._reauthing = True self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 1938bd960e7..68a8c066bd2 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -2,22 +2,20 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import date, datetime from typing import Any -from aioridwell.client import RidwellAccount +from aioridwell.client import RidwellAccount, RidwellPickupEvent from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import DEVICE_CLASS_DATE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util.dt import as_utc from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN @@ -26,14 +24,6 @@ ATTR_PICKUP_STATE = "pickup_state" ATTR_PICKUP_TYPES = "pickup_types" ATTR_QUANTITY = "quantity" -DEFAULT_ATTRIBUTION = "Pickup data provided by Ridwell" - - -@callback -def async_get_utc_midnight(target_date: date) -> datetime: - """Get UTC midnight for a given date.""" - return as_utc(datetime.combine(target_date, datetime.min.time())) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -49,7 +39,7 @@ async def async_setup_entry( class RidwellSensor(CoordinatorEntity, SensorEntity): """Define a Ridwell pickup sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = DEVICE_CLASS_DATE def __init__( self, coordinator: DataUpdateCoordinator, account: RidwellAccount @@ -67,7 +57,6 @@ class RidwellSensor(CoordinatorEntity, SensorEntity): event = self.coordinator.data[self._account.account_id] attrs: dict[str, Any] = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_PICKUP_TYPES: {}, ATTR_PICKUP_STATE: event.state, } @@ -82,12 +71,12 @@ class RidwellSensor(CoordinatorEntity, SensorEntity): # Ridwell's API will return distinct objects, even if they have the # same name (e.g. two pickups of Latex Paint will show up as two # objects) – so, we sum the quantities: - attrs[ATTR_PICKUP_TYPES][pickup.name]["quantity"] += pickup.quantity + attrs[ATTR_PICKUP_TYPES][pickup.name][ATTR_QUANTITY] += pickup.quantity return attrs @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - event = self.coordinator.data[self._account.account_id] - return async_get_utc_midnight(event.pickup_date).isoformat() + event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] + return event.pickup_date.isoformat() diff --git a/homeassistant/components/ridwell/strings.json b/homeassistant/components/ridwell/strings.json index 2c9d3708419..3f4cc1806a4 100644 --- a/homeassistant/components/ridwell/strings.json +++ b/homeassistant/components/ridwell/strings.json @@ -21,7 +21,7 @@ "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_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/ridwell/translations/en.json b/homeassistant/components/ridwell/translations/en.json index 43315a4e45a..e3200df9038 100644 --- a/homeassistant/components/ridwell/translations/en.json +++ b/homeassistant/components/ridwell/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured_account": "Account is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 62aae3baab5..957ad31affb 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -107,10 +107,15 @@ async def test_step_user(hass: HomeAssistant, client_login) -> None: @pytest.mark.parametrize( - "client", - [AsyncMock(side_effect=InvalidCredentialsError)], + "client,error", + [ + (AsyncMock(side_effect=InvalidCredentialsError), "invalid_auth"), + (AsyncMock(side_effect=RidwellError), "unknown"), + ], ) -async def test_step_user_invalid_credentials(hass: HomeAssistant, client_login) -> None: +async def test_step_user_invalid_credentials( + hass: HomeAssistant, client_login, error +) -> None: """Test that invalid credentials are handled correctly.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -119,20 +124,4 @@ async def test_step_user_invalid_credentials(hass: HomeAssistant, client_login) ) assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} - - -@pytest.mark.parametrize( - "client", - [AsyncMock(side_effect=RidwellError)], -) -async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> None: - """Test that an unknown Ridwell error is handled correctly.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + assert result["errors"]["base"] == error From fc7d4ed118d9a0f524ae549ea1947c1a14ce6d79 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 6 Nov 2021 19:31:25 +0100 Subject: [PATCH 0285/1452] Add decoded telegram payload to knx_event service (#57621) * decode knx_event telegram payload with given dpt * exception handling for invalid payloads * Update homeassistant/components/knx/__init__.py Co-authored-by: Marvin Wichmann Co-authored-by: Marvin Wichmann --- homeassistant/components/knx/__init__.py | 90 ++++++++++++++++++---- homeassistant/components/knx/schema.py | 23 ++++++ homeassistant/components/knx/services.yaml | 7 ++ tests/components/knx/test_events.py | 70 +++++++++++------ tests/components/knx/test_services.py | 30 ++++++-- 5 files changed, 180 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 57c88b84cc7..b61a825e97f 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -10,15 +10,22 @@ from xknx import XKNX from xknx.core import XknxConnectionState from xknx.core.telegram_queue import TelegramQueue from xknx.dpt import DPTArray, DPTBase, DPTBinary -from xknx.exceptions import XKNXException +from xknx.exceptions import ConversionError, XKNXException from xknx.io import ConnectionConfig, ConnectionType from xknx.telegram import AddressFilter, Telegram -from xknx.telegram.address import parse_device_group_address +from xknx.telegram.address import ( + DeviceGroupAddress, + GroupAddress, + InternalGroupAddress, + parse_device_group_address, +) from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( + CONF_EVENT, CONF_HOST, CONF_PORT, + CONF_TYPE, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) @@ -46,6 +53,7 @@ from .schema import ( ClimateSchema, ConnectionSchema, CoverSchema, + EventSchema, ExposeSchema, FanSchema, LightSchema, @@ -77,6 +85,8 @@ SERVICE_KNX_READ: Final = "read" CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( + # deprecated since 2021.12 + cv.deprecated(CONF_KNX_EVENT_FILTER), # deprecated since 2021.4 cv.deprecated("config_file"), # deprecated since 2021.2 @@ -89,6 +99,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_KNX_EVENT_FILTER, default=[]): vol.All( cv.ensure_list, [cv.string] ), + **EventSchema.SCHEMA, **ExposeSchema.platform_node(), **BinarySensorSchema.platform_node(), **ClimateSchema.platform_node(), @@ -149,6 +160,7 @@ SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( cv.ensure_list, [ga_validator], ), + vol.Optional(CONF_TYPE): sensor_type_validator, vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } ) @@ -268,11 +280,16 @@ class KNXModule: self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {} self.init_xknx() - self._knx_event_callback: TelegramQueue.Callback = self.register_callback() self.xknx.connection_manager.register_connection_state_changed_cb( self.connection_state_changed_cb ) + self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {} + self._group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {} + self._knx_event_callback: TelegramQueue.Callback = ( + self.register_event_callback() + ) + def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( @@ -332,38 +349,77 @@ class KNXModule: auto_reconnect=True, ) + async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: + """Call invoked after a KNX connection state change was received.""" + self.connected = state == XknxConnectionState.CONNECTED + if tasks := [device.after_update() for device in self.xknx.devices]: + await asyncio.gather(*tasks) + async def telegram_received_cb(self, telegram: Telegram) -> None: """Call invoked after a KNX telegram was received.""" - data = None # Not all telegrams have serializable data. + data: int | tuple[int, ...] | None = None + value = None if ( isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)) and telegram.payload.value is not None + and isinstance( + telegram.destination_address, (GroupAddress, InternalGroupAddress) + ) ): data = telegram.payload.value.value + if isinstance(data, tuple): + if transcoder := ( + self._group_address_transcoder.get(telegram.destination_address) + or next( + ( + _transcoder + for _filter, _transcoder in self._address_filter_transcoder.items() + if _filter.match(telegram.destination_address) + ), + None, + ) + ): + try: + value = transcoder.from_knx(data) + except ConversionError as err: + _LOGGER.warning( + "Error in `knx_event` at decoding type '%s' from telegram %s\n%s", + transcoder.__name__, + telegram, + err, + ) + self.hass.bus.async_fire( "knx_event", { "data": data, "destination": str(telegram.destination_address), "direction": telegram.direction.value, + "value": value, "source": str(telegram.source_address), "telegramtype": telegram.payload.__class__.__name__, }, ) - async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: - """Call invoked after a KNX connection state change was received.""" - self.connected = state == XknxConnectionState.CONNECTED - if tasks := [device.after_update() for device in self.xknx.devices]: - await asyncio.gather(*tasks) - - def register_callback(self) -> TelegramQueue.Callback: - """Register callback within XKNX TelegramQueue.""" + def register_event_callback(self) -> TelegramQueue.Callback: + """Register callback for knx_event within XKNX TelegramQueue.""" + # backwards compatibility for deprecated CONF_KNX_EVENT_FILTER + # use `address_filters = []` when this is not needed anymore address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) ) + for filter_set in self.config[DOMAIN][CONF_EVENT]: + _filters = list(map(AddressFilter, filter_set[KNX_ADDRESS])) + address_filters.extend(_filters) + if (dpt := filter_set.get(CONF_TYPE)) and ( + transcoder := DPTBase.parse_transcoder(dpt) + ): + self._address_filter_transcoder.update( + {_filter: transcoder for _filter in _filters} # type: ignore[misc] + ) + return self.xknx.telegram_queue.register_telegram_received_cb( self.telegram_received_cb, address_filters=address_filters, @@ -374,7 +430,7 @@ class KNXModule: async def service_event_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing a GroupAddress to the knx_event filter.""" attr_address = call.data[KNX_ADDRESS] - group_addresses = map(parse_device_group_address, attr_address) + group_addresses = list(map(parse_device_group_address, attr_address)) if call.data.get(SERVICE_KNX_ATTR_REMOVE): for group_address in group_addresses: @@ -385,8 +441,16 @@ class KNXModule: "Service event_register could not remove event for '%s'", str(group_address), ) + if group_address in self._group_address_transcoder: + del self._group_address_transcoder[group_address] return + if (dpt := call.data.get(CONF_TYPE)) and ( + transcoder := DPTBase.parse_transcoder(dpt) + ): + self._group_address_transcoder.update( + {_address: transcoder for _address in group_addresses} # type: ignore[misc] + ) for group_address in group_addresses: if group_address in self._knx_event_callback.group_addresses: continue diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 0e54a9abbc5..6cd8fc6bc0d 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ENTITY_ID, + CONF_EVENT, CONF_HOST, CONF_MODE, CONF_NAME, @@ -204,6 +205,28 @@ class ConnectionSchema: } +######### +# EVENT +######### + + +class EventSchema: + """Voluptuous schema for KNX events.""" + + KNX_EVENT_FILTER_SCHEMA = vol.Schema( + { + vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_TYPE): sensor_type_validator, + } + ) + + SCHEMA = { + vol.Optional(CONF_EVENT, default=[]): vol.All( + cv.ensure_list, [KNX_EVENT_FILTER_SCHEMA] + ) + } + + ############# # PLATFORMS ############# diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 1ea5d9b6faa..fca5f4fe07d 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -45,6 +45,13 @@ event_register: example: "1/1/0" selector: object: + type: + name: "Value type" + description: "If set, the payload will be decoded as given DPT in the event data `value` key. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + required: false + example: "2byte_float" + selector: + text: remove: name: "Remove event registration" description: "If `True` the group address(es) will be removed." diff --git a/tests/components/knx/test_events.py b/tests/components/knx/test_events.py index 6a9e021ff53..360a1963d2d 100644 --- a/tests/components/knx/test_events.py +++ b/tests/components/knx/test_events.py @@ -1,6 +1,11 @@ """Test KNX events.""" -from homeassistant.components.knx import CONF_KNX_EVENT_FILTER +from homeassistant.components.knx import ( + CONF_EVENT, + CONF_KNX_EVENT_FILTER, + CONF_TYPE, + KNX_ADDRESS, +) from homeassistant.core import HomeAssistant from .conftest import KNXTestKit @@ -9,7 +14,7 @@ from tests.common import async_capture_events async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): - """Test `knx_event` event.""" + """Test the `knx_event` event.""" test_group_a = "0/4/*" test_address_a_1 = "0/4/0" test_address_a_2 = "0/4/100" @@ -20,13 +25,15 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): test_address_c_1 = "2/6/4" test_address_c_2 = "2/6/5" test_address_d = "5/4/3" + test_address_e = "6/4/3" events = async_capture_events(hass, "knx_event") - async def test_event_data(address, payload): + async def test_event_data(address, payload, value=None): await hass.async_block_till_done() assert len(events) == 1 event = events.pop() assert event.data["data"] == payload + assert event.data["value"] == value assert event.data["direction"] == "Incoming" assert event.data["destination"] == address if payload is None: @@ -40,12 +47,24 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - CONF_KNX_EVENT_FILTER: [ - test_group_a, - test_group_b, - test_group_c, - test_address_d, - ] + CONF_EVENT: [ + { + KNX_ADDRESS: [ + test_group_a, + test_group_b, + ], + CONF_TYPE: "2byte_unsigned", + }, + { + KNX_ADDRESS: test_group_c, + CONF_TYPE: "2byte_float", + }, + { + KNX_ADDRESS: [test_address_d], + }, + ], + # test legacy `event_filter` config + CONF_KNX_EVENT_FILTER: [test_address_e], } ) @@ -54,28 +73,35 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): assert len(events) == 0 # receive telegrams for group addresses matching the filter - await knx.receive_write(test_address_a_1, True) - await test_event_data(test_address_a_1, True) + await knx.receive_write(test_address_a_1, (0x03, 0x2F)) + await test_event_data(test_address_a_1, (0x03, 0x2F), value=815) - await knx.receive_response(test_address_a_2, False) - await test_event_data(test_address_a_2, False) + await knx.receive_response(test_address_a_2, (0x12, 0x67)) + await test_event_data(test_address_a_2, (0x12, 0x67), value=4711) - await knx.receive_write(test_address_b_1, (1,)) - await test_event_data(test_address_b_1, (1,)) + await knx.receive_write(test_address_b_1, (0, 0)) + await test_event_data(test_address_b_1, (0, 0), value=0) - await knx.receive_response(test_address_b_2, (255,)) - await test_event_data(test_address_b_2, (255,)) + await knx.receive_response(test_address_b_2, (255, 255)) + await test_event_data(test_address_b_2, (255, 255), value=65535) - await knx.receive_write(test_address_c_1, (89, 43, 34, 11)) - await test_event_data(test_address_c_1, (89, 43, 34, 11)) + await knx.receive_write(test_address_c_1, (0x06, 0xA0)) + await test_event_data(test_address_c_1, (0x06, 0xA0), value=16.96) - await knx.receive_response(test_address_c_2, (255, 255, 255, 255)) - await test_event_data(test_address_c_2, (255, 255, 255, 255)) + await knx.receive_response(test_address_c_2, (0x8A, 0x24)) + await test_event_data(test_address_c_2, (0x8A, 0x24), value=-30.0) await knx.receive_read(test_address_d) await test_event_data(test_address_d, None) - # receive telegrams for group addresses not matching the filter + await knx.receive_write(test_address_d, True) + await test_event_data(test_address_d, True) + + # test legacy `event_filter` config + await knx.receive_write(test_address_e, (89, 43, 34, 11)) + await test_event_data(test_address_e, (89, 43, 34, 11)) + + # receive telegrams for group addresses not matching any filter await knx.receive_write("0/5/0", True) await knx.receive_write("1/7/0", True) await knx.receive_write("2/6/6", True) diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 80ed51e6aec..c61dc542586 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -86,14 +86,19 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): await hass.async_block_till_done() assert len(events) == 0 - # register event + # register event with `type` await hass.services.async_call( - "knx", "event_register", {"address": test_address}, blocking=True + "knx", + "event_register", + {"address": test_address, "type": "2byte_unsigned"}, + blocking=True, ) - await knx.receive_write(test_address, True) - await knx.receive_write(test_address, False) + await knx.receive_write(test_address, (0x04, 0xD2)) await hass.async_block_till_done() - assert len(events) == 2 + assert len(events) == 1 + typed_event = events.pop() + assert typed_event.data["data"] == (0x04, 0xD2) + assert typed_event.data["value"] == 1234 # remove event registration - no event added await hass.services.async_call( @@ -104,7 +109,22 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): ) await knx.receive_write(test_address, True) await hass.async_block_till_done() + assert len(events) == 0 + + # register event without `type` + await hass.services.async_call( + "knx", "event_register", {"address": test_address}, blocking=True + ) + await knx.receive_write(test_address, True) + await knx.receive_write(test_address, False) + await hass.async_block_till_done() assert len(events) == 2 + untyped_event_2 = events.pop() + assert untyped_event_2.data["data"] is False + assert untyped_event_2.data["value"] is None + untyped_event_1 = events.pop() + assert untyped_event_1.data["data"] is True + assert untyped_event_1.data["value"] is None async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit): From fdf1bfa1407308613814498d71e991ddb323455d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 6 Nov 2021 19:34:51 +0100 Subject: [PATCH 0286/1452] Add RDW Vehicle information integration (#59240) --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/rdw/__init__.py | 42 +++++++ homeassistant/components/rdw/config_flow.py | 56 ++++++++++ homeassistant/components/rdw/const.py | 14 +++ homeassistant/components/rdw/manifest.json | 10 ++ homeassistant/components/rdw/sensor.py | 103 ++++++++++++++++++ homeassistant/components/rdw/strings.json | 15 +++ .../components/rdw/translations/en.json | 15 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/rdw/__init__.py | 1 + tests/components/rdw/conftest.py | 69 ++++++++++++ tests/components/rdw/fixtures/11ZKZ3.json | 52 +++++++++ tests/components/rdw/test_config_flow.py | 92 ++++++++++++++++ tests/components/rdw/test_init.py | 45 ++++++++ tests/components/rdw/test_sensor.py | 61 +++++++++++ 19 files changed, 595 insertions(+) create mode 100644 homeassistant/components/rdw/__init__.py create mode 100644 homeassistant/components/rdw/config_flow.py create mode 100644 homeassistant/components/rdw/const.py create mode 100644 homeassistant/components/rdw/manifest.json create mode 100644 homeassistant/components/rdw/sensor.py create mode 100644 homeassistant/components/rdw/strings.json create mode 100644 homeassistant/components/rdw/translations/en.json create mode 100644 tests/components/rdw/__init__.py create mode 100644 tests/components/rdw/conftest.py create mode 100644 tests/components/rdw/fixtures/11ZKZ3.json create mode 100644 tests/components/rdw/test_config_flow.py create mode 100644 tests/components/rdw/test_init.py create mode 100644 tests/components/rdw/test_sensor.py diff --git a/.strict-typing b/.strict-typing index f574aeb79d6..ac5d2b6a8ac 100644 --- a/.strict-typing +++ b/.strict-typing @@ -96,6 +96,7 @@ homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* homeassistant.components.rainmachine.* +homeassistant.components.rdw.* homeassistant.components.recollect_waste.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack diff --git a/CODEOWNERS b/CODEOWNERS index 21e6526b03f..84ab3a80c5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -425,6 +425,7 @@ homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/rdw/* @frenck homeassistant/components/recollect_waste/* @bachya homeassistant/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py new file mode 100644 index 00000000000..32f5c81e86a --- /dev/null +++ b/homeassistant/components/rdw/__init__.py @@ -0,0 +1,42 @@ +"""Support for RDW.""" +from __future__ import annotations + +from vehicle import RDW, Vehicle + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_LICENSE_PLATE, DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = (SENSOR_DOMAIN,) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up RDW from a config entry.""" + session = async_get_clientsession(hass) + rdw = RDW(session=session, license_plate=entry.data[CONF_LICENSE_PLATE]) + + coordinator: DataUpdateCoordinator[Vehicle] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_APK", + update_interval=SCAN_INTERVAL, + update_method=rdw.vehicle, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(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 RDW config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/rdw/config_flow.py b/homeassistant/components/rdw/config_flow.py new file mode 100644 index 00000000000..a9fedc88dac --- /dev/null +++ b/homeassistant/components/rdw/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow to configure the RDW integration.""" +from __future__ import annotations + +from typing import Any + +from vehicle import RDW, RDWError, RDWUnknownLicensePlateError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_LICENSE_PLATE, DOMAIN + + +class RDWFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for RDW.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + session = async_get_clientsession(self.hass) + rdw = RDW(session=session) + try: + vehicle = await rdw.vehicle( + license_plate=user_input[CONF_LICENSE_PLATE] + ) + except RDWUnknownLicensePlateError: + errors["base"] = "unknown_license_plate" + except RDWError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(vehicle.license_plate) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_LICENSE_PLATE], + data={ + CONF_LICENSE_PLATE: vehicle.license_plate, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_LICENSE_PLATE): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/rdw/const.py b/homeassistant/components/rdw/const.py new file mode 100644 index 00000000000..10058019aa2 --- /dev/null +++ b/homeassistant/components/rdw/const.py @@ -0,0 +1,14 @@ +"""Constants for the RDW integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "rdw" + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(hours=1) + +ENTRY_TYPE_SERVICE: Final = "service" +CONF_LICENSE_PLATE: Final = "license_plate" diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json new file mode 100644 index 00000000000..d7614c5bfa9 --- /dev/null +++ b/homeassistant/components/rdw/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "rdw", + "name": "RDW", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rdw", + "requirements": ["vehicle==0.1.0"], + "codeowners": ["@frenck"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py new file mode 100644 index 00000000000..c6fc7157494 --- /dev/null +++ b/homeassistant/components/rdw/sensor.py @@ -0,0 +1,103 @@ +"""Support for RDW sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from vehicle import Vehicle + +from homeassistant.components.sensor import ( + DEVICE_CLASS_DATE, + SensorEntity, + SensorEntityDescription, +) +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 homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_LICENSE_PLATE, DOMAIN, ENTRY_TYPE_SERVICE + + +@dataclass +class RDWSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Vehicle], str | float | None] + + +@dataclass +class RDWSensorEntityDescription( + SensorEntityDescription, RDWSensorEntityDescriptionMixin +): + """Describes RDW sensor entity.""" + + +SENSORS: tuple[RDWSensorEntityDescription, ...] = ( + RDWSensorEntityDescription( + key="apk_expiration", + name="APK Expiration", + device_class=DEVICE_CLASS_DATE, + value_fn=lambda vehicle: vehicle.apk_expiration.isoformat(), + ), + RDWSensorEntityDescription( + key="name_registration_date", + name="Name Registration Date", + device_class=DEVICE_CLASS_DATE, + value_fn=lambda vehicle: vehicle.name_registration_date.isoformat(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up RDW sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + RDWSensorEntity( + coordinator=coordinator, + license_plate=entry.data[CONF_LICENSE_PLATE], + description=description, + ) + for description in SENSORS + ) + + +class RDWSensorEntity(CoordinatorEntity, SensorEntity): + """Defines an RDW sensor.""" + + entity_description: RDWSensorEntityDescription + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + license_plate: str, + description: RDWSensorEntityDescription, + ) -> None: + """Initialize RDW sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{license_plate}_{description.key}" + + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, f"{license_plate}")}, + manufacturer=coordinator.data.brand, + name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + model=coordinator.data.model, + configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/rdw/strings.json b/homeassistant/components/rdw/strings.json new file mode 100644 index 00000000000..48bcd8c0c5d --- /dev/null +++ b/homeassistant/components/rdw/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "license_plate": "License plate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown_license_plate": "Unknown license plate" + } + } +} diff --git a/homeassistant/components/rdw/translations/en.json b/homeassistant/components/rdw/translations/en.json new file mode 100644 index 00000000000..9d2827ed4de --- /dev/null +++ b/homeassistant/components/rdw/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "unknown_license_plate": "Unknown license plate" + }, + "step": { + "user": { + "data": { + "license_plate": "License plate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bff1305504f..f65cf964ef3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -235,6 +235,7 @@ FLOWS = [ "rachio", "rainforest_eagle", "rainmachine", + "rdw", "recollect_waste", "renault", "rfxtrx", diff --git a/mypy.ini b/mypy.ini index 425cfd1aa57..ccacbca2da2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1067,6 +1067,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rdw.*] +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.recollect_waste.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index cb99f907ace..92f9826c0ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2365,6 +2365,9 @@ uvcclient==0.11.0 # homeassistant.components.vallox vallox-websocket-api==2.8.1 +# homeassistant.components.rdw +vehicle==0.1.0 + # homeassistant.components.velbus velbus-aio==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a32bc0d5ea8..537f8c31be4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1372,6 +1372,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.rdw +vehicle==0.1.0 + # homeassistant.components.velbus velbus-aio==2021.11.0 diff --git a/tests/components/rdw/__init__.py b/tests/components/rdw/__init__.py new file mode 100644 index 00000000000..6a628ecb94c --- /dev/null +++ b/tests/components/rdw/__init__.py @@ -0,0 +1 @@ +"""Tests for the RDW integration.""" diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py new file mode 100644 index 00000000000..4be17f00264 --- /dev/null +++ b/tests/components/rdw/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for RDW integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest +from vehicle import Vehicle + +from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My Car", + domain=DOMAIN, + data={CONF_LICENSE_PLATE: "11ZKZ3"}, + unique_id="11ZKZ3", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.rdw.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_rdw_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked RDW client.""" + with patch( + "homeassistant.components.rdw.config_flow.RDW", autospec=True + ) as rdw_mock: + rdw = rdw_mock.return_value + rdw.vehicle.return_value = Vehicle.parse_raw(load_fixture("rdw/11ZKZ3.json")) + yield rdw + + +@pytest.fixture +def mock_rdw(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked WLED client.""" + fixture: str = "rdw/11ZKZ3.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + vehicle = Vehicle.parse_raw(load_fixture(fixture)) + with patch("homeassistant.components.rdw.RDW", autospec=True) as rdw_mock: + rdw = rdw_mock.return_value + rdw.vehicle.return_value = vehicle + yield rdw + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_rdw: MagicMock +) -> MockConfigEntry: + """Set up the RDW integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/rdw/fixtures/11ZKZ3.json b/tests/components/rdw/fixtures/11ZKZ3.json new file mode 100644 index 00000000000..caaaf57c19b --- /dev/null +++ b/tests/components/rdw/fixtures/11ZKZ3.json @@ -0,0 +1,52 @@ +{ + "kenteken": "11ZKZ3", + "voertuigsoort": "Personenauto", + "merk": "SKODA", + "handelsbenaming": "CITIGO", + "vervaldatum_apk": "20220104", + "datum_tenaamstelling": "20211104", + "inrichting": "hatchback", + "aantal_zitplaatsen": "4", + "eerste_kleur": "GRIJS", + "tweede_kleur": "Niet geregistreerd", + "aantal_cilinders": "3", + "cilinderinhoud": "999", + "massa_ledig_voertuig": "840", + "toegestane_maximum_massa_voertuig": "1290", + "massa_rijklaar": "940", + "zuinigheidslabel": "A", + "datum_eerste_toelating": "20130104", + "datum_eerste_afgifte_nederland": "20130104", + "wacht_op_keuren": "Geen verstrekking in Open Data", + "catalogusprijs": "10697", + "wam_verzekerd": "Nee", + "aantal_deuren": "0", + "aantal_wielen": "4", + "afstand_hart_koppeling_tot_achterzijde_voertuig": "0", + "afstand_voorzijde_voertuig_tot_hart_koppeling": "0", + "lengte": "356", + "breedte": "0", + "europese_voertuigcategorie": "M1", + "plaats_chassisnummer": "r. motorruimte", + "technische_max_massa_voertuig": "1290", + "type": "AA", + "typegoedkeuringsnummer": "e13*2007/46*1169*05", + "variant": "ABCHYA", + "uitvoering": "FM5FM5CF0037MGVR2N1FA1SK", + "volgnummer_wijziging_eu_typegoedkeuring": "0", + "vermogen_massarijklaar": "0.05", + "wielbasis": "241", + "export_indicator": "Nee", + "openstaande_terugroepactie_indicator": "Nee", + "maximum_massa_samenstelling": "0", + "aantal_rolstoelplaatsen": "0", + "jaar_laatste_registratie_tellerstand": "2021", + "tellerstandoordeel": "Logisch", + "code_toelichting_tellerstandoordeel": "00", + "tenaamstellen_mogelijk": "Ja", + "api_gekentekende_voertuigen_assen": "https://opendata.rdw.nl/resource/3huj-srit.json", + "api_gekentekende_voertuigen_brandstof": "https://opendata.rdw.nl/resource/8ys7-d773.json", + "api_gekentekende_voertuigen_carrosserie": "https://opendata.rdw.nl/resource/vezc-m2t6.json", + "api_gekentekende_voertuigen_carrosserie_specifiek": "https://opendata.rdw.nl/resource/jhie-znh9.json", + "api_gekentekende_voertuigen_voertuigklasse": "https://opendata.rdw.nl/resource/kmfi-hrps.json" +} diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py new file mode 100644 index 00000000000..20144768abe --- /dev/null +++ b/tests/components/rdw/test_config_flow.py @@ -0,0 +1,92 @@ +"""Tests for the RDW config flow.""" + +from unittest.mock import MagicMock + +from vehicle.exceptions import RDWConnectionError, RDWUnknownLicensePlateError + +from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_full_user_flow( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "11-ZKZ-3", + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "11-ZKZ-3" + assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} + + +async def test_full_flow_with_authentication_error( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Test the full user configuration flow with incorrect license plate. + + This tests tests a full config flow, with a case the user enters an invalid + license plate, but recover by entering the correct one. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "0001TJ", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "unknown_license_plate"} + assert "flow_id" in result2 + + mock_rdw_config_flow.vehicle.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "11-ZKZ-3", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "11-ZKZ-3" + assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} + + +async def test_connection_error( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock +) -> None: + """Test API connection error.""" + mock_rdw_config_flow.vehicle.side_effect = RDWConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_LICENSE_PLATE: "0001TJ"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/rdw/test_init.py b/tests/components/rdw/test_init.py new file mode 100644 index 00000000000..b31b0aa8d81 --- /dev/null +++ b/tests/components/rdw/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the RDW integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.rdw.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_rdw: AsyncMock, +) -> None: + """Test the RDW configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.rdw.RDW.vehicle", + side_effect=RuntimeError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the RDW configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py new file mode 100644 index 00000000000..c67a7459b0d --- /dev/null +++ b/tests/components/rdw/test_sensor.py @@ -0,0 +1,61 @@ +"""Tests for the sensors provided by the RDW integration.""" +from homeassistant.components.rdw.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_DATE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_vehicle_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the RDW vehicle sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.apk_expiration") + entry = entity_registry.async_get("sensor.apk_expiration") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_apk_expiration" + assert state.state == "2022-01-04" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert ATTR_ICON not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.name_registration_date") + entry = entity_registry.async_get("sensor.name_registration_date") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_name_registration_date" + assert state.state == "2021-11-04" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Name Registration Date" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert ATTR_ICON not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} + assert device_entry.manufacturer == "Skoda" + assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.model == "Citigo" + assert ( + device_entry.configuration_url + == "https://ovi.rdw.nl/default.aspx?kenteken=11ZKZ3" + ) + assert not device_entry.sw_version From c5b5c8c3cec093dd598c4e7178cced2fb7025858 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 6 Nov 2021 19:48:02 +0100 Subject: [PATCH 0287/1452] Remove deprecated usages of HomeAssistantType (#59241) --- homeassistant/components/netgear/__init__.py | 5 ++--- homeassistant/components/netgear/device_tracker.py | 5 ++--- homeassistant/components/netgear/router.py | 7 +++---- homeassistant/components/netgear/sensor.py | 5 ++--- homeassistant/components/rfxtrx/helpers.py | 5 ++--- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 657e7e06880..c3abbd59e27 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -3,14 +3,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .errors import CannotLoginException from .router import NetgearRouter -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Netgear component.""" router = NetgearRouter(hass, entry) try: @@ -38,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +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) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index f7c92a271b9..7d7b278f937 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -19,10 +19,9 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from .const import DEVICE_ICONS, DOMAIN from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry @@ -65,7 +64,7 @@ async def async_get_scanner(hass, config): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for Netgear component.""" diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 3c2497f2131..7339b9ea1ac 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.dispatcher import ( @@ -26,7 +26,6 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from .const import ( @@ -61,7 +60,7 @@ def get_api( @callback def async_setup_netgear_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, entity_class_generator: Callable[[NetgearRouter, dict], list], @@ -103,7 +102,7 @@ def async_add_new_entities(router, async_add_entities, tracked, entity_class_gen class NetgearRouter: """Representation of a Netgear router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a Netgear router.""" self.hass = hass self.entry = entry diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 57ffe6f98f2..d8e81cd639f 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -6,9 +6,8 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry @@ -40,7 +39,7 @@ SENSOR_TYPES = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for Netgear component.""" diff --git a/homeassistant/components/rfxtrx/helpers.py b/homeassistant/components/rfxtrx/helpers.py index ad7d049fb4c..7e567cff1cd 100644 --- a/homeassistant/components/rfxtrx/helpers.py +++ b/homeassistant/components/rfxtrx/helpers.py @@ -3,13 +3,12 @@ from RFXtrx import get_device -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType @callback -def async_get_device_object(hass: HomeAssistantType, device_id): +def async_get_device_object(hass: HomeAssistant, device_id): """Get a device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) From 3d0d0385976f76ee173df681c90aa05a9732c359 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 6 Nov 2021 14:10:58 -0600 Subject: [PATCH 0288/1452] Guard against missing data in ReCollect Waste (#59177) --- homeassistant/components/recollect_waste/sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 74a023fed94..619a12a42f7 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -78,8 +78,13 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): @callback def update_from_latest_data(self) -> None: """Update the state.""" - pickup_event = self.coordinator.data[0] - next_pickup_event = self.coordinator.data[1] + try: + pickup_event = self.coordinator.data[0] + next_pickup_event = self.coordinator.data[1] + except IndexError: + self._attr_native_value = None + self._attr_extra_state_attributes = {} + return self._attr_extra_state_attributes.update( { From 9aec8f61d7ba0490e4cfd810f0a34262c90d7928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20R=C3=B3=C5=BCa=C5=84ski?= Date: Sat, 6 Nov 2021 23:36:59 +0100 Subject: [PATCH 0289/1452] Rewrite signal_messenger unittest to pytest (#57777) * convert signal messenger unittest to pytest * more fixtures * more assertions and fixed test attachment sending * reverted unrelated changes * fixed flake errors * Flake8 related issues fixed * HHTPStatus instead of int --- tests/components/signal_messenger/conftest.py | 39 ++++ .../signal_messenger/test_notify.py | 167 ++++++++---------- 2 files changed, 111 insertions(+), 95 deletions(-) create mode 100644 tests/components/signal_messenger/conftest.py diff --git a/tests/components/signal_messenger/conftest.py b/tests/components/signal_messenger/conftest.py new file mode 100644 index 00000000000..bc7aa7f84a1 --- /dev/null +++ b/tests/components/signal_messenger/conftest.py @@ -0,0 +1,39 @@ +"""Signal notification test helpers.""" +from http import HTTPStatus + +from pysignalclirestapi import SignalCliRestApi +import pytest + +from homeassistant.components.signal_messenger.notify import SignalNotificationService + + +@pytest.fixture +def signal_notification_service(): + """Set up signal notification service.""" + recipients = ["+435565656565"] + number = "+43443434343" + client = SignalCliRestApi("http://127.0.0.1:8080", number) + return SignalNotificationService(recipients, client) + + +SIGNAL_SEND_PATH_SUFIX = "/v2/send" +MESSAGE = "Testing Signal Messenger platform :)" +NUMBER_FROM = "+43443434343" +NUMBERS_TO = ["+435565656565"] + + +@pytest.fixture +def signal_requests_mock(requests_mock): + """Prepare signal service mock.""" + requests_mock.register_uri( + "POST", + "http://127.0.0.1:8080" + SIGNAL_SEND_PATH_SUFIX, + status_code=HTTPStatus.CREATED, + ) + requests_mock.register_uri( + "GET", + "http://127.0.0.1:8080/v1/about", + status_code=HTTPStatus.OK, + json={"versions": ["v1", "v2"]}, + ) + return requests_mock diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 61d13b8c60b..1feefa28513 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -1,29 +1,31 @@ """The tests for the signal_messenger platform.""" -from http import HTTPStatus +import json +import logging import os import tempfile -import unittest from unittest.mock import patch -from pysignalclirestapi import SignalCliRestApi -import requests_mock - -import homeassistant.components.signal_messenger.notify as signalmessenger from homeassistant.setup import async_setup_component +from tests.components.signal_messenger.conftest import ( + MESSAGE, + NUMBER_FROM, + NUMBERS_TO, + SIGNAL_SEND_PATH_SUFIX, +) + BASE_COMPONENT = "notify" async def test_signal_messenger_init(hass): """Test that service loads successfully.""" - config = { BASE_COMPONENT: { "name": "test", "platform": "signal_messenger", "url": "http://127.0.0.1:8080", - "number": "+43443434343", - "recipients": ["+435565656565"], + "number": NUMBER_FROM, + "recipients": NUMBERS_TO, } } @@ -31,96 +33,71 @@ async def test_signal_messenger_init(hass): assert await async_setup_component(hass, BASE_COMPONENT, config) await hass.async_block_till_done() - # Test that service loads successfully assert hass.services.has_service(BASE_COMPONENT, "test") -class TestSignalMesssenger(unittest.TestCase): - """Test the signal_messenger notify.""" +def test_send_message(signal_notification_service, signal_requests_mock, caplog): + """Test send message.""" + with caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ): + signal_notification_service.send_message(MESSAGE) + assert "Sending signal message" in caplog.text + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 2 + assert_sending_requests(signal_requests_mock) - def setUp(self): - """Set up things to be run when tests are started.""" - recipients = ["+435565656565"] - number = "+43443434343" - client = SignalCliRestApi("http://127.0.0.1:8080", number) - self._signalmessenger = signalmessenger.SignalNotificationService( - recipients, client - ) - @requests_mock.Mocker() - def test_send_message(self, mock): - """Test send message.""" - message = "Testing Signal Messenger platform :)" - mock.register_uri( - "POST", - "http://127.0.0.1:8080/v2/send", - status_code=HTTPStatus.CREATED, - ) - mock.register_uri( - "GET", - "http://127.0.0.1:8080/v1/about", - status_code=HTTPStatus.OK, - json={"versions": ["v1", "v2"]}, - ) - with self.assertLogs( - "homeassistant.components.signal_messenger.notify", level="DEBUG" - ) as context: - self._signalmessenger.send_message(message) - self.assertIn("Sending signal message", context.output[0]) - self.assertTrue(mock.called) - self.assertEqual(mock.call_count, 2) +def test_send_message_should_show_deprecation_warning( + signal_notification_service, signal_requests_mock, caplog +): + """Test send message should show deprecation warning.""" + with caplog.at_level( + logging.WARNING, logger="homeassistant.components.signal_messenger.notify" + ): + send_message_with_attachment(signal_notification_service, True) - @requests_mock.Mocker() - def test_send_message_should_show_deprecation_warning(self, mock): - """Test send message.""" - message = "Testing Signal Messenger platform with attachment :)" - mock.register_uri( - "POST", - "http://127.0.0.1:8080/v2/send", - status_code=HTTPStatus.CREATED, - ) - mock.register_uri( - "GET", - "http://127.0.0.1:8080/v1/about", - status_code=HTTPStatus.OK, - json={"versions": ["v1", "v2"]}, - ) - with self.assertLogs( - "homeassistant.components.signal_messenger.notify", level="WARNING" - ) as context, tempfile.NamedTemporaryFile( - suffix=".png", prefix=os.path.basename(__file__) - ) as tf: - data = {"data": {"attachment": tf.name}} - self._signalmessenger.send_message(message, **data) - self.assertIn( - "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108", - context.output[0], - ) - self.assertTrue(mock.called) - self.assertEqual(mock.call_count, 2) + assert ( + "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108" + in caplog.text + ) + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 2 + assert_sending_requests(signal_requests_mock, 1) - @requests_mock.Mocker() - def test_send_message_with_attachment(self, mock): - """Test send message.""" - message = "Testing Signal Messenger platform :)" - mock.register_uri( - "POST", - "http://127.0.0.1:8080/v2/send", - status_code=HTTPStatus.CREATED, - ) - mock.register_uri( - "GET", - "http://127.0.0.1:8080/v1/about", - status_code=HTTPStatus.OK, - json={"versions": ["v1", "v2"]}, - ) - with self.assertLogs( - "homeassistant.components.signal_messenger.notify", level="DEBUG" - ) as context, tempfile.NamedTemporaryFile( - suffix=".png", prefix=os.path.basename(__file__) - ) as tf: - data = {"data": {"attachments": [tf.name]}} - self._signalmessenger.send_message(message, **data) - self.assertIn("Sending signal message", context.output[0]) - self.assertTrue(mock.called) - self.assertEqual(mock.call_count, 2) + +def test_send_message_with_attachment( + signal_notification_service, signal_requests_mock, caplog +): + """Test send message with attachment.""" + with caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ): + send_message_with_attachment(signal_notification_service, False) + + assert "Sending signal message" in caplog.text + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 2 + assert_sending_requests(signal_requests_mock, 1) + + +def send_message_with_attachment(signal_notification_service, deprecated=False): + """Send message with attachment.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".png", prefix=os.path.basename(__file__) + ) as tf: + tf.write("attachment_data") + data = {"attachment": tf.name} if deprecated else {"attachments": [tf.name]} + signal_notification_service.send_message(MESSAGE, **{"data": data}) + + +def assert_sending_requests(signal_requests_mock, attachments_num=0): + """Assert message was send with correct parameters.""" + send_request = signal_requests_mock.request_history[-1] + assert send_request.path == SIGNAL_SEND_PATH_SUFIX + + body_request = json.loads(send_request.text) + assert body_request["message"] == MESSAGE + assert body_request["number"] == NUMBER_FROM + assert body_request["recipients"] == NUMBERS_TO + assert len(body_request["base64_attachments"]) == attachments_num From 332a571bb480ccfa5dd42068346986099c0b1c5d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 7 Nov 2021 00:12:38 +0000 Subject: [PATCH 0290/1452] [ci skip] Translation update --- .../accuweather/translations/tr.json | 5 +- .../components/adax/translations/tr.json | 11 +++++ .../components/aemet/translations/tr.json | 11 +++++ .../components/airthings/translations/tr.json | 13 +++++ .../components/airtouch4/translations/tr.json | 12 +++++ .../airvisual/translations/sensor.tr.json | 19 +++++++ .../alarmdecoder/translations/tr.json | 6 +++ .../ambee/translations/sensor.tr.json | 9 ++++ .../components/ambee/translations/tr.json | 14 ++++++ .../amberelectric/translations/tr.json | 21 ++++++++ .../components/apple_tv/translations/tr.json | 8 +++ .../aurora_abb_powerone/translations/th.json | 11 +++++ .../aurora_abb_powerone/translations/tr.json | 21 ++++++++ .../binary_sensor/translations/af.json | 11 +++++ .../binary_sensor/translations/th.json | 9 ++++ .../binary_sensor/translations/tr.json | 34 ++++++++++++- .../components/bosch_shc/translations/tr.json | 19 +++++++ .../components/braviatv/translations/tr.json | 2 + .../components/broadlink/translations/tr.json | 3 ++ .../buienradar/translations/tr.json | 12 +++++ .../components/button/translations/af.json | 11 +++++ .../components/button/translations/he.json | 11 +++++ .../components/button/translations/hu.json | 2 +- .../components/button/translations/th.json | 3 ++ .../components/button/translations/tr.json | 11 +++++ .../button/translations/zh-Hans.json | 11 +++++ .../components/cast/translations/tr.json | 20 ++++++++ .../components/co2signal/translations/tr.json | 16 ++++++ .../components/coinbase/translations/tr.json | 31 ++++++++++++ .../crownstone/translations/tr.json | 7 +++ .../demo/translations/select.tr.json | 7 +++ .../components/denonavr/translations/tr.json | 9 ++++ .../devolo_home_control/translations/tr.json | 3 +- .../devolo_home_network/translations/af.json | 18 +++++++ .../devolo_home_network/translations/de.json | 4 +- .../devolo_home_network/translations/th.json | 15 ++++++ .../devolo_home_network/translations/tr.json | 19 +++++++ .../components/dlna_dmr/translations/hu.json | 2 +- .../components/dlna_dmr/translations/tr.json | 35 ++++++++++++- .../components/doorbird/translations/tr.json | 7 +++ .../components/dsmr/translations/tr.json | 24 +++++++++ .../components/elgato/translations/tr.json | 3 ++ .../components/energy/translations/tr.json | 3 ++ .../environment_canada/translations/de.json | 6 +-- .../components/epson/translations/tr.json | 3 +- .../components/flipr/translations/tr.json | 7 +++ .../components/flume/translations/tr.json | 4 ++ .../components/flux_led/translations/tr.json | 17 +++++++ .../forecast_solar/translations/tr.json | 21 ++++++++ .../components/foscam/translations/tr.json | 2 + .../components/fritz/translations/tr.json | 14 ++++++ .../garages_amsterdam/translations/tr.json | 13 +++++ .../components/goalzero/translations/tr.json | 4 ++ .../components/gogogate2/translations/tr.json | 1 + .../google_travel_time/translations/tr.json | 29 +++++++++++ .../growatt_server/translations/tr.json | 18 +++++++ .../components/guardian/translations/tr.json | 3 ++ .../components/hive/translations/tr.json | 11 +++++ .../home_plus_control/translations/tr.json | 3 ++ .../homeassistant/translations/tr.json | 1 + .../components/homekit/translations/tr.json | 19 ++++++- .../homekit_controller/translations/tr.json | 31 +++++++++++- .../components/honeywell/translations/tr.json | 9 ++++ .../huawei_lte/translations/tr.json | 5 +- .../components/hue/translations/tr.json | 12 ++++- .../humidifier/translations/tr.json | 1 + .../components/hyperion/translations/tr.json | 2 + .../components/icloud/translations/tr.json | 19 ++++++- .../components/iotawatt/translations/tr.json | 9 ++++ .../islamic_prayer_times/translations/tr.json | 9 +++- .../components/kodi/translations/tr.json | 1 + .../components/kraken/translations/tr.json | 12 +++++ .../logi_circle/translations/tr.json | 5 ++ .../components/lookin/translations/tr.json | 10 ++++ .../components/lyric/translations/tr.json | 3 ++ .../media_player/translations/tr.json | 5 +- .../mobile_app/translations/tr.json | 3 +- .../modem_callerid/translations/tr.json | 16 ++++++ .../modern_forms/translations/tr.json | 11 +++++ .../motion_blinds/translations/tr.json | 17 ++++++- .../components/motioneye/translations/tr.json | 16 ++++++ .../components/mqtt/translations/tr.json | 8 +++ .../components/myq/translations/tr.json | 4 ++ .../components/mysensors/translations/tr.json | 26 ++++++++++ .../components/nanoleaf/translations/tr.json | 11 +++++ .../components/nest/translations/af.json | 12 +++++ .../components/nest/translations/de.json | 2 +- .../components/nest/translations/he.json | 5 ++ .../components/nest/translations/hu.json | 2 +- .../components/nest/translations/it.json | 7 +++ .../components/nest/translations/th.json | 3 ++ .../components/nest/translations/tr.json | 9 ++++ .../components/netatmo/translations/tr.json | 5 ++ .../components/netgear/translations/tr.json | 21 ++++++++ .../nfandroidtv/translations/tr.json | 10 ++++ .../nmap_tracker/translations/tr.json | 25 ++++++++++ .../components/notion/translations/tr.json | 6 +++ .../components/nuheat/translations/tr.json | 3 +- .../components/octoprint/translations/tr.json | 21 ++++++++ .../components/omnilogic/translations/tr.json | 9 ++++ .../components/onvif/translations/tr.json | 6 +++ .../components/openuv/translations/tr.json | 7 +++ .../openweathermap/translations/tr.json | 9 +++- .../p1_monitor/translations/tr.json | 9 ++++ .../panasonic_viera/translations/tr.json | 3 +- .../philips_js/translations/tr.json | 9 ++++ .../components/picnic/translations/tr.json | 12 +++++ .../components/plugwise/translations/tr.json | 10 ++++ .../components/prosegur/translations/tr.json | 11 +++++ .../components/ps4/translations/tr.json | 9 +++- .../pvpc_hourly_pricing/translations/tr.json | 18 ++++++- .../rainforest_eagle/translations/tr.json | 12 +++++ .../components/rdw/translations/ca.json | 15 ++++++ .../components/rdw/translations/de.json | 15 ++++++ .../components/rdw/translations/et.json | 15 ++++++ .../components/rdw/translations/it.json | 15 ++++++ .../components/rdw/translations/tr.json | 14 ++++++ .../recollect_waste/translations/tr.json | 7 +++ .../components/remote/translations/tr.json | 5 ++ .../components/renault/translations/tr.json | 18 ++++++- .../components/rfxtrx/translations/tr.json | 33 ++++++++++++- .../components/ridwell/translations/af.json | 26 ++++++++++ .../components/ridwell/translations/ca.json | 2 +- .../components/ridwell/translations/de.json | 2 +- .../components/ridwell/translations/en.json | 2 +- .../components/ridwell/translations/et.json | 2 +- .../components/ridwell/translations/hu.json | 2 +- .../components/ridwell/translations/it.json | 2 +- .../components/ridwell/translations/ru.json | 2 +- .../components/ridwell/translations/th.json | 24 +++++++++ .../components/ridwell/translations/tr.json | 19 +++++++ .../components/samsungtv/translations/tr.json | 4 ++ .../season/translations/sensor.tr.json | 10 ++++ .../components/sense/translations/af.json | 11 +++++ .../components/sense/translations/th.json | 11 +++++ .../components/sense/translations/tr.json | 3 +- .../components/sensor/translations/tr.json | 15 ++++++ .../components/shelly/translations/de.json | 4 +- .../components/shelly/translations/tr.json | 3 +- .../components/sia/translations/tr.json | 34 +++++++++++++ .../simplisafe/translations/tr.json | 13 ++++- .../components/sma/translations/tr.json | 16 ++++++ .../smartthings/translations/tr.json | 1 + .../components/smarttub/translations/tr.json | 12 +++++ .../components/smhi/translations/tr.json | 3 +- .../components/soma/translations/hu.json | 2 +- .../components/sonos/translations/tr.json | 1 + .../components/subaru/translations/tr.json | 14 ++++++ .../components/switchbot/translations/tr.json | 21 ++++++++ .../components/syncthing/translations/tr.json | 13 +++++ .../synology_dsm/translations/tr.json | 6 ++- .../components/toon/translations/tr.json | 1 + .../components/tplink/translations/tr.json | 15 ++++++ .../components/tuya/translations/de.json | 2 +- .../components/tuya/translations/hu.json | 2 +- .../tuya/translations/select.tr.json | 49 +++++++++++++++++++ .../tuya/translations/sensor.tr.json | 15 ++++++ .../components/tuya/translations/tr.json | 9 +++- .../uptimerobot/translations/tr.json | 18 +++++++ .../components/venstar/translations/tr.json | 15 ++++++ .../components/verisure/translations/tr.json | 34 +++++++++++++ .../vlc_telnet/translations/tr.json | 3 ++ .../water_heater/translations/tr.json | 10 ++++ .../components/watttime/translations/tr.json | 29 +++++++++++ .../waze_travel_time/translations/tr.json | 9 ++++ .../components/wemo/translations/tr.json | 5 ++ .../wolflink/translations/sensor.tr.json | 12 ++++- .../xiaomi_miio/translations/select.tr.json | 9 ++++ .../xiaomi_miio/translations/tr.json | 47 +++++++++++++++++- .../yamaha_musiccast/translations/tr.json | 16 ++++++ .../components/yeelight/translations/tr.json | 4 ++ .../components/zha/translations/tr.json | 14 +++++- .../components/zwave_js/translations/tr.json | 45 ++++++++++++++++- 173 files changed, 1920 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/adax/translations/tr.json create mode 100644 homeassistant/components/aemet/translations/tr.json create mode 100644 homeassistant/components/airthings/translations/tr.json create mode 100644 homeassistant/components/airtouch4/translations/tr.json create mode 100644 homeassistant/components/airvisual/translations/sensor.tr.json create mode 100644 homeassistant/components/ambee/translations/sensor.tr.json create mode 100644 homeassistant/components/ambee/translations/tr.json create mode 100644 homeassistant/components/amberelectric/translations/tr.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/th.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/tr.json create mode 100644 homeassistant/components/bosch_shc/translations/tr.json create mode 100644 homeassistant/components/buienradar/translations/tr.json create mode 100644 homeassistant/components/button/translations/af.json create mode 100644 homeassistant/components/button/translations/he.json create mode 100644 homeassistant/components/button/translations/th.json create mode 100644 homeassistant/components/button/translations/tr.json create mode 100644 homeassistant/components/button/translations/zh-Hans.json create mode 100644 homeassistant/components/co2signal/translations/tr.json create mode 100644 homeassistant/components/coinbase/translations/tr.json create mode 100644 homeassistant/components/crownstone/translations/tr.json create mode 100644 homeassistant/components/demo/translations/select.tr.json create mode 100644 homeassistant/components/devolo_home_network/translations/af.json create mode 100644 homeassistant/components/devolo_home_network/translations/th.json create mode 100644 homeassistant/components/devolo_home_network/translations/tr.json create mode 100644 homeassistant/components/energy/translations/tr.json create mode 100644 homeassistant/components/flipr/translations/tr.json create mode 100644 homeassistant/components/forecast_solar/translations/tr.json create mode 100644 homeassistant/components/fritz/translations/tr.json create mode 100644 homeassistant/components/garages_amsterdam/translations/tr.json create mode 100644 homeassistant/components/google_travel_time/translations/tr.json create mode 100644 homeassistant/components/growatt_server/translations/tr.json create mode 100644 homeassistant/components/hive/translations/tr.json create mode 100644 homeassistant/components/home_plus_control/translations/tr.json create mode 100644 homeassistant/components/honeywell/translations/tr.json create mode 100644 homeassistant/components/iotawatt/translations/tr.json create mode 100644 homeassistant/components/kraken/translations/tr.json create mode 100644 homeassistant/components/lookin/translations/tr.json create mode 100644 homeassistant/components/modem_callerid/translations/tr.json create mode 100644 homeassistant/components/modern_forms/translations/tr.json create mode 100644 homeassistant/components/motioneye/translations/tr.json create mode 100644 homeassistant/components/mysensors/translations/tr.json create mode 100644 homeassistant/components/nanoleaf/translations/tr.json create mode 100644 homeassistant/components/nest/translations/af.json create mode 100644 homeassistant/components/netgear/translations/tr.json create mode 100644 homeassistant/components/nfandroidtv/translations/tr.json create mode 100644 homeassistant/components/nmap_tracker/translations/tr.json create mode 100644 homeassistant/components/octoprint/translations/tr.json create mode 100644 homeassistant/components/p1_monitor/translations/tr.json create mode 100644 homeassistant/components/philips_js/translations/tr.json create mode 100644 homeassistant/components/picnic/translations/tr.json create mode 100644 homeassistant/components/prosegur/translations/tr.json create mode 100644 homeassistant/components/rainforest_eagle/translations/tr.json create mode 100644 homeassistant/components/rdw/translations/ca.json create mode 100644 homeassistant/components/rdw/translations/de.json create mode 100644 homeassistant/components/rdw/translations/et.json create mode 100644 homeassistant/components/rdw/translations/it.json create mode 100644 homeassistant/components/rdw/translations/tr.json create mode 100644 homeassistant/components/ridwell/translations/af.json create mode 100644 homeassistant/components/ridwell/translations/th.json create mode 100644 homeassistant/components/ridwell/translations/tr.json create mode 100644 homeassistant/components/season/translations/sensor.tr.json create mode 100644 homeassistant/components/sense/translations/af.json create mode 100644 homeassistant/components/sense/translations/th.json create mode 100644 homeassistant/components/sia/translations/tr.json create mode 100644 homeassistant/components/sma/translations/tr.json create mode 100644 homeassistant/components/smarttub/translations/tr.json create mode 100644 homeassistant/components/subaru/translations/tr.json create mode 100644 homeassistant/components/switchbot/translations/tr.json create mode 100644 homeassistant/components/syncthing/translations/tr.json create mode 100644 homeassistant/components/tuya/translations/select.tr.json create mode 100644 homeassistant/components/tuya/translations/sensor.tr.json create mode 100644 homeassistant/components/uptimerobot/translations/tr.json create mode 100644 homeassistant/components/venstar/translations/tr.json create mode 100644 homeassistant/components/verisure/translations/tr.json create mode 100644 homeassistant/components/waze_travel_time/translations/tr.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.tr.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/tr.json diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index f79f9a0e327..7b0fa476458 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "requests_exceeded": "Accuweather API i\u00e7in izin verilen istek say\u0131s\u0131 a\u015f\u0131ld\u0131. API Anahtar\u0131n\u0131 beklemeniz veya de\u011fi\u015ftirmeniz gerekir." }, "step": { "user": { @@ -15,6 +16,7 @@ "longitude": "Boylam", "name": "Ad" }, + "description": "Yap\u0131land\u0131rmayla ilgili yard\u0131ma ihtiyac\u0131n\u0131z varsa buraya bak\u0131n: https://www.home-assistant.io/integrations/accuweather/ \n\n Baz\u0131 sens\u00f6rler varsay\u0131lan olarak etkin de\u011fildir. Bunlar\u0131, entegrasyon yap\u0131land\u0131rmas\u0131ndan sonra varl\u0131k kay\u0131t defterinde etkinle\u015ftirebilirsiniz.\n Hava tahmini varsay\u0131lan olarak etkin de\u011fildir. Entegrasyon se\u00e7eneklerinde etkinle\u015ftirebilirsiniz.", "title": "AccuWeather" } } @@ -25,6 +27,7 @@ "data": { "forecast": "Hava Durumu tahmini" }, + "description": "AccuWeather API anahtar\u0131n\u0131n \u00fccretsiz s\u00fcr\u00fcm\u00fcn\u00fcn s\u0131n\u0131rlamalar\u0131 nedeniyle, hava tahminini etkinle\u015ftirdi\u011finizde, veri g\u00fcncellemeleri her 40 dakikada bir yerine 80 dakikada bir ger\u00e7ekle\u015ftirilir.", "title": "AccuWeather Se\u00e7enekleri" } } diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json new file mode 100644 index 00000000000..13f8e62f8be --- /dev/null +++ b/homeassistant/components/adax/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "account_id": "Hesap Kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/tr.json b/homeassistant/components/aemet/translations/tr.json new file mode 100644 index 00000000000..1a0eadce3ba --- /dev/null +++ b/homeassistant/components/aemet/translations/tr.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "station_updates": "AEMET hava istasyonlar\u0131ndan veri toplay\u0131n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/tr.json b/homeassistant/components/airthings/translations/tr.json new file mode 100644 index 00000000000..865ea58abeb --- /dev/null +++ b/homeassistant/components/airthings/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "description": "Kimlik bilgilerinizi bulmak i\u00e7in {url} adresinden giri\u015f yap\u0131n", + "id": "ID", + "secret": "Gizli" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/tr.json b/homeassistant/components/airtouch4/translations/tr.json new file mode 100644 index 00000000000..3c5799ee47e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "no_units": "Herhangi bir AirTouch 4 Grubu bulunamad\u0131." + }, + "step": { + "user": { + "title": "AirTouch 4 ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.tr.json b/homeassistant/components/airvisual/translations/sensor.tr.json new file mode 100644 index 00000000000..8fe31db40ce --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.tr.json @@ -0,0 +1,19 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Karbonmonoksit", + "n2": "Nitrojen dioksit", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "K\u00fck\u00fcrt dioksit" + }, + "airvisual__pollutant_level": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_sensitive": "Hassas gruplar i\u00e7in sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok sa\u011fl\u0131ks\u0131z" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index 2334f9fb99f..ee629cdbf7d 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -9,9 +9,15 @@ "step": { "protocol": { "data": { + "device_path": "Cihaz Yolu", "host": "Ana Bilgisayar", "port": "Port" } + }, + "user": { + "data": { + "protocol": "Protokol" + } } } }, diff --git a/homeassistant/components/ambee/translations/sensor.tr.json b/homeassistant/components/ambee/translations/sensor.tr.json new file mode 100644 index 00000000000..dcd012b5dad --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "very high": "\u00c7ok y\u00fcksek" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json new file mode 100644 index 00000000000..6655af0bad9 --- /dev/null +++ b/homeassistant/components/ambee/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "description": "Ambee hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." + } + }, + "user": { + "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/tr.json b/homeassistant/components/amberelectric/translations/tr.json new file mode 100644 index 00000000000..128f1842021 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Site Ad\u0131", + "site_nmi": "Site NMI" + }, + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API Anahtar\u0131", + "site_id": "Site Kimli\u011fi" + }, + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in {api_url} konumuna gidin", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index f33e3998af6..0a21f514946 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -3,6 +3,8 @@ "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "backoff": "Cihaz \u015fu anda e\u015fle\u015ftirme isteklerini kabul etmiyor (\u00e7ok say\u0131da ge\u00e7ersiz PIN kodu girmi\u015f olabilirsiniz), daha sonra tekrar deneyin.", + "device_did_not_pair": "Cihazdan e\u015fle\u015ftirme i\u015flemini bitirmek i\u00e7in herhangi bir giri\u015fimde bulunulmad\u0131.", "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "unknown": "Beklenmeyen hata" @@ -11,20 +13,24 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "no_usable_service": "Bir ayg\u0131t bulundu, ancak ba\u011flant\u0131 kurman\u0131n herhangi bir yolunu tan\u0131mlayamad\u0131. Bu iletiyi g\u00f6rmeye devam ederseniz, IP adresini belirtmeye veya Apple TV'nizi yeniden ba\u015flatmaya \u00e7al\u0131\u015f\u0131n.", "unknown": "Beklenmeyen hata" }, "flow_title": "Apple TV: {name}", "step": { "confirm": { + "description": "'{name}' adl\u0131 Apple TV'yi Ev Asistan\u0131'na eklemek \u00fczeresiniz.\n\n**\u0130\u015flemi tamamlamak i\u00e7in birden fazla PIN kodu girmeniz gerekebilir.**\n\nBu entegrasyonla Apple TV'nizi kapatamayaca\u011f\u0131n\u0131z\u0131 l\u00fctfen unutmay\u0131n. Yaln\u0131zca Ev Asistan\u0131'ndaki medya oynat\u0131c\u0131 kapan\u0131r!", "title": "Apple TV eklemeyi onaylay\u0131n" }, "pair_no_pin": { + "description": "'{protocol}' hizmeti i\u00e7in e\u015fle\u015ftirme gerekiyor. Devam etmek i\u00e7in l\u00fctfen Apple TV'nize {pin} PIN'ini girin.", "title": "E\u015fle\u015ftirme" }, "pair_with_pin": { "data": { "pin": "PIN Kodu" }, + "description": "'{protocol}' protokol\u00fc i\u00e7in e\u015fle\u015ftirme gerekiyor. L\u00fctfen ekranda g\u00f6r\u00fcnt\u00fclenen PIN kodunu girin. Ba\u015ftaki s\u0131f\u0131rlar atlan\u0131r, yani g\u00f6r\u00fcnt\u00fclenen kod 0123 ise 123 girin.", "title": "E\u015fle\u015ftirme" }, "reconfigure": { @@ -32,12 +38,14 @@ "title": "Cihaz\u0131n yeniden yap\u0131land\u0131r\u0131lmas\u0131" }, "service_problem": { + "description": "{protocol} ` e\u015fle\u015ftirilirken bir sorun olu\u015ftu. G\u00f6z ard\u0131 edilecek.", "title": "Hizmet eklenemedi" }, "user": { "data": { "device_input": "Cihaz" }, + "description": "Eklemek istedi\u011finiz Apple TV'nin cihaz ad\u0131n\u0131 (\u00f6rn. Mutfak veya Yatak Odas\u0131) veya IP adresini girerek ba\u015flay\u0131n. A\u011f\u0131n\u0131zda otomatik olarak herhangi bir cihaz bulunduysa, bunlar a\u015fa\u011f\u0131da g\u00f6sterilmi\u015ftir. \n\n Cihaz\u0131n\u0131z\u0131 g\u00f6remiyorsan\u0131z veya herhangi bir sorun ya\u015f\u0131yorsan\u0131z, cihaz\u0131n IP adresini belirtmeyi deneyin. \n\n {devices}", "title": "Yeni bir Apple TV kurun" } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/th.json b/homeassistant/components/aurora_abb_powerone/translations/th.json new file mode 100644 index 00000000000..5db99ad99e4 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u0e2d\u0e30\u0e41\u0e14\u0e1b\u0e40\u0e15\u0e2d\u0e23\u0e4c \u0e1e\u0e2d\u0e23\u0e4c\u0e15 RS485 \u0e2b\u0e23\u0e37\u0e2d USB-RS485" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/tr.json b/homeassistant/components/aurora_abb_powerone/translations/tr.json new file mode 100644 index 00000000000..db915bd5714 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "no_serial_ports": "com ba\u011flant\u0131 noktas\u0131 bulunamad\u0131. \u0130leti\u015fim kurmak i\u00e7in ge\u00e7erli bir RS485 cihaz\u0131na ihtiyac\u0131n\u0131z var." + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 kurulam\u0131yor, l\u00fctfen seri portu, adresi, elektrik ba\u011flant\u0131s\u0131n\u0131 ve invert\u00f6r\u00fcn a\u00e7\u0131k oldu\u011funu (g\u00fcn \u0131\u015f\u0131\u011f\u0131nda) kontrol edin.", + "cannot_open_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 a\u00e7\u0131lam\u0131yor, l\u00fctfen kontrol edip tekrar deneyin", + "invalid_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 ge\u00e7erli bir ayg\u0131t de\u011fil veya a\u00e7\u0131lamad\u0131" + }, + "step": { + "user": { + "data": { + "address": "R\u00f6le Adresi", + "port": "RS485 veya USB-RS485 Adapt\u00f6r Ba\u011flant\u0131 Noktas\u0131" + }, + "description": "\u0130nverter bir RS485 adapt\u00f6r\u00fc ile ba\u011flanmal\u0131d\u0131r, l\u00fctfen seri portu ve inverterin adresini LCD panelde konfig\u00fcre edildi\u011fi \u015fekilde se\u00e7iniz." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/af.json b/homeassistant/components/binary_sensor/translations/af.json index c0988c3aa68..37dab20a22e 100644 --- a/homeassistant/components/binary_sensor/translations/af.json +++ b/homeassistant/components/binary_sensor/translations/af.json @@ -1,4 +1,15 @@ { + "device_class": { + "cold": "hideg", + "gas": "g\u00e1z", + "heat": "h\u0151", + "moisture": "p\u00e1ratartalom", + "motion": "mozg\u00e1s", + "problem": "probl\u00e9ma", + "smoke": "f\u00fcst", + "sound": "hang", + "vibration": "rezg\u00e9s" + }, "state": { "_": { "off": "Af", diff --git a/homeassistant/components/binary_sensor/translations/th.json b/homeassistant/components/binary_sensor/translations/th.json index b8f41eb2b73..30c0a5fbe2e 100644 --- a/homeassistant/components/binary_sensor/translations/th.json +++ b/homeassistant/components/binary_sensor/translations/th.json @@ -1,4 +1,13 @@ { + "device_class": { + "cold": "\u0e40\u0e22\u0e47\u0e19", + "gas": "\u0e41\u0e01\u0e4a\u0e2a", + "heat": "\u0e04\u0e27\u0e32\u0e21\u0e23\u0e49\u0e2d\u0e19", + "problem": "\u0e1b\u0e31\u0e0d\u0e2b\u0e32", + "smoke": "\u0e04\u0e27\u0e31\u0e19", + "sound": "\u0e40\u0e2a\u0e35\u0e22\u0e07", + "vibration": "\u0e01\u0e32\u0e23\u0e2a\u0e31\u0e48\u0e19" + }, "state": { "_": { "off": "\u0e1b\u0e34\u0e14", diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index daf44cc967b..42847622a1f 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -1,10 +1,34 @@ { "device_automation": { + "condition_type": { + "is_no_update": "{entity_name} g\u00fcncel", + "is_not_running": "{entity_name} \u00e7al\u0131\u015fm\u0131yor", + "is_running": "{entity_name} \u00e7al\u0131\u015f\u0131yor", + "is_update": "{entity_name} i\u00e7in bir g\u00fcncelleme mevcut" + }, "trigger_type": { "moist": "{entity_name} nemli oldu", - "not_opened": "{entity_name} kapat\u0131ld\u0131" + "no_update": "{entity_name} g\u00fcncellendi", + "not_opened": "{entity_name} kapat\u0131ld\u0131", + "not_running": "{entity_name} art\u0131k \u00e7al\u0131\u015fm\u0131yor", + "running": "{entity_name} \u00e7al\u0131\u015fmaya ba\u015flad\u0131", + "update": "{entity_name} bir g\u00fcncelleme ald\u0131", + "vibration": "{entity_name} , titre\u015fimi alg\u0131lamaya ba\u015flad\u0131" } }, + "device_class": { + "cold": "so\u011fuk", + "gas": "gaz", + "heat": "s\u0131cakl\u0131k", + "moisture": "nem", + "motion": "hareket", + "occupancy": "doluluk", + "power": "g\u00fc\u00e7", + "problem": "sorun", + "smoke": "duman", + "sound": "ses", + "vibration": "titre\u015fim" + }, "state": { "_": { "off": "Kapal\u0131", @@ -82,6 +106,10 @@ "off": "Tamam", "on": "Sorun" }, + "running": { + "off": "\u00c7al\u0131\u015fm\u0131yor", + "on": "\u00c7al\u0131\u015f" + }, "safety": { "off": "G\u00fcvenli", "on": "G\u00fcvensiz" @@ -94,6 +122,10 @@ "off": "Temiz", "on": "Alg\u0131land\u0131" }, + "update": { + "off": "G\u00fcncel", + "on": "G\u00fcncelle\u015ftirme kullan\u0131labilir" + }, "vibration": { "off": "Temiz", "on": "Alg\u0131land\u0131" diff --git a/homeassistant/components/bosch_shc/translations/tr.json b/homeassistant/components/bosch_shc/translations/tr.json new file mode 100644 index 00000000000..270fffdbf26 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "L\u00fctfen LED yan\u0131p s\u00f6nmeye ba\u015flayana kadar Bosch Ak\u0131ll\u0131 Ev Denetleyicisinin \u00f6n taraf\u0131ndaki d\u00fc\u011fmeye bas\u0131n.\n Home Assistant ile {model} @ {host} kurulumuna devam etmeye haz\u0131r m\u0131s\u0131n\u0131z?" + }, + "credentials": { + "data": { + "password": "Ak\u0131ll\u0131 Ev Denetleyicisinin \u015eifresi" + } + }, + "user": { + "title": "SHC kimlik do\u011frulama parametreleri" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 0853c8028fc..ee3fa869ca1 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -9,12 +9,14 @@ }, "step": { "authorize": { + "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmiyorsa, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 iptal et.Home Assistant", "title": "Sony Bravia TV'yi yetkilendirin" }, "user": { "data": { "host": "Ana Bilgisayar" }, + "description": "Sony Bravia TV entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/braviatv \n\n TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json index d37a3203476..83d660a595f 100644 --- a/homeassistant/components/broadlink/translations/tr.json +++ b/homeassistant/components/broadlink/translations/tr.json @@ -11,6 +11,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({model} at {host})", "step": { "auth": { "title": "Cihaza kimlik do\u011frulama" @@ -19,12 +20,14 @@ "title": "Cihaz i\u00e7in bir isim se\u00e7in" }, "reset": { + "description": "{name} ( {model} at {host} ) kilitli. Yap\u0131land\u0131rmay\u0131 do\u011frulamak ve tamamlamak i\u00e7in cihaz\u0131n kilidini a\u00e7man\u0131z gerekir. Talimatlar:\n 1. Broadlink uygulamas\u0131n\u0131 a\u00e7\u0131n.\n 2. Cihaza t\u0131klay\u0131n.\n 3. Sa\u011f \u00fcstteki `...` se\u00e7ene\u011fine t\u0131klay\u0131n.\n 4. Sayfan\u0131n en alt\u0131na gidin.\n 5. Kilidi devre d\u0131\u015f\u0131 b\u0131rak\u0131n.", "title": "Cihaz\u0131n kilidini a\u00e7\u0131n" }, "unlock": { "data": { "unlock": "Evet, yap." }, + "description": "{name} ( {model} at {host} ) kilitli. Bu, Home Assistant'ta kimlik do\u011frulama sorunlar\u0131na yol a\u00e7abilir. Kilidini a\u00e7mak ister misin?", "title": "Cihaz\u0131n kilidini a\u00e7\u0131n (iste\u011fe ba\u011fl\u0131)" }, "user": { diff --git a/homeassistant/components/buienradar/translations/tr.json b/homeassistant/components/buienradar/translations/tr.json new file mode 100644 index 00000000000..64d6dee2feb --- /dev/null +++ b/homeassistant/components/buienradar/translations/tr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "country_code": "Kamera g\u00f6r\u00fcnt\u00fclerinin g\u00f6r\u00fcnt\u00fclenece\u011fi \u00fclkenin \u00fclke kodu.", + "delta": "Kamera g\u00f6r\u00fcnt\u00fcs\u00fc g\u00fcncellemeleri aras\u0131ndaki saniye cinsinden zaman aral\u0131\u011f\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/af.json b/homeassistant/components/button/translations/af.json new file mode 100644 index 00000000000..7c1e00817ee --- /dev/null +++ b/homeassistant/components/button/translations/af.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Nyomd meg a(z) [entity_name] gombot" + }, + "trigger_type": { + "pressed": "[entity_name] megnyomva" + } + }, + "title": "Gomb" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/he.json b/homeassistant/components/button/translations/he.json new file mode 100644 index 00000000000..ea695684d60 --- /dev/null +++ b/homeassistant/components/button/translations/he.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05dc\u05d7\u05e6\u05df {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} \u05e0\u05dc\u05d7\u05e5" + } + }, + "title": "\u05dc\u05d7\u05e6\u05df" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/hu.json b/homeassistant/components/button/translations/hu.json index 4a3e9274623..07d1b33e401 100644 --- a/homeassistant/components/button/translations/hu.json +++ b/homeassistant/components/button/translations/hu.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "press": "Nyomja meg: {entity_name}" + "press": "{entity_name} megnyom\u00e1sa" }, "trigger_type": { "pressed": "{entity_name} megnyomva" diff --git a/homeassistant/components/button/translations/th.json b/homeassistant/components/button/translations/th.json new file mode 100644 index 00000000000..fbcccf87d30 --- /dev/null +++ b/homeassistant/components/button/translations/th.json @@ -0,0 +1,3 @@ +{ + "title": "\u0e1b\u0e38\u0e48\u0e21" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/tr.json b/homeassistant/components/button/translations/tr.json new file mode 100644 index 00000000000..a02a9f5e75b --- /dev/null +++ b/homeassistant/components/button/translations/tr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "{entity_name} d\u00fc\u011fmesine bas\u0131n" + }, + "trigger_type": { + "pressed": "{entity_name} tu\u015funa bas\u0131ld\u0131" + } + }, + "title": "D\u00fc\u011fme" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/zh-Hans.json b/homeassistant/components/button/translations/zh-Hans.json new file mode 100644 index 00000000000..88c70556aa1 --- /dev/null +++ b/homeassistant/components/button/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u6309\u4e0b {entity_name} \u6309\u94ae" + }, + "trigger_type": { + "pressed": "{entity_name} \u88ab\u6309\u4e0b" + } + }, + "title": "\u6309\u94ae" +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json index 8de4663957e..3a904866600 100644 --- a/homeassistant/components/cast/translations/tr.json +++ b/homeassistant/components/cast/translations/tr.json @@ -4,9 +4,29 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { + "config": { + "data": { + "known_hosts": "Bilinen ana bilgisayarlar" + }, + "title": "Google Cast yap\u0131land\u0131rmas\u0131" + }, "confirm": { "description": "Kuruluma ba\u015flamak ister misiniz?" } } + }, + "options": { + "step": { + "advanced_options": { + "title": "Geli\u015fmi\u015f Google Cast yap\u0131land\u0131rmas\u0131" + }, + "basic_options": { + "data": { + "known_hosts": "Bilinen ana bilgisayarlar" + }, + "description": "Bilinen Ana Bilgisayarlar - Yay\u0131n cihazlar\u0131n\u0131n ana bilgisayar adlar\u0131n\u0131n veya IP adreslerinin virg\u00fclle ayr\u0131lm\u0131\u015f listesi, mDNS ke\u015ffi \u00e7al\u0131\u015fm\u0131yorsa kullan\u0131n.", + "title": "Google Cast yap\u0131land\u0131rmas\u0131" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/tr.json b/homeassistant/components/co2signal/translations/tr.json new file mode 100644 index 00000000000..3ff3b6034d5 --- /dev/null +++ b/homeassistant/components/co2signal/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "country": { + "data": { + "country_code": "\u00dclke kodu" + } + }, + "user": { + "data": { + "location": "Veri alma" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json new file mode 100644 index 00000000000..a1c6c73b192 --- /dev/null +++ b/homeassistant/components/coinbase/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "API Gizli Anahtar\u0131", + "currencies": "Hesap Bakiyesi Para Birimleri", + "exchange_rates": "D\u00f6viz Kurlar\u0131" + }, + "description": "L\u00fctfen API anahtar\u0131n\u0131z\u0131n ayr\u0131nt\u0131lar\u0131n\u0131 Coinbase taraf\u0131ndan sa\u011flanan \u015fekilde girin.", + "title": "Coinbase API Anahtar Ayr\u0131nt\u0131lar\u0131" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.", + "exchange_rate_unavaliable": "\u0130stenen d\u00f6viz kurlar\u0131ndan biri veya daha fazlas\u0131 Coinbase taraf\u0131ndan sa\u011flanm\u0131yor." + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Rapor edilecek c\u00fczdan bakiyeleri.", + "exchange_base": "D\u00f6viz kuru sens\u00f6rleri i\u00e7in temel para birimi.", + "exchange_rate_currencies": "Raporlanacak d\u00f6viz kurlar\u0131." + }, + "description": "Coinbase Se\u00e7eneklerini Ayarlay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/tr.json b/homeassistant/components/crownstone/translations/tr.json new file mode 100644 index 00000000000..d9db65f74d7 --- /dev/null +++ b/homeassistant/components/crownstone/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "account_not_verified": "Hesap do\u011frulanmad\u0131. L\u00fctfen hesab\u0131n\u0131z\u0131 Crownstone'dan gelen aktivasyon e-postas\u0131 arac\u0131l\u0131\u011f\u0131yla etkinle\u015ftirin." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.tr.json b/homeassistant/components/demo/translations/select.tr.json new file mode 100644 index 00000000000..a56fc5db252 --- /dev/null +++ b/homeassistant/components/demo/translations/select.tr.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "I\u015f\u0131k h\u0131z\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json index f618d3a3038..fe33aae6cda 100644 --- a/homeassistant/components/denonavr/translations/tr.json +++ b/homeassistant/components/denonavr/translations/tr.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "update_audyssey": "Audyssey ayarlar\u0131n\u0131 g\u00fcncelleyin" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/tr.json b/homeassistant/components/devolo_home_control/translations/tr.json index 4c6b158f694..bbd666e77d0 100644 --- a/homeassistant/components/devolo_home_control/translations/tr.json +++ b/homeassistant/components/devolo_home_control/translations/tr.json @@ -4,7 +4,8 @@ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_failed": "L\u00fctfen \u00f6ncekiyle ayn\u0131 mydevolo kullan\u0131c\u0131s\u0131n\u0131 kullan\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_network/translations/af.json b/homeassistant/components/devolo_home_network/translations/af.json new file mode 100644 index 00000000000..47aa8910aaa --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/af.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r be van \u00e1ll\u00edtva" + }, + "error": { + "cannot_connect": "Sikertelen kapcsol\u00f3d\u00e1s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/de.json b/homeassistant/components/devolo_home_network/translations/de.json index 005d8fb8fdc..c018c757d16 100644 --- a/homeassistant/components/devolo_home_network/translations/de.json +++ b/homeassistant/components/devolo_home_network/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "home_control": "Die devolo Home Control-Zentrale funktioniert nicht mit dieser Integration." + "home_control": "Die devolo Home Control-Zentraleinheit funktioniert nicht mit dieser Integration." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -17,7 +17,7 @@ "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, "zeroconf_confirm": { - "description": "M\u00f6chtest das devolo-Heimnetzwerkger\u00e4t mit dem Hostnamen `{host_name}` zum Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du das devolo-Heimnetzwerkger\u00e4t mit dem Hostnamen `{host_name}` zum Home Assistant hinzuf\u00fcgen?", "title": "Gefundenes devolo Heimnetzwerkger\u00e4t" } } diff --git a/homeassistant/components/devolo_home_network/translations/th.json b/homeassistant/components/devolo_home_network/translations/th.json new file mode 100644 index 00000000000..2fd6d1c083a --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/th.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0e01\u0e32\u0e23\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d\u0e25\u0e49\u0e21\u0e40\u0e2b\u0e25\u0e27", + "unknown": "\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0e41\u0e2d\u0e14\u0e40\u0e14\u0e23\u0e2a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json new file mode 100644 index 00000000000..c409891f3c6 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "home_control": "devolo Ev Kontrol Merkezi Birimi bu entegrasyonla \u00e7al\u0131\u015fmaz." + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP Adresleri" + } + }, + "zeroconf_confirm": { + "description": "{host_name} ` ana bilgisayar ad\u0131na sahip devolo ev a\u011f\u0131 cihaz\u0131n\u0131 Home Assistant'a eklemek ister misiniz?", + "title": "Ke\u015ffedilen devolo ev a\u011f\u0131 cihaz\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index 603e14b9e30..6596e71e05e 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -35,7 +35,7 @@ "host": "C\u00edm", "url": "URL" }, - "description": "Az eszk\u00f6z le\u00edr\u00e1s\u00e1nak XML-f\u00e1jl URL-c\u00edme", + "description": "V\u00e1lassz egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt vagy adj meg egy URL-t", "title": "DLNA digit\u00e1lis m\u00e9dia renderel\u0151" } } diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 64e3f950b25..504ccc27cb8 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -1,16 +1,47 @@ { "config": { + "abort": { + "alternative_integration": "Cihaz ba\u015fka bir entegrasyon taraf\u0131ndan daha iyi destekleniyor", + "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", + "discovery_error": "E\u015fle\u015fen bir DLNA cihaz\u0131 bulunamad\u0131", + "incomplete_config": "Yap\u0131land\u0131rmada gerekli bir de\u011fi\u015fken eksik", + "non_unique_id": "Ayn\u0131 benzersiz kimli\u011fe sahip birden fazla cihaz bulundu", + "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" + }, + "error": { + "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", + "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" + }, + "flow_title": "{name}", "step": { - "user": { + "manual": { "data": { "url": "URL" - } + }, + "title": "Manuel DLNA DMR ayg\u0131t ba\u011flant\u0131s\u0131" + }, + "user": { + "data": { + "host": "Sunucu", + "url": "URL" + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in veya bir URL girmek i\u00e7in bo\u015f b\u0131rak\u0131n", + "title": "Ke\u015ffedilen DLNA DMR cihazlar\u0131" } } }, "options": { "error": { "invalid_url": "Ge\u00e7ersiz URL" + }, + "step": { + "init": { + "data": { + "listen_port": "Olay dinleyici ba\u011flant\u0131 noktas\u0131 (ayarlanmam\u0131\u015fsa rastgele)", + "poll_availability": "Cihaz kullan\u0131labilirli\u011fi i\u00e7in anket" + }, + "title": "DLNA Dijital Medya \u0130\u015fleyici yap\u0131land\u0131rmas\u0131" + } } } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index d7a1ca8a93a..04ed9c69c9c 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -18,5 +18,12 @@ } } } + }, + "options": { + "step": { + "init": { + "description": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. https://www.home-assistant.io/integrations/doorbird/#events adresindeki belgelere bak\u0131n. \u00d6rnek: birisi_pressed_the_button, hareket" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 0857160dc51..07b783f1d9b 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -2,6 +2,30 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "DSMR s\u00fcr\u00fcm\u00fcn\u00fc se\u00e7in" + }, + "title": "Ba\u011flant\u0131 adresini se\u00e7in" + }, + "setup_serial": { + "data": { + "dsmr_version": "DSMR s\u00fcr\u00fcm\u00fcn\u00fc se\u00e7in", + "port": "Cihaz se\u00e7" + }, + "title": "Cihaz" + }, + "setup_serial_manual_path": { + "title": "Yol" + }, + "user": { + "data": { + "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + }, + "title": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in" + } } }, "options": { diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json index b2d1753fd68..4515d7de584 100644 --- a/homeassistant/components/elgato/translations/tr.json +++ b/homeassistant/components/elgato/translations/tr.json @@ -13,6 +13,9 @@ "host": "Ana Bilgisayar", "port": "Port" } + }, + "zeroconf_confirm": { + "title": "Ke\u015ffedilen Elgato Light cihaz\u0131" } } } diff --git a/homeassistant/components/energy/translations/tr.json b/homeassistant/components/energy/translations/tr.json new file mode 100644 index 00000000000..4198959715c --- /dev/null +++ b/homeassistant/components/energy/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Enerji" +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json index 573e006aa3d..c351683007f 100644 --- a/homeassistant/components/environment_canada/translations/de.json +++ b/homeassistant/components/environment_canada/translations/de.json @@ -3,8 +3,8 @@ "error": { "bad_station_id": "Die Stations-ID ist ung\u00fcltig, fehlt oder wurde in der Stations-ID-Datenbank nicht gefunden", "cannot_connect": "Verbindung fehlgeschlagen", - "error_response": "Fehlerhafte Antwort von Environment Canada", - "too_many_attempts": "Verbindungen zu Environment Canada sind in ihrer Geschwindigkeit begrenzt; versuches in 60 Sekunden erneut.", + "error_response": "Fehlerhafte Antwort vom Standort Kanada", + "too_many_attempts": "Verbindungen zum Standort Kanada sind in ihrer Geschwindigkeit begrenzt; versuche es in 60 Sekunden erneut.", "unknown": "Unerwarteter Fehler" }, "step": { @@ -16,7 +16,7 @@ "station": "ID der Wetterstation" }, "description": "Es muss entweder eine Stations-ID oder der Breitengrad/L\u00e4ngengrad angegeben werden. Als Standardwerte f\u00fcr Breitengrad/L\u00e4ngengrad werden die in Ihrer Home Assistant-Installation konfigurierten Werte verwendet. Bei Angabe von Koordinaten wird die den Koordinaten am n\u00e4chsten gelegene Wetterstation verwendet. Wenn ein Stationscode verwendet wird, muss er dem Format entsprechen: PP/Code, wobei PP f\u00fcr die zweistellige Provinz und Code f\u00fcr die Stationskennung steht. Die Liste der Stations-IDs findest du hier: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Die Wetterinformationen k\u00f6nnen entweder in Englisch oder Franz\u00f6sisch abgerufen werden.", - "title": "Environment Canada: Wetterstandort und Sprache" + "title": "Standort Kanada: Wetterstandort und Sprache" } } } diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index cb0a09cb26a..e32e705b005 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "powered_off": "Projekt\u00f6r a\u00e7\u0131k m\u0131? \u0130lk yap\u0131land\u0131rma i\u00e7in projekt\u00f6r\u00fc a\u00e7man\u0131z gerekir." }, "step": { "user": { diff --git a/homeassistant/components/flipr/translations/tr.json b/homeassistant/components/flipr/translations/tr.json new file mode 100644 index 00000000000..c89dd0ba004 --- /dev/null +++ b/homeassistant/components/flipr/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "no_flipr_id_found": "\u015eu anda hesab\u0131n\u0131zla ili\u015fkilendirilmi\u015f bir flipr kimli\u011fi yok. \u00d6nce Flipr'\u0131n mobil uygulamas\u0131yla \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131 do\u011frulaman\u0131z gerekir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/tr.json b/homeassistant/components/flume/translations/tr.json index a83e1936fb4..f35dfeb3e40 100644 --- a/homeassistant/components/flume/translations/tr.json +++ b/homeassistant/components/flume/translations/tr.json @@ -9,6 +9,10 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "description": "{username} i\u00e7in \u015fifre art\u0131k ge\u00e7erli de\u011fil.", + "title": "Flume Hesab\u0131n\u0131z\u0131 Yeniden Do\u011frulay\u0131n" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 3be9b8e3c26..266db1b5fb4 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -5,10 +5,27 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_devices_found": "A\u011fda cihaz bulunamad\u0131" }, + "flow_title": "{model} {id} ({ipaddr})", "step": { + "discovery_confirm": { + "description": "{model} {id} ( {ipaddr} ) kurulumu yapmak istiyor musunuz?" + }, "user": { "data": { "host": "Ana Bilgisayar" + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u00d6zel Efekt: 1 ila 16 [R,G,B] renk listesi. \u00d6rnek: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u00d6zel Efekt: Renkleri de\u011fi\u015ftiren efekt i\u00e7in y\u00fczde cinsinden h\u0131z.", + "custom_effect_transition": "\u00d6zel Efekt: Renkler aras\u0131ndaki ge\u00e7i\u015f t\u00fcr\u00fc.", + "mode": "Se\u00e7ilen parlakl\u0131k modu." } } } diff --git a/homeassistant/components/forecast_solar/translations/tr.json b/homeassistant/components/forecast_solar/translations/tr.json new file mode 100644 index 00000000000..44d98ab670c --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)", + "declination": "Sapma (0 = Yatay, 90 = Dikey)" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json index b3e964ae08e..8a31a391629 100644 --- a/homeassistant/components/foscam/translations/tr.json +++ b/homeassistant/components/foscam/translations/tr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_response": "Cihazdan ge\u00e7ersiz yan\u0131t", "unknown": "Beklenmeyen Hata" }, "step": { @@ -14,6 +15,7 @@ "host": "Ana Bilgisayar", "password": "\u015eifre", "port": "Port", + "rtsp_port": "RTSP port", "stream": "Ak\u0131\u015f", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json new file mode 100644 index 00000000000..93d5f2aa36f --- /dev/null +++ b/homeassistant/components/fritz/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "start_config": { + "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", + "title": "FRITZ!Box Tools Kurulumu - zorunlu" + }, + "user": { + "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", + "title": "FRITZ!Box Tools Kurulumu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/tr.json b/homeassistant/components/garages_amsterdam/translations/tr.json new file mode 100644 index 00000000000..c1cb8dca51b --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "garage_name": "Garaj ad\u0131" + }, + "title": "\u0130zlemek i\u00e7in bir garaj se\u00e7in" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index ae77262b2b3..8579e2d71ec 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -8,6 +8,10 @@ "unknown": "Beklenmeyen hata" }, "step": { + "confirm_discovery": { + "description": "Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Ana Bilgisayar" diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json index e912e7f8012..2893d89d2ec 100644 --- a/homeassistant/components/gogogate2/translations/tr.json +++ b/homeassistant/components/gogogate2/translations/tr.json @@ -7,6 +7,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json new file mode 100644 index 00000000000..0e31b33a598 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "destination": "Hedef", + "origin": "Men\u015fei" + }, + "description": "Ba\u015flang\u0131\u00e7 ve var\u0131\u015f yerini belirtirken, bir adres, enlem/boylam koordinatlar\u0131 veya bir Google yer kimli\u011fi bi\u00e7iminde dikey \u00e7izgi karakteriyle ayr\u0131lm\u0131\u015f bir veya daha fazla konum sa\u011flayabilirsiniz. Bir Google yer kimli\u011fi kullanarak konumu belirtirken, kimli\u011fin \u00f6n\u00fcne 'place_id:' eklenmelidir." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Dil", + "mode": "Seyahat Modu", + "time": "Zaman", + "time_type": "Zaman T\u00fcr\u00fc", + "transit_routing_preference": "Toplu Ta\u015f\u0131ma Tercihi", + "units": "Birimler" + }, + "description": "\u0130ste\u011fe ba\u011fl\u0131 olarak bir Kalk\u0131\u015f Saati veya Var\u0131\u015f Saati belirtebilirsiniz. Bir hareket saati belirtiyorsan\u0131z, \"\u015fimdi\", bir Unix zaman damgas\u0131 veya \"08:00:00\" gibi 24 saatlik bir zaman dizesi girebilirsiniz. Bir var\u0131\u015f saati belirtiyorsan\u0131z, bir Unix zaman damgas\u0131 veya \"08:00:00\" gibi 24 saatlik bir zaman dizesi kullanabilirsiniz." + } + } + }, + "title": "Google Haritalar Seyahat S\u00fcresi" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/tr.json b/homeassistant/components/growatt_server/translations/tr.json new file mode 100644 index 00000000000..b460f88dfae --- /dev/null +++ b/homeassistant/components/growatt_server/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "no_plants": "Bu hesapta bitki bulunamad\u0131" + }, + "step": { + "plant": { + "data": { + "plant_id": "Bitki" + } + }, + "user": { + "title": "Growatt bilgilerinizi girin" + } + } + }, + "title": "Growatt Sunucusu" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index 1e520a16995..f1326dab9b0 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -6,6 +6,9 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "discovery_confirm": { + "description": "Bu Guardian cihaz\u0131n\u0131 kurmak istiyor musunuz?" + }, "user": { "data": { "ip_address": "\u0130p Adresi", diff --git a/homeassistant/components/hive/translations/tr.json b/homeassistant/components/hive/translations/tr.json new file mode 100644 index 00000000000..14a363501e9 --- /dev/null +++ b/homeassistant/components/hive/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/tr.json b/homeassistant/components/home_plus_control/translations/tr.json new file mode 100644 index 00000000000..aa4a5a5c029 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Legrand Home+ Kontrol" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 9c57c8f2cee..438b8064138 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -10,6 +10,7 @@ "os_version": "\u0130\u015fletim Sistemi S\u00fcr\u00fcm\u00fc", "python_version": "Python S\u00fcr\u00fcm\u00fc", "timezone": "Saat dilimi", + "user": "Kullan\u0131c\u0131", "version": "S\u00fcr\u00fcm", "virtualenv": "Sanal Ortam" } diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index 6b196c25da5..7161cd3db5a 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -7,13 +7,28 @@ "pairing": { "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.", "title": "HomeKit'i E\u015fle\u015ftir" + }, + "user": { + "data": { + "include_domains": "\u0130\u00e7erecek etki alanlar\u0131" + }, + "description": "Dahil edilecek alanlar\u0131 se\u00e7in. Etki alan\u0131ndaki t\u00fcm desteklenen varl\u0131klar dahil edilecektir. Her TV medya oynat\u0131c\u0131, aktivite tabanl\u0131 uzaktan kumanda, kilit ve kamera i\u00e7in aksesuar modunda ayr\u0131 bir HomeKit \u00f6rne\u011fi olu\u015fturulacakt\u0131r.", + "title": "Dahil edilecek etki alanlar\u0131n\u0131 se\u00e7in" } } }, "options": { "step": { + "advanced": { + "data": { + "auto_start": "Otomatik ba\u015flatma (homekit.start hizmetini manuel olarak ar\u0131yorsan\u0131z devre d\u0131\u015f\u0131 b\u0131rak\u0131n)", + "devices": "Cihazlar (Tetikleyiciler)" + }, + "title": "Geli\u015fmi\u015f yap\u0131land\u0131rma" + }, "cameras": { "data": { + "camera_audio": "Sesi destekleyen kameralar", "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar" }, "description": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen t\u00fcm kameralar\u0131 kontrol edin. Kamera bir H.264 ak\u0131\u015f\u0131 vermezse, sistem videoyu HomeKit i\u00e7in H.264'e d\u00f6n\u00fc\u015ft\u00fcr\u00fcr. Kod d\u00f6n\u00fc\u015ft\u00fcrme, y\u00fcksek performansl\u0131 bir CPU gerektirir ve tek kartl\u0131 bilgisayarlarda \u00e7al\u0131\u015fma olas\u0131l\u0131\u011f\u0131 d\u00fc\u015f\u00fckt\u00fcr.", @@ -24,12 +39,14 @@ "entities": "Varl\u0131klar", "mode": "Mod" }, + "description": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in. Aksesuar modunda yaln\u0131zca tek bir varl\u0131k dahil edilir. K\u00f6pr\u00fc dahil modunda, belirli varl\u0131klar se\u00e7ilmedi\u011fi s\u00fcrece etki alan\u0131ndaki t\u00fcm varl\u0131klar dahil edilecektir. K\u00f6pr\u00fc hari\u00e7 tutma modunda, hari\u00e7 tutulan varl\u0131klar d\u0131\u015f\u0131nda etki alan\u0131ndaki t\u00fcm varl\u0131klar dahil edilecektir. En iyi performans i\u00e7in, her TV medya oynat\u0131c\u0131, aktivite tabanl\u0131 uzaktan kumanda, kilit ve kamera i\u00e7in ayr\u0131 bir HomeKit aksesuar\u0131 olu\u015fturulacakt\u0131r.", "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in" }, "init": { "data": { "mode": "Mod" - } + }, + "description": "HomeKit, bir k\u00f6pr\u00fcy\u00fc veya tek bir aksesuar\u0131 g\u00f6sterecek \u015fekilde yap\u0131land\u0131r\u0131labilir. Aksesuar modunda yaln\u0131zca tek bir varl\u0131k kullan\u0131labilir. TV cihaz s\u0131n\u0131f\u0131na sahip medya oynat\u0131c\u0131lar\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in aksesuar modu gereklidir. \"Eklenecek alan adlar\u0131\"ndaki varl\u0131klar HomeKit'e dahil edilecektir. Bir sonraki ekranda bu listeye dahil edilecek veya hari\u00e7 tutulacak varl\u0131klar\u0131 se\u00e7ebileceksiniz." } } } diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 9d72049ba21..b4bf28379fd 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -2,18 +2,44 @@ "config": { "abort": { "already_configured": "Aksesuar zaten bu denetleyici ile yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "already_paired": "Bu aksesuar zaten ba\u015fka bir cihazla e\u015fle\u015ftirilmi\u015f. L\u00fctfen aksesuar\u0131 s\u0131f\u0131rlay\u0131n ve tekrar deneyin.", + "ignored_model": "Daha \u00f6zellikli tam yerel entegrasyon kullan\u0131labilir oldu\u011fundan, bu model i\u00e7in HomeKit deste\u011fi engellendi.", + "invalid_config_entry": "Bu ayg\u0131t e\u015fle\u015fmeye haz\u0131r olarak g\u00f6steriliyor, ancak ev asistan\u0131nda ilk olarak kald\u0131r\u0131lmas\u0131 gereken \u00e7ak\u0131\u015fan bir yap\u0131land\u0131rma girdisi zaten var.", + "no_devices": "E\u015flenmemi\u015f cihaz bulunamad\u0131" }, "error": { "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", + "insecure_setup_code": "\u0130stenen kurulum kodu, \u00f6nemsiz do\u011fas\u0131 nedeniyle g\u00fcvenli de\u011fil. Bu aksesuar, temel g\u00fcvenlik gereksinimlerini kar\u015f\u0131lam\u0131yor.", + "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, "step": { "busy_error": { + "description": "T\u00fcm denetleyicilerde e\u015fle\u015ftirmeyi durdurun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", "title": "Cihaz zaten ba\u015fka bir oyun kumandas\u0131yla e\u015fle\u015fiyor" }, "max_tries_error": { + "description": "Cihaz, 100'den fazla ba\u015far\u0131s\u0131z kimlik do\u011frulama giri\u015fimi ald\u0131. Cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", "title": "Maksimum kimlik do\u011frulama giri\u015fimi a\u015f\u0131ld\u0131" + }, + "pair": { + "data": { + "allow_insecure_setup_codes": "G\u00fcvenli olmayan kurulum kodlar\u0131yla e\u015fle\u015ftirmeye izin verin.", + "pairing_code": "E\u015fle\u015ftirme Kodu" + }, + "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", + "title": "HomeKit Aksesuar Protokol\u00fc arac\u0131l\u0131\u011f\u0131yla bir cihazla e\u015fle\u015ftirin" + }, + "protocol_error": { + "description": "Cihaz e\u015fle\u015ftirme modunda olmayabilir ve fiziksel veya sanal bir d\u00fc\u011fmeye bas\u0131lmas\u0131n\u0131 gerektirebilir. Cihaz\u0131n e\u015fle\u015ftirme modunda oldu\u011fundan emin olun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin." + }, + "user": { + "data": { + "device": "Cihaz" + }, + "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden ileti\u015fim kurar. E\u015fle\u015ftirmek istedi\u011finiz cihaz\u0131 se\u00e7in:", + "title": "Cihaz se\u00e7imi" } } }, @@ -31,5 +57,6 @@ "button9": "D\u00fc\u011fme 9", "doorbell": "Kap\u0131 zili" } - } + }, + "title": "HomeKit Denetleyicisi" } \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/tr.json b/homeassistant/components/honeywell/translations/tr.json new file mode 100644 index 00000000000..d40551917e1 --- /dev/null +++ b/homeassistant/components/honeywell/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "L\u00fctfen mytotalconnectcomfort.com'da oturum a\u00e7mak i\u00e7in kullan\u0131lan kimlik bilgilerini girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index ba934acc39b..fbbce0e6e48 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -14,6 +14,7 @@ "response_error": "Cihazdan bilinmeyen hata", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name}", "step": { "user": { "data": { @@ -30,7 +31,9 @@ "init": { "data": { "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131", - "track_new_devices": "Yeni cihazlar\u0131 izle" + "track_new_devices": "Yeni cihazlar\u0131 izle", + "track_wired_clients": "Kablolu a\u011f istemcilerini izleyin", + "unauthenticated_mode": "Kimli\u011fi do\u011frulanmam\u0131\u015f mod (de\u011fi\u015fiklik yeniden y\u00fckleme gerektirir)" } } } diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index 984c91e8f36..a162c947024 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -1,19 +1,27 @@ { "config": { "abort": { + "all_configured": "T\u00fcm Philips Hue k\u00f6pr\u00fcleri zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", + "discover_timeout": "Hue k\u00f6pr\u00fcleri bulunam\u0131yor", + "no_bridges": "Philips Hue k\u00f6pr\u00fcs\u00fc bulunamad\u0131", "unknown": "Beklenmeyen hata" }, "error": { - "linking": "Beklenmeyen hata" + "linking": "Beklenmeyen hata", + "register_failed": "Kay\u0131t ba\u015far\u0131s\u0131z oldu, l\u00fctfen tekrar deneyin" }, "step": { "init": { "data": { "host": "Ana Bilgisayar" - } + }, + "title": "Hue k\u00f6pr\u00fcs\u00fcn\u00fc se\u00e7in" + }, + "link": { + "description": "Philips Hue'yu Home Assistant'a kaydetmek i\u00e7in k\u00f6pr\u00fcdeki d\u00fc\u011fmeye bas\u0131n. \n\n ![K\u00f6pr\u00fcdeki d\u00fc\u011fmenin konumu](/static/images/config_philips_hue.jpg)" }, "manual": { "data": { diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json index 7bcdbc46a0b..920a4e99cbe 100644 --- a/homeassistant/components/humidifier/translations/tr.json +++ b/homeassistant/components/humidifier/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "set_humidity": "{entity_name} i\u00e7in nemi ayarla", "set_mode": "{entity_name} \u00fczerindeki mod de\u011fi\u015ftirme", "turn_on": "{entity_name} a\u00e7\u0131n" }, diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 7b3f9f845a1..23485014065 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -22,9 +22,11 @@ } }, "confirm": { + "description": "Bu Hyperion Ambilight'\u0131 Home Assistant'a eklemek istiyor musunuz? \n\n **Ana bilgisayar:** {host}\n **:** {port}\n **Kimlik**: {id}", "title": "Hyperion Ambilight hizmetinin eklenmesini onaylay\u0131n" }, "create_token": { + "description": "Yeni bir kimlik do\u011frulama belirteci istemek i\u00e7in a\u015fa\u011f\u0131dan **G\u00f6nder**'i se\u00e7in. \u0130ste\u011fi onaylamak i\u00e7in Hyperion kullan\u0131c\u0131 aray\u00fcz\u00fcne y\u00f6nlendirileceksiniz. L\u00fctfen g\u00f6sterilen kimli\u011fin \" {auth_id} \" oldu\u011funu do\u011frulay\u0131n", "title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun" }, "create_token_external": { diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 86581625d96..d8431c666bf 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -7,6 +7,7 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "send_verification_code": "Do\u011frulama kodu g\u00f6nderilemedi", "validate_verification_code": "Do\u011frulama kodunuzu do\u011frulamay\u0131 ba\u015faramad\u0131n\u0131z, bir g\u00fcven ayg\u0131t\u0131 se\u00e7in ve do\u011frulamay\u0131 yeniden ba\u015flat\u0131n" }, "step": { @@ -16,12 +17,28 @@ }, "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin." }, + "trusted_device": { + "data": { + "trusted_device": "G\u00fcvenilir ayg\u0131t" + }, + "description": "G\u00fcvenilir cihaz\u0131n\u0131z\u0131 se\u00e7in", + "title": "iCloud g\u00fcvenilen ayg\u0131t" + }, "user": { "data": { "password": "Parola", "username": "E-posta", "with_family": "Aileyle" - } + }, + "description": "Kimlik bilgilerinizi girin", + "title": "iCloud kimlik bilgileri" + }, + "verification_code": { + "data": { + "verification_code": "Do\u011frulama kodu" + }, + "description": "L\u00fctfen iCloud'dan yeni ald\u0131\u011f\u0131n\u0131z do\u011frulama kodunu girin", + "title": "iCloud do\u011frulama kodu" } } } diff --git a/homeassistant/components/iotawatt/translations/tr.json b/homeassistant/components/iotawatt/translations/tr.json new file mode 100644 index 00000000000..28c3dd587ff --- /dev/null +++ b/homeassistant/components/iotawatt/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth": { + "description": "IoTawatt cihaz\u0131 kimlik do\u011frulama gerektirir. L\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131 ve \u015fifreyi girin ve G\u00f6nder d\u00fc\u011fmesine t\u0131klay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/tr.json b/homeassistant/components/islamic_prayer_times/translations/tr.json index a152eb19468..6385ef1e89f 100644 --- a/homeassistant/components/islamic_prayer_times/translations/tr.json +++ b/homeassistant/components/islamic_prayer_times/translations/tr.json @@ -2,6 +2,13 @@ "config": { "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "\u0130slami Namaz Vakitleri kurmak ister misiniz?", + "title": "\u0130slami Namaz Vakitlerini Ayarlay\u0131n" + } } - } + }, + "title": "\u0130slami Namaz Vakitleri" } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json index 54ad8e0b6fd..1aac43cefec 100644 --- a/homeassistant/components/kodi/translations/tr.json +++ b/homeassistant/components/kodi/translations/tr.json @@ -11,6 +11,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json new file mode 100644 index 00000000000..ac79bb4746f --- /dev/null +++ b/homeassistant/components/kraken/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "one": "Bo\u015f", + "other": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/tr.json b/homeassistant/components/logi_circle/translations/tr.json index 0b0f58116c2..54fdecd7dec 100644 --- a/homeassistant/components/logi_circle/translations/tr.json +++ b/homeassistant/components/logi_circle/translations/tr.json @@ -5,6 +5,11 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "auth": { + "description": "L\u00fctfen a\u015fa\u011f\u0131daki ba\u011flant\u0131y\u0131 takip edin ve Logi Circle hesab\u0131n\u0131za eri\u015fimi **Kabul** edin, ard\u0131ndan geri d\u00f6n\u00fcn ve a\u015fa\u011f\u0131daki **G\u00f6nder**'e bas\u0131n. \n\n [Ba\u011flant\u0131]( {authorization_url} )" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/tr.json b/homeassistant/components/lookin/translations/tr.json new file mode 100644 index 00000000000..d31859f55ab --- /dev/null +++ b/homeassistant/components/lookin/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json index 773577271d2..765c28565cd 100644 --- a/homeassistant/components/lyric/translations/tr.json +++ b/homeassistant/components/lyric/translations/tr.json @@ -10,6 +10,9 @@ "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + }, + "reauth_confirm": { + "description": "Lyric entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor." } } } diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index f7b9be9da53..5c175a50324 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -5,8 +5,11 @@ "is_off": "{entity_name} kapal\u0131" }, "trigger_type": { + "idle": "{entity_name} bo\u015fta", + "paused": "{entity_name} duraklat\u0131ld\u0131", "playing": "{entity_name} oynamaya ba\u015flar", - "turned_off": "{entity_name} kapat\u0131ld\u0131" + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } }, "state": { diff --git a/homeassistant/components/mobile_app/translations/tr.json b/homeassistant/components/mobile_app/translations/tr.json index 10d79751ec1..ed278186c16 100644 --- a/homeassistant/components/mobile_app/translations/tr.json +++ b/homeassistant/components/mobile_app/translations/tr.json @@ -3,5 +3,6 @@ "action_type": { "notify": "Bildirim g\u00f6nder" } - } + }, + "title": "Mobil uygulama" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/tr.json b/homeassistant/components/modem_callerid/translations/tr.json new file mode 100644 index 00000000000..1faa8c56406 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kalan cihaz bulunamad\u0131" + }, + "step": { + "usb_confirm": { + "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir.", + "title": "Telefon Modemi" + }, + "user": { + "title": "Telefon Modemi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json new file mode 100644 index 00000000000..41246299cb1 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "zeroconf_confirm": { + "description": "'{name}' adl\u0131 Modern Formlar fan\u0131n\u0131 Ev Asistan\u0131'na eklemek istiyor musunuz?", + "title": "Ke\u015ffedilen Modern Formlar fan cihaz\u0131" + } + } + }, + "title": "Modern Formlar" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index 194608780c9..e3289f1c3be 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -5,11 +5,15 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "connection_error": "Ba\u011flanma hatas\u0131" }, + "error": { + "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc" + }, "flow_title": "Hareketli Panjurlar", "step": { "connect": { "data": { - "api_key": "API Anahtar\u0131" + "api_key": "API Anahtar\u0131", + "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc" } }, "select": { @@ -27,5 +31,16 @@ "title": "Hareketli Panjurlar" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "G\u00fcncellemede \u00e7ok noktaya yay\u0131n i\u00e7in bekleyin" + }, + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", + "title": "Hareketli Panjurlar" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/tr.json b/homeassistant/components/motioneye/translations/tr.json new file mode 100644 index 00000000000..4fed79615aa --- /dev/null +++ b/homeassistant/components/motioneye/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_url": "Ge\u00e7ersiz URL" + } + }, + "options": { + "step": { + "init": { + "data": { + "stream_url_template": "Ak\u0131\u015f URL \u015fablonu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 86dce2b6ea4..71f8e2c1055 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -46,6 +46,14 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" } + }, + "options": { + "data": { + "discovery": "Ke\u015ffetmeyi etkinle\u015ftir", + "will_retain": "Mesaj korunacak m\u0131", + "will_topic": "Mesaj konusu olacak" + }, + "description": "Ke\u015fif - Ke\u015fif etkinle\u015ftirilirse (\u00f6nerilir), Home Assistant, yap\u0131land\u0131rmalar\u0131n\u0131 MQTT arac\u0131s\u0131nda yay\u0131nlayan cihazlar\u0131 ve varl\u0131klar\u0131 otomatik olarak ke\u015ffeder. Ke\u015fif devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, t\u00fcm yap\u0131land\u0131rma manuel olarak yap\u0131lmal\u0131d\u0131r.\n Do\u011fum mesaj\u0131 - Do\u011fum mesaj\u0131, Home Assistant (yeniden) MQTT arac\u0131s\u0131na her ba\u011fland\u0131\u011f\u0131nda g\u00f6nderilir.\n Will mesaj\u0131 - Will mesaj\u0131, Home Assistant arac\u0131yla olan ba\u011flant\u0131s\u0131n\u0131 her kaybetti\u011finde, hem temizlik durumunda (\u00f6rn. Home Assistant kapan\u0131yor) hem de kirli bir durumda (\u00f6rn. ba\u011flant\u0131y\u0131 kes." } } } diff --git a/homeassistant/components/myq/translations/tr.json b/homeassistant/components/myq/translations/tr.json index 7347d18bc34..3cbaf7169ee 100644 --- a/homeassistant/components/myq/translations/tr.json +++ b/homeassistant/components/myq/translations/tr.json @@ -9,6 +9,10 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "description": "{username} i\u00e7in \u015fifre art\u0131k ge\u00e7erli de\u011fil.", + "title": "MyQ Hesab\u0131n\u0131z\u0131 yeniden do\u011frulay\u0131n" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json new file mode 100644 index 00000000000..c83997435a2 --- /dev/null +++ b/homeassistant/components/mysensors/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f" + }, + "step": { + "gw_mqtt": { + "description": "MQTT a\u011f ge\u00e7idi kurulumu" + }, + "gw_tcp": { + "data": { + "tcp_port": "port", + "version": "MySensors s\u00fcr\u00fcm\u00fc" + }, + "description": "Ethernet a\u011f ge\u00e7idi kurulumu" + }, + "user": { + "data": { + "gateway_type": "A\u011f ge\u00e7idi t\u00fcr\u00fc" + }, + "description": "A\u011f ge\u00e7idine ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + } + } + }, + "title": "Sens\u00f6rlerim" +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json new file mode 100644 index 00000000000..2b23e027d22 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "link": { + "description": "Nanoleaf'inizdeki g\u00fc\u00e7 d\u00fc\u011fmesini d\u00fc\u011fme LED'leri yan\u0131p s\u00f6nmeye ba\u015flayana kadar 5 saniye bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde **G\u00d6NDER**'e t\u0131klay\u0131n.", + "title": "Ba\u011flant\u0131 Nanoleaf" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/af.json b/homeassistant/components/nest/translations/af.json new file mode 100644 index 00000000000..cedc2123597 --- /dev/null +++ b/homeassistant/components/nest/translations/af.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "code": "Hozz\u00e1f\u00e9r\u00e9si token" + }, + "title": "Google fi\u00f3k kapcsol\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index ba561ebc371..49563b55589 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -22,7 +22,7 @@ "data": { "code": "Zugangstoken" }, - "description": "Um dein Google-Konto zu verkn\u00fcpfen, w\u00e4hle [Konto autorisieren]({url}).\n\nKopiere nach der Autorisierung den unten angegebenen Auth Token Code.", + "description": "Um dein Google-Konto zu verkn\u00fcpfen, w\u00e4hle [Konto autorisieren]({url}).\n\nKopiere nach der Autorisierung den unten angegebenen Authentifizierungstoken-Code.", "title": "Google-Konto verkn\u00fcpfen" }, "init": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 6efee1d74bd..b1f12277c22 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -18,6 +18,11 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "auth": { + "data": { + "code": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + }, "init": { "data": { "flow_impl": "\u05e1\u05e4\u05e7" diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 15ad2d79a9f..3d73e84cbe0 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -22,7 +22,7 @@ "data": { "code": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "[Enged\u00e9lyezze] Google-fi\u00f3kj\u00e1t az \u00f6sszekapcsol\u00e1hoz ({url}).\n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja \u00e1t a kapott token k\u00f3dot.", + "description": "[Enged\u00e9lyezze]({url}) Google-fi\u00f3kj\u00e1t az \u00f6sszekapcsol\u00e1hoz.\n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja \u00e1t a kapott token k\u00f3dot.", "title": "\u00d6sszekapcsol\u00e1s Google-al" }, "init": { diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index bb4c916384d..6227dae21db 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -18,6 +18,13 @@ "unknown": "Errore imprevisto" }, "step": { + "auth": { + "data": { + "code": "Token di accesso" + }, + "description": "Per collegare l'account Google, [authorize your account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito.", + "title": "Connetti l'account Google" + }, "init": { "data": { "flow_impl": "Provider" diff --git a/homeassistant/components/nest/translations/th.json b/homeassistant/components/nest/translations/th.json index 5f14558e2b5..99efbb30cad 100644 --- a/homeassistant/components/nest/translations/th.json +++ b/homeassistant/components/nest/translations/th.json @@ -1,6 +1,9 @@ { "config": { "step": { + "auth": { + "title": "\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d\u0e1a\u0e31\u0e0d\u0e0a\u0e35\u0e02\u0e2d\u0e07 oogle" + }, "link": { "data": { "code": "Pin code" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 003c1ccc0c2..b36d5dc483d 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -7,6 +7,15 @@ }, "error": { "unknown": "Beklenmeyen hata" + }, + "step": { + "auth": { + "data": { + "code": "Eri\u015fim Anahtar\u0131" + }, + "description": "Google hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [hesab\u0131n\u0131z\u0131 yetkilendirin]( {url} ). \n\n Yetkilendirmeden sonra, sa\u011flanan Auth Token kodunu a\u015fa\u011f\u0131ya kopyalay\u0131p yap\u0131\u015ft\u0131r\u0131n.", + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + } } }, "device_automation": { diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json index 69646be2292..3c6302ec6c9 100644 --- a/homeassistant/components/netatmo/translations/tr.json +++ b/homeassistant/components/netatmo/translations/tr.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "reauth_confirm": { + "description": "Netatmo entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor" + } } }, "device_automation": { diff --git a/homeassistant/components/netgear/translations/tr.json b/homeassistant/components/netgear/translations/tr.json new file mode 100644 index 00000000000..899657b6edf --- /dev/null +++ b/homeassistant/components/netgear/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "config": "Ba\u011flant\u0131 veya oturum a\u00e7ma hatas\u0131: l\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 kontrol edin" + }, + "step": { + "user": { + "description": "Varsay\u0131lan ana bilgisayar: {host}\n Varsay\u0131lan ba\u011flant\u0131 noktas\u0131: {port}\n Varsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/tr.json b/homeassistant/components/nfandroidtv/translations/tr.json new file mode 100644 index 00000000000..f8411bc1ea0 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Bu entegrasyon, Android TV i\u00e7in Bildirimler uygulamas\u0131n\u0131 gerektirir. \n\n Android TV i\u00e7in: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n Fire TV i\u00e7in: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Y\u00f6nlendiricinizde DHCP rezervasyonu (y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n) veya cihazda statik bir IP adresi ayarlamal\u0131s\u0131n\u0131z. Aksi takdirde, cihaz sonunda kullan\u0131lamaz hale gelecektir.", + "title": "Android TV / Fire TV i\u00e7in Bildirimler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/tr.json b/homeassistant/components/nmap_tracker/translations/tr.json new file mode 100644 index 00000000000..a21bf3780a8 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", + "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", + "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri" + }, + "description": "Nmap taraf\u0131ndan taranacak ana bilgisayarlar\u0131 yap\u0131land\u0131r\u0131n. A\u011f adresi ve hari\u00e7 tutulanlar, IP Adresleri (192.168.1.1), IP A\u011flar\u0131 (192.168.0.0/24) veya IP Aral\u0131klar\u0131 (192.168.1.0-32) olabilir." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Tarama aral\u0131\u011f\u0131", + "track_new_devices": "Yeni cihazlar\u0131 izle" + } + } + } + }, + "title": "Nmap \u0130zleyici" +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index f89e3fb7533..2f6da34d2ed 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -8,6 +8,12 @@ "no_devices": "Hesapta cihaz bulunamad\u0131" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u015eifre" + }, + "description": "{username} \u015fifresini tekrar girin." + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json index 5123f1c7d9a..1947fc31e63 100644 --- a/homeassistant/components/nuheat/translations/tr.json +++ b/homeassistant/components/nuheat/translations/tr.json @@ -15,7 +15,8 @@ "password": "Parola", "serial_number": "Termostat\u0131n seri numaras\u0131.", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "https://MyNuHeat.com'da oturum a\u00e7\u0131p termostat(lar)\u0131n\u0131z\u0131 se\u00e7erek termostat\u0131n\u0131z\u0131n say\u0131sal seri numaras\u0131n\u0131 veya kimli\u011fini alman\u0131z gerekecektir." } } } diff --git a/homeassistant/components/octoprint/translations/tr.json b/homeassistant/components/octoprint/translations/tr.json new file mode 100644 index 00000000000..769f9e2504f --- /dev/null +++ b/homeassistant/components/octoprint/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "auth_failed": "Uygulama API anahtar\u0131 al\u0131namad\u0131" + }, + "flow_title": "OctoPrint Yaz\u0131c\u0131: {host}", + "progress": { + "get_api_key": "OctoPrint UI'sini a\u00e7\u0131n ve 'Ev Asistan\u0131' i\u00e7in Eri\u015fim \u0130ste\u011finde '\u0130zin Ver'i t\u0131klay\u0131n." + }, + "step": { + "user": { + "data": { + "path": "Uygulama Yolu", + "port": "Ba\u011flant\u0131 Noktas\u0131 Numaras\u0131", + "ssl": "SSL Kullan", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/tr.json b/homeassistant/components/omnilogic/translations/tr.json index ab93b71de84..a2939122ef6 100644 --- a/homeassistant/components/omnilogic/translations/tr.json +++ b/homeassistant/components/omnilogic/translations/tr.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "ph_offset": "pH ofseti (pozitif veya negatif)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 683dfbe7b92..73a89ad5a85 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -14,6 +14,9 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" } }, + "configure": { + "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" + }, "configure_profile": { "data": { "include": "Kamera varl\u0131\u011f\u0131 olu\u015ftur" @@ -35,6 +38,9 @@ "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" }, "user": { + "data": { + "auto": "Otomatik olarak ara" + }, "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun.", "title": "ONVIF cihaz kurulumu" } diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json index 241c588f691..aef4a5ef8ee 100644 --- a/homeassistant/components/openuv/translations/tr.json +++ b/homeassistant/components/openuv/translations/tr.json @@ -15,5 +15,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "OpenUV'yi yap\u0131land\u0131r\u0131n" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json index 0f845a4df73..83109869e39 100644 --- a/homeassistant/components/openweathermap/translations/tr.json +++ b/homeassistant/components/openweathermap/translations/tr.json @@ -11,10 +11,14 @@ "user": { "data": { "api_key": "API Anahtar\u0131", + "language": "Dil", "latitude": "Enlem", "longitude": "Boylam", - "mode": "Mod" - } + "mode": "Mod", + "name": "Cihaz\u0131n ad\u0131" + }, + "description": "OpenWeatherMap entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://openweathermap.org/appid adresine gidin.", + "title": "OpenWeatherMap" } } }, @@ -22,6 +26,7 @@ "step": { "init": { "data": { + "language": "Dil", "mode": "Mod" } } diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json new file mode 100644 index 00000000000..acdccc57615 --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Home Assistant ile entegre etmek i\u00e7in P1 Monitor'\u00fc kurun." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/tr.json b/homeassistant/components/panasonic_viera/translations/tr.json index d0e573fdcf9..f221ec5a3b4 100644 --- a/homeassistant/components/panasonic_viera/translations/tr.json +++ b/homeassistant/components/panasonic_viera/translations/tr.json @@ -12,7 +12,8 @@ "user": { "data": { "host": "\u0130p Adresi" - } + }, + "title": "TV'nizi kurun" } } } diff --git a/homeassistant/components/philips_js/translations/tr.json b/homeassistant/components/philips_js/translations/tr.json new file mode 100644 index 00000000000..aed6030c02a --- /dev/null +++ b/homeassistant/components/philips_js/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pair": { + "description": "TV'nizde g\u00f6r\u00fcnt\u00fclenen PIN'i girin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/tr.json b/homeassistant/components/picnic/translations/tr.json new file mode 100644 index 00000000000..9ffb10e1b88 --- /dev/null +++ b/homeassistant/components/picnic/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "country_code": "\u00dclke kodu" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index 60d6b1f92be..6f09f1e8fe1 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -10,6 +10,9 @@ }, "flow_title": "Smile: {name}", "step": { + "user": { + "description": "\u00dcr\u00fcn:" + }, "user_gateway": { "data": { "host": "\u0130p Adresi", @@ -20,5 +23,12 @@ "description": "L\u00fctfen girin" } } + }, + "options": { + "step": { + "init": { + "description": "Plugwise Se\u00e7eneklerini Ayarlay\u0131n" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/tr.json b/homeassistant/components/prosegur/translations/tr.json new file mode 100644 index 00000000000..f3fa57430ea --- /dev/null +++ b/homeassistant/components/prosegur/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "country": "\u00dclke" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index 4e3e0b53445..535c9467fb0 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -7,10 +7,15 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "creds": { + "title": "PlayStation 4" + }, "link": { "data": { - "ip_address": "\u0130p Adresi" - } + "ip_address": "\u0130p Adresi", + "region": "B\u00f6lge" + }, + "title": "PlayStation 4" }, "mode": { "data": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json index 394f876401b..fc6f4973da0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -6,8 +6,22 @@ "step": { "user": { "data": { - "name": "Sens\u00f6r Ad\u0131" - } + "name": "Sens\u00f6r Ad\u0131", + "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)" + }, + "title": "Sens\u00f6r kurulumu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", + "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" + }, + "description": "Bu sens\u00f6r, \u0130spanya'da [saatlik elektrik fiyatland\u0131rmas\u0131 (PVPC)](https://www.esios.ree.es/es/pvpc) almak i\u00e7in resmi API'yi kullan\u0131r.\n Daha kesin a\u00e7\u0131klama i\u00e7in [entegrasyon belgelerini](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) ziyaret edin.", + "title": "Sens\u00f6r kurulumu" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/tr.json b/homeassistant/components/rainforest_eagle/translations/tr.json new file mode 100644 index 00000000000..20704f9583d --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cloud_id": "Bulut kimli\u011fi", + "install_code": "Y\u00fckleme kodu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/ca.json b/homeassistant/components/rdw/translations/ca.json new file mode 100644 index 00000000000..3871995e288 --- /dev/null +++ b/homeassistant/components/rdw/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown_license_plate": "Matr\u00edcula desconeguda" + }, + "step": { + "user": { + "data": { + "license_plate": "Matr\u00edcula" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/de.json b/homeassistant/components/rdw/translations/de.json new file mode 100644 index 00000000000..b7b25359183 --- /dev/null +++ b/homeassistant/components/rdw/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown_license_plate": "Unbekanntes Nummernschild" + }, + "step": { + "user": { + "data": { + "license_plate": "Nummernschild" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/et.json b/homeassistant/components/rdw/translations/et.json new file mode 100644 index 00000000000..48911f2bcb7 --- /dev/null +++ b/homeassistant/components/rdw/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown_license_plate": "Tundmatu numbrim\u00e4rk" + }, + "step": { + "user": { + "data": { + "license_plate": "Numbrim\u00e4rk" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/it.json b/homeassistant/components/rdw/translations/it.json new file mode 100644 index 00000000000..36ed596282f --- /dev/null +++ b/homeassistant/components/rdw/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown_license_plate": "Targa sconosciuta" + }, + "step": { + "user": { + "data": { + "license_plate": "Targa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/tr.json b/homeassistant/components/rdw/translations/tr.json new file mode 100644 index 00000000000..14835007351 --- /dev/null +++ b/homeassistant/components/rdw/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown_license_plate": "Bilinmeyen plaka" + }, + "step": { + "user": { + "data": { + "license_plate": "Plaka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/tr.json b/homeassistant/components/recollect_waste/translations/tr.json index 5307276a71d..3e21df87207 100644 --- a/homeassistant/components/recollect_waste/translations/tr.json +++ b/homeassistant/components/recollect_waste/translations/tr.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "service_id": "Hizmet Kimli\u011fi" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/tr.json b/homeassistant/components/remote/translations/tr.json index 5359c99a78a..59c6f9a6a6e 100644 --- a/homeassistant/components/remote/translations/tr.json +++ b/homeassistant/components/remote/translations/tr.json @@ -1,9 +1,14 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} de\u011fi\u015ftir", "turn_off": "{entity_name} kapat", "turn_on": "{entity_name} a\u00e7\u0131n" }, + "condition_type": { + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, "trigger_type": { "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" diff --git a/homeassistant/components/renault/translations/tr.json b/homeassistant/components/renault/translations/tr.json index 866fc513d4a..ddb98324782 100644 --- a/homeassistant/components/renault/translations/tr.json +++ b/homeassistant/components/renault/translations/tr.json @@ -1,10 +1,26 @@ { "config": { + "abort": { + "kamereon_no_account": "Kamereon hesab\u0131 bulunamad\u0131" + }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon hesap kimli\u011fi" + }, + "title": "Kamereon hesap kimli\u011fini se\u00e7in" + }, "reauth_confirm": { "data": { "password": "\u015eifre" - } + }, + "description": "{username} i\u00e7in \u015fifrenizi g\u00fcncelleyin" + }, + "user": { + "data": { + "locale": "Yerel ayar" + }, + "title": "Renault kimlik bilgilerini ayarla" } } } diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json index 1c3ad8b9e05..8627e635483 100644 --- a/homeassistant/components/rfxtrx/translations/tr.json +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -13,9 +13,34 @@ "host": "Ana Bilgisayar", "port": "Port" } + }, + "setup_serial": { + "data": { + "device": "Cihaz se\u00e7" + }, + "title": "Cihaz" + }, + "setup_serial_manual_path": { + "title": "Yol" + }, + "user": { + "data": { + "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + }, + "title": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in" } } }, + "device_automation": { + "action_type": { + "send_command": "Komut g\u00f6nder: {subtype}", + "send_status": "Durum g\u00fcncellemesi g\u00f6nder: {subtype}" + }, + "trigger_type": { + "command": "Al\u0131nan komut: {subtype}", + "status": "Al\u0131nan durum: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", @@ -24,8 +49,14 @@ "step": { "set_device_options": { "data": { + "data_bit": "Veri biti say\u0131s\u0131", + "fire_event": "Cihaz etkinli\u011fini etkinle\u015ftir", + "off_delay": "Kapanma gecikmesi", + "off_delay_enabled": "Kapatma gecikmesini etkinle\u015ftir", + "replace_device": "De\u011fi\u015ftirilecek cihaz\u0131 se\u00e7in", "venetian_blind_mode": "Jaluzi modu" - } + }, + "title": "Cihaz se\u00e7eneklerini yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/ridwell/translations/af.json b/homeassistant/components/ridwell/translations/af.json new file mode 100644 index 00000000000..84f128a6d22 --- /dev/null +++ b/homeassistant/components/ridwell/translations/af.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r be van \u00e1ll\u00edtva" + }, + "error": { + "invalid_auth": "Sikertelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "Add meg a jelsz\u00f3t \u00fajra" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Add meg a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/ca.json b/homeassistant/components/ridwell/translations/ca.json index 6e84fe58325..68e355cbf9e 100644 --- a/homeassistant/components/ridwell/translations/ca.json +++ b/homeassistant/components/ridwell/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/ridwell/translations/de.json b/homeassistant/components/ridwell/translations/de.json index 6849ba28022..d7b6cefa827 100644 --- a/homeassistant/components/ridwell/translations/de.json +++ b/homeassistant/components/ridwell/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_configured": "Konto wurde bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/ridwell/translations/en.json b/homeassistant/components/ridwell/translations/en.json index e3200df9038..8399f6242cd 100644 --- a/homeassistant/components/ridwell/translations/en.json +++ b/homeassistant/components/ridwell/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_account": "Account is already configured", + "already_configured": "Account is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/ridwell/translations/et.json b/homeassistant/components/ridwell/translations/et.json index ee9abfe9d17..ffee1fa8727 100644 --- a/homeassistant/components/ridwell/translations/et.json +++ b/homeassistant/components/ridwell/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/ridwell/translations/hu.json b/homeassistant/components/ridwell/translations/hu.json index b79c5204f49..19046e3e0b8 100644 --- a/homeassistant/components/ridwell/translations/hu.json +++ b/homeassistant/components/ridwell/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_configured": "A fi\u00f3k m\u00e1r be van \u00e1ll\u00edtva", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { diff --git a/homeassistant/components/ridwell/translations/it.json b/homeassistant/components/ridwell/translations/it.json index bb7a0862268..b603f14a1ac 100644 --- a/homeassistant/components/ridwell/translations/it.json +++ b/homeassistant/components/ridwell/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { diff --git a/homeassistant/components/ridwell/translations/ru.json b/homeassistant/components/ridwell/translations/ru.json index db59743f1fe..2a319f1bb70 100644 --- a/homeassistant/components/ridwell/translations/ru.json +++ b/homeassistant/components/ridwell/translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "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_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": { diff --git a/homeassistant/components/ridwell/translations/th.json b/homeassistant/components/ridwell/translations/th.json new file mode 100644 index 00000000000..a30127b4e9e --- /dev/null +++ b/homeassistant/components/ridwell/translations/th.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0e2d\u0e38\u0e1b\u0e01\u0e23\u0e13\u0e4c\u0e19\u0e35\u0e49\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e04\u0e48\u0e32\u0e41\u0e25\u0e49\u0e27" + }, + "error": { + "unknown": "\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19" + } + }, + "user": { + "data": { + "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19", + "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" + }, + "description": "\u0e1b\u0e49\u0e2d\u0e19\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49 \u0e41\u0e25\u0e30 \u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/tr.json b/homeassistant/components/ridwell/translations/tr.json new file mode 100644 index 00000000000..0062ba735c8 --- /dev/null +++ b/homeassistant/components/ridwell/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u015eifre" + }, + "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:" + }, + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 ad\u0131" + }, + "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 6b3900e9aa5..f2d6fc7e978 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -5,6 +5,7 @@ "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", "cannot_connect": "Ba\u011flanma hatas\u0131", + "missing_config_entry": "Bu Samsung cihaz\u0131nda bir yap\u0131land\u0131rma giri\u015fi yok.", "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." }, "flow_title": "Samsung TV: {model}", @@ -12,6 +13,9 @@ "confirm": { "title": "Samsung TV" }, + "reauth_confirm": { + "description": "G\u00f6nderdikten sonra, 30 saniye i\u00e7inde yetkilendirme isteyen {device} \u00fczerindeki a\u00e7\u0131l\u0131r pencereyi kabul edin." + }, "user": { "data": { "host": "Ana Bilgisayar", diff --git a/homeassistant/components/season/translations/sensor.tr.json b/homeassistant/components/season/translations/sensor.tr.json new file mode 100644 index 00000000000..c70e7cc881e --- /dev/null +++ b/homeassistant/components/season/translations/sensor.tr.json @@ -0,0 +1,10 @@ +{ + "state": { + "season__season": { + "autumn": "Sonbahar", + "spring": "\u0130lkbahar", + "summer": "Yaz", + "winter": "K\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/af.json b/homeassistant/components/sense/translations/af.json new file mode 100644 index 00000000000..f369e42e25e --- /dev/null +++ b/homeassistant/components/sense/translations/af.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/th.json b/homeassistant/components/sense/translations/th.json new file mode 100644 index 00000000000..01a1bc8a513 --- /dev/null +++ b/homeassistant/components/sense/translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "timeout": "\u0e2b\u0e21\u0e14\u0e40\u0e27\u0e25\u0e32" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json index 0e335265325..7eee55f5724 100644 --- a/homeassistant/components/sense/translations/tr.json +++ b/homeassistant/components/sense/translations/tr.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-posta", - "password": "Parola" + "password": "Parola", + "timeout": "Zaman a\u015f\u0131m\u0131" } } } diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index bfdd538306f..7dd30efed8f 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -1,24 +1,39 @@ { "device_automation": { "condition_type": { + "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", + "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", "is_energy": "Mevcut {entity_name} enerjisi", + "is_gas": "Mevcut {entity_name} gaz\u0131", "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", "is_value": "Mevcut {entity_name} de\u011feri", "is_voltage": "Mevcut {entity_name} voltaj\u0131" }, "trigger_type": { "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", + "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", + "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", + "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "nitrogen_dioxide": "{entity_name} nitrojen dioksit konsantrasyonu de\u011fi\u015fiklikleri", + "nitrogen_monoxide": "{entity_name} nitrojen monoksit konsantrasyonu de\u011fi\u015fiklikleri", + "nitrous_oxide": "{entity_name} nitr\u00f6z oksit konsantrasyonu de\u011fi\u015fiklikleri", + "ozone": "{entity_name} ozon konsantrasyonu de\u011fi\u015fiklikleri", + "pm1": "{entity_name} PM1 konsantrasyonu de\u011fi\u015fiklikleri", + "pm10": "{entity_name} PM10 konsantrasyon de\u011fi\u015fiklikleri", + "pm25": "{entity_name} PM2.5 konsantrasyon de\u011fi\u015fiklikleri", "power": "{entity_name} g\u00fc\u00e7 de\u011fi\u015fiklikleri", "power_factor": "{entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc de\u011fi\u015fiklikleri", "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 75487b5047a..a8d8bdbdf99 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -40,13 +40,13 @@ "btn_down": "{subtype} Taste nach unten", "btn_up": "{subtype} Taste nach oben", "double": "{subtype} zweifach bet\u00e4tigt", - "double_push": "{subtype} Doppelter Push", + "double_push": "{subtype} Doppel-Druck", "long": "{subtype} lange angeklickt", "long_push": "{subtype} langer Druck", "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt", "single": "{subtype} einfach bet\u00e4tigt", "single_long": "{subtype} einfach bet\u00e4tigt und dann gehalten", - "single_push": "{subtype} einzelner Push", + "single_push": "{subtype} einfacher Druck", "triple": "{subtype} dreifach bet\u00e4tigt" } } diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index f577c73787f..a89379ef0d6 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -19,7 +19,8 @@ "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar uyand\u0131r\u0131lmal\u0131d\u0131r, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 uyand\u0131rabilirsiniz." } } }, diff --git a/homeassistant/components/sia/translations/tr.json b/homeassistant/components/sia/translations/tr.json new file mode 100644 index 00000000000..c67fcc6698b --- /dev/null +++ b/homeassistant/components/sia/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "invalid_account_format": "Hesap onalt\u0131l\u0131k bir de\u011fer de\u011fil, l\u00fctfen sadece 0-9 ve AF kullan\u0131n.", + "invalid_account_length": "Hesap do\u011fru uzunlukta de\u011fil, 3 ile 16 karakter aras\u0131nda olmas\u0131 gerekiyor.", + "invalid_key_format": "Anahtar onalt\u0131l\u0131k bir de\u011fer de\u011fildir, l\u00fctfen yaln\u0131zca 0-9 ve AF kullan\u0131n.", + "invalid_key_length": "Anahtar do\u011fru uzunlukta de\u011fil, 16, 24 veya 32 onalt\u0131l\u0131k karakterden olu\u015fmal\u0131d\u0131r.", + "invalid_ping": "Ping aral\u0131\u011f\u0131 1 ile 1440 dakika aras\u0131nda olmal\u0131d\u0131r.", + "invalid_zones": "En az 1 b\u00f6lge olmas\u0131 gerekir." + }, + "step": { + "additional_account": { + "title": "Ge\u00e7erli ba\u011flant\u0131 noktas\u0131na ba\u015fka bir hesap ekleyin." + }, + "user": { + "data": { + "account": "Hesap Kimli\u011fi", + "additional_account": "Ek hesaplar", + "encryption_key": "\u015eifreleme anahtar\u0131", + "ping_interval": "Ping Aral\u0131\u011f\u0131 (dk)", + "protocol": "Protokol" + } + } + } + }, + "options": { + "step": { + "options": { + "description": "Hesap i\u00e7in se\u00e7enekleri ayarlay\u0131n: {account}", + "title": "SIA Kurulumu i\u00e7in se\u00e7enekler." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 94506fb426b..2a7936a0158 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", @@ -10,6 +11,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Yetkilendirme Kodu" + }, + "description": "SimpliSafe web uygulamas\u0131 URL'sinden yetkilendirme kodunu girin:", + "title": "Yetkilendirmeyi Bitir" + }, "mfa": { "description": "SimpliSafe'den bir ba\u011flant\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin. Ba\u011flant\u0131y\u0131 do\u011frulad\u0131ktan sonra, entegrasyonun kurulumunu tamamlamak i\u00e7in buraya geri d\u00f6n\u00fcn.", "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" @@ -25,7 +33,8 @@ "data": { "password": "Parola", "username": "E-posta adresi" - } + }, + "description": "SimpliSafe, 2021'den itibaren web uygulamas\u0131 \u00fczerinden yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle, bu s\u00fcrecin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](yetkilendirme kodu http://home assistant.io/integrations/simplisafe#getting) okudu\u011funuzdan emin olun.\n\nHaz\u0131r oldu\u011funuzda, SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]({url}) t\u0131klat\u0131n. \u0130\u015flem tamamland\u0131\u011f\u0131nda, buraya d\u00f6n\u00fcn ve G\u00f6nder'i t\u0131klat\u0131n." } } } diff --git a/homeassistant/components/sma/translations/tr.json b/homeassistant/components/sma/translations/tr.json new file mode 100644 index 00000000000..852cfb66adb --- /dev/null +++ b/homeassistant/components/sma/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_retrieve_device_info": "Ba\u015far\u0131yla ba\u011fland\u0131, ancak cihaz bilgileri al\u0131namad\u0131" + }, + "step": { + "user": { + "data": { + "group": "Grup" + }, + "description": "SMA cihaz bilgilerinizi girin.", + "title": "SMA Solar'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json index 5e7463c1c74..5f96ada17ba 100644 --- a/homeassistant/components/smartthings/translations/tr.json +++ b/homeassistant/components/smartthings/translations/tr.json @@ -10,6 +10,7 @@ } }, "select_location": { + "description": "L\u00fctfen Home Assistant'a eklemek istedi\u011finiz SmartThings Konumunu se\u00e7in. Ard\u0131ndan yeni bir pencere a\u00e7aca\u011f\u0131z ve sizden oturum a\u00e7man\u0131z\u0131 ve Home Assistant entegrasyonunun se\u00e7ilen konuma y\u00fcklenmesine izin vermenizi isteyece\u011fiz.", "title": "Konum Se\u00e7in" } } diff --git a/homeassistant/components/smarttub/translations/tr.json b/homeassistant/components/smarttub/translations/tr.json new file mode 100644 index 00000000000..e3dac83de80 --- /dev/null +++ b/homeassistant/components/smarttub/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "SmartTub entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor" + }, + "user": { + "title": "Oturum a\u00e7ma" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/tr.json b/homeassistant/components/smhi/translations/tr.json index bb50f1e2a8d..1cf772ce6fa 100644 --- a/homeassistant/components/smhi/translations/tr.json +++ b/homeassistant/components/smhi/translations/tr.json @@ -9,7 +9,8 @@ "data": { "latitude": "Enlem", "longitude": "Boylam" - } + }, + "title": "\u0130sve\u00e7'teki konum" } } } diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index e7ac9d8d71c..89194a7b204 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -3,7 +3,7 @@ "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.", + "connection_error": "Nem siker\u00fclt csatlakozni.", "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." }, diff --git a/homeassistant/components/sonos/translations/tr.json b/homeassistant/components/sonos/translations/tr.json index 42bd46ce7c0..5fae97936e4 100644 --- a/homeassistant/components/sonos/translations/tr.json +++ b/homeassistant/components/sonos/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "not_sonos_device": "Bulunan cihaz bir Sonos cihaz\u0131 de\u011fil", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/subaru/translations/tr.json b/homeassistant/components/subaru/translations/tr.json new file mode 100644 index 00000000000..dd2bd5a4a29 --- /dev/null +++ b/homeassistant/components/subaru/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "pin": { + "data": { + "pin": "PIN" + } + }, + "user": { + "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json new file mode 100644 index 00000000000..f108af331e3 --- /dev/null +++ b/homeassistant/components/switchbot/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", + "switchbot_unsupported_type": "Desteklenmeyen Switchbot T\u00fcr\u00fc." + }, + "error": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Cihaz MAC adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/tr.json b/homeassistant/components/syncthing/translations/tr.json new file mode 100644 index 00000000000..c46a8154d6f --- /dev/null +++ b/homeassistant/components/syncthing/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "title": "Senkronizasyon entegrasyonunu kurun", + "token": "Anahtar" + } + } + } + }, + "title": "E\u015fitleme" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index f2b93648da0..0b730d77629 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reconfigure_successful": "Yeniden yap\u0131land\u0131rma ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -17,6 +18,9 @@ "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" } }, + "reauth": { + "description": "Sebep: {details}" + }, "reauth_confirm": { "data": { "password": "\u015eifre", diff --git a/homeassistant/components/toon/translations/tr.json b/homeassistant/components/toon/translations/tr.json index 97765a99a7f..a52d914241d 100644 --- a/homeassistant/components/toon/translations/tr.json +++ b/homeassistant/components/toon/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Se\u00e7ilen anla\u015fma zaten yap\u0131land\u0131r\u0131lm\u0131\u015f.", + "no_agreements": "Bu hesapta Toon ekran\u0131 yok.", "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." }, "step": { diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index e8f7a5aaf6d..a8834ae4c92 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -3,9 +3,24 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "TP-Link ak\u0131ll\u0131 cihazlar\u0131 kurmak istiyor musunuz?" + }, + "discovery_confirm": { + "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + }, + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } } } diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 57e429b9de1..61bdf308246 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -16,7 +16,7 @@ "access_id": "Zugangs-ID", "access_secret": "Zugangsgeheimnis", "country_code": "L\u00e4ndercode", - "endpoint": "Verf\u00fcgbarkeitszone", + "endpoint": "Verf\u00fcgbarkeitsbereich", "password": "Passwort", "tuya_app_type": "Mobile App", "username": "Konto" diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index c01d8dbb2e7..2c2969e589c 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -36,7 +36,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", - "title": "Tuya" + "title": "Tuya integr\u00e1ci\u00f3" } } }, diff --git a/homeassistant/components/tuya/translations/select.tr.json b/homeassistant/components/tuya/translations/select.tr.json new file mode 100644 index 00000000000..e6bdd2038b0 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.tr.json @@ -0,0 +1,49 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "0": "Devre d\u0131\u015f\u0131", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Otomatik", + "1": "Kapal\u0131", + "2": "A\u00e7\u0131k" + }, + "tuya__decibel_sensitivity": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Y\u00fcksek hassasiyet" + }, + "tuya__ipc_work_mode": { + "0": "D\u00fc\u015f\u00fck g\u00fc\u00e7 modu", + "1": "S\u00fcrekli \u00e7al\u0131\u015fma modu" + }, + "tuya__led_type": { + "halogen": "Halojen", + "incandescent": "Akkor", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Kapal\u0131", + "pos": "Anahtar konumunu belirtin", + "relay": "A\u00e7ma/kapama durumunu belirtin" + }, + "tuya__motion_sensitivity": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Orta hassasiyet", + "2": "Y\u00fcksek hassasiyet" + }, + "tuya__record_mode": { + "1": "Yaln\u0131zca olaylar\u0131 kaydet", + "2": "S\u00fcrekli kay\u0131t" + }, + "tuya__relay_status": { + "last": "Son durumu hat\u0131rla", + "memory": "Son durumu hat\u0131rla", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "power_off": "Kapal\u0131", + "power_on": "A\u00e7\u0131k" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.tr.json b/homeassistant/components/tuya/translations/sensor.tr.json new file mode 100644 index 00000000000..3a3088f51f5 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.tr.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Kaynama s\u0131cakl\u0131\u011f\u0131", + "cooling": "So\u011futma", + "heating": "Is\u0131tma", + "heating_temp": "Is\u0131tma s\u0131cakl\u0131\u011f\u0131", + "reserve_1": "Rezerv 1", + "reserve_2": "Rezerv 2", + "reserve_3": "Rezerv 3", + "standby": "Bekleme modu", + "warm": "Is\u0131 korumas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 37eae2e8ae0..9de7fea55d4 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -6,17 +6,21 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "login_error": "Giri\u015f hatas\u0131 ( {code} ): {msg}" }, "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", "step": { "login": { "data": { + "access_id": "Eri\u015fim Kimli\u011fi", "country_code": "\u00dclke Kodu", + "endpoint": "Kullan\u0131labilirlik Alan\u0131", "password": "\u015eifre", "tuya_app_type": "Tuya uygulama tipi", "username": "Kullan\u0131c\u0131 Ad\u0131" }, + "description": "Tuya kimlik bilgilerinizi girin", "title": "Tuya" }, "user": { @@ -25,6 +29,7 @@ "password": "Parola", "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", "region": "B\u00f6lge", + "tuya_project_type": "Tuya bulut proje t\u00fcr\u00fc", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Tuya kimlik bilgilerinizi girin.", @@ -37,6 +42,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { + "dev_multi_type": "Yap\u0131land\u0131r\u0131lacak birden \u00e7ok se\u00e7ili cihaz ayn\u0131 t\u00fcrde olmal\u0131d\u0131r", "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz", "dev_not_found": "Cihaz bulunamad\u0131" }, @@ -44,6 +50,7 @@ "device": { "data": { "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", + "max_kelvin": "Kelvin'de desteklenen maksimum renk s\u0131cakl\u0131\u011f\u0131", "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", diff --git a/homeassistant/components/uptimerobot/translations/tr.json b/homeassistant/components/uptimerobot/translations/tr.json new file mode 100644 index 00000000000..007b33db388 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_failed_existing": "Yap\u0131land\u0131rma giri\u015fi g\u00fcncellenemedi, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." + }, + "error": { + "reauth_failed_matching_account": "Sa\u011flad\u0131\u011f\u0131n\u0131z API anahtar\u0131, mevcut yap\u0131land\u0131rman\u0131n hesap kimli\u011fiyle e\u015fle\u015fmiyor." + }, + "step": { + "reauth_confirm": { + "description": "UptimeRobot'tan yeni bir salt okunur API anahtar\u0131 sa\u011flaman\u0131z gerekiyor" + }, + "user": { + "description": "UptimeRobot'tan salt okunur bir API anahtar\u0131 sa\u011flaman\u0131z gerekiyor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json new file mode 100644 index 00000000000..9efa5c3c214 --- /dev/null +++ b/homeassistant/components/venstar/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "\u015eifre", + "pin": "PIN Kodu", + "username": "Kullan\u0131c\u0131 ad\u0131" + }, + "title": "Venstar Termostat'a ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/tr.json b/homeassistant/components/verisure/translations/tr.json new file mode 100644 index 00000000000..3396fdd0692 --- /dev/null +++ b/homeassistant/components/verisure/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "installation": { + "data": { + "giid": "Kurulum" + }, + "description": "Home Assistant, Sayfalar\u0131m hesab\u0131n\u0131zda birden fazla Verisure y\u00fcklemesi buldu. L\u00fctfen Home Assistant'a eklemek i\u00e7in kurulumu se\u00e7in." + }, + "reauth_confirm": { + "data": { + "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." + } + }, + "user": { + "data": { + "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n." + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Varsay\u0131lan PIN kodu, gerekli basamak say\u0131s\u0131yla e\u015fle\u015fmiyor" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Kilitler i\u00e7in PIN kodundaki hane say\u0131s\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json index 40379347fd4..aeee8ca6eec 100644 --- a/homeassistant/components/vlc_telnet/translations/tr.json +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -8,6 +8,9 @@ "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" }, "step": { + "hassio_confirm": { + "description": "{addon} eklentisine ba\u011flanmak istiyor musunuz?" + }, "reauth_confirm": { "data": { "password": "\u015eifre" diff --git a/homeassistant/components/water_heater/translations/tr.json b/homeassistant/components/water_heater/translations/tr.json index 3010c9e622b..5b5115889ee 100644 --- a/homeassistant/components/water_heater/translations/tr.json +++ b/homeassistant/components/water_heater/translations/tr.json @@ -4,5 +4,15 @@ "turn_off": "{entity_name} kapat", "turn_on": "{entity_name} a\u00e7\u0131n" } + }, + "state": { + "_": { + "eco": "Eko", + "electric": "Elektrik", + "gas": "Gaz", + "heat_pump": "Is\u0131 pompas\u0131", + "high_demand": "Y\u00fcksek talep", + "performance": "Performans" + } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/tr.json b/homeassistant/components/watttime/translations/tr.json index 866fc513d4a..00c82748832 100644 --- a/homeassistant/components/watttime/translations/tr.json +++ b/homeassistant/components/watttime/translations/tr.json @@ -1,10 +1,39 @@ { "config": { + "error": { + "unknown_coordinates": "Enlem/boylam i\u00e7in veri yok" + }, "step": { + "coordinates": { + "description": "\u0130zlenecek enlem ve boylam\u0131 girin:" + }, + "location": { + "data": { + "location_type": "Lokasyon" + }, + "description": "\u0130zlemek i\u00e7in bir konum se\u00e7in:" + }, "reauth_confirm": { "data": { "password": "\u015eifre" } + }, + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 ad\u0131" + }, + "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin:" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Haritada izlenen konumu g\u00f6ster" + }, + "title": "WattTime'\u0131 yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/waze_travel_time/translations/tr.json b/homeassistant/components/waze_travel_time/translations/tr.json new file mode 100644 index 00000000000..afc6741e97a --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Ba\u015flang\u0131\u00e7 ve Var\u0131\u015f Yeri i\u00e7in, konumun adresini veya GPS koordinatlar\u0131n\u0131 girin (GPS koordinatlar\u0131 virg\u00fclle ayr\u0131lmal\u0131d\u0131r). Bu bilgiyi kendi durumunda sa\u011flayan bir varl\u0131k kimli\u011fi, enlem ve boylam niteliklerine sahip bir varl\u0131k kimli\u011fi veya b\u00f6lge dostu ad da girebilirsiniz." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/tr.json b/homeassistant/components/wemo/translations/tr.json index a87d832eece..79059b00202 100644 --- a/homeassistant/components/wemo/translations/tr.json +++ b/homeassistant/components/wemo/translations/tr.json @@ -9,5 +9,10 @@ "description": "Wemo'yu kurmak istiyor musunuz?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo d\u00fc\u011fmesine 2 saniye bas\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json index 4b1e2778af1..c276cbfc7eb 100644 --- a/homeassistant/components/wolflink/translations/sensor.tr.json +++ b/homeassistant/components/wolflink/translations/sensor.tr.json @@ -6,14 +6,24 @@ "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "smart_home": "Ak\u0131ll\u0131 Ev", + "softstart": "Yumu\u015fak ba\u015flang\u0131\u00e7", "solarbetrieb": "G\u00fcne\u015f modu", "sparbetrieb": "Ekonomi modu", + "sparen": "Ekonomi", + "stabilisierung": "Stabilizasyon", "standby": "Bekleme", "start": "Ba\u015flat", "storung": "Hata", + "taktsperre": "Anti-d\u00f6ng\u00fc", + "telefonfernschalter": "Telefon uzaktan anahtar\u0131", "test": "Test", + "tpw": "TPW", "urlaubsmodus": "Tatil modu", - "warmwasserbetrieb": "DHW modu" + "ventilprufung": "Valf testi", + "warmwasser": "DHW", + "warmwasserbetrieb": "DHW modu", + "zunden": "Ate\u015fleme" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.tr.json b/homeassistant/components/xiaomi_miio/translations/select.tr.json new file mode 100644 index 00000000000..7767a54fe2d --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Ayd\u0131nl\u0131k", + "dim": "Dim", + "off": "Kapal\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 3dbf08bd6f1..83857a6147a 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -2,13 +2,34 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin", + "cloud_login_error": "Xiaomi Miio Cloud'da oturum a\u00e7\u0131lamad\u0131, kimlik bilgilerini kontrol edin.", + "cloud_no_devices": "Bu Xiaomi Miio bulut hesab\u0131nda cihaz bulunamad\u0131.", "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in." }, "step": { + "cloud": { + "data": { + "cloud_country": "Bulut sunucusu \u00fclkesi", + "cloud_password": "Bulut \u015fifresi", + "cloud_username": "Bulut kullan\u0131c\u0131 ad\u0131", + "manual": "Manuel olarak yap\u0131land\u0131r\u0131n (\u00f6nerilmez)" + }, + "description": "Xiaomi Miio bulutunda oturum a\u00e7\u0131n, bulut sunucusunun kullanmas\u0131 i\u00e7in https://www.openhab.org/addons/bindings/miio/#country-servers adresine bak\u0131n.", + "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + }, + "connect": { + "data": { + "model": "Cihaz modeli" + }, + "description": "Desteklenen modellerden cihaz modelini manuel olarak se\u00e7in.", + "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + }, "device": { "data": { "name": "Cihaz\u0131n ad\u0131" @@ -22,6 +43,16 @@ }, "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" }, + "manual": { + "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + }, + "select": { + "data": { + "select_device": "Miio cihaz\u0131" + }, + "description": "Kurulumu i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in.", + "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + }, "user": { "data": { "gateway": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" @@ -30,5 +61,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Ba\u011fl\u0131 alt cihazlar almak i\u00e7in bulutu kullan\u0131n" + }, + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json new file mode 100644 index 00000000000..271a6590b48 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "yxc_control_url_missing": "Kontrol URL'si ssdp a\u00e7\u0131klamas\u0131nda verilmez." + }, + "error": { + "no_musiccast_device": "Bu cihaz, MusicCast Cihaz\u0131 de\u011fil gibi g\u00f6r\u00fcn\u00fcyor." + }, + "flow_title": "MusicCast: {name}", + "step": { + "user": { + "description": "Home Assistant ile entegre etmek i\u00e7in MusicCast'i kurun." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index 322f13f47b0..75c9cdf8f67 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -6,7 +6,11 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{model} {id} ({host})", "step": { + "discovery_confirm": { + "description": "{model} ( {host} ) kurulumu yapmak istiyor musunuz?" + }, "pick_device": { "data": { "device": "Cihaz" diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index a74f56a2f4e..bc0345f6301 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "usb_probe_failed": "USB ayg\u0131t\u0131 ara\u015ft\u0131r\u0131lamad\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" @@ -18,6 +19,17 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kurma eylemleri i\u00e7in gerekli kod", + "alarm_failed_tries": "Bir alarm\u0131 tetiklemek i\u00e7in ard\u0131\u015f\u0131k ba\u015far\u0131s\u0131z kod giri\u015flerinin say\u0131s\u0131", + "alarm_master_code": "Alarm kontrol panel(ler)i i\u00e7in ana kod", + "title": "Alarm Kontrol Paneli Se\u00e7enekleri" + }, + "zha_options": { + "title": "Genel Se\u00e7enekler" + } + }, "device_automation": { "trigger_type": { "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131" diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 5fe4f92b857..dd00286cb7c 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -22,6 +22,9 @@ "configure_addon": { "data": { "network_key": "A\u011f Anahtar\u0131", + "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", + "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", + "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar", "usb_path": "USB Ayg\u0131t Yolu" }, "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" @@ -43,12 +46,52 @@ }, "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "usb_confirm": { + "description": "{name} Z-Wave JS eklentisiyle kurmak istiyor musunuz?" } } }, "device_automation": { "action_type": { - "ping": "ping" + "clear_lock_usercode": "{entity_name} \u00fczerindeki kullan\u0131c\u0131 kodunu temizle", + "ping": "ping", + "refresh_value": "{entity_name} i\u00e7in de\u011ferleri yenileme", + "reset_meter": "{subtype} \u00fczerindeki saya\u00e7lar\u0131 s\u0131f\u0131rla", + "set_config_parameter": "{subtype} yap\u0131land\u0131rma parametresinin de\u011ferini ayarlama", + "set_lock_usercode": "{entity_name} \u00fczerinde kullan\u0131c\u0131 kodu ayarlama" + }, + "condition_type": { + "config_parameter": "Yap\u0131land\u0131rma parametresi {subtype} de\u011feri" + }, + "trigger_type": { + "event.notification.entry_control": "Giri\u015f Kontrol\u00fc bildirimi g\u00f6nderdi", + "event.notification.notification": "Bildirim g\u00f6nderdi", + "event.value_notification.central_scene": "{subtype} \u00fczerinde Merkezi Sahne eylemi", + "event.value_notification.scene_activation": "{subtype} \u00fczerinde Sahne Aktivasyonu" + } + }, + "options": { + "abort": { + "different_device": "Ba\u011fl\u0131 USB ayg\u0131t\u0131, bu yap\u0131land\u0131rma giri\u015fi i\u00e7in daha \u00f6nce yap\u0131land\u0131r\u0131lanla ayn\u0131 de\u011fil. L\u00fctfen bunun yerine yeni cihaz i\u00e7in yeni bir yap\u0131land\u0131rma giri\u015fi olu\u015fturun." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Donan\u0131m\u0131 Taklit Et", + "log_level": "G\u00fcnl\u00fck d\u00fczeyi", + "network_key": "A\u011f Anahtar\u0131", + "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", + "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", + "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar" + } + }, + "on_supervisor": { + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "title": "Z-Wave JS eklentisi ba\u015fl\u0131yor." + } } }, "title": "Z-Wave JS" From 8dd7e4a39b096cedc35d4c4c7df2e197ba9b9c0e Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Sun, 7 Nov 2021 04:35:50 -0500 Subject: [PATCH 0291/1452] Bump pyEmby to 1.8 (#59267) --- homeassistant/components/emby/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 7c1295b0e58..00c05702db7 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -2,7 +2,7 @@ "domain": "emby", "name": "Emby", "documentation": "https://www.home-assistant.io/integrations/emby", - "requirements": ["pyemby==1.7"], + "requirements": ["pyemby==1.8"], "codeowners": ["@mezz64"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 92f9826c0ac..a43db7135ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1459,7 +1459,7 @@ pyefergy==0.1.4 pyeight==0.1.9 # homeassistant.components.emby -pyemby==1.7 +pyemby==1.8 # homeassistant.components.envisalink pyenvisalink==4.0 From c6a1fe0a5d75df7b7034ec9fbf8eb9bf32d4e80f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 7 Nov 2021 15:17:50 +0100 Subject: [PATCH 0292/1452] Fix condition for fritz integration (#59281) --- homeassistant/components/fritz/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 78b3f2073a7..d6df32fa931 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -370,7 +370,7 @@ class FritzBoxTools: device_reg = async_get(self.hass) device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id) for device_entry in device_list: - if async_entries_for_device( + if not async_entries_for_device( entity_reg, device_entry.id, include_disabled_entities=True, From ae1592b27a9239f90365da4d5b8afd8cfd10569a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 7 Nov 2021 15:57:34 +0100 Subject: [PATCH 0293/1452] Fix typing issues - wallbox.number (#59286) --- homeassistant/components/wallbox/number.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 837e880afcb..1bc561cb7c5 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -23,7 +23,7 @@ class WallboxNumberEntityDescription(NumberEntityDescription): min_value: float = 0 -NUMBER_TYPES: dict[str, NumberEntityDescription] = { +NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CONF_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CONF_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", @@ -57,6 +57,8 @@ async def async_setup_entry(hass, config, async_add_entities): class WallboxNumber(CoordinatorEntity, NumberEntity): """Representation of the Wallbox portal.""" + entity_description: WallboxNumberEntityDescription + def __init__( self, coordinator, config, description: WallboxNumberEntityDescription ): From 37259a1448067a89c5598909e845941825521208 Mon Sep 17 00:00:00 2001 From: micha91 Date: Sun, 7 Nov 2021 16:38:12 +0100 Subject: [PATCH 0294/1452] Fix udp ports (#59293) --- homeassistant/components/yamaha_musiccast/__init__.py | 3 +++ homeassistant/components/yamaha_musiccast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 9f691773e11..a8ef373e3dc 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -70,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.musiccast.device.enable_polling() + hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -80,6 +82,7 @@ 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][entry.entry_id].musiccast.device.disable_polling() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 0ace71dc7dd..8a7e9cf2c79 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.11.0" + "aiomusiccast==0.13.1" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a43db7135ea..6473c6b9a86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -216,7 +216,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.11.0 +aiomusiccast==0.13.1 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 537f8c31be4..4ec11f0a6a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.11.0 +aiomusiccast==0.13.1 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 From ddf0941275ce13997d8f5db4e5339d39c1ff4538 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 7 Nov 2021 16:41:26 +0100 Subject: [PATCH 0295/1452] Upgrade async-upnp-client to 0.22.12 (#59284) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 0f2135094c9..9bc020f3693 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.11"], + "requirements": ["async-upnp-client==0.22.12"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 21f1af73823..ea8b8ff73a4 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.11"], + "requirements": ["async-upnp-client==0.22.12"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 6d329788477..da12c25c7d1 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.11"], + "requirements": ["async-upnp-client==0.22.12"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 117ec5ea620..13eb73cdd0c 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.11"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.12"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b8cb9d075a5..d3e9475e442 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.11 +async-upnp-client==0.22.12 async_timeout==4.0.0 attrs==21.2.0 awesomeversion==21.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6473c6b9a86..0a650219541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.11 +async-upnp-client==0.22.12 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ec11f0a6a1..1ddee47b044 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.11 +async-upnp-client==0.22.12 # homeassistant.components.aurora auroranoaa==0.0.2 From b75476e84466f184e584d21d941fb1dbd7efe2ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Nov 2021 07:44:15 -0800 Subject: [PATCH 0296/1452] Add support for matching the zeroconf model property (#58922) --- homeassistant/components/zeroconf/__init__.py | 10 ++++ script/hassfest/manifest.py | 1 + tests/components/zeroconf/test_init.py | 51 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f49fc708a1f..44efc77d222 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -397,6 +397,11 @@ class ZeroconfDiscovery: else: lowercase_manufacturer = None + if "model" in info[ATTR_PROPERTIES]: + lowercase_model: str | None = info[ATTR_PROPERTIES]["model"].lower() + else: + lowercase_model = None + # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for matcher in self.zeroconf_types.get(service_type, []): @@ -418,6 +423,11 @@ class ZeroconfDiscovery: ) ): continue + if "model" in matcher and ( + lowercase_model is None + or not fnmatch.fnmatch(lowercase_model, matcher["model"]) + ): + continue discovery_flow.async_create_flow( self.hass, diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0bec9e51436..ecc00142e30 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -187,6 +187,7 @@ MANIFEST_SCHEMA = vol.Schema( str, verify_uppercase, verify_wildcard ), vol.Optional("manufacturer"): vol.All(str, verify_lowercase), + vol.Optional("model"): vol.All(str, verify_lowercase), vol.Optional("name"): vol.All(str, verify_lowercase), } ), diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index edf29a32f69..d04ddd3dd4b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -126,6 +126,24 @@ def get_zeroconf_info_mock_manufacturer(manufacturer): return mock_zc_info +def get_zeroconf_info_mock_model(model): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return AsyncServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"model": model.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -330,6 +348,39 @@ async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" +async def test_zeroconf_match_model(hass, mock_async_zeroconf): + """Test matching a specific model in zeroconf.""" + + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "appletv", "model": "appletv*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock_model("appletv"), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "appletv" + + async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf): """Test matchers reject when a property is missing.""" From cc82976d1572aba21f92549b815313dabbf262b1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 7 Nov 2021 12:53:28 -0500 Subject: [PATCH 0297/1452] Add Battery sensor regardless if the battery_percent_remaining attribute is supported or not (#59264) --- homeassistant/components/zha/sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d7c3e0797b8..2281d5295bc 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -226,6 +226,22 @@ class Battery(Sensor): _unit = PERCENTAGE _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Unlike any other entity, PowerConfiguration cluster may not support + battery_percent_remaining attribute, but zha-device-handlers takes care of it + so create the entity regardless + """ + return cls(unique_id, zha_device, channels, **kwargs) + @staticmethod def formatter(value: int) -> int: """Return the state of the entity.""" From c85db9a39d8d33faf75c5cc63c929074ba892af6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Nov 2021 23:17:15 +0100 Subject: [PATCH 0298/1452] Remove illuminance sensor (#59305) --- homeassistant/components/xiaomi_miio/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index aefeea7c20a..ac26bc97fce 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -374,7 +374,6 @@ AIRFRESH_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, ATTR_HUMIDITY, - ATTR_ILLUMINANCE_LUX, ATTR_PM25, ATTR_TEMPERATURE, ATTR_USE_TIME, From 55cd1ffb7c780c032153ca180c27eb5cf7f1c973 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Nov 2021 01:29:29 +0200 Subject: [PATCH 0299/1452] Revert "Use DeviceInfo in shelly (#58520)" (#59315) This reverts commit df6351f86b50451c7ecd8e70cfb5bbc97829cc73. --- homeassistant/components/shelly/entity.py | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index f8383ccd297..5df87b0531c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -282,9 +282,6 @@ class ShellyBlockEntity(entity.Entity): self.wrapper = wrapper self.block = block self._name = get_block_entity_name(wrapper.device, block) - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) @property def name(self) -> str: @@ -296,6 +293,13 @@ class ShellyBlockEntity(entity.Entity): """If device should be polled.""" return False + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + @property def available(self) -> bool: """Available.""" @@ -344,9 +348,9 @@ class ShellyRpcEntity(entity.Entity): self.wrapper = wrapper self.key = key self._attr_should_poll = False - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) + self._attr_device_info = { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + } self._attr_unique_id = f"{wrapper.mac}-{key}" self._attr_name = get_rpc_entity_name(wrapper.device, key) @@ -488,15 +492,19 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): self.description = description self._name = get_block_entity_name(wrapper.device, None, self.description.name) self._last_value = None - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) @property def name(self) -> str: """Name of sensor.""" return self._name + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + @property def entity_registry_enabled_default(self) -> bool: """Return if it should be enabled by default.""" From 72aaeda8a037ba0429fd4579e2df37954facdf28 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 7 Nov 2021 15:30:34 -0800 Subject: [PATCH 0300/1452] Bump total_connect_client to 2021.11.2 (#58818) * update total_connect_client to 2021.10 * update for total_connect_client changes * remove unused return value * bump total_connect_client to 2021.11.1 * bump total_connect_client to 2021.11.2 * Move to public ResultCode * load locations to prevent 'unknown error occurred' * add test for zero locations * Revert "load locations to prevent 'unknown error occurred'" This reverts commit 28b8984be5b1c8839fc8077d8d59bdba97eacc38. * Revert "add test for zero locations" This reverts commit 77bf7908d508d539d6165fc986930b041b13ca97. --- .../components/totalconnect/__init__.py | 4 +--- .../components/totalconnect/config_flow.py | 4 ++-- .../components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/totalconnect/common.py | 22 +++++++++---------- .../totalconnect/test_config_flow.py | 8 +++---- tests/components/totalconnect/test_init.py | 2 +- 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index bd1f693fd07..8acc7801de8 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: TotalConnectClient, username, password, usercodes ) - if not client.is_valid_credentials(): + if not client.is_logged_in(): raise ConfigEntryAuthFailed("TotalConnect authentication failed") coordinator = TotalConnectDataUpdateCoordinator(hass, client) @@ -88,5 +88,3 @@ class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(exception) from exception except ValueError as exception: raise UpdateFailed("Unknown state from TotalConnect") from exception - - return True diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 7ba96f11a1e..f3550722de5 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -40,7 +40,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): TotalConnectClient, username, password, None ) - if client.is_valid_credentials(): + if client.is_logged_in(): # username/password valid so show user locations self.username = username self.password = password @@ -136,7 +136,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.usercodes, ) - if not client.is_valid_credentials(): + if not client.is_logged_in(): errors["base"] = "invalid_auth" return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 25fb2fd2c75..c70eafd9c31 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2021.8.3"], + "requirements": ["total_connect_client==2021.11.2"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 0a650219541..78daaf173a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2320,7 +2320,7 @@ todoist-python==8.0.0 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.8.3 +total_connect_client==2021.11.2 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ddee47b044..272c18b9f4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1339,7 +1339,7 @@ tesla-powerwall==0.3.12 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.8.3 +total_connect_client==2021.11.2 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 29278d33273..ec0c182895f 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -1,9 +1,7 @@ """Common methods used across tests for TotalConnect.""" from unittest.mock import patch -from total_connect_client.client import TotalConnectClient -from total_connect_client.const import ArmingState -from total_connect_client.zone import ZoneStatus, ZoneType +from total_connect_client import ArmingState, ResultCode, ZoneStatus, ZoneType from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -44,7 +42,7 @@ USER = { } RESPONSE_AUTHENTICATE = { - "ResultCode": TotalConnectClient.SUCCESS, + "ResultCode": ResultCode.SUCCESS.value, "SessionID": 1, "Locations": LOCATIONS, "ModuleFlags": MODULE_FLAGS, @@ -52,7 +50,7 @@ RESPONSE_AUTHENTICATE = { } RESPONSE_AUTHENTICATE_FAILED = { - "ResultCode": TotalConnectClient.BAD_USER_OR_PASSWORD, + "ResultCode": ResultCode.BAD_USER_OR_PASSWORD.value, "ResultData": "test bad authentication", } @@ -255,18 +253,18 @@ RESPONSE_UNKNOWN = { "ArmingState": ArmingState.DISARMED, } -RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.ARM_SUCCESS} -RESPONSE_ARM_FAILURE = {"ResultCode": TotalConnectClient.COMMAND_FAILED} -RESPONSE_DISARM_SUCCESS = {"ResultCode": TotalConnectClient.DISARM_SUCCESS} +RESPONSE_ARM_SUCCESS = {"ResultCode": ResultCode.ARM_SUCCESS.value} +RESPONSE_ARM_FAILURE = {"ResultCode": ResultCode.COMMAND_FAILED.value} +RESPONSE_DISARM_SUCCESS = {"ResultCode": ResultCode.DISARM_SUCCESS.value} RESPONSE_DISARM_FAILURE = { - "ResultCode": TotalConnectClient.COMMAND_FAILED, + "ResultCode": ResultCode.COMMAND_FAILED.value, "ResultData": "Command Failed", } RESPONSE_USER_CODE_INVALID = { - "ResultCode": TotalConnectClient.USER_CODE_INVALID, + "ResultCode": ResultCode.USER_CODE_INVALID.value, "ResultData": "testing user code invalid", } -RESPONSE_SUCCESS = {"ResultCode": TotalConnectClient.SUCCESS} +RESPONSE_SUCCESS = {"ResultCode": ResultCode.SUCCESS.value} USERNAME = "username@me.com" PASSWORD = "password" @@ -292,7 +290,7 @@ PARTITION_DETAILS_2 = { PARTITION_DETAILS = {"PartitionDetails": [PARTITION_DETAILS_1, PARTITION_DETAILS_2]} RESPONSE_PARTITION_DETAILS = { - "ResultCode": TotalConnectClient.SUCCESS, + "ResultCode": ResultCode.SUCCESS.value, "ResultData": "testing partition details", "PartitionsInfoList": PARTITION_DETAILS, } diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 20497102c6d..a9debb26dd4 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -95,7 +95,7 @@ async def test_abort_if_already_setup(hass): with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient" ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = True + client_mock.return_value.is_logged_in.return_value = True result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -111,7 +111,7 @@ async def test_login_failed(hass): with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient" ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = False + client_mock.return_value.is_logged_in.return_value = False result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -143,7 +143,7 @@ async def test_reauth(hass): "homeassistant.components.totalconnect.async_setup_entry", return_value=True ): # first test with an invalid password - client_mock.return_value.is_valid_credentials.return_value = False + client_mock.return_value.is_logged_in.return_value = False result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -153,7 +153,7 @@ async def test_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} # now test with the password valid - client_mock.return_value.is_valid_credentials.return_value = True + client_mock.return_value.is_logged_in.return_value = True result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index 41cd8bbae90..f1797f840ab 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -22,7 +22,7 @@ async def test_reauth_started(hass): "homeassistant.components.totalconnect.TotalConnectClient", autospec=True, ) as mock_client: - mock_client.return_value.is_valid_credentials.return_value = False + mock_client.return_value.is_logged_in.return_value = False assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 8ebd47b430f9913b79514d96c8f486ee4c8e2e66 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Nov 2021 16:34:25 -0700 Subject: [PATCH 0301/1452] Guard against flaky SimpliSafe API calls (#59175) --- .../components/simplisafe/__init__.py | 19 ++++++++++++++++++- .../simplisafe/alarm_control_panel.py | 17 ----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 3647c1bda57..a25f76bcbc8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -102,9 +102,11 @@ ATTR_TIMESTAMP = "timestamp" DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel" +DEFAULT_REST_API_ERROR_COUNT = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 + DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" @@ -553,6 +555,8 @@ class SimpliSafeEntity(CoordinatorEntity): assert simplisafe.coordinator super().__init__(simplisafe.coordinator) + self._rest_api_errors = 0 + if device: model = device.type.name device_name = device.name @@ -615,11 +619,24 @@ class SimpliSafeEntity(CoordinatorEntity): else: system_offline = False - return super().available and self._online and not system_offline + return ( + self._rest_api_errors < DEFAULT_REST_API_ERROR_COUNT + and self._online + and not system_offline + ) @callback def _handle_coordinator_update(self) -> None: """Update the entity with new REST API data.""" + # SimpliSafe can incorrectly return an error state when there isn't any + # error. This can lead to the system having an unknown state frequently. + # To protect against that, we measure how many "error states" we receive + # and only alter the state if we detect a few in a row: + if self.coordinator.last_update_success: + self._rest_api_errors = 0 + else: + self._rest_api_errors += 1 + self.async_update_from_rest_api() self.async_write_ha_state() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 15887b91532..de571f1291d 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -71,8 +71,6 @@ ATTR_RF_JAMMING = "rf_jamming" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -DEFAULT_ERRORS_TO_ACCOMMODATE = 2 - VOLUME_STRING_MAP = { VOLUME_HIGH: "high", VOLUME_LOW: "low", @@ -140,8 +138,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - self._errors = 0 - if code := self._simplisafe.entry.options.get(CONF_CODE): if code.isdigit(): self._attr_code_format = FORMAT_NUMBER @@ -248,19 +244,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): } ) - # SimpliSafe can incorrectly return an error state when there isn't any - # error. This can lead to the system having an unknown state frequently. - # To protect against that, we measure how many "error states" we receive - # and only alter the state if we detect a few in a row: - if self._system.state == SystemStates.error: - if self._errors > DEFAULT_ERRORS_TO_ACCOMMODATE: - self._attr_state = None - else: - self._errors += 1 - return - - self._errors = 0 - self._set_state_from_system_data() @callback From 563eba768487c73464355736a6a309c31f92a109 Mon Sep 17 00:00:00 2001 From: Jim Shank Date: Sun, 7 Nov 2021 16:06:56 -0800 Subject: [PATCH 0302/1452] Clean up transmission empty containers evaluation (#59304) --- homeassistant/components/transmission/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 40edc8aeab9..f40f0bd7d82 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -441,13 +441,13 @@ class TransmissionData: def start_torrents(self): """Start all torrents.""" - if len(self._torrents) <= 0: + if not self._torrents: return self._api.start_all() def stop_torrents(self): """Stop all active torrents.""" - if len(self._torrents) == 0: + if not self._torrents: return torrent_ids = [torrent.id for torrent in self._torrents] self._api.stop_torrent(torrent_ids) From e35b83081e0dd14734a63fa82e967e43c70ce982 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 8 Nov 2021 00:12:31 +0000 Subject: [PATCH 0303/1452] [ci skip] Translation update --- .../components/abode/translations/tr.json | 3 +- .../accuweather/translations/sensor.tr.json | 9 ++ .../components/acmeda/translations/tr.json | 6 +- .../components/adax/translations/tr.json | 11 ++- .../components/adguard/translations/tr.json | 15 ++- .../advantage_air/translations/tr.json | 1 + .../components/aemet/translations/tr.json | 20 ++++ .../components/airly/translations/tr.json | 14 ++- .../components/airnow/translations/tr.json | 1 + .../components/airthings/translations/tr.json | 8 ++ .../components/airtouch4/translations/tr.json | 7 ++ .../airvisual/translations/sensor.tr.json | 1 + .../components/airvisual/translations/tr.json | 15 ++- .../alarm_control_panel/translations/tr.json | 4 + .../alarmdecoder/translations/tr.json | 30 ++++-- .../components/almond/translations/tr.json | 11 +++ .../ambee/translations/sensor.tr.json | 1 + .../components/ambee/translations/tr.json | 14 +++ .../amberelectric/translations/tr.json | 1 + .../ambiclimate/translations/tr.json | 17 +++- .../ambient_station/translations/tr.json | 9 +- .../components/arcam_fmj/translations/tr.json | 16 +++- .../components/asuswrt/translations/tr.json | 45 +++++++++ .../components/august/translations/tr.json | 16 ++++ .../components/aurora/translations/tr.json | 12 ++- .../aurora_abb_powerone/translations/pl.json | 7 ++ .../aurora_abb_powerone/translations/tr.json | 4 +- .../components/auth/translations/tr.json | 19 +++- .../components/awair/translations/tr.json | 4 +- .../components/axis/translations/tr.json | 18 +++- .../azure_devops/translations/tr.json | 4 + .../binary_sensor/translations/pl.json | 11 +++ .../binary_sensor/translations/tr.json | 87 +++++++++++++++++- .../components/blebox/translations/tr.json | 5 +- .../components/blink/translations/tr.json | 17 +++- .../bmw_connected_drive/translations/tr.json | 11 +++ .../components/bond/translations/tr.json | 5 +- .../components/bosch_shc/translations/tr.json | 19 ++++ .../components/braviatv/translations/tr.json | 10 +- .../components/broadlink/translations/bg.json | 27 +++++- .../components/broadlink/translations/tr.json | 5 + .../components/brother/translations/tr.json | 11 ++- .../components/bsblan/translations/tr.json | 6 +- .../buienradar/translations/tr.json | 19 +++- .../components/button/translations/bg.json | 11 +++ .../components/canary/translations/tr.json | 12 +++ .../components/cast/translations/tr.json | 12 +++ .../cert_expiry/translations/tr.json | 15 ++- .../components/climacell/translations/tr.json | 34 +++++++ .../components/climate/translations/tr.json | 9 ++ .../components/cloud/translations/tr.json | 2 + .../cloudflare/translations/tr.json | 8 ++ .../components/co2signal/translations/tr.json | 20 +++- .../components/coinbase/translations/tr.json | 12 ++- .../components/control4/translations/tr.json | 10 ++ .../coolmaster/translations/tr.json | 11 ++- .../coronavirus/translations/tr.json | 3 +- .../components/cover/translations/tr.json | 23 ++++- .../crownstone/translations/tr.json | 91 ++++++++++++++++++- .../components/daikin/translations/tr.json | 5 +- .../components/deconz/translations/tr.json | 61 ++++++++++++- .../demo/translations/select.tr.json | 4 +- .../components/demo/translations/tr.json | 17 +++- .../components/denonavr/translations/tr.json | 32 ++++++- .../device_tracker/translations/tr.json | 4 + .../devolo_home_control/translations/tr.json | 11 ++- .../devolo_home_network/translations/tr.json | 8 +- .../components/dexcom/translations/tr.json | 5 +- .../dialogflow/translations/tr.json | 9 ++ .../components/directv/translations/tr.json | 1 + .../components/dlna_dmr/translations/tr.json | 11 +++ .../components/doorbird/translations/tr.json | 11 ++- .../components/dsmr/translations/tr.json | 20 +++- .../components/dunehd/translations/tr.json | 4 +- .../components/eafm/translations/bg.json | 7 ++ .../components/eafm/translations/tr.json | 1 + .../components/ebusd/translations/tr.json | 6 ++ .../components/ecobee/translations/tr.json | 12 ++- .../components/efergy/translations/tr.json | 6 +- .../components/elgato/translations/tr.json | 5 +- .../components/elkm1/translations/tr.json | 8 +- .../components/emonitor/translations/tr.json | 23 +++++ .../emulated_roku/translations/tr.json | 16 +++- .../components/enocean/translations/tr.json | 6 +- .../enphase_envoy/translations/tr.json | 23 +++++ .../environment_canada/translations/tr.json | 5 +- .../components/esphome/translations/tr.json | 25 ++++- .../components/ezviz/translations/tr.json | 52 +++++++++++ .../faa_delays/translations/tr.json | 21 +++++ .../components/fan/translations/tr.json | 4 + .../fjaraskupan/translations/tr.json | 13 +++ .../flick_electric/translations/tr.json | 5 +- .../components/flipr/translations/tr.json | 25 ++++- .../components/flo/translations/bg.json | 16 +++- .../components/flume/translations/tr.json | 12 ++- .../flunearyou/translations/tr.json | 4 +- .../components/flux_led/translations/tr.json | 3 + .../forecast_solar/translations/tr.json | 18 +++- .../forked_daapd/translations/tr.json | 27 +++++- .../components/freebox/translations/tr.json | 5 + .../freedompro/translations/tr.json | 20 ++++ .../components/fritz/translations/tr.json | 50 ++++++++++ .../components/fritzbox/translations/tr.json | 6 +- .../garages_amsterdam/translations/tr.json | 5 + .../components/gdacs/translations/tr.json | 3 +- .../components/geofency/translations/tr.json | 9 ++ .../geonetnz_quakes/translations/tr.json | 9 ++ .../geonetnz_volcano/translations/tr.json | 3 +- .../components/gios/translations/tr.json | 19 +++- .../components/glances/translations/tr.json | 15 ++- .../components/goalzero/translations/tr.json | 12 ++- .../components/gogogate2/translations/tr.json | 4 +- .../google_travel_time/translations/tr.json | 10 ++ .../components/gpslogger/translations/tr.json | 9 ++ .../components/gree/translations/tr.json | 1 + .../growatt_server/translations/tr.json | 12 ++- .../components/guardian/translations/tr.json | 6 +- .../components/habitica/translations/tr.json | 17 ++++ .../components/hangouts/translations/tr.json | 17 +++- .../components/harmony/translations/tr.json | 22 ++++- .../components/heos/translations/tr.json | 4 +- .../hisense_aehw4a1/translations/tr.json | 1 + .../components/hive/translations/tr.json | 44 ++++++++- .../home_connect/translations/tr.json | 16 ++++ .../home_plus_control/translations/tr.json | 18 ++++ .../components/homekit/translations/tr.json | 9 +- .../homekit_controller/translations/tr.json | 13 ++- .../homematicip_cloud/translations/tr.json | 20 ++++ .../components/honeywell/translations/tr.json | 10 +- .../huawei_lte/translations/bg.json | 1 + .../huawei_lte/translations/tr.json | 7 +- .../components/hue/translations/tr.json | 13 ++- .../huisbaasje/translations/tr.json | 1 + .../humidifier/translations/tr.json | 2 + .../translations/tr.json | 8 +- .../hvv_departures/translations/tr.json | 31 ++++++- .../components/hyperion/translations/tr.json | 4 +- .../components/ialarm/translations/tr.json | 7 ++ .../components/iaqualink/translations/tr.json | 3 +- .../components/icloud/translations/tr.json | 3 +- .../components/ifttt/translations/tr.json | 9 ++ .../components/insteon/translations/bg.json | 8 +- .../components/insteon/translations/tr.json | 26 +++++- .../components/iotawatt/translations/tr.json | 14 +++ .../components/ipma/translations/tr.json | 10 +- .../components/ipp/translations/tr.json | 13 ++- .../components/iqvia/translations/tr.json | 12 +++ .../islamic_prayer_times/translations/tr.json | 9 ++ .../components/isy994/translations/tr.json | 20 +++- .../components/izone/translations/tr.json | 1 + .../keenetic_ndms2/translations/tr.json | 38 ++++++++ .../components/kmtronic/translations/tr.json | 30 ++++++ .../components/kodi/translations/bg.json | 12 ++- .../components/kodi/translations/tr.json | 20 +++- .../components/konnected/translations/tr.json | 62 +++++++++++-- .../kostal_plenticore/translations/tr.json | 21 +++++ .../components/kraken/translations/tr.json | 18 ++++ .../components/life360/translations/tr.json | 7 +- .../components/lifx/translations/tr.json | 1 + .../components/light/translations/tr.json | 18 ++++ .../components/litejet/translations/tr.json | 20 ++++ .../litterrobot/translations/tr.json | 20 ++++ .../components/locative/translations/tr.json | 9 ++ .../components/lock/translations/tr.json | 9 ++ .../logi_circle/translations/tr.json | 17 +++- .../components/lookin/translations/tr.json | 21 +++++ .../components/luftdaten/translations/tr.json | 12 ++- .../components/lyric/translations/tr.json | 6 +- .../components/mailgun/translations/tr.json | 9 ++ .../components/mazda/translations/tr.json | 26 ++++++ .../media_player/translations/tr.json | 5 +- .../components/melcloud/translations/tr.json | 4 +- .../components/met/translations/tr.json | 11 ++- .../met_eireann/translations/tr.json | 19 ++++ .../meteo_france/translations/tr.json | 8 ++ .../meteoclimatic/translations/tr.json | 20 ++++ .../components/metoffice/translations/tr.json | 3 +- .../components/mikrotik/translations/tr.json | 19 +++- .../mobile_app/translations/tr.json | 10 ++ .../modem_callerid/translations/tr.json | 10 ++ .../modern_forms/translations/tr.json | 17 ++++ .../components/monoprice/translations/tr.json | 15 +++ .../moon/translations/sensor.tr.json | 14 +++ .../motion_blinds/translations/tr.json | 6 +- .../components/motioneye/translations/tr.json | 28 +++++- .../components/mqtt/translations/tr.json | 38 +++++++- .../components/mullvad/translations/tr.json | 16 ++++ .../components/mutesync/translations/tr.json | 16 ++++ .../components/myq/translations/tr.json | 6 +- .../components/mysensors/translations/tr.json | 56 +++++++++++- .../components/nam/translations/tr.json | 24 +++++ .../components/nanoleaf/translations/tr.json | 17 ++++ .../components/neato/translations/tr.json | 12 ++- .../components/nest/translations/pl.json | 3 + .../components/nest/translations/tr.json | 30 ++++++ .../components/netatmo/translations/tr.json | 21 ++++- .../components/netgear/translations/tr.json | 13 +++ .../components/nexia/translations/tr.json | 1 + .../nfandroidtv/translations/tr.json | 11 +++ .../nightscout/translations/bg.json | 3 +- .../nightscout/translations/tr.json | 4 +- .../nmap_tracker/translations/tr.json | 18 +++- .../components/notion/translations/tr.json | 12 ++- .../components/nuheat/translations/tr.json | 3 +- .../components/nuki/translations/tr.json | 10 ++ .../components/nut/translations/tr.json | 19 +++- .../components/nws/translations/tr.json | 6 +- .../components/nzbget/translations/tr.json | 10 +- .../components/octoprint/translations/tr.json | 10 +- .../components/omnilogic/translations/tr.json | 3 +- .../ondilo_ico/translations/tr.json | 7 ++ .../components/onewire/translations/tr.json | 6 +- .../components/onvif/translations/tr.json | 16 +++- .../opengarage/translations/tr.json | 7 +- .../opentherm_gw/translations/tr.json | 20 +++- .../components/openuv/translations/tr.json | 8 +- .../ovo_energy/translations/tr.json | 4 +- .../components/owntracks/translations/tr.json | 9 ++ .../components/ozw/translations/tr.json | 12 ++- .../p1_monitor/translations/tr.json | 8 ++ .../panasonic_viera/translations/tr.json | 14 ++- .../philips_js/translations/tr.json | 35 ++++++- .../components/pi_hole/translations/tr.json | 4 +- .../components/picnic/translations/tr.json | 12 ++- .../components/plaato/translations/tr.json | 13 ++- .../components/plex/translations/tr.json | 39 +++++++- .../components/plugwise/translations/tr.json | 12 ++- .../components/point/translations/tr.json | 20 ++++ .../components/poolsense/translations/tr.json | 4 +- .../components/powerwall/translations/tr.json | 14 ++- .../components/profiler/translations/tr.json | 5 + .../components/prosegur/translations/tr.json | 20 +++- .../components/ps4/translations/tr.json | 22 ++++- .../pvpc_hourly_pricing/translations/tr.json | 6 +- .../components/rachio/translations/tr.json | 11 +++ .../rainforest_eagle/translations/tr.json | 9 ++ .../rainmachine/translations/tr.json | 4 +- .../components/rdw/translations/he.json | 7 ++ .../components/rdw/translations/hu.json | 15 +++ .../components/rdw/translations/pl.json | 7 ++ .../components/rdw/translations/ru.json | 15 +++ .../components/rdw/translations/tr.json | 1 + .../components/rdw/translations/zh-Hant.json | 15 +++ .../recollect_waste/translations/tr.json | 14 +++ .../components/renault/translations/tr.json | 14 ++- .../components/rfxtrx/translations/tr.json | 33 ++++++- .../components/ridwell/translations/pl.json | 5 + .../components/ridwell/translations/tr.json | 11 ++- .../components/ring/translations/tr.json | 9 +- .../components/risco/translations/tr.json | 17 +++- .../translations/tr.json | 21 +++++ .../components/roku/translations/tr.json | 16 +++- .../components/roomba/translations/tr.json | 5 +- .../components/roon/translations/bg.json | 18 ++++ .../components/roon/translations/tr.json | 3 +- .../components/rpi_power/translations/tr.json | 1 + .../components/samsungtv/translations/da.json | 2 +- .../samsungtv/translations/es-419.json | 2 +- .../components/samsungtv/translations/fr.json | 2 +- .../components/samsungtv/translations/ko.json | 2 +- .../components/samsungtv/translations/lb.json | 2 +- .../components/samsungtv/translations/sl.json | 2 +- .../components/samsungtv/translations/sv.json | 2 +- .../components/samsungtv/translations/tr.json | 9 +- .../components/samsungtv/translations/uk.json | 2 +- .../screenlogic/translations/tr.json | 39 ++++++++ .../season/translations/sensor.tr.json | 6 ++ .../components/select/translations/tr.json | 14 +++ .../components/sense/translations/tr.json | 3 +- .../components/sensor/translations/tr.json | 14 +++ .../components/sentry/translations/bg.json | 14 +++ .../components/sentry/translations/tr.json | 13 ++- .../components/shelly/translations/tr.json | 15 ++- .../components/sia/translations/tr.json | 24 ++++- .../simplisafe/translations/tr.json | 15 ++- .../components/sma/translations/tr.json | 15 ++- .../components/smappee/translations/bg.json | 6 ++ .../components/smappee/translations/tr.json | 15 ++- .../smart_meter_texas/translations/bg.json | 15 ++- .../components/smarthab/translations/tr.json | 2 + .../smartthings/translations/tr.json | 22 ++++- .../components/smarttub/translations/tr.json | 15 ++- .../components/smhi/translations/tr.json | 3 +- .../components/sms/translations/tr.json | 3 + .../components/solaredge/translations/tr.json | 7 +- .../components/solarlog/translations/tr.json | 6 +- .../components/soma/translations/tr.json | 14 ++- .../components/somfy/translations/tr.json | 11 +++ .../components/sonarr/translations/tr.json | 20 +++- .../components/songpal/translations/tr.json | 7 +- .../components/sonos/translations/tr.json | 1 + .../components/spider/translations/tr.json | 3 +- .../components/spotify/translations/tr.json | 8 +- .../squeezebox/translations/tr.json | 8 +- .../components/starline/translations/tr.json | 10 +- .../stookalert/translations/tr.json | 7 ++ .../components/subaru/translations/tr.json | 31 ++++++- .../surepetcare/translations/tr.json | 20 ++++ .../components/switch/translations/tr.json | 15 +++ .../components/switchbot/translations/pl.json | 6 +- .../components/switchbot/translations/tr.json | 22 ++++- .../switcher_kis/translations/tr.json | 13 +++ .../components/syncthing/translations/tr.json | 11 ++- .../components/syncthru/translations/tr.json | 7 ++ .../synology_dsm/translations/tr.json | 36 +++++++- .../system_bridge/translations/tr.json | 32 +++++++ .../components/tado/translations/tr.json | 8 +- .../components/tag/translations/tr.json | 3 + .../components/tasmota/translations/tr.json | 6 ++ .../tellduslive/translations/tr.json | 9 +- .../components/tibber/translations/tr.json | 7 +- .../components/tile/translations/tr.json | 3 +- .../components/toon/translations/tr.json | 3 + .../totalconnect/translations/tr.json | 21 ++++- .../components/tplink/translations/tr.json | 5 + .../components/traccar/translations/tr.json | 4 + .../components/tractive/translations/tr.json | 21 +++++ .../components/tradfri/translations/tr.json | 12 ++- .../transmission/translations/tr.json | 19 +++- .../tuya/translations/select.pl.json | 25 +++++ .../tuya/translations/sensor.pl.json | 15 +++ .../components/tuya/translations/tr.json | 6 ++ .../twentemilieu/translations/tr.json | 14 ++- .../components/twilio/translations/tr.json | 9 ++ .../components/unifi/translations/tr.json | 47 +++++++++- .../components/upb/translations/tr.json | 8 +- .../components/upcloud/translations/tr.json | 9 ++ .../components/upnp/translations/tr.json | 22 ++++- .../uptimerobot/translations/tr.json | 19 +++- .../components/vacuum/translations/tr.json | 14 +++ .../components/velbus/translations/tr.json | 9 ++ .../components/venstar/translations/pl.json | 10 ++ .../components/venstar/translations/tr.json | 8 ++ .../components/vera/translations/tr.json | 16 ++++ .../components/verisure/translations/tr.json | 19 +++- .../components/vilfo/translations/tr.json | 4 +- .../components/vizio/translations/tr.json | 42 ++++++++- .../vlc_telnet/translations/tr.json | 10 +- .../components/volumio/translations/tr.json | 4 + .../components/wallbox/translations/tr.json | 22 +++++ .../water_heater/translations/tr.json | 1 + .../components/watttime/translations/tr.json | 14 ++- .../waze_travel_time/translations/tr.json | 32 ++++++- .../components/whirlpool/translations/tr.json | 17 ++++ .../components/wiffi/translations/tr.json | 3 +- .../components/wilight/translations/tr.json | 11 ++- .../components/withings/translations/tr.json | 17 +++- .../components/wled/translations/tr.json | 12 ++- .../wolflink/translations/sensor.tr.json | 58 ++++++++++++ .../components/wolflink/translations/tr.json | 9 +- .../components/xbox/translations/tr.json | 10 ++ .../xiaomi_aqara/translations/tr.json | 9 +- .../xiaomi_miio/translations/tr.json | 28 +++++- .../yale_smart_alarm/translations/tr.json | 6 ++ .../yamaha_musiccast/translations/tr.json | 7 ++ .../components/yeelight/translations/tr.json | 10 +- .../components/youless/translations/tr.json | 15 +++ .../components/zha/translations/tr.json | 76 +++++++++++++++- .../components/zone/translations/tr.json | 15 ++- .../zoneminder/translations/tr.json | 16 +++- .../components/zwave/translations/tr.json | 9 +- .../components/zwave_js/translations/tr.json | 59 ++++++++++-- 362 files changed, 4587 insertions(+), 354 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.tr.json create mode 100644 homeassistant/components/asuswrt/translations/tr.json create mode 100644 homeassistant/components/button/translations/bg.json create mode 100644 homeassistant/components/climacell/translations/tr.json create mode 100644 homeassistant/components/eafm/translations/bg.json create mode 100644 homeassistant/components/ebusd/translations/tr.json create mode 100644 homeassistant/components/emonitor/translations/tr.json create mode 100644 homeassistant/components/enphase_envoy/translations/tr.json create mode 100644 homeassistant/components/ezviz/translations/tr.json create mode 100644 homeassistant/components/faa_delays/translations/tr.json create mode 100644 homeassistant/components/fjaraskupan/translations/tr.json create mode 100644 homeassistant/components/freedompro/translations/tr.json create mode 100644 homeassistant/components/home_connect/translations/tr.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/tr.json create mode 100644 homeassistant/components/kmtronic/translations/tr.json create mode 100644 homeassistant/components/kostal_plenticore/translations/tr.json create mode 100644 homeassistant/components/litterrobot/translations/tr.json create mode 100644 homeassistant/components/mazda/translations/tr.json create mode 100644 homeassistant/components/met_eireann/translations/tr.json create mode 100644 homeassistant/components/meteoclimatic/translations/tr.json create mode 100644 homeassistant/components/moon/translations/sensor.tr.json create mode 100644 homeassistant/components/mullvad/translations/tr.json create mode 100644 homeassistant/components/mutesync/translations/tr.json create mode 100644 homeassistant/components/nam/translations/tr.json create mode 100644 homeassistant/components/rdw/translations/he.json create mode 100644 homeassistant/components/rdw/translations/hu.json create mode 100644 homeassistant/components/rdw/translations/pl.json create mode 100644 homeassistant/components/rdw/translations/ru.json create mode 100644 homeassistant/components/rdw/translations/zh-Hant.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/tr.json create mode 100644 homeassistant/components/roon/translations/bg.json create mode 100644 homeassistant/components/screenlogic/translations/tr.json create mode 100644 homeassistant/components/select/translations/tr.json create mode 100644 homeassistant/components/sentry/translations/bg.json create mode 100644 homeassistant/components/surepetcare/translations/tr.json create mode 100644 homeassistant/components/switcher_kis/translations/tr.json create mode 100644 homeassistant/components/system_bridge/translations/tr.json create mode 100644 homeassistant/components/tag/translations/tr.json create mode 100644 homeassistant/components/tractive/translations/tr.json create mode 100644 homeassistant/components/tuya/translations/select.pl.json create mode 100644 homeassistant/components/tuya/translations/sensor.pl.json create mode 100644 homeassistant/components/wallbox/translations/tr.json create mode 100644 homeassistant/components/whirlpool/translations/tr.json create mode 100644 homeassistant/components/youless/translations/tr.json diff --git a/homeassistant/components/abode/translations/tr.json b/homeassistant/components/abode/translations/tr.json index d469e43f1f4..6214803198a 100644 --- a/homeassistant/components/abode/translations/tr.json +++ b/homeassistant/components/abode/translations/tr.json @@ -27,7 +27,8 @@ "data": { "password": "Parola", "username": "E-posta" - } + }, + "title": "Abode giri\u015f bilgilerinizi doldurun" } } } diff --git a/homeassistant/components/accuweather/translations/sensor.tr.json b/homeassistant/components/accuweather/translations/sensor.tr.json new file mode 100644 index 00000000000..53c7067062a --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "D\u00fc\u015f\u00fcyor", + "rising": "Y\u00fckseliyor", + "steady": "Sabit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/tr.json b/homeassistant/components/acmeda/translations/tr.json index aea81abdcba..3a870463feb 100644 --- a/homeassistant/components/acmeda/translations/tr.json +++ b/homeassistant/components/acmeda/translations/tr.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, "step": { "user": { "data": { "id": "Ana bilgisayar kimli\u011fi" - } + }, + "title": "Eklemek i\u00e7in bir merkez se\u00e7in" } } } diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json index 13f8e62f8be..af5cdd5d16a 100644 --- a/homeassistant/components/adax/translations/tr.json +++ b/homeassistant/components/adax/translations/tr.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "user": { "data": { - "account_id": "Hesap Kimli\u011fi" + "account_id": "Hesap Kimli\u011fi", + "host": "Ana bilgisayar", + "password": "Parola" } } } diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json index 065af7b49cf..0d78b058f8d 100644 --- a/homeassistant/components/adguard/translations/tr.json +++ b/homeassistant/components/adguard/translations/tr.json @@ -1,16 +1,27 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." + }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "hassio_confirm": { + "description": "{addon} taraf\u0131ndan sa\u011flanan AdGuard Home'a ba\u011flanmak i\u00e7in Home Assistant'\u0131 yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla AdGuard Home" + }, "user": { "data": { "host": "Ana Bilgisayar", "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "description": "AdGuard Home \u00f6rne\u011finizi, izleme ve kontrole izin verecek \u015fekilde ayarlay\u0131n." } } } diff --git a/homeassistant/components/advantage_air/translations/tr.json b/homeassistant/components/advantage_air/translations/tr.json index db639c59376..1501c90a66b 100644 --- a/homeassistant/components/advantage_air/translations/tr.json +++ b/homeassistant/components/advantage_air/translations/tr.json @@ -12,6 +12,7 @@ "ip_address": "\u0130p Adresi", "port": "Port" }, + "description": "Advantage Air duvara monte tabletinizin API'sine ba\u011flan\u0131n.", "title": "Ba\u011flan" } } diff --git a/homeassistant/components/aemet/translations/tr.json b/homeassistant/components/aemet/translations/tr.json index 1a0eadce3ba..7cb0048b0e0 100644 --- a/homeassistant/components/aemet/translations/tr.json +++ b/homeassistant/components/aemet/translations/tr.json @@ -1,4 +1,24 @@ { + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Entegrasyonun ad\u0131" + }, + "description": "AEMET OpenData entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://opendata.aemet.es/centrodedescargas/altaUsuario adresine gidin.", + "title": "AEMET OpenData" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 144acc1e1ae..fcae9294da2 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -4,21 +4,27 @@ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "wrong_location": "Bu b\u00f6lgede Airly \u00f6l\u00e7\u00fcm istasyonu yok." }, "step": { "user": { "data": { "api_key": "API Anahtar\u0131", "latitude": "Enlem", - "longitude": "Boylam" - } + "longitude": "Boylam", + "name": "Ad" + }, + "description": "Airly hava kalitesi entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.airly.eu/register adresine gidin.", + "title": "Airly" } } }, "system_health": { "info": { - "can_reach_server": "Airly sunucusuna eri\u015fin" + "can_reach_server": "Airly sunucusuna eri\u015fin", + "requests_per_day": "G\u00fcnl\u00fck izin verilen istek say\u0131s\u0131", + "requests_remaining": "Kalan izin verilen istekler" } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json index 06af714dc87..590332b496c 100644 --- a/homeassistant/components/airnow/translations/tr.json +++ b/homeassistant/components/airnow/translations/tr.json @@ -17,6 +17,7 @@ "longitude": "Boylam", "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" }, + "description": "AirNow hava kalitesi entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://docs.airnowapi.org/account/request/ adresine gidin.", "title": "AirNow" } } diff --git a/homeassistant/components/airthings/translations/tr.json b/homeassistant/components/airthings/translations/tr.json index 865ea58abeb..211b7e6dc89 100644 --- a/homeassistant/components/airthings/translations/tr.json +++ b/homeassistant/components/airthings/translations/tr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airtouch4/translations/tr.json b/homeassistant/components/airtouch4/translations/tr.json index 3c5799ee47e..852639e6350 100644 --- a/homeassistant/components/airtouch4/translations/tr.json +++ b/homeassistant/components/airtouch4/translations/tr.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", "no_units": "Herhangi bir AirTouch 4 Grubu bulunamad\u0131." }, "step": { "user": { + "data": { + "host": "Ana bilgisayar" + }, "title": "AirTouch 4 ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." } } diff --git a/homeassistant/components/airvisual/translations/sensor.tr.json b/homeassistant/components/airvisual/translations/sensor.tr.json index 8fe31db40ce..47b8e7402b0 100644 --- a/homeassistant/components/airvisual/translations/sensor.tr.json +++ b/homeassistant/components/airvisual/translations/sensor.tr.json @@ -11,6 +11,7 @@ "airvisual__pollutant_level": { "good": "\u0130yi", "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", "unhealthy": "Sa\u011fl\u0131ks\u0131z", "unhealthy_sensitive": "Hassas gruplar i\u00e7in sa\u011fl\u0131ks\u0131z", "very_unhealthy": "\u00c7ok sa\u011fl\u0131ks\u0131z" diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index 6f27841ea13..cbe63637bcb 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f veya Node/Pro Kimli\u011fi zaten kay\u0131tl\u0131.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -26,6 +27,7 @@ "country": "\u00dclke", "state": "durum" }, + "description": "Bir \u015fehri/eyalet/\u00fclkeyi izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" }, "node_pro": { @@ -33,18 +35,27 @@ "ip_address": "Ana Bilgisayar", "password": "Parola" }, - "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.", + "title": "Bir AirVisual Node/Pro'yu yap\u0131land\u0131r\u0131n" }, "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" - } + }, + "title": "AirVisual'\u0131 yeniden do\u011frulay\u0131n" + }, + "user": { + "description": "Ne t\u00fcr AirVisual verilerini izlemek istedi\u011finizi se\u00e7in.", + "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n" } } }, "options": { "step": { "init": { + "data": { + "show_on_map": "\u0130zlenen co\u011frafyay\u0131 haritada g\u00f6ster" + }, "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n" } } diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index c7a8235d5c9..12796cb5a87 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -4,6 +4,7 @@ "arm_away": "D\u0131\u015farda", "arm_home": "Evde", "arm_night": "Gece", + "arm_vacation": "{entity_name} Alarm - Tatil Modu", "disarm": "Devre d\u0131\u015f\u0131 {entity_name}", "trigger": "Tetikle {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", "is_armed_home": "{entity_name} Evde Modu Aktif", "is_armed_night": "{entity_name} Gece Modu Aktif", + "is_armed_vacation": "{entity_name} Alarm a\u00e7\u0131k - Tatil Modu", "is_disarmed": "{entity_name} Devre D\u0131\u015f\u0131", "is_triggered": "{entity_name} tetiklendi" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", "armed_home": "{entity_name} Evde Modu Aktif", "armed_night": "{entity_name} Gece Modu Aktif", + "armed_vacation": "{entity_name} Alarm Tatil Modunda", "disarmed": "{entity_name} Devre D\u0131\u015f\u0131", "triggered": "{entity_name} tetiklendi" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "\u00d6zel Mod Aktif", "armed_home": "Evde Aktif", "armed_night": "Gece Aktif", + "armed_vacation": "Alarm - Tatil Modu", "arming": "Alarm etkinle\u015fiyor", "disarmed": "Devre D\u0131\u015f\u0131", "disarming": "Alarm devre d\u0131\u015f\u0131", diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index ee629cdbf7d..231c06a3955 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -3,52 +3,70 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "create_entry": { + "default": "AlarmDecoder'a ba\u015far\u0131yla ba\u011fland\u0131." + }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "protocol": { "data": { + "device_baudrate": "Cihaz Baud H\u0131z\u0131", "device_path": "Cihaz Yolu", "host": "Ana Bilgisayar", "port": "Port" - } + }, + "title": "Ba\u011flant\u0131 ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" }, "user": { "data": { "protocol": "Protokol" - } + }, + "title": "AlarmDecoder Protokol\u00fcn\u00fc Se\u00e7in" } } }, "options": { "error": { + "int": "A\u015fa\u011f\u0131daki alan bir tamsay\u0131 olmal\u0131d\u0131r.", + "loop_range": "RF D\u00f6ng\u00fcs\u00fc 1 ile 4 aras\u0131nda bir tam say\u0131 olmal\u0131d\u0131r.", + "loop_rfid": "RF D\u00f6ng\u00fcs\u00fc, RF Seri olmadan kullan\u0131lamaz.", "relay_inclusive": "R\u00f6le Adresi ve R\u00f6le Kanal\u0131 birbirine ba\u011fl\u0131d\u0131r ve birlikte eklenmelidir." }, "step": { "arm_settings": { "data": { - "alt_night_mode": "Alternatif Gece Modu" - } + "alt_night_mode": "Alternatif Gece Modu", + "auto_bypass": "Alarm a\u00e7\u0131kken otomatik Atlatma", + "code_arm_required": "Kurmak i\u00e7in Gerekli Kod" + }, + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" }, "init": { "data": { "edit_select": "D\u00fczenle" - } + }, + "description": "Ne d\u00fczenlemek istersiniz?", + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" }, "zone_details": { "data": { + "zone_loop": "RF D\u00f6ng\u00fcs\u00fc", "zone_name": "B\u00f6lge Ad\u0131", "zone_relayaddr": "R\u00f6le Adresi", "zone_relaychan": "R\u00f6le Kanal\u0131", "zone_rfid": "RF Id", "zone_type": "B\u00f6lge Tipi" - } + }, + "description": "{zone_number} b\u00f6lgesi i\u00e7in ayr\u0131nt\u0131lar\u0131 girin. {zone_number} b\u00f6lgesini silmek i\u00e7in B\u00f6lge Ad\u0131n\u0131 bo\u015f b\u0131rak\u0131n.", + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" }, "zone_select": { "data": { "zone_number": "B\u00f6lge Numaras\u0131" }, + "description": "Eklemek, d\u00fczenlemek veya kald\u0131rmak istedi\u011finiz b\u00f6lge numaras\u0131n\u0131 girin.", "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" } } diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json index dc270099fcd..a0808fde8ef 100644 --- a/homeassistant/components/almond/translations/tr.json +++ b/homeassistant/components/almond/translations/tr.json @@ -2,7 +2,18 @@ "config": { "abort": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "hassio_confirm": { + "description": "{addon} taraf\u0131ndan sa\u011flanan Almond'a ba\u011flanacak \u015fekilde yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla Almond" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.tr.json b/homeassistant/components/ambee/translations/sensor.tr.json index dcd012b5dad..087bea4ed99 100644 --- a/homeassistant/components/ambee/translations/sensor.tr.json +++ b/homeassistant/components/ambee/translations/sensor.tr.json @@ -3,6 +3,7 @@ "ambee__risk": { "high": "Y\u00fcksek", "low": "D\u00fc\u015f\u00fck", + "moderate": "Moderate", "very high": "\u00c7ok y\u00fcksek" } } diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json index 6655af0bad9..45eacf30987 100644 --- a/homeassistant/components/ambee/translations/tr.json +++ b/homeassistant/components/ambee/translations/tr.json @@ -1,12 +1,26 @@ { "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, "step": { "reauth_confirm": { "data": { + "api_key": "API Anahtar\u0131", "description": "Ambee hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." } }, "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." } } diff --git a/homeassistant/components/amberelectric/translations/tr.json b/homeassistant/components/amberelectric/translations/tr.json index 128f1842021..274a347e578 100644 --- a/homeassistant/components/amberelectric/translations/tr.json +++ b/homeassistant/components/amberelectric/translations/tr.json @@ -6,6 +6,7 @@ "site_name": "Site Ad\u0131", "site_nmi": "Site NMI" }, + "description": "Eklemek istedi\u011finiz sitenin NMI'sini se\u00e7in", "title": "Amber Electric" }, "user": { diff --git a/homeassistant/components/ambiclimate/translations/tr.json b/homeassistant/components/ambiclimate/translations/tr.json index bcaeba84558..76d0292dd3d 100644 --- a/homeassistant/components/ambiclimate/translations/tr.json +++ b/homeassistant/components/ambiclimate/translations/tr.json @@ -1,7 +1,22 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "access_token": "Eri\u015fim anahtar\u0131 olu\u015ftururken bilinmeyen hata.", + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "error": { + "follow_link": "L\u00fctfen ba\u011flant\u0131y\u0131 takip edin ve G\u00f6nder'e basmadan \u00f6nce kimlik do\u011frulamas\u0131 yap\u0131n", + "no_token": "Ambiclimate ile kimli\u011fi do\u011frulanmad\u0131" + }, + "step": { + "auth": { + "description": "L\u00fctfen bu [ba\u011flant\u0131y\u0131]( {authorization_url} ) takip edin ve Ambiclimate hesab\u0131n\u0131za **izin verin**, ard\u0131ndan geri d\u00f6n\u00fcn ve a\u015fa\u011f\u0131daki **G\u00f6nder**'e bas\u0131n.\n (Belirtilen geri \u00e7a\u011f\u0131rma URL'sinin {cb_url} oldu\u011fundan emin olun)", + "title": "Ambiclimate kimlik do\u011frulamas\u0131" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/tr.json b/homeassistant/components/ambient_station/translations/tr.json index 908d97f5758..ed4b15c1449 100644 --- a/homeassistant/components/ambient_station/translations/tr.json +++ b/homeassistant/components/ambient_station/translations/tr.json @@ -4,13 +4,16 @@ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "invalid_key": "Ge\u00e7ersiz API anahtar\u0131" + "invalid_key": "Ge\u00e7ersiz API anahtar\u0131", + "no_devices": "Hesapta cihaz bulunamad\u0131" }, "step": { "user": { "data": { - "api_key": "API Anahtar\u0131" - } + "api_key": "API Anahtar\u0131", + "app_key": "Uygulama Anahtar\u0131" + }, + "title": "Bilgilerinizi doldurun" } } } diff --git a/homeassistant/components/arcam_fmj/translations/tr.json b/homeassistant/components/arcam_fmj/translations/tr.json index dd15f57212c..7943dece765 100644 --- a/homeassistant/components/arcam_fmj/translations/tr.json +++ b/homeassistant/components/arcam_fmj/translations/tr.json @@ -5,13 +5,27 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "error": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, + "flow_title": "{host}", "step": { + "confirm": { + "description": "Arcam FMJ'yi ` {host} ` \u00fczerinde Home Assistant'a eklemek istiyor musunuz?" + }, "user": { "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "description": "L\u00fctfen cihaz\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin." } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} nin a\u00e7\u0131lmas\u0131 istendi" + } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/tr.json b/homeassistant/components/asuswrt/translations/tr.json new file mode 100644 index 00000000000..2bae8499c02 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/tr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", + "pwd_and_ssh": "Yaln\u0131zca parola veya SSH anahtar dosyas\u0131 sa\u011flay\u0131n", + "pwd_or_ssh": "L\u00fctfen parola veya SSH anahtar dosyas\u0131 sa\u011flay\u0131n", + "ssh_not_file": "SSH anahtar dosyas\u0131 bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "mode": "Mod", + "name": "Ad", + "password": "Parola", + "port": "Port", + "protocol": "Kullan\u0131lacak ileti\u015fim protokol\u00fc", + "ssh_key": "SSH anahtar dosyan\u0131z\u0131n yolu (\u015fifre yerine)", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Y\u00f6nlendiricinize ba\u011flanmak i\u00e7in gerekli parametreyi ayarlay\u0131n", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Bir cihaz uzakta iken beklenecek saniyeler", + "dnsmasq": "dnsmasq.leases dosyalar\u0131n\u0131n y\u00f6nlendiricisindeki konum", + "interface": "\u0130statistik almak istedi\u011finiz aray\u00fcz (\u00f6rn. eth0,eth1 vb.)", + "require_ip": "Cihazlar\u0131n IP'si olmal\u0131d\u0131r (eri\u015fim noktas\u0131 modu i\u00e7in)", + "track_unknown": "Bilinmeyen / ads\u0131z cihazlar\u0131 takip edin" + }, + "title": "AsusWRT Se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json index d3b32080466..93c21154faa 100644 --- a/homeassistant/components/august/translations/tr.json +++ b/homeassistant/components/august/translations/tr.json @@ -10,6 +10,22 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_validate": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u015fifreyi girin.", + "title": "Bir August hesab\u0131n\u0131 yeniden do\u011frulay\u0131n" + }, + "user_validate": { + "data": { + "login_method": "Giri\u015f Y\u00f6ntemi", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r.", + "title": "Bir August hesab\u0131 olu\u015fturun" + }, "validation": { "data": { "code": "Do\u011frulama kodu" diff --git a/homeassistant/components/aurora/translations/tr.json b/homeassistant/components/aurora/translations/tr.json index 0c3bb75ed6e..78dc0f86d0f 100644 --- a/homeassistant/components/aurora/translations/tr.json +++ b/homeassistant/components/aurora/translations/tr.json @@ -12,5 +12,15 @@ } } } - } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "E\u015fik (%)" + } + } + } + }, + "title": "NOAA Aurora Sens\u00f6r\u00fc" } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/pl.json b/homeassistant/components/aurora_abb_powerone/translations/pl.json index c931afdae8d..8c8c7e9a8fc 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/pl.json +++ b/homeassistant/components/aurora_abb_powerone/translations/pl.json @@ -5,6 +5,13 @@ }, "error": { "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "address": "Adres falownika" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/tr.json b/homeassistant/components/aurora_abb_powerone/translations/tr.json index db915bd5714..ec8ee6da4a3 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/tr.json +++ b/homeassistant/components/aurora_abb_powerone/translations/tr.json @@ -1,12 +1,14 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_serial_ports": "com ba\u011flant\u0131 noktas\u0131 bulunamad\u0131. \u0130leti\u015fim kurmak i\u00e7in ge\u00e7erli bir RS485 cihaz\u0131na ihtiyac\u0131n\u0131z var." }, "error": { "cannot_connect": "Ba\u011flant\u0131 kurulam\u0131yor, l\u00fctfen seri portu, adresi, elektrik ba\u011flant\u0131s\u0131n\u0131 ve invert\u00f6r\u00fcn a\u00e7\u0131k oldu\u011funu (g\u00fcn \u0131\u015f\u0131\u011f\u0131nda) kontrol edin.", "cannot_open_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 a\u00e7\u0131lam\u0131yor, l\u00fctfen kontrol edip tekrar deneyin", - "invalid_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 ge\u00e7erli bir ayg\u0131t de\u011fil veya a\u00e7\u0131lamad\u0131" + "invalid_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 ge\u00e7erli bir ayg\u0131t de\u011fil veya a\u00e7\u0131lamad\u0131", + "unknown": "Beklenmeyen hata" }, "step": { "user": { diff --git a/homeassistant/components/auth/translations/tr.json b/homeassistant/components/auth/translations/tr.json index 7d273214574..1cab13d4dc6 100644 --- a/homeassistant/components/auth/translations/tr.json +++ b/homeassistant/components/auth/translations/tr.json @@ -1,22 +1,35 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Kullan\u0131labilir bildirim hizmeti yok." + }, + "error": { + "invalid_code": "Ge\u00e7ersiz kod, l\u00fctfen tekrar deneyiniz." + }, "step": { "init": { + "description": "L\u00fctfen bildirim hizmetlerinden birini se\u00e7in:", "title": "Bilgilendirme bile\u015feni taraf\u0131ndan verilen tek seferlik parolay\u0131 ayarlay\u0131n" }, "setup": { - "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:" + "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:", + "title": "Kurulumu do\u011frulay\u0131n" } }, "title": "Tek Seferlik Parolay\u0131 Bildir" }, "totp": { + "error": { + "invalid_code": "Ge\u00e7ersiz kod, l\u00fctfen tekrar deneyiniz. S\u00fcrekli olarak bu hatay\u0131 al\u0131yorsan\u0131z, l\u00fctfen Home Assistant sisteminizin saatinin do\u011fru oldu\u011fundan emin olun." + }, "step": { "init": { - "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n." + "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n.", + "title": "TOTP kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 ayarlay\u0131n" } - } + }, + "title": "TOTP" } } } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 84da92b97d3..ac6fefae913 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -13,7 +14,8 @@ "data": { "access_token": "Eri\u015fim Belirteci", "email": "E-posta" - } + }, + "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, "user": { "data": { diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json index b2d609747d1..5921a5aa499 100644 --- a/homeassistant/components/axis/translations/tr.json +++ b/homeassistant/components/axis/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "link_local_address": "Ba\u011flant\u0131 yerel adresleri desteklenmiyor", + "not_axis_device": "Bulunan cihaz bir Axis cihaz\u0131 de\u011fil" }, "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", @@ -9,6 +11,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -16,7 +19,18 @@ "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Axis cihaz\u0131n\u0131 kurun" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Kullan\u0131lacak ak\u0131\u015f profilini se\u00e7in" + }, + "title": "Axis cihaz\u0131 video ak\u0131\u015f\u0131 se\u00e7enekleri" } } } diff --git a/homeassistant/components/azure_devops/translations/tr.json b/homeassistant/components/azure_devops/translations/tr.json index 11a15956f63..9eed8014aa8 100644 --- a/homeassistant/components/azure_devops/translations/tr.json +++ b/homeassistant/components/azure_devops/translations/tr.json @@ -12,6 +12,10 @@ "flow_title": "Azure DevOps: {project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "Ki\u015fisel Eri\u015fim Anahtar\u0131 (PAT)" + }, + "description": "{project_url} i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", "title": "Yeniden kimlik do\u011frulama" }, "user": { diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 648b48a178f..49dcc90717f 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -101,6 +101,17 @@ "vibration": "sensor {entity_name} wykryje wibracje" } }, + "device_class": { + "cold": "zimno", + "gas": "gaz", + "moisture": "wilgnotno\u015b\u0107", + "motion": "ruch", + "occupancy": "obecno\u015b\u0107", + "problem": "problem", + "smoke": "dym", + "sound": "d\u017awi\u0119k", + "vibration": "wibracje" + }, "state": { "_": { "off": "wy\u0142.", diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 42847622a1f..6c9193fa6bc 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -1,17 +1,102 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} pili zay\u0131f", + "is_cold": "{entity_name} so\u011fuk", + "is_connected": "{entity_name} ba\u011fl\u0131", + "is_gas": "{entity_name} gaz alg\u0131l\u0131yor", + "is_hot": "{entity_name} s\u0131cak", + "is_light": "{entity_name} \u0131\u015f\u0131k alg\u0131l\u0131yor", + "is_locked": "{entity_name} kilitli", + "is_moist": "{entity_name} nemli", + "is_motion": "{entity_name} hareket alg\u0131l\u0131yor", + "is_moving": "{entity_name} ta\u015f\u0131n\u0131yor", + "is_no_gas": "{entity_name} gaz alg\u0131lam\u0131yor", + "is_no_light": "{entity_name} \u0131\u015f\u0131\u011f\u0131 alg\u0131lam\u0131yor", + "is_no_motion": "{entity_name} hareketi alg\u0131lam\u0131yor", + "is_no_problem": "{entity_name} sorun alg\u0131lam\u0131yor", + "is_no_smoke": "{entity_name} duman alg\u0131lam\u0131yor", + "is_no_sound": "{entity_name} sesi alg\u0131lam\u0131yor", "is_no_update": "{entity_name} g\u00fcncel", + "is_no_vibration": "{entity_name} titre\u015fim alg\u0131lam\u0131yor", + "is_not_bat_low": "{entity_name} pili normal", + "is_not_cold": "{entity_name} so\u011fuk de\u011fil", + "is_not_connected": "{entity_name} ba\u011flant\u0131s\u0131 kesildi", + "is_not_hot": "{entity_name} s\u0131cak de\u011fil", + "is_not_locked": "{entity_name} kilidi a\u00e7\u0131ld\u0131", + "is_not_moist": "{entity_name} kuru", + "is_not_moving": "{entity_name} hareket etmiyor", + "is_not_occupied": "{entity_name} me\u015fgul de\u011fil", + "is_not_open": "{entity_name} kapat\u0131ld\u0131", + "is_not_plugged_in": "{entity_name} fi\u015fi \u00e7ekildi", + "is_not_powered": "{entity_name} desteklenmiyor", + "is_not_present": "{entity_name} mevcut de\u011fil", "is_not_running": "{entity_name} \u00e7al\u0131\u015fm\u0131yor", + "is_not_tampered": "{entity_name} , kurcalamay\u0131 alg\u0131lam\u0131yor", + "is_not_unsafe": "{entity_name} g\u00fcvenli", + "is_occupied": "{entity_name} dolu", + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k", + "is_open": "{entity_name} a\u00e7\u0131k", + "is_plugged_in": "{entity_name} tak\u0131l\u0131", + "is_powered": "{entity_name} destekleniyor", + "is_present": "{entity_name} mevcut", + "is_problem": "{entity_name} sorun alg\u0131l\u0131yor", "is_running": "{entity_name} \u00e7al\u0131\u015f\u0131yor", - "is_update": "{entity_name} i\u00e7in bir g\u00fcncelleme mevcut" + "is_smoke": "{entity_name} duman alg\u0131l\u0131yor", + "is_sound": "{entity_name} sesi alg\u0131l\u0131yor", + "is_tampered": "{entity_name} , kurcalama alg\u0131l\u0131yor", + "is_unsafe": "{entity_name} g\u00fcvenli de\u011fil", + "is_update": "{entity_name} i\u00e7in bir g\u00fcncelleme mevcut", + "is_vibration": "{entity_name} titre\u015fim alg\u0131l\u0131yor" }, "trigger_type": { + "bat_low": "{entity_name} pil seviyesi d\u00fc\u015f\u00fck", + "cold": "{entity_name} so\u011fudu", + "connected": "{entity_name} ba\u011fland\u0131", + "gas": "{entity_name} gaz alg\u0131lamaya ba\u015flad\u0131", + "hot": "{entity_name} \u0131s\u0131nd\u0131", + "is_not_tampered": "{entity_name} kurcalamay\u0131 alg\u0131lamay\u0131 durdurdu", + "is_tampered": "{entity_name} , kurcalamay\u0131 alg\u0131lamaya ba\u015flad\u0131", + "light": "{entity_name} \u0131\u015f\u0131\u011f\u0131 alg\u0131lamaya ba\u015flad\u0131", + "locked": "{entity_name} kilitlendi", "moist": "{entity_name} nemli oldu", + "motion": "{entity_name} hareket alg\u0131lamaya ba\u015flad\u0131", + "moving": "{entity_name} ta\u015f\u0131nmaya ba\u015flad\u0131", + "no_gas": "{entity_name} gaz alg\u0131lamay\u0131 durdurdu", + "no_light": "{entity_name} \u0131\u015f\u0131\u011f\u0131 alg\u0131lamay\u0131 durdurdu", + "no_motion": "{entity_name} hareket alg\u0131lamay\u0131 durdurdu", + "no_problem": "{entity_name} sorunu alg\u0131lamay\u0131 durdurdu", + "no_smoke": "{entity_name} duman alg\u0131lamay\u0131 durdurdu", + "no_sound": "{entity_name} ses alg\u0131lamay\u0131 durdurdu", "no_update": "{entity_name} g\u00fcncellendi", + "no_vibration": "{entity_name} titre\u015fimi alg\u0131lamay\u0131 durdurdu", + "not_bat_low": "{entity_name} pil normal", + "not_cold": "{entity_name} so\u011fuk olmad\u0131", + "not_connected": "{entity_name} ba\u011flant\u0131s\u0131 kesildi", + "not_hot": "{entity_name} s\u0131cak olmad\u0131", + "not_locked": "{entity_name} kilidi a\u00e7\u0131ld\u0131", + "not_moist": "{entity_name} kuru hale geldi", + "not_moving": "{entity_name} hareket etmeyi durdurdu", + "not_occupied": "{entity_name} dolu de\u011fil", "not_opened": "{entity_name} kapat\u0131ld\u0131", + "not_plugged_in": "{entity_name} fi\u015fi \u00e7ekildi", + "not_powered": "{entity_name} desteklenmiyor", + "not_present": "{entity_name} mevcut de\u011fil", "not_running": "{entity_name} art\u0131k \u00e7al\u0131\u015fm\u0131yor", + "not_unsafe": "{entity_name} g\u00fcvenli hale geldi", + "occupied": "{entity_name} i\u015fgal edildi", + "opened": "{entity_name} a\u00e7\u0131ld\u0131", + "plugged_in": "{entity_name} tak\u0131l\u0131", + "powered": "{entity_name} destekleniyor", + "present": "{entity_name} mevcut", + "problem": "{entity_name} sorun alg\u0131lamaya ba\u015flad\u0131", "running": "{entity_name} \u00e7al\u0131\u015fmaya ba\u015flad\u0131", + "smoke": "{entity_name} duman alg\u0131lamaya ba\u015flad\u0131", + "sound": "{entity_name} sesi alg\u0131lamaya ba\u015flad\u0131", + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131", + "unsafe": "{entity_name} g\u00fcvensiz hale geldi", "update": "{entity_name} bir g\u00fcncelleme ald\u0131", "vibration": "{entity_name} , titre\u015fimi alg\u0131lamaya ba\u015flad\u0131" } diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json index 6acd2cf7d43..6baa79c32f4 100644 --- a/homeassistant/components/blebox/translations/tr.json +++ b/homeassistant/components/blebox/translations/tr.json @@ -9,12 +9,15 @@ "unknown": "Beklenmeyen hata", "unsupported_version": "BleBox cihaz\u0131n\u0131n g\u00fcncel olmayan bellenimi var. L\u00fctfen \u00f6nce y\u00fckseltin." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "\u0130p Adresi", "port": "Port" - } + }, + "description": "BleBox'\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n.", + "title": "BleBox cihaz\u0131n\u0131z\u0131 kurun" } } } diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index cef806cb309..aa42c87c2e8 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -14,13 +14,26 @@ "data": { "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" }, - "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin" + "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Blink hesab\u0131yla oturum a\u00e7\u0131n" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" + }, + "description": "Blink entegrasyonunu yap\u0131land\u0131r\u0131n", + "title": "Blink Se\u00e7enekleri" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/tr.json b/homeassistant/components/bmw_connected_drive/translations/tr.json index 153aa4126b0..d38f950392b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/tr.json +++ b/homeassistant/components/bmw_connected_drive/translations/tr.json @@ -11,9 +11,20 @@ "user": { "data": { "password": "Parola", + "region": "ConnectedDrive B\u00f6lgesi", "username": "Kullan\u0131c\u0131 Ad\u0131" } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Salt okunur (yaln\u0131zca sens\u00f6rler ve bildirim, hizmetlerin y\u00fcr\u00fct\u00fclmesi yok, kilit yok)", + "use_location": "Araba konumu anketleri i\u00e7in Home Assistant konumunu kullan\u0131n (7/2014 tarihinden \u00f6nce \u00fcretilmi\u015f i3/i8 olmayan ara\u00e7lar i\u00e7in gereklidir)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json index 3488480a218..7848022e261 100644 --- a/homeassistant/components/bond/translations/tr.json +++ b/homeassistant/components/bond/translations/tr.json @@ -6,13 +6,16 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "old_firmware": "Bond cihaz\u0131nda desteklenmeyen eski s\u00fcr\u00fcm - l\u00fctfen devam etmeden \u00f6nce y\u00fckseltin", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { "access_token": "Eri\u015fim Belirteci" - } + }, + "description": "{name} kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/tr.json b/homeassistant/components/bosch_shc/translations/tr.json index 270fffdbf26..b974ef63ebf 100644 --- a/homeassistant/components/bosch_shc/translations/tr.json +++ b/homeassistant/components/bosch_shc/translations/tr.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "pairing_failed": "E\u015fle\u015ftirme ba\u015far\u0131s\u0131z; l\u00fctfen Bosch Ak\u0131ll\u0131 Ev Kumandas\u0131n\u0131n e\u015fle\u015ftirme modunda (LED yan\u0131p s\u00f6n\u00fcyor) ve \u015fifrenizin do\u011fru oldu\u011funu kontrol edin.", + "session_error": "Oturum hatas\u0131: API, Tamam Olmayan sonucu d\u00f6nd\u00fcr\u00fcr.", + "unknown": "Beklenmeyen hata" + }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { @@ -10,7 +21,15 @@ "password": "Ak\u0131ll\u0131 Ev Denetleyicisinin \u015eifresi" } }, + "reauth_confirm": { + "description": "bosch_shc entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { + "data": { + "host": "Ana bilgisayar" + }, + "description": "Ev Asistan\u0131 ile izleme ve kontrole izin vermek i\u00e7in Bosch Ak\u0131ll\u0131 Ev Denetleyicinizi kurun.", "title": "SHC kimlik do\u011frulama parametreleri" } } diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index ee3fa869ca1..6d0f82e29a4 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -1,14 +1,19 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "unsupported_model": "TV modeliniz desteklenmiyor." }, "step": { "authorize": { + "data": { + "pin": "PIN Kodu" + }, "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmiyorsa, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 iptal et.Home Assistant", "title": "Sony Bravia TV'yi yetkilendirin" }, @@ -24,6 +29,9 @@ "options": { "step": { "user": { + "data": { + "ignored_sources": "Yok say\u0131lan kaynaklar\u0131n listesi" + }, "title": "Sony Bravia TV i\u00e7in se\u00e7enekler" } } diff --git a/homeassistant/components/broadlink/translations/bg.json b/homeassistant/components/broadlink/translations/bg.json index 7534f7228f9..8c5b5347516 100644 --- a/homeassistant/components/broadlink/translations/bg.json +++ b/homeassistant/components/broadlink/translations/bg.json @@ -1,11 +1,34 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({model} at {host})", + "step": { + "finish": { + "data": { + "name": "\u0418\u043c\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435 \u0437\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + }, + "unlock": { + "data": { + "unlock": "\u0414\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0433\u043e." + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json index 83d660a595f..3db7efe4863 100644 --- a/homeassistant/components/broadlink/translations/tr.json +++ b/homeassistant/components/broadlink/translations/tr.json @@ -4,11 +4,13 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "not_supported": "Cihaz desteklenmiyor", "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "unknown": "Beklenmeyen hata" }, "flow_title": "{name} ({model} at {host})", @@ -17,6 +19,9 @@ "title": "Cihaza kimlik do\u011frulama" }, "finish": { + "data": { + "name": "Ad" + }, "title": "Cihaz i\u00e7in bir isim se\u00e7in" }, "reset": { diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index cd91a485252..c731685c6a9 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -5,7 +5,9 @@ "unsupported_model": "Bu yaz\u0131c\u0131 modeli desteklenmiyor." }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "snmp_error": "SNMP sunucusu kapal\u0131 veya yaz\u0131c\u0131 desteklenmiyor.", + "wrong_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi." }, "flow_title": "Brother Yaz\u0131c\u0131: {model} {serial_number}", "step": { @@ -13,9 +15,14 @@ "data": { "host": "Ana Bilgisayar", "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" - } + }, + "description": "Brother yaz\u0131c\u0131 entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/brother" }, "zeroconf_confirm": { + "data": { + "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" + }, + "description": "Seri numaras\u0131 ` {serial_number} ` olan Brother Yaz\u0131c\u0131 {model} }'i Home Assistant'a eklemek ister misiniz?", "title": "Ke\u015ffedilen Brother Yaz\u0131c\u0131" } } diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json index 803b5102a07..9b30ef8517f 100644 --- a/homeassistant/components/bsblan/translations/tr.json +++ b/homeassistant/components/bsblan/translations/tr.json @@ -6,14 +6,18 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { "user": { "data": { "host": "Ana Bilgisayar", + "passkey": "Ge\u00e7i\u015f anahtar\u0131 dizesi", "password": "\u015eifre", "port": "Port", "username": "Kullan\u0131c\u0131 ad\u0131" - } + }, + "description": "BSB-Lan cihaz\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n.", + "title": "BSB-Lan cihaz\u0131na ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/buienradar/translations/tr.json b/homeassistant/components/buienradar/translations/tr.json index 64d6dee2feb..db8c7be0ef6 100644 --- a/homeassistant/components/buienradar/translations/tr.json +++ b/homeassistant/components/buienradar/translations/tr.json @@ -1,10 +1,27 @@ { + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + }, "options": { "step": { "init": { "data": { "country_code": "Kamera g\u00f6r\u00fcnt\u00fclerinin g\u00f6r\u00fcnt\u00fclenece\u011fi \u00fclkenin \u00fclke kodu.", - "delta": "Kamera g\u00f6r\u00fcnt\u00fcs\u00fc g\u00fcncellemeleri aras\u0131ndaki saniye cinsinden zaman aral\u0131\u011f\u0131" + "delta": "Kamera g\u00f6r\u00fcnt\u00fcs\u00fc g\u00fcncellemeleri aras\u0131ndaki saniye cinsinden zaman aral\u0131\u011f\u0131", + "timeframe": "Ya\u011f\u0131\u015f tahmini i\u00e7in dakikalar" } } } diff --git a/homeassistant/components/button/translations/bg.json b/homeassistant/components/button/translations/bg.json new file mode 100644 index 00000000000..85b46c01967 --- /dev/null +++ b/homeassistant/components/button/translations/bg.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} \u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442" + } + }, + "title": "\u0411\u0443\u0442\u043e\u043d" +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/tr.json b/homeassistant/components/canary/translations/tr.json index 6d18629b067..2e5b05f58c7 100644 --- a/homeassistant/components/canary/translations/tr.json +++ b/homeassistant/components/canary/translations/tr.json @@ -7,11 +7,23 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Canary'ya ba\u011flan" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Kameralar i\u00e7in ffmpeg'e ge\u00e7irilen arg\u00fcmanlar", + "timeout": "\u0130stek Zaman A\u015f\u0131m\u0131 (saniye)" } } } diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json index 3a904866600..3a2609c302a 100644 --- a/homeassistant/components/cast/translations/tr.json +++ b/homeassistant/components/cast/translations/tr.json @@ -3,11 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "invalid_known_hosts": "Bilinen ana bilgisayarlar, virg\u00fclle ayr\u0131lm\u0131\u015f bir ana bilgisayar listesi olmal\u0131d\u0131r." + }, "step": { "config": { "data": { "known_hosts": "Bilinen ana bilgisayarlar" }, + "description": "Bilinen Ana Bilgisayarlar - Yay\u0131n cihazlar\u0131n\u0131n ana bilgisayar adlar\u0131n\u0131n veya IP adreslerinin virg\u00fclle ayr\u0131lm\u0131\u015f listesi, mDNS ke\u015ffi \u00e7al\u0131\u015fm\u0131yorsa kullan\u0131n.", "title": "Google Cast yap\u0131land\u0131rmas\u0131" }, "confirm": { @@ -16,8 +20,16 @@ } }, "options": { + "error": { + "invalid_known_hosts": "Bilinen ana bilgisayarlar, virg\u00fclle ayr\u0131lm\u0131\u015f bir ana bilgisayar listesi olmal\u0131d\u0131r." + }, "step": { "advanced_options": { + "data": { + "ignore_cec": "CEC'yi yoksay", + "uuid": "\u0130zin verilen UUID'ler" + }, + "description": "\u0130zin Verilen UUID'ler - Home Asistan\u0131'na eklenecek Cast cihazlar\u0131n\u0131n UUID'lerinin virg\u00fclle ayr\u0131lm\u0131\u015f listesi. Yaln\u0131zca mevcut t\u00fcm yay\u0131n cihazlar\u0131n\u0131 eklemek istemiyorsan\u0131z kullan\u0131n.\n CEC'yi Yoksay - Etkin giri\u015fi belirlemek i\u00e7in CEC verilerini yoksaymas\u0131 gereken virg\u00fclle ayr\u0131lm\u0131\u015f bir Chromecast listesi. Bu, pychromecast.IGNORE_CEC'e iletilecektir.", "title": "Geli\u015fmi\u015f Google Cast yap\u0131land\u0131rmas\u0131" }, "basic_options": { diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json index 6c05bef3a65..d550ccbede8 100644 --- a/homeassistant/components/cert_expiry/translations/tr.json +++ b/homeassistant/components/cert_expiry/translations/tr.json @@ -1,15 +1,24 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "import_failed": "Yap\u0131land\u0131rmadan i\u00e7e aktarma ba\u015far\u0131s\u0131z oldu" + }, + "error": { + "connection_refused": "Ana bilgisayara ba\u011flan\u0131rken ba\u011flant\u0131 reddedildi", + "connection_timeout": "Bu ana bilgisayara ba\u011flan\u0131rken zaman a\u015f\u0131m\u0131", + "resolve_failed": "Bu ana bilgisayar \u00e7\u00f6z\u00fcmlenemedi" }, "step": { "user": { "data": { "host": "Ana Bilgisayar", + "name": "Sertifikan\u0131n ad\u0131", "port": "Port" - } + }, + "title": "Test edilecek sertifikay\u0131 tan\u0131mlay\u0131n" } } - } + }, + "title": "Sertifikan\u0131n Sona Erme Tarihi" } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json new file mode 100644 index 00000000000..3481e0d61b1 --- /dev/null +++ b/homeassistant/components/climacell/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "rate_limited": "\u015eu anda oran s\u0131n\u0131rl\u0131, l\u00fctfen daha sonra tekrar deneyin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "api_version": "API S\u00fcr\u00fcm\u00fc", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, + "description": "Enlem ve Boylam sa\u011flanmazsa, Home Assistant yap\u0131land\u0131rmas\u0131ndaki varsay\u0131lan de\u011ferler kullan\u0131l\u0131r. Her tahmin t\u00fcr\u00fc i\u00e7in bir varl\u0131k olu\u015fturulacak, ancak varsay\u0131lan olarak yaln\u0131zca se\u00e7tikleriniz etkinle\u015ftirilecektir." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. NowCast Tahminleri Aras\u0131nda" + }, + "description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", + "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelle" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 201fec4c4b6..469bebbf620 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -3,6 +3,15 @@ "action_type": { "set_hvac_mode": "{entity_name} \u00fczerinde HVAC modunu de\u011fi\u015ftir", "set_preset_mode": "{entity_name} \u00fczerindeki \u00f6n ayar\u0131 de\u011fi\u015ftir" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} , belirli bir HVAC moduna ayarland\u0131", + "is_preset_mode": "{entity_name} , belirli bir \u00f6n ayar moduna ayarland\u0131" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u00f6l\u00e7\u00fclen nem de\u011fi\u015fti", + "current_temperature_changed": "{entity_name} \u00f6l\u00e7\u00fclen s\u0131cakl\u0131k de\u011fi\u015fti", + "hvac_mode_changed": "{entity_name} HVAC modu de\u011fi\u015fti" } }, "state": { diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json index b9b82f2c08c..5f3edb5d3f1 100644 --- a/homeassistant/components/cloud/translations/tr.json +++ b/homeassistant/components/cloud/translations/tr.json @@ -2,7 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa Etkin", + "can_reach_cert_server": "Sertifika Sunucusuna Ula\u015f\u0131n", "can_reach_cloud": "Home Assistant Cloud'a ula\u015f\u0131n", + "can_reach_cloud_auth": "Kimlik Do\u011frulama Sunucusuna Ula\u015f\u0131n", "google_enabled": "Google Etkin", "logged_in": "Giri\u015f Yapt\u0131", "relayer_connected": "Yeniden Katman ba\u011fl\u0131", diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json index 5d1180961f6..17415c0b2f6 100644 --- a/homeassistant/components/cloudflare/translations/tr.json +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "unknown": "Beklenmeyen hata" }, @@ -11,6 +12,12 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API Belirteci", + "description": "Cloudflare hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." + } + }, "records": { "data": { "records": "Kay\u0131tlar" @@ -21,6 +28,7 @@ "data": { "api_token": "API Belirteci" }, + "description": "Bu entegrasyon, hesab\u0131n\u0131zdaki t\u00fcm b\u00f6lgeler i\u00e7in Zone:Zone:Read ve Zone:DNS:Edit izinleriyle olu\u015fturulmu\u015f bir API Simgesi gerektirir.", "title": "Cloudflare'ye ba\u011flan\u0131n" }, "zone": { diff --git a/homeassistant/components/co2signal/translations/tr.json b/homeassistant/components/co2signal/translations/tr.json index 3ff3b6034d5..038d8e85ff5 100644 --- a/homeassistant/components/co2signal/translations/tr.json +++ b/homeassistant/components/co2signal/translations/tr.json @@ -1,6 +1,22 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "api_ratelimit": "API Ratelimit a\u015f\u0131ld\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "api_ratelimit": "API Ratelimit a\u015f\u0131ld\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { + "coordinates": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + }, "country": { "data": { "country_code": "\u00dclke kodu" @@ -8,8 +24,10 @@ }, "user": { "data": { + "api_key": "Eri\u015fim Anahtar\u0131", "location": "Veri alma" - } + }, + "description": "Bir anahtar istemek i\u00e7in https://co2signal.com/ adresini ziyaret edin." } } } diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json index a1c6c73b192..285d2f7bb96 100644 --- a/homeassistant/components/coinbase/translations/tr.json +++ b/homeassistant/components/coinbase/translations/tr.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { + "api_key": "API Anahtar\u0131", "api_token": "API Gizli Anahtar\u0131", "currencies": "Hesap Bakiyesi Para Birimleri", "exchange_rates": "D\u00f6viz Kurlar\u0131" @@ -15,7 +24,8 @@ "options": { "error": { "currency_unavaliable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.", - "exchange_rate_unavaliable": "\u0130stenen d\u00f6viz kurlar\u0131ndan biri veya daha fazlas\u0131 Coinbase taraf\u0131ndan sa\u011flanm\u0131yor." + "exchange_rate_unavaliable": "\u0130stenen d\u00f6viz kurlar\u0131ndan biri veya daha fazlas\u0131 Coinbase taraf\u0131ndan sa\u011flanm\u0131yor.", + "unknown": "Beklenmeyen hata" }, "step": { "init": { diff --git a/homeassistant/components/control4/translations/tr.json b/homeassistant/components/control4/translations/tr.json index aed7e564a76..111b6abb975 100644 --- a/homeassistant/components/control4/translations/tr.json +++ b/homeassistant/components/control4/translations/tr.json @@ -14,6 +14,16 @@ "host": "\u0130p Adresi", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen Control4 hesap ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ve yerel denetleyicinizin IP adresini girin." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncellemeler aras\u0131ndaki saniyeler" } } } diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json index 4848a34362c..93a85de95da 100644 --- a/homeassistant/components/coolmaster/translations/tr.json +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -1,14 +1,21 @@ { "config": { "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_units": "CoolMasterNet ana bilgisayar\u0131nda herhangi bir HVAC birimi bulunamad\u0131." }, "step": { "user": { "data": { + "cool": "So\u011fuk modu destekler", + "dry": "Kuru modu destekler", + "fan_only": "Yaln\u0131zca fan modunu destekler", + "heat": "Is\u0131tma modunu destekler", + "heat_cool": "Otomatik \u0131s\u0131tma/so\u011futma modunu destekler", "host": "Ana Bilgisayar", "off": "Kapat\u0131labilir" - } + }, + "title": "CoolMasterNet ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." } } } diff --git a/homeassistant/components/coronavirus/translations/tr.json b/homeassistant/components/coronavirus/translations/tr.json index b608d60f824..118f8997d1f 100644 --- a/homeassistant/components/coronavirus/translations/tr.json +++ b/homeassistant/components/coronavirus/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/tr.json b/homeassistant/components/cover/translations/tr.json index f042233a6d1..357e868e0fa 100644 --- a/homeassistant/components/cover/translations/tr.json +++ b/homeassistant/components/cover/translations/tr.json @@ -2,7 +2,28 @@ "device_automation": { "action_type": { "close": "{entity_name} kapat", - "open": "{entity_name} a\u00e7\u0131n" + "close_tilt": "{entity_name} e\u011fimini kapat", + "open": "{entity_name} a\u00e7\u0131n", + "open_tilt": "{entity_name} e\u011fimini a\u00e7", + "set_position": "{entity_name} konumunu ayarla", + "set_tilt_position": "{entity_name} e\u011fim konumunu ayarla", + "stop": "{entity_name} durdur" + }, + "condition_type": { + "is_closed": "{entity_name} kapat\u0131ld\u0131", + "is_closing": "{entity_name} kapan\u0131yor", + "is_open": "{entity_name} a\u00e7\u0131k", + "is_opening": "{entity_name} a\u00e7\u0131l\u0131yor", + "is_position": "Ge\u00e7erli {entity_name} konumu:", + "is_tilt_position": "Ge\u00e7erli {entity_name} e\u011fim konumu:" + }, + "trigger_type": { + "closed": "{entity_name} kapat\u0131ld\u0131", + "closing": "{entity_name} kapan\u0131yor", + "opened": "{entity_name} a\u00e7\u0131ld\u0131", + "opening": "{entity_name} a\u00e7\u0131l\u0131yor", + "position": "{entity_name} konum de\u011fi\u015fiklikleri", + "tilt_position": "{entity_name} e\u011fim konumu de\u011fi\u015fiklikleri" } }, "state": { diff --git a/homeassistant/components/crownstone/translations/tr.json b/homeassistant/components/crownstone/translations/tr.json index d9db65f74d7..1c97cbdf170 100644 --- a/homeassistant/components/crownstone/translations/tr.json +++ b/homeassistant/components/crownstone/translations/tr.json @@ -1,7 +1,96 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "usb_setup_complete": "Crownstone USB kurulumu tamamland\u0131.", + "usb_setup_unsuccessful": "Crownstone USB kurulumu ba\u015far\u0131s\u0131z oldu." + }, "error": { - "account_not_verified": "Hesap do\u011frulanmad\u0131. L\u00fctfen hesab\u0131n\u0131z\u0131 Crownstone'dan gelen aktivasyon e-postas\u0131 arac\u0131l\u0131\u011f\u0131yla etkinle\u015ftirin." + "account_not_verified": "Hesap do\u011frulanmad\u0131. L\u00fctfen hesab\u0131n\u0131z\u0131 Crownstone'dan gelen aktivasyon e-postas\u0131 arac\u0131l\u0131\u011f\u0131yla etkinle\u015ftirin.", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in veya bir USB donan\u0131m kilidi kurmak istemiyorsan\u0131z 'USB kullanma' se\u00e7ene\u011fini se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", + "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB dongle'\u0131n yolunu manuel olarak girin.", + "title": "Crownstone USB dongle manuel yolu" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Stick" + }, + "description": "USB'nin bulundu\u011fu bir Crownstone Stick se\u00e7in.", + "title": "Crownstone USB Stick" + }, + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "title": "Crownstone hesab\u0131" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "USB'nin bulundu\u011fu Crownstone Stick", + "use_usb_option": "Yerel veri iletimi i\u00e7in Crownstone USB dongle kullan\u0131n" + } + }, + "usb_config": { + "data": { + "usb_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", + "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" + }, + "usb_config_option": { + "data": { + "usb_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", + "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB dongle'\u0131n yolunu manuel olarak girin.", + "title": "Crownstone USB dongle manuel yolu" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB Cihaz Yolu" + }, + "description": "Crownstone USB dongle'\u0131n yolunu manuel olarak girin.", + "title": "Crownstone USB dongle manuel yolu" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Stick" + }, + "description": "USB'nin bulundu\u011fu bir Crownstone Stick se\u00e7in.", + "title": "Crownstone USB Stick" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Stick" + }, + "description": "USB'nin bulundu\u011fu bir Crownstone Stick se\u00e7in.", + "title": "Crownstone USB Stick" + } } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json index 4148bf2b9f1..d277e887f2a 100644 --- a/homeassistant/components/daikin/translations/tr.json +++ b/homeassistant/components/daikin/translations/tr.json @@ -5,6 +5,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { + "api_password": "Ge\u00e7ersiz kimlik do\u011frulama , API Anahtar\u0131 veya Parola kullan\u0131n.", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" @@ -15,7 +16,9 @@ "api_key": "API Anahtar\u0131", "host": "Ana Bilgisayar", "password": "Parola" - } + }, + "description": "IP Adresi de\u011ferini girin. \n\n API Anahtar\u0131 ve Parola \u00f6\u011felerinin s\u0131ras\u0131yla BRP072Cxx ve SKYFi cihazlar\u0131 taraf\u0131ndan kullan\u0131ld\u0131\u011f\u0131n\u0131 unutmay\u0131n.", + "title": "Daikin AC'yi yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index 22eea1278d7..72ac69e1645 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -2,9 +2,25 @@ "config": { "abort": { "already_configured": "K\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_bridges": "DeCONZ k\u00f6pr\u00fcs\u00fc bulunamad\u0131", + "no_hardware_available": "deCONZ'a ba\u011fl\u0131 radyo donan\u0131m\u0131 yok", + "not_deconz_bridge": "deCONZ k\u00f6pr\u00fcs\u00fc de\u011fil", + "updated_instance": "DeCONZ yeni ana bilgisayar adresiyle g\u00fcncelle\u015ftirildi" }, + "error": { + "no_key": "API anahtar\u0131 al\u0131namad\u0131" + }, + "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "{addon} taraf\u0131ndan sa\u011flanan deCONZ a\u011f ge\u00e7idine ba\u011flanacak \u015fekilde yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla deCONZ Zigbee a\u011f ge\u00e7idi" + }, + "link": { + "description": "Home Assistant'a kaydolmak i\u00e7in deCONZ a\u011f ge\u00e7idinizin kilidini a\u00e7\u0131n. \n\n 1. deCONZ Ayarlar\u0131 - > A\u011f Ge\u00e7idi - > Geli\u015fmi\u015f se\u00e7ene\u011fine gidin\n 2. \"Uygulaman\u0131n kimli\u011fini do\u011frula\" d\u00fc\u011fmesine bas\u0131n", + "title": "deCONZ ile ba\u011flant\u0131" + }, "manual_input": { "data": { "host": "Ana Bilgisayar", @@ -20,17 +36,51 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "\u00c7ift d\u00fc\u011fmeler", + "bottom_buttons": "Alt d\u00fc\u011fmeler", + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "button_5": "Be\u015finci d\u00fc\u011fme", + "button_6": "Alt\u0131nc\u0131 d\u00fc\u011fme", + "button_7": "Yedinci d\u00fc\u011fme", + "button_8": "Sekizinci d\u00fc\u011fme", + "close": "Kapat", + "dim_down": "K\u0131sma", + "dim_up": "A\u00e7ma", + "left": "Sol", + "open": "A\u00e7\u0131k", + "right": "Sa\u011f", + "side_1": "Yan 1", + "side_2": "Yan 2", + "side_3": "Yan 3", "side_4": "Yan 4", "side_5": "Yan 5", - "side_6": "Yan 6" + "side_6": "Yan 6", + "top_buttons": "\u00dcst d\u00fc\u011fmeler", + "turn_off": "Kapat", + "turn_on": "A\u00e7\u0131n" }, "trigger_type": { "remote_awakened": "Cihaz uyand\u0131", + "remote_button_double_press": "\" {subtype} \" d\u00fc\u011fmesine \u00e7ift t\u0131kland\u0131", + "remote_button_long_press": "\"{alt t\u00fcr}\" d\u00fc\u011fmesine s\u00fcrekli bas\u0131ld\u0131", + "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", + "remote_button_quadruple_press": "\" {subtype} \" d\u00fc\u011fmesi d\u00f6rt kez t\u0131kland\u0131", + "remote_button_quintuple_press": "\" {subtype} \" d\u00fc\u011fmesi be\u015f kez t\u0131kland\u0131", + "remote_button_rotated": "D\u00fc\u011fme d\u00f6nd\u00fcr\u00fcld\u00fc \" {subtype} \"", + "remote_button_rotated_fast": "D\u00fc\u011fme h\u0131zl\u0131 d\u00f6nd\u00fcr\u00fcld\u00fc \" {subtype} \"", + "remote_button_rotation_stopped": "{subtype} \" d\u00fc\u011fmesinin d\u00f6nd\u00fcr\u00fclmesi durduruldu", + "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", + "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "remote_button_triple_press": "\" {subtype} \" d\u00fc\u011fmesine \u00fc\u00e7 kez t\u0131kland\u0131", "remote_double_tap": "\" {subtype} \" cihaz\u0131na iki kez hafif\u00e7e vuruldu", "remote_double_tap_any_side": "Cihaz herhangi bir tarafta \u00e7ift dokundu", "remote_falling": "Serbest d\u00fc\u015f\u00fc\u015fte cihaz", "remote_flip_180_degrees": "Cihaz 180 derece d\u00f6nd\u00fcr\u00fcld\u00fc", "remote_flip_90_degrees": "Cihaz 90 derece d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_gyro_activated": "Cihaz salland\u0131", "remote_moved": "Cihaz \" {subtype} \" yukar\u0131 ta\u015f\u0131nd\u0131", "remote_moved_any_side": "Cihaz herhangi bir taraf\u0131 yukar\u0131 gelecek \u015fekilde ta\u015f\u0131nd\u0131", "remote_rotate_from_side_1": "Cihaz, \"1. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", @@ -38,6 +88,7 @@ "remote_rotate_from_side_3": "Cihaz \"3. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", "remote_rotate_from_side_4": "Cihaz, \"4. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", "remote_rotate_from_side_5": "Cihaz, \"5. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_6": "Cihaz \"yan 6\" \" {subtype} \" konumuna d\u00f6nd\u00fcr\u00fcld\u00fc", "remote_turned_clockwise": "Cihaz saat y\u00f6n\u00fcnde d\u00f6nd\u00fc", "remote_turned_counter_clockwise": "Cihaz saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6nd\u00fc" } @@ -45,6 +96,12 @@ "options": { "step": { "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP sens\u00f6rlerine izin ver", + "allow_deconz_groups": "deCONZ \u0131\u015f\u0131k gruplar\u0131na izin ver", + "allow_new_devices": "Yeni cihazlar\u0131n otomatik eklenmesine izin ver" + }, + "description": "deCONZ cihaz t\u00fcrlerinin g\u00f6r\u00fcn\u00fcrl\u00fc\u011f\u00fcn\u00fc yap\u0131land\u0131r\u0131n", "title": "deCONZ se\u00e7enekleri" } } diff --git a/homeassistant/components/demo/translations/select.tr.json b/homeassistant/components/demo/translations/select.tr.json index a56fc5db252..b24f9d925b9 100644 --- a/homeassistant/components/demo/translations/select.tr.json +++ b/homeassistant/components/demo/translations/select.tr.json @@ -1,7 +1,9 @@ { "state": { "demo__speed": { - "light_speed": "I\u015f\u0131k h\u0131z\u0131" + "light_speed": "I\u015f\u0131k h\u0131z\u0131", + "ludicrous_speed": "Sa\u00e7ma H\u0131z", + "ridiculous_speed": "Anlams\u0131z H\u0131z" } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 1ca389b0b97..1eea23e1bc5 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,9 +1,24 @@ { "options": { "step": { + "init": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + } + }, "options_1": { "data": { - "constant": "Sabit" + "bool": "\u0130ste\u011fe ba\u011fl\u0131 boolean", + "constant": "Sabit", + "int": "Say\u0131sal giri\u015f" + } + }, + "options_2": { + "data": { + "multi": "\u00c7oklu se\u00e7im", + "select": "Bir se\u00e7enek se\u00e7in", + "string": "Dize de\u011feri" } } } diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json index fe33aae6cda..f86d61b08c7 100644 --- a/homeassistant/components/denonavr/translations/tr.json +++ b/homeassistant/components/denonavr/translations/tr.json @@ -3,13 +3,32 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "cannot_connect": "Ba\u011flan\u0131lamad\u0131, l\u00fctfen tekrar deneyin, ana g\u00fc\u00e7 ve ethernet kablolar\u0131n\u0131n ba\u011flant\u0131s\u0131n\u0131 kesip yeniden ba\u011flamak yard\u0131mc\u0131 olabilir" + "cannot_connect": "Ba\u011flan\u0131lamad\u0131, l\u00fctfen tekrar deneyin, ana g\u00fc\u00e7 ve ethernet kablolar\u0131n\u0131n ba\u011flant\u0131s\u0131n\u0131 kesip yeniden ba\u011flamak yard\u0131mc\u0131 olabilir", + "not_denonavr_manufacturer": "Denon AVR A\u011f Al\u0131c\u0131s\u0131 de\u011fil, \u00fcreticinin e\u015fle\u015fmedi\u011fi ke\u015ffedildi", + "not_denonavr_missing": "Denon AVR A\u011f Al\u0131c\u0131s\u0131 de\u011fil, ke\u015fif bilgileri tamamlanmad\u0131" }, + "error": { + "discovery_error": "Denon AVR A\u011f Al\u0131c\u0131s\u0131 bulunamad\u0131" + }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "L\u00fctfen al\u0131c\u0131y\u0131 eklemeyi onaylay\u0131n", + "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" + }, + "select": { + "data": { + "select_host": "Al\u0131c\u0131 IP adresi" + }, + "description": "Ek al\u0131c\u0131lar ba\u011flamak istiyorsan\u0131z kurulumu yeniden \u00e7al\u0131\u015ft\u0131r\u0131n", + "title": "Ba\u011flamak istedi\u011finiz al\u0131c\u0131y\u0131 se\u00e7in" + }, "user": { "data": { "host": "\u0130p Adresi" - } + }, + "description": "Al\u0131c\u0131n\u0131za ba\u011flan\u0131n, IP adresi ayarlanmazsa otomatik bulma kullan\u0131l\u0131r", + "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" } } }, @@ -17,8 +36,13 @@ "step": { "init": { "data": { - "update_audyssey": "Audyssey ayarlar\u0131n\u0131 g\u00fcncelleyin" - } + "show_all_sources": "T\u00fcm kaynaklar\u0131 g\u00f6ster", + "update_audyssey": "Audyssey ayarlar\u0131n\u0131 g\u00fcncelleyin", + "zone2": "B\u00f6lge 2'yi kurun", + "zone3": "B\u00f6lge 3'\u00fc kurun" + }, + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", + "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" } } } diff --git a/homeassistant/components/device_tracker/translations/tr.json b/homeassistant/components/device_tracker/translations/tr.json index 87042b6500e..c520ae1057b 100644 --- a/homeassistant/components/device_tracker/translations/tr.json +++ b/homeassistant/components/device_tracker/translations/tr.json @@ -1,5 +1,9 @@ { "device_automation": { + "condition_type": { + "is_home": "{entity_name} evde", + "is_not_home": "{entity_name} evde de\u011fil" + }, "trigger_type": { "enters": "{entity_name} bir b\u00f6lgeye girdi", "leaves": "{entity_name} bir b\u00f6lgeden ayr\u0131l\u0131yor" diff --git a/homeassistant/components/devolo_home_control/translations/tr.json b/homeassistant/components/devolo_home_control/translations/tr.json index bbd666e77d0..0eebd9ec5ea 100644 --- a/homeassistant/components/devolo_home_control/translations/tr.json +++ b/homeassistant/components/devolo_home_control/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", @@ -10,9 +11,17 @@ "step": { "user": { "data": { + "mydevolo_url": "mydevolo URL", "password": "Parola", "username": "E-posta / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Parola", + "username": "E-posta / devolo kimli\u011fi" + } } } } diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json index c409891f3c6..ec3d8520c74 100644 --- a/homeassistant/components/devolo_home_network/translations/tr.json +++ b/homeassistant/components/devolo_home_network/translations/tr.json @@ -1,14 +1,20 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "home_control": "devolo Ev Kontrol Merkezi Birimi bu entegrasyonla \u00e7al\u0131\u015fmaz." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "flow_title": "{product} ({name})", "step": { "user": { "data": { "ip_address": "IP Adresleri" - } + }, + "description": "Kuruluma ba\u015flamak ister misiniz?" }, "zeroconf_confirm": { "description": "{host_name} ` ana bilgisayar ad\u0131na sahip devolo ev a\u011f\u0131 cihaz\u0131n\u0131 Home Assistant'a eklemek ister misiniz?", diff --git a/homeassistant/components/dexcom/translations/tr.json b/homeassistant/components/dexcom/translations/tr.json index ec93dc078af..6a1f86ea9aa 100644 --- a/homeassistant/components/dexcom/translations/tr.json +++ b/homeassistant/components/dexcom/translations/tr.json @@ -12,8 +12,11 @@ "user": { "data": { "password": "Parola", + "server": "Sunucu", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Dexcom Share kimlik bilgilerini girin", + "title": "Dexcom entegrasyonunu kurun" } } }, diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json index 84adcdf8225..520424e434f 100644 --- a/homeassistant/components/dialogflow/translations/tr.json +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Dialogflow'un webhook entegrasyonunu]( {dialogflow_url} ) ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "Dialogflow'u kurmak istedi\u011finizden emin misiniz?", + "title": "Dialogflow Webhook'u ayarlay\u0131n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json index daca8f1ef62..67ed9646a0c 100644 --- a/homeassistant/components/directv/translations/tr.json +++ b/homeassistant/components/directv/translations/tr.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "{name} kurmak istiyor musunuz?" diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 504ccc27cb8..59c5221b870 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "alternative_integration": "Cihaz ba\u015fka bir entegrasyon taraf\u0131ndan daha iyi destekleniyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", "discovery_error": "E\u015fle\u015fen bir DLNA cihaz\u0131 bulunamad\u0131", "incomplete_config": "Yap\u0131land\u0131rmada gerekli bir de\u011fi\u015fken eksik", @@ -9,15 +11,23 @@ "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "import_turn_on": { + "description": "L\u00fctfen cihaz\u0131 a\u00e7\u0131n ve ta\u015f\u0131maya devam etmek i\u00e7in g\u00f6nder'i t\u0131klay\u0131n" + }, "manual": { "data": { "url": "URL" }, + "description": "Ayg\u0131t a\u00e7\u0131klamas\u0131 XML dosyas\u0131n\u0131n URL'si", "title": "Manuel DLNA DMR ayg\u0131t ba\u011flant\u0131s\u0131" }, "user": { @@ -37,6 +47,7 @@ "step": { "init": { "data": { + "callback_url_override": "Olay dinleyici geri \u00e7a\u011f\u0131rma URL'si", "listen_port": "Olay dinleyici ba\u011flant\u0131 noktas\u0131 (ayarlanmam\u0131\u015fsa rastgele)", "poll_availability": "Cihaz kullan\u0131labilirli\u011fi i\u00e7in anket" }, diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index 04ed9c69c9c..1de21e034fe 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "link_local_address": "Ba\u011flant\u0131 yerel adresleri desteklenmiyor", + "not_doorbird_device": "Bu cihaz bir DoorBird de\u011fil" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -15,13 +18,17 @@ "name": "Cihaz ad\u0131", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "DoorBird'e ba\u011flan\u0131n" } } }, "options": { "step": { "init": { + "data": { + "events": "Virg\u00fclle ayr\u0131lm\u0131\u015f olaylar\u0131n listesi." + }, "description": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. https://www.home-assistant.io/integrations/doorbird/#events adresindeki belgelere bak\u0131n. \u00d6rnek: birisi_pressed_the_button, hareket" } } diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 07b783f1d9b..1b282b456d6 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_communicate": "\u0130leti\u015fim kurulamad\u0131", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_communicate": "\u0130leti\u015fim kurulamad\u0131", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "one": "Bo\u015f", + "other": "Bo\u015f" }, "step": { + "one": "Bo\u015f", + "other": "Bo\u015f", "setup_network": { "data": { - "dsmr_version": "DSMR s\u00fcr\u00fcm\u00fcn\u00fc se\u00e7in" + "dsmr_version": "DSMR s\u00fcr\u00fcm\u00fcn\u00fc se\u00e7in", + "host": "Ana bilgisayar", + "port": "Port" }, "title": "Ba\u011flant\u0131 adresini se\u00e7in" }, @@ -18,6 +31,9 @@ "title": "Cihaz" }, "setup_serial_manual_path": { + "data": { + "port": "USB Cihaz Yolu" + }, "title": "Yol" }, "user": { diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json index 0f8c17228fd..61f22197bcf 100644 --- a/homeassistant/components/dunehd/translations/tr.json +++ b/homeassistant/components/dunehd/translations/tr.json @@ -5,13 +5,15 @@ }, "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi" }, "step": { "user": { "data": { "host": "Ana Bilgisayar" }, + "description": "Dune HD entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/dunehd \n\n Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Dune HD" } } diff --git a/homeassistant/components/eafm/translations/bg.json b/homeassistant/components/eafm/translations/bg.json new file mode 100644 index 00000000000..37b6f40c82e --- /dev/null +++ b/homeassistant/components/eafm/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/tr.json b/homeassistant/components/eafm/translations/tr.json index 4ed0f406e57..6755aa58de2 100644 --- a/homeassistant/components/eafm/translations/tr.json +++ b/homeassistant/components/eafm/translations/tr.json @@ -9,6 +9,7 @@ "data": { "station": "\u0130stasyon" }, + "description": "\u0130zlemek istedi\u011finiz istasyonu se\u00e7in", "title": "Ak\u0131\u015f izleme istasyonunu takip edin" } } diff --git a/homeassistant/components/ebusd/translations/tr.json b/homeassistant/components/ebusd/translations/tr.json new file mode 100644 index 00000000000..5e802f16a5d --- /dev/null +++ b/homeassistant/components/ebusd/translations/tr.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "G\u00fcn", + "night": "Gece" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/tr.json b/homeassistant/components/ecobee/translations/tr.json index 23ece38682d..049af38b514 100644 --- a/homeassistant/components/ecobee/translations/tr.json +++ b/homeassistant/components/ecobee/translations/tr.json @@ -3,11 +3,21 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "pin_request_failed": "ecobee'den PIN istenirken hata olu\u015ftu; l\u00fctfen API anahtar\u0131n\u0131n do\u011fru oldu\u011funu do\u011frulay\u0131n.", + "token_request_failed": "ecobee'den anahtar istenirken hata olu\u015ftu; l\u00fctfen tekrar deneyin." + }, "step": { + "authorize": { + "description": "L\u00fctfen bu uygulamay\u0131 https://www.ecobee.com/consumerportal/index.html adresinde PIN koduyla yetkilendirin: \n\n {pin}\n\n Ard\u0131ndan G\u00f6nder'e bas\u0131n.", + "title": "Uygulamay\u0131 ecobee.com'da yetkilendirin" + }, "user": { "data": { "api_key": "API Anahtar\u0131" - } + }, + "description": "L\u00fctfen ecobee.com'dan al\u0131nan API anahtar\u0131n\u0131 girin.", + "title": "ecobee API anahtar\u0131" } } } diff --git a/homeassistant/components/efergy/translations/tr.json b/homeassistant/components/efergy/translations/tr.json index 212abb7cb64..cc219a26772 100644 --- a/homeassistant/components/efergy/translations/tr.json +++ b/homeassistant/components/efergy/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", @@ -12,7 +13,8 @@ "user": { "data": { "api_key": "API Anahtar\u0131" - } + }, + "title": "Efergy" } } } diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json index 4515d7de584..bf3ab23fae3 100644 --- a/homeassistant/components/elgato/translations/tr.json +++ b/homeassistant/components/elgato/translations/tr.json @@ -7,14 +7,17 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{serial_number}", "step": { "user": { "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "description": "Elgato Light'\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." }, "zeroconf_confirm": { + "description": "Seri numaras\u0131 ` {serial_number} ` olan Elgato Light'\u0131 Home Assistant'a eklemek ister misiniz?", "title": "Ke\u015ffedilen Elgato Light cihaz\u0131" } } diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json index 9259220985b..3b62ad5e079 100644 --- a/homeassistant/components/elkm1/translations/tr.json +++ b/homeassistant/components/elkm1/translations/tr.json @@ -12,9 +12,15 @@ "step": { "user": { "data": { + "address": "Seri yoluyla ba\u011flan\u0131l\u0131yorsa IP adresi veya etki alan\u0131 veya seri ba\u011flant\u0131 noktas\u0131.", "password": "Parola", + "prefix": "Benzersiz bir \u00f6nek (yaln\u0131zca bir ElkM1'iniz varsa bo\u015f b\u0131rak\u0131n).", + "protocol": "Protokol", + "temperature_unit": "ElkM1'in kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi.", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Adres dizesi, 'g\u00fcvenli' ve 'g\u00fcvenli olmayan' i\u00e7in 'adres[:port]' bi\u00e7iminde olmal\u0131d\u0131r. \u00d6rnek: '192.168.1.1'. Ba\u011flant\u0131 noktas\u0131 iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 'g\u00fcvenli olmayan' i\u00e7in 2101 ve 'g\u00fcvenli' i\u00e7in 2601'dir. Seri protokol i\u00e7in adres 'tty[:baud]' bi\u00e7iminde olmal\u0131d\u0131r. \u00d6rnek: '/dev/ttyS1'. Baud iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 115200'd\u00fcr.", + "title": "Elk-M1 Kontrol\u00fcne Ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/emonitor/translations/tr.json b/homeassistant/components/emonitor/translations/tr.json new file mode 100644 index 00000000000..58c48f5f688 --- /dev/null +++ b/homeassistant/components/emonitor/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "title": "SiteSage Emonitor Kurulumu" + }, + "user": { + "data": { + "host": "Ana bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/tr.json b/homeassistant/components/emulated_roku/translations/tr.json index 5307276a71d..271d43d8763 100644 --- a/homeassistant/components/emulated_roku/translations/tr.json +++ b/homeassistant/components/emulated_roku/translations/tr.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP Adresini Tan\u0131t", + "advertise_port": "Ba\u011flant\u0131 Noktas\u0131n\u0131 Tan\u0131t", + "host_ip": "Ana Bilgisayar IP Adresi", + "listen_port": "Ba\u011flant\u0131 Noktas\u0131n\u0131 Dinle", + "name": "Ad", + "upnp_bind_multicast": "\u00c7ok noktaya yay\u0131n\u0131 ba\u011fla (Do\u011fru/Yanl\u0131\u015f)" + }, + "title": "Sunucu yap\u0131land\u0131rmas\u0131n\u0131 tan\u0131mlay\u0131n" + } } - } + }, + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/tr.json b/homeassistant/components/enocean/translations/tr.json index b4e6be555ff..b070fddf012 100644 --- a/homeassistant/components/enocean/translations/tr.json +++ b/homeassistant/components/enocean/translations/tr.json @@ -11,12 +11,14 @@ "detect": { "data": { "path": "USB dongle yolu" - } + }, + "title": "ENOcean dongle yolunu se\u00e7in" }, "manual": { "data": { "path": "USB dongle yolu" - } + }, + "title": "Enocean dongle yolunu girin" } } } diff --git a/homeassistant/components/enphase_envoy/translations/tr.json b/homeassistant/components/enphase_envoy/translations/tr.json new file mode 100644 index 00000000000..5cbf60dbc06 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{serial} ( {host} )", + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/tr.json b/homeassistant/components/environment_canada/translations/tr.json index afd8eb43d46..41fe7a8046c 100644 --- a/homeassistant/components/environment_canada/translations/tr.json +++ b/homeassistant/components/environment_canada/translations/tr.json @@ -3,6 +3,8 @@ "error": { "bad_station_id": "\u0130stasyon Kimli\u011fi ge\u00e7ersiz, eksik veya istasyon kimli\u011fi veritaban\u0131nda bulunamad\u0131", "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "error_response": "Environment Canada'dan hatal\u0131 yan\u0131t", + "too_many_attempts": "Environment Kanada'ya ba\u011flant\u0131lar s\u0131n\u0131rl\u0131d\u0131r; 60 saniye sonra tekrar deneyin", "unknown": "Beklenmeyen hata" }, "step": { @@ -13,7 +15,8 @@ "longitude": "Boylam", "station": "Hava istasyonu ID" }, - "description": "Bir istasyon kimli\u011fi veya enlem/boylam belirtilmelidir. Kullan\u0131lan varsay\u0131lan enlem/boylam, Home Assistant kurulumunuzda yap\u0131land\u0131r\u0131lan de\u011ferlerdir. Koordinatlar belirtilirse, koordinatlara en yak\u0131n meteoroloji istasyonu kullan\u0131lacakt\u0131r. Bir istasyon kodu kullan\u0131l\u0131yorsa, \u015fu bi\u00e7imde olmal\u0131d\u0131r: PP/kod, burada PP iki harfli ildir ve kod istasyon kimli\u011fidir. \u0130stasyon kimliklerinin listesi burada bulunabilir: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Hava durumu bilgileri \u0130ngilizce veya Frans\u0131zca olarak al\u0131nabilir." + "description": "Bir istasyon kimli\u011fi veya enlem/boylam belirtilmelidir. Kullan\u0131lan varsay\u0131lan enlem/boylam, Home Assistant kurulumunuzda yap\u0131land\u0131r\u0131lan de\u011ferlerdir. Koordinatlar belirtilirse, koordinatlara en yak\u0131n meteoroloji istasyonu kullan\u0131lacakt\u0131r. Bir istasyon kodu kullan\u0131l\u0131yorsa, \u015fu bi\u00e7imde olmal\u0131d\u0131r: PP/kod, burada PP iki harfli ildir ve kod istasyon kimli\u011fidir. \u0130stasyon kimliklerinin listesi burada bulunabilir: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Hava durumu bilgileri \u0130ngilizce veya Frans\u0131zca olarak al\u0131nabilir.", + "title": "Environment Canada: hava konumu durumu ve dili" } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index 81f85d4980b..a1124c59b29 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -2,11 +2,16 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_psk": "Aktar\u0131m \u015fifreleme anahtar\u0131 ge\u00e7ersiz. L\u00fctfen yap\u0131land\u0131rman\u0131zda sahip oldu\u011funuzla e\u015fle\u015fti\u011finden emin olun", + "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "{name}", "step": { "authenticate": { "data": { @@ -15,13 +20,27 @@ "description": "L\u00fctfen yap\u0131land\u0131rman\u0131zda {name} i\u00e7in belirledi\u011finiz parolay\u0131 girin." }, "discovery_confirm": { + "description": "ESPHome d\u00fc\u011f\u00fcm\u00fcn\u00fc ` {name} ` Home Assistant'a eklemek istiyor musunuz?", "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" }, + "encryption_key": { + "data": { + "noise_psk": "\u015eifreleme anahtar\u0131" + }, + "description": "{name} i\u00e7in yap\u0131land\u0131rman\u0131zda belirledi\u011finiz \u015fifreleme anahtar\u0131n\u0131 girin." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u015eifreleme anahtar\u0131" + }, + "description": "ESPHome cihaz\u0131 {name} aktar\u0131m \u015fifrelemesini etkinle\u015ftirdi veya \u015fifreleme anahtar\u0131n\u0131 de\u011fi\u015ftirdi. L\u00fctfen g\u00fcncellenmi\u015f anahtar\u0131 girin." + }, "user": { "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } } diff --git a/homeassistant/components/ezviz/translations/tr.json b/homeassistant/components/ezviz/translations/tr.json new file mode 100644 index 00000000000..a1ba775da7f --- /dev/null +++ b/homeassistant/components/ezviz/translations/tr.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "ezviz_cloud_account_missing": "Ezviz bulut hesab\u0131 eksik. L\u00fctfen Ezviz bulut hesab\u0131n\u0131 yeniden yap\u0131land\u0131r\u0131n", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "IP {ip_address} {serial} i\u00e7in RTSP kimlik bilgilerini girin", + "title": "Ke\u015ffedilen Ezviz Kamera" + }, + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Ezviz Cloud'a ba\u011flan\u0131n" + }, + "user_custom_url": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "B\u00f6lge URL'nizi manuel olarak belirtin", + "title": "\u00d6zel Ezviz URL'sine ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Kameralar i\u00e7in ffmpeg'e ge\u00e7irilen arg\u00fcmanlar", + "timeout": "\u0130stek Zaman A\u015f\u0131m\u0131 (saniye)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/tr.json b/homeassistant/components/faa_delays/translations/tr.json new file mode 100644 index 00000000000..bcca79ce392 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Bu havaalan\u0131 zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_airport": "Havaalan\u0131 kodu ge\u00e7erli de\u011fil", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "id": "Havaliman\u0131" + }, + "description": "ABD Havaalan\u0131 Kodu'nu IATA Format\u0131nda Girin", + "title": "FAA Gecikmeleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json index 52a07c35d83..5f43af3e005 100644 --- a/homeassistant/components/fan/translations/tr.json +++ b/homeassistant/components/fan/translations/tr.json @@ -4,6 +4,10 @@ "turn_off": "{entity_name} kapat", "turn_on": "{entity_name} a\u00e7\u0131n" }, + "condition_type": { + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, "trigger_type": { "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" diff --git a/homeassistant/components/fjaraskupan/translations/tr.json b/homeassistant/components/fjaraskupan/translations/tr.json new file mode 100644 index 00000000000..c64f41f5c88 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Fj\u00e4r\u00e5skupan'\u0131 kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/tr.json b/homeassistant/components/flick_electric/translations/tr.json index a83e1936fb4..64be92a8e5f 100644 --- a/homeassistant/components/flick_electric/translations/tr.json +++ b/homeassistant/components/flick_electric/translations/tr.json @@ -11,9 +11,12 @@ "step": { "user": { "data": { + "client_id": "\u0130stemci Kimli\u011fi (iste\u011fe ba\u011fl\u0131)", + "client_secret": "\u0130stemci Gizlili\u011fi (iste\u011fe ba\u011fl\u0131)", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Flick oturum a\u00e7ma kimlik bilgileri" } } } diff --git a/homeassistant/components/flipr/translations/tr.json b/homeassistant/components/flipr/translations/tr.json index c89dd0ba004..e5649496f38 100644 --- a/homeassistant/components/flipr/translations/tr.json +++ b/homeassistant/components/flipr/translations/tr.json @@ -1,7 +1,30 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { - "no_flipr_id_found": "\u015eu anda hesab\u0131n\u0131zla ili\u015fkilendirilmi\u015f bir flipr kimli\u011fi yok. \u00d6nce Flipr'\u0131n mobil uygulamas\u0131yla \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131 do\u011frulaman\u0131z gerekir." + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_flipr_id_found": "\u015eu anda hesab\u0131n\u0131zla ili\u015fkilendirilmi\u015f bir flipr kimli\u011fi yok. \u00d6nce Flipr'\u0131n mobil uygulamas\u0131yla \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131 do\u011frulaman\u0131z gerekir.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr Kimli\u011fi" + }, + "description": "Listeden Flipr kimli\u011finizi se\u00e7in", + "title": "Flipr'inizi se\u00e7in" + }, + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "description": "Flipr hesab\u0131n\u0131z\u0131 kullanarak ba\u011flan\u0131n.", + "title": "Flipr'e ba\u011flan" + } } } } \ No newline at end of file diff --git a/homeassistant/components/flo/translations/bg.json b/homeassistant/components/flo/translations/bg.json index 2ac8a444100..7b92255e7c9 100644 --- a/homeassistant/components/flo/translations/bg.json +++ b/homeassistant/components/flo/translations/bg.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flume/translations/tr.json b/homeassistant/components/flume/translations/tr.json index f35dfeb3e40..dbdd21a3d49 100644 --- a/homeassistant/components/flume/translations/tr.json +++ b/homeassistant/components/flume/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -10,14 +11,21 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "Parola" + }, "description": "{username} i\u00e7in \u015fifre art\u0131k ge\u00e7erli de\u011fil.", "title": "Flume Hesab\u0131n\u0131z\u0131 Yeniden Do\u011frulay\u0131n" }, "user": { "data": { + "client_id": "\u0130stemci Kimli\u011fi", + "client_secret": "\u0130stemci Anahtar\u0131", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Flume Ki\u015fisel API'sine eri\u015fmek i\u00e7in https://portal.flumetech.com/settings#token adresinden bir 'M\u00fc\u015fteri Kimli\u011fi' ve '\u0130stemci Anahtar\u0131' talep etmeniz gerekir.", + "title": "Flume Hesab\u0131n\u0131za ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json index 6e749e3c827..3a21364502e 100644 --- a/homeassistant/components/flunearyou/translations/tr.json +++ b/homeassistant/components/flunearyou/translations/tr.json @@ -11,7 +11,9 @@ "data": { "latitude": "Enlem", "longitude": "Boylam" - } + }, + "description": "Bir \u00e7ift koordinat i\u00e7in kullan\u0131c\u0131 tabanl\u0131 raporlar\u0131 ve CDC raporlar\u0131n\u0131 izleyin.", + "title": "Flu Near You'yu Yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 266db1b5fb4..58d6e514adc 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -5,6 +5,9 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_devices_found": "A\u011fda cihaz bulunamad\u0131" }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { diff --git a/homeassistant/components/forecast_solar/translations/tr.json b/homeassistant/components/forecast_solar/translations/tr.json index 44d98ab670c..fecd8d7889a 100644 --- a/homeassistant/components/forecast_solar/translations/tr.json +++ b/homeassistant/components/forecast_solar/translations/tr.json @@ -4,8 +4,13 @@ "user": { "data": { "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)", - "declination": "Sapma (0 = Yatay, 90 = Dikey)" - } + "declination": "Sapma (0 = Yatay, 90 = Dikey)", + "latitude": "Enlem", + "longitude": "Boylam", + "modules power": "Solar mod\u00fcllerinizin toplam en y\u00fcksek Watt g\u00fcc\u00fc", + "name": "Ad" + }, + "description": "G\u00fcne\u015f panellerinizin verilerini doldurun. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." } } }, @@ -13,8 +18,13 @@ "step": { "init": { "data": { - "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)" - } + "api_key": "Forecast.Solar API Anahtar\u0131 (iste\u011fe ba\u011fl\u0131)", + "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)", + "damping": "S\u00f6n\u00fcmleme fakt\u00f6r\u00fc: sonu\u00e7lar\u0131 sabah ve ak\u015fam ayarlar", + "declination": "Sapma (0 = Yatay, 90 = Dikey)", + "modules power": "Solar mod\u00fcllerinizin toplam en y\u00fcksek Watt g\u00fcc\u00fc" + }, + "description": "Bu de\u011ferler Solar.Forecast sonucunun ayarlanmas\u0131na izin verir. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." } } } diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index 1ca2f9d4715..fd23faf2e96 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "not_forked_daapd": "Cihaz, forked-daapd sunucusu de\u011fil." }, "error": { + "forbidden": "Ba\u011flan\u0131lam\u0131yor. L\u00fctfen forked-daapd a\u011f izinlerinizi kontrol edin.", "unknown_error": "Beklenmeyen hata", - "wrong_password": "Yanl\u0131\u015f parola." + "websocket_not_enabled": "forked-daapd sunucu websocket etkin de\u011fil.", + "wrong_host_or_port": "Ba\u011flan\u0131lam\u0131yor. L\u00fctfen ana bilgisayar\u0131 ve ba\u011flant\u0131 noktas\u0131n\u0131 kontrol edin.", + "wrong_password": "Yanl\u0131\u015f parola.", + "wrong_server_type": "> = 27.0 s\u00fcr\u00fcm\u00fcne sahip bir forked-daapd sunucusu gerektirir." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -14,7 +20,22 @@ "name": "Kolay Ad\u0131", "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", "port": "API Port" - } + }, + "title": "Forked-daapd cihaz\u0131n\u0131 kurun" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "librespot-java boru kontrol\u00fc i\u00e7in ba\u011flant\u0131 noktas\u0131 (kullan\u0131l\u0131yorsa)", + "max_playlists": "Kaynak olarak kullan\u0131lan maksimum oynatma listesi say\u0131s\u0131", + "tts_pause_time": "TTS'den \u00f6nce ve sonra duraklatmak i\u00e7in saniyeler", + "tts_volume": "TTS ses seviyesi (aral\u0131k [0,1])" + }, + "description": "Forked-daapd entegrasyonu i\u00e7in \u00e7e\u015fitli se\u00e7enekleri ayarlay\u0131n.", + "title": "Forked-daapd se\u00e7eneklerini yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json index b675d38057d..7fa8a8a485c 100644 --- a/homeassistant/components/freebox/translations/tr.json +++ b/homeassistant/components/freebox/translations/tr.json @@ -5,9 +5,14 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "register_failed": "Kay\u0131t ba\u015far\u0131s\u0131z oldu, l\u00fctfen tekrar deneyin", "unknown": "Beklenmeyen hata" }, "step": { + "link": { + "description": "\"G\u00f6nder\"e t\u0131klay\u0131n, ard\u0131ndan Freebox'\u0131 Home Assistant ile kaydetmek i\u00e7in y\u00f6nlendiricideki sa\u011f oka dokunun. \n\n ![Y\u00f6nlendiricideki d\u00fc\u011fmenin konumu](/static/images/config_freebox.png)", + "title": "Freebox y\u00f6nlendiriciyi ba\u011fla" + }, "user": { "data": { "host": "Ana Bilgisayar", diff --git a/homeassistant/components/freedompro/translations/tr.json b/homeassistant/components/freedompro/translations/tr.json new file mode 100644 index 00000000000..4d846a17117 --- /dev/null +++ b/homeassistant/components/freedompro/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "L\u00fctfen https://home.freedompro.eu adresinden al\u0131nan API anahtar\u0131n\u0131 girin", + "title": "Freedompro API anahtar\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 93d5f2aa36f..febbd1999cc 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -1,14 +1,64 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_error": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Bulunan FRITZ!Box: {name} \n\n {name} kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun", + "title": "FRITZ!Box Tools Kurulumu" + }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "{host} i\u00e7in FRITZ!Box Tools kimlik bilgilerini g\u00fcncelleyin. \n\n FRITZ!Box Tools, FRITZ!Box'\u0131n\u0131zda oturum a\u00e7am\u0131yor.", + "title": "FRITZ!Box Tools - kimlik bilgilerinin g\u00fcncellenmesi" + }, "start_config": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", "title": "FRITZ!Box Tools Kurulumu - zorunlu" }, "user": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", "title": "FRITZ!Box Tools Kurulumu" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Bir cihaz\u0131 'evde' varsaymak i\u00e7in saniye" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json index 746fe594e19..fafec643d28 100644 --- a/homeassistant/components/fritzbox/translations/tr.json +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -3,11 +3,14 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "AVM FRITZ!Box'a ba\u011fl\u0131 ancak Ak\u0131ll\u0131 Ev cihazlar\u0131n\u0131 kontrol edemiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -28,7 +31,8 @@ "host": "Ana Bilgisayar", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "AVM FRITZ!Box bilgilerinizi giriniz." } } } diff --git a/homeassistant/components/garages_amsterdam/translations/tr.json b/homeassistant/components/garages_amsterdam/translations/tr.json index c1cb8dca51b..49ddee0ef7b 100644 --- a/homeassistant/components/garages_amsterdam/translations/tr.json +++ b/homeassistant/components/garages_amsterdam/translations/tr.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gdacs/translations/tr.json b/homeassistant/components/gdacs/translations/tr.json index aeb6a5a345e..a5f849405ea 100644 --- a/homeassistant/components/gdacs/translations/tr.json +++ b/homeassistant/components/gdacs/translations/tr.json @@ -7,7 +7,8 @@ "user": { "data": { "radius": "Yar\u0131\u00e7ap" - } + }, + "title": "Filtre ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 doldurun." } } } diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json index 84adcdf8225..4cd04c64d7b 100644 --- a/homeassistant/components/geofency/translations/tr.json +++ b/homeassistant/components/geofency/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Geofency'de webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "Geofency Webhook'u kurmak istedi\u011finizden emin misiniz?", + "title": "Geofency Webhook'u kurun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/tr.json b/homeassistant/components/geonetnz_quakes/translations/tr.json index 717f6d72b94..a7d80261b8b 100644 --- a/homeassistant/components/geonetnz_quakes/translations/tr.json +++ b/homeassistant/components/geonetnz_quakes/translations/tr.json @@ -2,6 +2,15 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Yar\u0131\u00e7ap" + }, + "title": "Filtre ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 doldurun." + } } } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/tr.json b/homeassistant/components/geonetnz_volcano/translations/tr.json index 980be333568..5cb07962f5b 100644 --- a/homeassistant/components/geonetnz_volcano/translations/tr.json +++ b/homeassistant/components/geonetnz_volcano/translations/tr.json @@ -7,7 +7,8 @@ "user": { "data": { "radius": "Yar\u0131\u00e7ap" - } + }, + "title": "Filtre ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 doldurun." } } } diff --git a/homeassistant/components/gios/translations/tr.json b/homeassistant/components/gios/translations/tr.json index 590aec1894c..c0444ed99ed 100644 --- a/homeassistant/components/gios/translations/tr.json +++ b/homeassistant/components/gios/translations/tr.json @@ -4,7 +4,24 @@ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_sensors_data": "Bu \u00f6l\u00e7\u00fcm istasyonu i\u00e7in ge\u00e7ersiz sens\u00f6r verileri.", + "wrong_station_id": "\u00d6l\u00e7\u00fcm istasyonunun kimli\u011fi do\u011fru de\u011fil." + }, + "step": { + "user": { + "data": { + "name": "Ad", + "station_id": "\u00d6l\u00e7\u00fcm istasyonunun kimli\u011fi" + }, + "description": "GIO\u015a (Polonya \u00c7evre Koruma Ba\u015f M\u00fcfetti\u015fli\u011fi) hava kalitesi entegrasyonunu kurun. Yap\u0131land\u0131rmayla ilgili yard\u0131ma ihtiyac\u0131n\u0131z varsa buraya bak\u0131n: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polonya \u00c7evre Koruma Ba\u015f M\u00fcfetti\u015fli\u011fi)" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "GIO\u015a sunucusuna ula\u015f\u0131n" } } } \ No newline at end of file diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json index 69f0cd7ceb1..8815382da1e 100644 --- a/homeassistant/components/glances/translations/tr.json +++ b/homeassistant/components/glances/translations/tr.json @@ -4,16 +4,22 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "wrong_version": "S\u00fcr\u00fcm desteklenmiyor (yaln\u0131zca 2 veya 3)" }, "step": { "user": { "data": { "host": "Ana Bilgisayar", + "name": "Ad", "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n", + "version": "Glances API S\u00fcr\u00fcm\u00fc (2 veya 3)" + }, + "title": "Glances Kurulumu" } } }, @@ -22,7 +28,8 @@ "init": { "data": { "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" - } + }, + "description": "Glances i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index 8579e2d71ec..dbecadaed75 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", + "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "unknown": "Beklenmeyen hata" }, "step": { @@ -14,8 +17,11 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" - } + "host": "Ana Bilgisayar", + "name": "Ad" + }, + "description": "\u00d6ncelikle Goal Zero uygulamas\u0131n\u0131 indirmeniz gerekiyor: https://www.goalzero.com/product-features/yeti-app/ \n\n Yeti'nizi Wi-fi a\u011f\u0131n\u0131za ba\u011flamak i\u00e7in talimatlar\u0131 izleyin. Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json index 2893d89d2ec..cc4ae70d7b2 100644 --- a/homeassistant/components/gogogate2/translations/tr.json +++ b/homeassistant/components/gogogate2/translations/tr.json @@ -14,7 +14,9 @@ "ip_address": "\u0130p Adresi", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "A\u015fa\u011f\u0131da gerekli bilgileri sa\u011flay\u0131n.", + "title": "Gogogate2 veya ismartgate'i kurun" } } } diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json index 0e31b33a598..0b004c92986 100644 --- a/homeassistant/components/google_travel_time/translations/tr.json +++ b/homeassistant/components/google_travel_time/translations/tr.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { + "api_key": "API Anahtar\u0131", "destination": "Hedef", + "name": "Ad", "origin": "Men\u015fei" }, "description": "Ba\u015flang\u0131\u00e7 ve var\u0131\u015f yerini belirtirken, bir adres, enlem/boylam koordinatlar\u0131 veya bir Google yer kimli\u011fi bi\u00e7iminde dikey \u00e7izgi karakteriyle ayr\u0131lm\u0131\u015f bir veya daha fazla konum sa\u011flayabilirsiniz. Bir Google yer kimli\u011fi kullanarak konumu belirtirken, kimli\u011fin \u00f6n\u00fcne 'place_id:' eklenmelidir." @@ -14,10 +22,12 @@ "step": { "init": { "data": { + "avoid": "Ka\u00e7\u0131nmak", "language": "Dil", "mode": "Seyahat Modu", "time": "Zaman", "time_type": "Zaman T\u00fcr\u00fc", + "transit_mode": "Transit Modu", "transit_routing_preference": "Toplu Ta\u015f\u0131ma Tercihi", "units": "Birimler" }, diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json index 84adcdf8225..ef10b98c5df 100644 --- a/homeassistant/components/gpslogger/translations/tr.json +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in GPSLogger'da webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "GPSLogger Webhook'u kurmak istedi\u011finizden emin misiniz?", + "title": "GPSLogger Webhook'u kurun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json index 8de4663957e..3df15466f03 100644 --- a/homeassistant/components/gree/translations/tr.json +++ b/homeassistant/components/gree/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/growatt_server/translations/tr.json b/homeassistant/components/growatt_server/translations/tr.json index b460f88dfae..482ed6a427c 100644 --- a/homeassistant/components/growatt_server/translations/tr.json +++ b/homeassistant/components/growatt_server/translations/tr.json @@ -3,13 +3,23 @@ "abort": { "no_plants": "Bu hesapta bitki bulunamad\u0131" }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "plant": { "data": { "plant_id": "Bitki" - } + }, + "title": "Tesisinizi se\u00e7in" }, "user": { + "data": { + "name": "Ad", + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, "title": "Growatt bilgilerinizi girin" } } diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index f1326dab9b0..92e0f484370 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -13,7 +13,11 @@ "data": { "ip_address": "\u0130p Adresi", "port": "Port" - } + }, + "description": "Yerel bir Elexa Guardian cihaz\u0131 yap\u0131land\u0131r\u0131n." + }, + "zeroconf_confirm": { + "description": "Bu Guardian cihaz\u0131n\u0131 kurmak istiyor musunuz?" } } } diff --git a/homeassistant/components/habitica/translations/tr.json b/homeassistant/components/habitica/translations/tr.json index f77cc77798c..32ad5aa4957 100644 --- a/homeassistant/components/habitica/translations/tr.json +++ b/homeassistant/components/habitica/translations/tr.json @@ -1,3 +1,20 @@ { + "config": { + "error": { + "invalid_credentials": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "api_user": "Habitica'n\u0131n API kullan\u0131c\u0131 kimli\u011fi", + "name": "Habitica'n\u0131n kullan\u0131c\u0131 ad\u0131n\u0131 ge\u00e7ersiz k\u0131l. Servis \u00e7a\u011fr\u0131lar\u0131 i\u00e7in kullan\u0131lacakt\u0131r", + "url": "URL" + }, + "description": "Kullan\u0131c\u0131n\u0131z\u0131n profilinin ve g\u00f6revlerinin izlenmesine izin vermek i\u00e7in Habitica profilinizi ba\u011flay\u0131n. api_id ve api_key'in https://habitica.com/user/settings/api adresinden al\u0131nmas\u0131 gerekti\u011fini unutmay\u0131n." + } + } + }, "title": "Habitica" } \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json index a204200a2d8..84fb80abaf5 100644 --- a/homeassistant/components/hangouts/translations/tr.json +++ b/homeassistant/components/hangouts/translations/tr.json @@ -4,12 +4,27 @@ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "unknown": "Beklenmeyen hata" }, + "error": { + "invalid_2fa": "Ge\u00e7ersiz 2 Fakt\u00f6rl\u00fc Kimlik Do\u011frulama, l\u00fctfen tekrar deneyin.", + "invalid_2fa_method": "Ge\u00e7ersiz 2FA Y\u00f6ntemi (Telefonda do\u011frulay\u0131n).", + "invalid_login": "Ge\u00e7ersiz Giri\u015f, l\u00fctfen tekrar deneyin." + }, "step": { + "2fa": { + "data": { + "2fa": "2FA PIN'i" + }, + "description": "Bo\u015f", + "title": "2-Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" + }, "user": { "data": { + "authorization_code": "Yetkilendirme Kodu (manuel kimlik do\u011frulama i\u00e7in gereklidir)", "email": "E-posta", "password": "Parola" - } + }, + "description": "Bo\u015f", + "title": "Google Hangouts Giri\u015fi" } } } diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index c77f0f8e07e..4236ff33f66 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -7,11 +7,29 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name}", "step": { + "link": { + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "title": "Logitech Harmony Hub'\u0131 Kur" + }, "user": { "data": { - "host": "Ana Bilgisayar" - } + "host": "Ana Bilgisayar", + "name": "Hub Ad\u0131" + }, + "title": "Logitech Harmony Hub'\u0131 Kur" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Hi\u00e7biri belirtilmedi\u011finde y\u00fcr\u00fct\u00fclecek varsay\u0131lan etkinlik.", + "delay_secs": "Komut g\u00f6nderme aras\u0131ndaki gecikme." + }, + "description": "Harmony Hub Se\u00e7eneklerini Ayarlay\u0131n" } } } diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json index 4f1ad775905..cd4a7edb067 100644 --- a/homeassistant/components/heos/translations/tr.json +++ b/homeassistant/components/heos/translations/tr.json @@ -10,7 +10,9 @@ "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "L\u00fctfen bir Heos cihaz\u0131n\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin (tercihen a\u011fa kabloyla ba\u011fl\u0131 olan).", + "title": "Heos'a ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/tr.json b/homeassistant/components/hisense_aehw4a1/translations/tr.json index a893a653a78..e2404aaa686 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/tr.json +++ b/homeassistant/components/hisense_aehw4a1/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/hive/translations/tr.json b/homeassistant/components/hive/translations/tr.json index 14a363501e9..afc0ee66f03 100644 --- a/homeassistant/components/hive/translations/tr.json +++ b/homeassistant/components/hive/translations/tr.json @@ -1,10 +1,52 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown_entry": "Mevcut giri\u015f bulunamad\u0131." + }, + "error": { + "invalid_code": "Hive'da oturum a\u00e7\u0131lamad\u0131. \u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama kodunuz yanl\u0131\u015ft\u0131.", + "invalid_password": "Hive'da oturum a\u00e7\u0131lamad\u0131. Yanl\u0131\u015f \u015fifre. L\u00fctfen tekrar deneyin.", + "invalid_username": "Hive'da oturum a\u00e7\u0131lamad\u0131. E-posta adresiniz tan\u0131nm\u0131yor.", + "no_internet_available": "Hive'a ba\u011flanmak i\u00e7in internet ba\u011flant\u0131s\u0131 gereklidir.", + "unknown": "Beklenmeyen hata" + }, "step": { "2fa": { "data": { "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" - } + }, + "description": "Hive kimlik do\u011frulama kodunuzu girin. \n\n Ba\u015fka bir kod istemek i\u00e7in l\u00fctfen 0000 kodunu girin.", + "title": "Hive \u0130ki Fakt\u00f6rl\u00fc Kimlik Do\u011frulama." + }, + "reauth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Hive giri\u015f bilgilerinizi tekrar girin.", + "title": "Hive Giri\u015f yap" + }, + "user": { + "data": { + "password": "Parola", + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Hive oturum a\u00e7ma bilgilerinizi ve yap\u0131land\u0131rman\u0131z\u0131 girin.", + "title": "Hive Giri\u015f yap" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" + }, + "description": "Verileri daha s\u0131k kontrol etmek i\u00e7in tarama aral\u0131\u011f\u0131n\u0131 g\u00fcncelleyin.", + "title": "Hive i\u00e7in Se\u00e7enekler" } } } diff --git a/homeassistant/components/home_connect/translations/tr.json b/homeassistant/components/home_connect/translations/tr.json new file mode 100644 index 00000000000..58624199557 --- /dev/null +++ b/homeassistant/components/home_connect/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/tr.json b/homeassistant/components/home_plus_control/translations/tr.json index aa4a5a5c029..0138716d548 100644 --- a/homeassistant/components/home_plus_control/translations/tr.json +++ b/homeassistant/components/home_plus_control/translations/tr.json @@ -1,3 +1,21 @@ { + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } + } + }, "title": "Legrand Home+ Kontrol" } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index 7161cd3db5a..6a93009fee4 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -24,6 +24,7 @@ "auto_start": "Otomatik ba\u015flatma (homekit.start hizmetini manuel olarak ar\u0131yorsan\u0131z devre d\u0131\u015f\u0131 b\u0131rak\u0131n)", "devices": "Cihazlar (Tetikleyiciler)" }, + "description": "Se\u00e7ilen her cihaz i\u00e7in programlanabilir anahtarlar olu\u015fturulur. Bir cihaz tetikleyicisi tetiklendi\u011finde, HomeKit bir otomasyon veya sahne \u00e7al\u0131\u015ft\u0131racak \u015fekilde yap\u0131land\u0131r\u0131labilir.", "title": "Geli\u015fmi\u015f yap\u0131land\u0131rma" }, "cameras": { @@ -44,9 +45,15 @@ }, "init": { "data": { + "include_domains": "\u0130\u00e7erecek etki alanlar\u0131", "mode": "Mod" }, - "description": "HomeKit, bir k\u00f6pr\u00fcy\u00fc veya tek bir aksesuar\u0131 g\u00f6sterecek \u015fekilde yap\u0131land\u0131r\u0131labilir. Aksesuar modunda yaln\u0131zca tek bir varl\u0131k kullan\u0131labilir. TV cihaz s\u0131n\u0131f\u0131na sahip medya oynat\u0131c\u0131lar\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in aksesuar modu gereklidir. \"Eklenecek alan adlar\u0131\"ndaki varl\u0131klar HomeKit'e dahil edilecektir. Bir sonraki ekranda bu listeye dahil edilecek veya hari\u00e7 tutulacak varl\u0131klar\u0131 se\u00e7ebileceksiniz." + "description": "HomeKit, bir k\u00f6pr\u00fcy\u00fc veya tek bir aksesuar\u0131 g\u00f6sterecek \u015fekilde yap\u0131land\u0131r\u0131labilir. Aksesuar modunda yaln\u0131zca tek bir varl\u0131k kullan\u0131labilir. TV cihaz s\u0131n\u0131f\u0131na sahip medya oynat\u0131c\u0131lar\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in aksesuar modu gereklidir. \"Eklenecek alan adlar\u0131\"ndaki varl\u0131klar HomeKit'e dahil edilecektir. Bir sonraki ekranda bu listeye dahil edilecek veya hari\u00e7 tutulacak varl\u0131klar\u0131 se\u00e7ebileceksiniz.", + "title": "Dahil edilecek alanlar\u0131 se\u00e7in." + }, + "yaml": { + "description": "Bu giri\u015f YAML arac\u0131l\u0131\u011f\u0131yla kontrol edilir", + "title": "HomeKit Se\u00e7eneklerini Ayarlay\u0131n" } } } diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index b4bf28379fd..7ddb32ade8e 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -1,19 +1,24 @@ { "config": { "abort": { + "accessory_not_found_error": "Cihaz art\u0131k bulunamad\u0131\u011f\u0131ndan e\u015fle\u015ftirme eklenemiyor.", "already_configured": "Aksesuar zaten bu denetleyici ile yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "already_paired": "Bu aksesuar zaten ba\u015fka bir cihazla e\u015fle\u015ftirilmi\u015f. L\u00fctfen aksesuar\u0131 s\u0131f\u0131rlay\u0131n ve tekrar deneyin.", "ignored_model": "Daha \u00f6zellikli tam yerel entegrasyon kullan\u0131labilir oldu\u011fundan, bu model i\u00e7in HomeKit deste\u011fi engellendi.", "invalid_config_entry": "Bu ayg\u0131t e\u015fle\u015fmeye haz\u0131r olarak g\u00f6steriliyor, ancak ev asistan\u0131nda ilk olarak kald\u0131r\u0131lmas\u0131 gereken \u00e7ak\u0131\u015fan bir yap\u0131land\u0131rma girdisi zaten var.", + "invalid_properties": "Cihaz taraf\u0131ndan a\u00e7\u0131klanan ge\u00e7ersiz \u00f6zellikler.", "no_devices": "E\u015flenmemi\u015f cihaz bulunamad\u0131" }, "error": { "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", "insecure_setup_code": "\u0130stenen kurulum kodu, \u00f6nemsiz do\u011fas\u0131 nedeniyle g\u00fcvenli de\u011fil. Bu aksesuar, temel g\u00fcvenlik gereksinimlerini kar\u015f\u0131lam\u0131yor.", + "max_peers_error": "Cihaz, \u00fccretsiz e\u015fle\u015ftirme depolama alan\u0131 olmad\u0131\u011f\u0131 i\u00e7in e\u015fle\u015ftirme eklemeyi reddetti.", + "pairing_failed": "Bu cihazla e\u015fle\u015fmeye \u00e7al\u0131\u015f\u0131l\u0131rken i\u015flenmeyen bir hata olu\u015ftu. Bu ge\u00e7ici bir hata olabilir veya cihaz\u0131n\u0131z \u015fu anda desteklenmiyor olabilir.", "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, + "flow_title": "{name}", "step": { "busy_error": { "description": "T\u00fcm denetleyicilerde e\u015fle\u015ftirmeyi durdurun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", @@ -32,7 +37,8 @@ "title": "HomeKit Aksesuar Protokol\u00fc arac\u0131l\u0131\u011f\u0131yla bir cihazla e\u015fle\u015ftirin" }, "protocol_error": { - "description": "Cihaz e\u015fle\u015ftirme modunda olmayabilir ve fiziksel veya sanal bir d\u00fc\u011fmeye bas\u0131lmas\u0131n\u0131 gerektirebilir. Cihaz\u0131n e\u015fle\u015ftirme modunda oldu\u011fundan emin olun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin." + "description": "Cihaz e\u015fle\u015ftirme modunda olmayabilir ve fiziksel veya sanal bir d\u00fc\u011fmeye bas\u0131lmas\u0131n\u0131 gerektirebilir. Cihaz\u0131n e\u015fle\u015ftirme modunda oldu\u011fundan emin olun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", + "title": "Aksesuarla ileti\u015fim kurma hatas\u0131" }, "user": { "data": { @@ -56,6 +62,11 @@ "button8": "D\u00fc\u011fme 8", "button9": "D\u00fc\u011fme 9", "doorbell": "Kap\u0131 zili" + }, + "trigger_type": { + "double_press": "\" {subtype} \" iki kez bas\u0131ld\u0131", + "long_press": "\" {subtype} \" bas\u0131l\u0131 tutuldu", + "single_press": "\" {subtype} \" bas\u0131ld\u0131" } }, "title": "HomeKit Denetleyicisi" diff --git a/homeassistant/components/homematicip_cloud/translations/tr.json b/homeassistant/components/homematicip_cloud/translations/tr.json index 72f139217ca..3654357064e 100644 --- a/homeassistant/components/homematicip_cloud/translations/tr.json +++ b/homeassistant/components/homematicip_cloud/translations/tr.json @@ -4,6 +4,26 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "connection_aborted": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" + }, + "error": { + "invalid_sgtin_or_pin": "Ge\u00e7ersiz SGTIN veya PIN Kodu , l\u00fctfen tekrar deneyin.", + "press_the_button": "L\u00fctfen mavi d\u00fc\u011fmeye bas\u0131n.", + "register_failed": "Kay\u0131t ba\u015far\u0131s\u0131z oldu, l\u00fctfen tekrar deneyin.", + "timeout_button": "Mavi d\u00fc\u011fmeye basma zaman a\u015f\u0131m\u0131, l\u00fctfen tekrar deneyin." + }, + "step": { + "init": { + "data": { + "hapid": "Eri\u015fim noktas\u0131 kimli\u011fi (SGTIN)", + "name": "Ad (iste\u011fe ba\u011fl\u0131, t\u00fcm cihazlar i\u00e7in ad \u00f6neki olarak kullan\u0131l\u0131r)", + "pin": "PIN Kodu" + }, + "title": "HomematicIP Eri\u015fim noktas\u0131 se\u00e7in" + }, + "link": { + "description": "HomematicIP'i Home Assistant ile kaydetmek i\u00e7in eri\u015fim noktas\u0131ndaki mavi d\u00fc\u011fmeye ve g\u00f6nder d\u00fc\u011fmesine bas\u0131n. \n\n ![K\u00f6pr\u00fcdeki d\u00fc\u011fmenin konumu](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Ba\u011flant\u0131 Eri\u015fim noktas\u0131" + } } } } \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/tr.json b/homeassistant/components/honeywell/translations/tr.json index d40551917e1..e6eb57aca1f 100644 --- a/homeassistant/components/honeywell/translations/tr.json +++ b/homeassistant/components/honeywell/translations/tr.json @@ -1,8 +1,16 @@ { "config": { + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "user": { - "description": "L\u00fctfen mytotalconnectcomfort.com'da oturum a\u00e7mak i\u00e7in kullan\u0131lan kimlik bilgilerini girin." + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen mytotalconnectcomfort.com'da oturum a\u00e7mak i\u00e7in kullan\u0131lan kimlik bilgilerini girin.", + "title": "Honeywell Toplam Ba\u011flant\u0131 Konforu (ABD)" } } } diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 0ec49564e62..741b8ec7d47 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -18,6 +18,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e. \u041f\u043e\u0441\u043e\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435 \u0435 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e, \u043d\u043e \u0434\u0430\u0432\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0437\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435. \u041e\u0442 \u0434\u0440\u0443\u0433\u0430 \u0441\u0442\u0440\u0430\u043d\u0430, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0434\u043e\u0432\u0435\u0434\u0435 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u0432\u044a\u043d Home Assistant, \u0434\u043e\u043a\u0430\u0442\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0442\u043e.", diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index fbbce0e6e48..23c47fd9288 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil" }, "error": { "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", @@ -22,7 +23,8 @@ "url": "URL", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi." + "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi.", + "title": "Huawei LTE'yi yap\u0131land\u0131r\u0131n" } } }, @@ -30,6 +32,7 @@ "step": { "init": { "data": { + "name": "Bildirim hizmeti ad\u0131 (de\u011fi\u015fiklik yeniden ba\u015flatmay\u0131 gerektirir)", "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131", "track_new_devices": "Yeni cihazlar\u0131 izle", "track_wired_clients": "Kablolu a\u011f istemcilerini izleyin", diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index a162c947024..eabbf86d1dd 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -7,6 +7,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "discover_timeout": "Hue k\u00f6pr\u00fcleri bulunam\u0131yor", "no_bridges": "Philips Hue k\u00f6pr\u00fcs\u00fc bulunamad\u0131", + "not_hue_bridge": "Hue k\u00f6pr\u00fcs\u00fc de\u011fil", "unknown": "Beklenmeyen hata" }, "error": { @@ -21,7 +22,8 @@ "title": "Hue k\u00f6pr\u00fcs\u00fcn\u00fc se\u00e7in" }, "link": { - "description": "Philips Hue'yu Home Assistant'a kaydetmek i\u00e7in k\u00f6pr\u00fcdeki d\u00fc\u011fmeye bas\u0131n. \n\n ![K\u00f6pr\u00fcdeki d\u00fc\u011fmenin konumu](/static/images/config_philips_hue.jpg)" + "description": "Philips Hue'yu Home Assistant'a kaydetmek i\u00e7in k\u00f6pr\u00fcdeki d\u00fc\u011fmeye bas\u0131n. \n\n ![K\u00f6pr\u00fcdeki d\u00fc\u011fmenin konumu](/static/images/config_philips_hue.jpg)", + "title": "Ba\u011flant\u0131 Merkezi" }, "manual": { "data": { @@ -37,10 +39,19 @@ "button_2": "\u0130kinci d\u00fc\u011fme", "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "dim_down": "K\u0131sma", + "dim_up": "A\u00e7ma", "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler", "double_buttons_2_4": "\u0130kinci ve D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fmeler", "turn_off": "Kapat", "turn_on": "A\u00e7" + }, + "trigger_type": { + "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", + "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", + "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", + "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131" } }, "options": { diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json index 80f1529066b..c7d38c19457 100644 --- a/homeassistant/components/huisbaasje/translations/tr.json +++ b/homeassistant/components/huisbaasje/translations/tr.json @@ -4,6 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen Hata" }, diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json index 920a4e99cbe..08808ca696d 100644 --- a/homeassistant/components/humidifier/translations/tr.json +++ b/homeassistant/components/humidifier/translations/tr.json @@ -3,6 +3,8 @@ "action_type": { "set_humidity": "{entity_name} i\u00e7in nemi ayarla", "set_mode": "{entity_name} \u00fczerindeki mod de\u011fi\u015ftirme", + "toggle": "{entity_name} de\u011fi\u015ftir", + "turn_off": "{entity_name} kapat", "turn_on": "{entity_name} a\u00e7\u0131n" }, "condition_type": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json index 01b0359789e..e63fe26ec76 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/tr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -7,11 +7,17 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({host})", "step": { + "link": { + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "title": "PowerView Hub'a ba\u011flan\u0131n" + }, "user": { "data": { "host": "\u0130p Adresi" - } + }, + "title": "PowerView Hub'a ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/hvv_departures/translations/tr.json b/homeassistant/components/hvv_departures/translations/tr.json index 74fc593062b..ce6250d10bd 100644 --- a/homeassistant/components/hvv_departures/translations/tr.json +++ b/homeassistant/components/hvv_departures/translations/tr.json @@ -5,15 +5,42 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_results": "Sonu\u00e7 yok. Farkl\u0131 bir istasyon/adres ile deneyin" }, "step": { + "station": { + "data": { + "station": "\u0130stasyon/Adres" + }, + "title": "\u0130stasyon/Adres Girin" + }, + "station_select": { + "data": { + "station": "\u0130stasyon/Adres" + }, + "title": "\u0130stasyon/Adres Se\u00e7in" + }, "user": { "data": { "host": "Ana Bilgisayar", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "HVV API'sine ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "Sat\u0131rlar\u0131 se\u00e7in", + "offset": "Uzakl\u0131k (dakika)", + "real_time": "Ger\u00e7ek zamanl\u0131 verileri kullan\u0131n" + }, + "description": "Bu hareket sens\u00f6r\u00fc i\u00e7in se\u00e7enekleri de\u011fi\u015ftirin", + "title": "Se\u00e7enekler" } } } diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 23485014065..821d7df43e3 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -19,7 +19,8 @@ "data": { "create_token": "Otomatik olarak yeni belirte\u00e7 olu\u015fturma", "token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n" - } + }, + "description": "Hyperion Ambilight sunucunuz i\u00e7in yetkilendirmeyi yap\u0131land\u0131r\u0131n" }, "confirm": { "description": "Bu Hyperion Ambilight'\u0131 Home Assistant'a eklemek istiyor musunuz? \n\n **Ana bilgisayar:** {host}\n **:** {port}\n **Kimlik**: {id}", @@ -44,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Hyperion efektleri g\u00f6sterilecek", "priority": "Renkler ve efektler i\u00e7in kullan\u0131lacak hyperion \u00f6nceli\u011fi" } } diff --git a/homeassistant/components/ialarm/translations/tr.json b/homeassistant/components/ialarm/translations/tr.json index 21a477c75a7..b93c9b04dc3 100644 --- a/homeassistant/components/ialarm/translations/tr.json +++ b/homeassistant/components/ialarm/translations/tr.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/tr.json b/homeassistant/components/iaqualink/translations/tr.json index c2c70f3e45b..43d3a4ef044 100644 --- a/homeassistant/components/iaqualink/translations/tr.json +++ b/homeassistant/components/iaqualink/translations/tr.json @@ -12,7 +12,8 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "L\u00fctfen iAqualink hesab\u0131n\u0131z i\u00e7in kullan\u0131c\u0131 ad\u0131 ve parolay\u0131 girin." + "description": "L\u00fctfen iAqualink hesab\u0131n\u0131z i\u00e7in kullan\u0131c\u0131 ad\u0131 ve parolay\u0131 girin.", + "title": "iAqualink'e ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index d8431c666bf..405b73571f1 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -15,7 +15,8 @@ "data": { "password": "Parola" }, - "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin." + "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin.", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/tr.json b/homeassistant/components/ifttt/translations/tr.json index 84adcdf8225..b42268fa889 100644 --- a/homeassistant/components/ifttt/translations/tr.json +++ b/homeassistant/components/ifttt/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [IFTTT Webhook uygulamas\u0131ndan]( {applet_url} ) \"Web iste\u011fi yap\" eylemini kullanman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131 hakk\u0131nda [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "IFTTT'yi kurmak istedi\u011finizden emin misiniz?", + "title": "IFTTT Webhook Uygulamas\u0131n\u0131 ayarlay\u0131n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index 3f29fc43cbd..65bf4bad59a 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -5,7 +5,8 @@ "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "select_single": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u0430 \u043e\u043f\u0446\u0438\u044f." }, "step": { "hubv1": { @@ -31,7 +32,10 @@ "step": { "change_hub_config": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index 6c41f53b31e..f5d5abefbfa 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -5,7 +5,8 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "select_single": "Bir se\u00e7enek belirleyin." }, "step": { "hubv1": { @@ -13,6 +14,7 @@ "host": "\u0130p Adresi", "port": "Port" }, + "description": "Insteon Hub S\u00fcr\u00fcm 1'i (2014 \u00f6ncesi) yap\u0131land\u0131r\u0131n.", "title": "Insteon Hub S\u00fcr\u00fcm 1" }, "hubv2": { @@ -26,7 +28,11 @@ "title": "Insteon Hub S\u00fcr\u00fcm 2" }, "plm": { - "description": "Insteon PowerLink Modemini (PLM) yap\u0131land\u0131r\u0131n." + "data": { + "device": "USB Cihaz Yolu" + }, + "description": "Insteon PowerLink Modemini (PLM) yap\u0131land\u0131r\u0131n.", + "title": "Insteon PLM'si" }, "user": { "data": { @@ -40,11 +46,24 @@ "options": { "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "input_error": "Ge\u00e7ersiz giri\u015fler, l\u00fctfen de\u011ferlerinizi kontrol edin." + "input_error": "Ge\u00e7ersiz giri\u015fler, l\u00fctfen de\u011ferlerinizi kontrol edin.", + "select_single": "Bir se\u00e7enek belirleyin." }, "step": { + "add_override": { + "data": { + "address": "Cihaz adresi (\u00f6rnek: 1a2b3c)", + "cat": "Cihaz alt kategorisi (yani 0x0a)", + "subcat": "Cihaz alt kategorisi (yani 0x0a)" + }, + "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.", + "title": "Insteon" + }, "add_x10": { "data": { + "housecode": "Ev kodu (a - p)", + "platform": "Platform", + "steps": "Dimmer ad\u0131mlar\u0131 (yaln\u0131zca hafif cihazlar i\u00e7in varsay\u0131lan 22)", "unitcode": "Birim kodu (1-16)" }, "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.", @@ -57,6 +76,7 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" }, + "description": "Insteon Hub ba\u011flant\u0131 bilgilerini de\u011fi\u015ftirin. Bu de\u011fi\u015fikli\u011fi yapt\u0131ktan sonra Home Assistant'\u0131 yeniden ba\u015flatman\u0131z gerekir. Bu, Hub'\u0131n yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirmez. Hub'daki yap\u0131land\u0131rmay\u0131 de\u011fi\u015ftirmek i\u00e7in Hub uygulamas\u0131n\u0131 kullan\u0131n.", "title": "Insteon" }, "init": { diff --git a/homeassistant/components/iotawatt/translations/tr.json b/homeassistant/components/iotawatt/translations/tr.json index 28c3dd587ff..af6af5b8633 100644 --- a/homeassistant/components/iotawatt/translations/tr.json +++ b/homeassistant/components/iotawatt/translations/tr.json @@ -1,8 +1,22 @@ { "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "auth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, "description": "IoTawatt cihaz\u0131 kimlik do\u011frulama gerektirir. L\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131 ve \u015fifreyi girin ve G\u00f6nder d\u00fc\u011fmesine t\u0131klay\u0131n." + }, + "user": { + "data": { + "host": "Ana bilgisayar" + } } } } diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json index a8df63645ab..6ef99de4832 100644 --- a/homeassistant/components/ipma/translations/tr.json +++ b/homeassistant/components/ipma/translations/tr.json @@ -1,12 +1,18 @@ { "config": { + "error": { + "name_exists": "Bu ad zaten var" + }, "step": { "user": { "data": { "latitude": "Enlem", "longitude": "Boylam", - "mode": "Mod" - } + "mode": "Mod", + "name": "Ad" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Konum" } } }, diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index 78b9a868bd2..a7907cc6cda 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -2,7 +2,12 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_upgrade": "Ba\u011flant\u0131 y\u00fckseltmesi gerekti\u011finden yaz\u0131c\u0131ya ba\u011flan\u0131lamad\u0131.", + "ipp_error": "IPP hatas\u0131yla kar\u015f\u0131la\u015f\u0131ld\u0131.", + "ipp_version_error": "IPP s\u00fcr\u00fcm\u00fc yaz\u0131c\u0131 taraf\u0131ndan desteklenmiyor.", + "parse_error": "Yaz\u0131c\u0131dan gelen yan\u0131t ayr\u0131\u015ft\u0131r\u0131lamad\u0131.", + "unique_id_required": "Ke\u015fif i\u00e7in cihaz\u0131n benzersiz kimli\u011fi eksik." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -12,9 +17,13 @@ "step": { "user": { "data": { + "base_path": "Yaz\u0131c\u0131n\u0131n yolu", "host": "Ana Bilgisayar", - "port": "Port" + "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, + "description": "Home Assistant ile entegre olmas\u0131 i\u00e7in yaz\u0131c\u0131n\u0131z\u0131 \u0130nternet Yazd\u0131rma Protokol\u00fc (IPP) arac\u0131l\u0131\u011f\u0131yla kurun.", "title": "Yaz\u0131c\u0131n\u0131z\u0131 ba\u011flay\u0131n" }, "zeroconf_confirm": { diff --git a/homeassistant/components/iqvia/translations/tr.json b/homeassistant/components/iqvia/translations/tr.json index 717f6d72b94..0b8a702d696 100644 --- a/homeassistant/components/iqvia/translations/tr.json +++ b/homeassistant/components/iqvia/translations/tr.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_zip_code": "Posta kodu ge\u00e7ersiz" + }, + "step": { + "user": { + "data": { + "zip_code": "Posta kodu" + }, + "description": "ABD veya Kanada Posta kodunuzu doldurun.", + "title": "IQVIA" + } } } } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/tr.json b/homeassistant/components/islamic_prayer_times/translations/tr.json index 6385ef1e89f..a3cf464873d 100644 --- a/homeassistant/components/islamic_prayer_times/translations/tr.json +++ b/homeassistant/components/islamic_prayer_times/translations/tr.json @@ -10,5 +10,14 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Dua hesaplama y\u00f6ntemi" + } + } + } + }, "title": "\u0130slami Namaz Vakitleri" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index d2d58a89f31..78fc66196f1 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -9,13 +9,17 @@ "invalid_host": "Ana bilgisayar giri\u015fi tam URL bi\u00e7iminde de\u011fildi, \u00f6r. http://192.168.10.100:80", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "URL", "password": "Parola", + "tls": "ISY denetleyicisinin TLS s\u00fcr\u00fcm\u00fc.", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Ana bilgisayar giri\u015fi tam URL bi\u00e7iminde olmal\u0131d\u0131r, \u00f6r. http://192.168.10.100:80", + "title": "ISY994'\u00fcn\u00fcze ba\u011flan\u0131n" } } }, @@ -23,10 +27,22 @@ "step": { "init": { "data": { + "ignore_string": "Dizeyi Yoksay", + "restore_light_state": "I\u015f\u0131k Parlakl\u0131\u011f\u0131n\u0131 Geri Y\u00fckle", + "sensor_string": "D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi", "variable_sensor_string": "De\u011fi\u015fken Sens\u00f6r Dizesi" }, - "description": "ISY Entegrasyonu i\u00e7in se\u00e7enekleri ayarlay\u0131n:\n \u2022 D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi: Ad\u0131nda 'D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi' i\u00e7eren herhangi bir cihaz veya klas\u00f6r, bir sens\u00f6r veya ikili sens\u00f6r olarak ele al\u0131nacakt\u0131r.\n \u2022 Ignore String: Ad\u0131nda 'Ignore String' olan herhangi bir cihaz yoksay\u0131lacakt\u0131r.\n \u2022 De\u011fi\u015fken Sens\u00f6r Dizisi: 'De\u011fi\u015fken Sens\u00f6r Dizisi' i\u00e7eren herhangi bir de\u011fi\u015fken sens\u00f6r olarak eklenecektir.\n \u2022 I\u015f\u0131k Parlakl\u0131\u011f\u0131n\u0131 Geri Y\u00fckle: Etkinle\u015ftirilirse, bir \u0131\u015f\u0131k a\u00e7\u0131ld\u0131\u011f\u0131nda cihaz\u0131n yerle\u015fik On-Level yerine \u00f6nceki parlakl\u0131k geri y\u00fcklenir." + "description": "ISY Entegrasyonu i\u00e7in se\u00e7enekleri ayarlay\u0131n:\n \u2022 D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi: Ad\u0131nda 'D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi' i\u00e7eren herhangi bir cihaz veya klas\u00f6r, bir sens\u00f6r veya ikili sens\u00f6r olarak ele al\u0131nacakt\u0131r.\n \u2022 Ignore String: Ad\u0131nda 'Ignore String' olan herhangi bir cihaz yoksay\u0131lacakt\u0131r.\n \u2022 De\u011fi\u015fken Sens\u00f6r Dizisi: 'De\u011fi\u015fken Sens\u00f6r Dizisi' i\u00e7eren herhangi bir de\u011fi\u015fken sens\u00f6r olarak eklenecektir.\n \u2022 I\u015f\u0131k Parlakl\u0131\u011f\u0131n\u0131 Geri Y\u00fckle: Etkinle\u015ftirilirse, bir \u0131\u015f\u0131k a\u00e7\u0131ld\u0131\u011f\u0131nda cihaz\u0131n yerle\u015fik On-Level yerine \u00f6nceki parlakl\u0131k geri y\u00fcklenir.", + "title": "ISY994 Se\u00e7enekleri" } } + }, + "system_health": { + "info": { + "device_connected": "ISY Ba\u011fl\u0131", + "host_reachable": "Ana Bilgisayara Ula\u015f\u0131labilir", + "last_heartbeat": "Son Kalp At\u0131\u015f\u0131 Zaman\u0131", + "websocket_status": "Olay Soketi Durumu" + } } } \ No newline at end of file diff --git a/homeassistant/components/izone/translations/tr.json b/homeassistant/components/izone/translations/tr.json index faa20ed0ece..01a709e9924 100644 --- a/homeassistant/components/izone/translations/tr.json +++ b/homeassistant/components/izone/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/keenetic_ndms2/translations/tr.json b/homeassistant/components/keenetic_ndms2/translations/tr.json new file mode 100644 index 00000000000..099ca45fc71 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/tr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_udn": "SSDP ke\u015fif bilgisinin UDN'si yok", + "not_keenetic_ndms2": "Bulunan \u00f6\u011fe bir Keenetic y\u00f6nlendirici de\u011fil" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Keenetic NDMS2 Router'\u0131 kurun" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Ev aral\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun", + "include_arp": "ARP verilerini kullan (hotspot verileri kullan\u0131l\u0131yorsa yoksay\u0131l\u0131r)", + "include_associated": "WiFi AP ili\u015fkilendirme verilerini kullan (hotspot verileri kullan\u0131l\u0131yorsa yoksay\u0131l\u0131r)", + "interfaces": "Taranacak aray\u00fczleri se\u00e7in", + "scan_interval": "Tarama aral\u0131\u011f\u0131", + "try_hotspot": "'ip hotspot' verilerini kullan\u0131n (en do\u011frusu)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/tr.json b/homeassistant/components/kmtronic/translations/tr.json new file mode 100644 index 00000000000..c10979d7c83 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Ters anahtar mant\u0131\u011f\u0131 (NC kullan\u0131n)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/bg.json b/homeassistant/components/kodi/translations/bg.json index 2d54c793bb5..f92d4a5901c 100644 --- a/homeassistant/components/kodi/translations/bg.json +++ b/homeassistant/components/kodi/translations/bg.json @@ -1,14 +1,24 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{name}", "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json index 1aac43cefec..652dc2b1fc4 100644 --- a/homeassistant/components/kodi/translations/tr.json +++ b/homeassistant/components/kodi/translations/tr.json @@ -4,6 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_uuid": "Kodi \u00f6rne\u011finin benzersiz bir kimli\u011fi yok. Bu b\u00fcy\u00fck olas\u0131l\u0131kla eski bir Kodi s\u00fcr\u00fcm\u00fcnden (17.x veya alt\u0131) kaynaklanmaktad\u0131r. Entegrasyonu manuel olarak yap\u0131land\u0131rabilir veya daha yeni bir Kodi s\u00fcr\u00fcm\u00fcne y\u00fckseltebilirsiniz.", "unknown": "Beklenmeyen hata" }, "error": { @@ -20,17 +21,30 @@ }, "description": "L\u00fctfen Kodi kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bunlar Sistem / Ayarlar / A\u011f / Hizmetler'de bulunabilir." }, + "discovery_confirm": { + "description": "Ev Asistan\u0131'na Kodi ('{name}') eklemek istiyor musunuz?", + "title": "Ke\u015ffedilen Kodi" + }, "user": { "data": { "host": "Ana Bilgisayar", - "port": "Port" - } + "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r" + }, + "description": "Kodi ba\u011flant\u0131 bilgileri. L\u00fctfen Sistem/Ayarlar/A\u011f/Hizmetler'de \"Kodi'nin HTTP \u00fczerinden denetimine izin ver\" se\u00e7ene\u011fini etkinle\u015ftirdi\u011finizden emin olun." }, "ws_port": { "data": { "ws_port": "Port" - } + }, + "description": "WebSocket ba\u011flant\u0131 noktas\u0131 (bazen Kodi'de TCP ba\u011flant\u0131 noktas\u0131 olarak adland\u0131r\u0131l\u0131r). WebSocket \u00fczerinden ba\u011flanmak i\u00e7in Sistem/Ayarlar/A\u011f/Hizmetler'de \"Programlar\u0131n Kodi'yi kontrol etmesine izin ver\" se\u00e7ene\u011fini etkinle\u015ftirmeniz gerekir. WebSocket etkin de\u011filse, ba\u011flant\u0131 noktas\u0131n\u0131 kald\u0131r\u0131n ve bo\u015f b\u0131rak\u0131n." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} nin kapat\u0131lmas\u0131 istendi", + "turn_on": "{entity_name} nin a\u00e7\u0131lmas\u0131 istendi" + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index f86f09eeea7..af63e286172 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_konn_panel": "Bilinen bir Konnected.io cihaz\u0131 de\u011fil", "unknown": "Beklenmeyen hata" }, "error": { @@ -10,38 +11,63 @@ }, "step": { "confirm": { - "description": "Model: {model}\nID: {id}\nSunucu: {host}\nPort: {port}\n\nIO ve panel davran\u0131\u015f\u0131n\u0131 Ba\u011fl\u0131 Alarm Paneli ayarlar\u0131nda yap\u0131land\u0131rabilirsiniz." + "description": "Model: {model}\nID: {id}\nSunucu: {host}\nPort: {port}\n\nIO ve panel davran\u0131\u015f\u0131n\u0131 Ba\u011fl\u0131 Alarm Paneli ayarlar\u0131nda yap\u0131land\u0131rabilirsiniz.", + "title": "Konnected Cihaz\u0131 Haz\u0131r" }, "import_confirm": { - "description": "configuration.yaml'de {id} kimli\u011fine sahip Ba\u011fl\u0131 bir Alarm Paneli ke\u015ffedildi. Bu ak\u0131\u015f, onu bir yap\u0131land\u0131rma giri\u015fine aktarman\u0131za olanak tan\u0131r." + "description": "configuration.yaml'de {id} kimli\u011fine sahip Ba\u011fl\u0131 bir Alarm Paneli ke\u015ffedildi. Bu ak\u0131\u015f, onu bir yap\u0131land\u0131rma giri\u015fine aktarman\u0131za olanak tan\u0131r.", + "title": "Konnected Ayg\u0131t\u0131n\u0131 \u0130\u00e7eri Aktar" }, "user": { "data": { "host": "\u0130p Adresi", "port": "Port" - } + }, + "description": "L\u00fctfen Konnected Paneliniz i\u00e7in ana bilgisayar bilgilerini girin." } } }, "options": { + "abort": { + "not_konn_panel": "Bilinen bir Konnected.io cihaz\u0131 de\u011fil" + }, "error": { - "bad_host": "Ge\u00e7ersiz, Ge\u00e7ersiz K\u0131lma API ana makine url'si" + "bad_host": "Ge\u00e7ersiz, Ge\u00e7ersiz K\u0131lma API ana makine url'si", + "one": "Bo\u015f", + "other": "Bo\u015f" }, "step": { "options_binary": { "data": { - "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin" - } + "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin", + "name": "Ad (iste\u011fe ba\u011fl\u0131)", + "type": "\u0130kili Sens\u00f6r Tipi" + }, + "description": "{zone} se\u00e7enekleri", + "title": "\u0130kili Sens\u00f6r\u00fc Yap\u0131land\u0131r" + }, + "options_digital": { + "data": { + "name": "Ad (iste\u011fe ba\u011fl\u0131)", + "poll_interval": "Yoklama Aral\u0131\u011f\u0131 (dakika) (iste\u011fe ba\u011fl\u0131)", + "type": "Sens\u00f6r Tipi" + }, + "description": "{zone} se\u00e7enekleri", + "title": "Dijital Sens\u00f6r\u00fc Yap\u0131land\u0131r" }, "options_io": { "data": { + "1": "B\u00f6lge 1", + "2": "B\u00f6lge 2", "3": "B\u00f6lge 3", "4": "B\u00f6lge 4", "5": "B\u00f6lge 5", "6": "B\u00f6lge 6", "7": "B\u00f6lge 7", "out": "OUT" - } + }, + "description": "{host} bir {model} ke\u015ffetti. A\u015fa\u011f\u0131dan her G/\u00c7'\u0131n temel yap\u0131land\u0131rmas\u0131n\u0131 se\u00e7in - G/\u00c7'a ba\u011fl\u0131 olarak ikili sens\u00f6rlere (a\u00e7\u0131k/kapal\u0131 kontaklar), dijital sens\u00f6rlere (dht ve ds18b20) veya de\u011fi\u015ftirilebilir \u00e7\u0131k\u0131\u015flara izin verebilir. Sonraki ad\u0131mlarda ayr\u0131nt\u0131l\u0131 se\u00e7enekleri yap\u0131land\u0131rabileceksiniz.", + "title": "G/\u00c7'\u0131 yap\u0131land\u0131r" }, "options_io_ext": { "data": { @@ -53,13 +79,31 @@ "alarm1": "ALARM1", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" - } + }, + "description": "A\u015fa\u011f\u0131da kalan G/\u00c7'nin yap\u0131land\u0131rmas\u0131n\u0131 se\u00e7in. Sonraki ad\u0131mlarda ayr\u0131nt\u0131l\u0131 se\u00e7enekleri yap\u0131land\u0131rabileceksiniz.", + "title": "Geni\u015fletilmi\u015f G/\u00c7'yi Yap\u0131land\u0131r" }, "options_misc": { "data": { "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l (iste\u011fe ba\u011fl\u0131)", + "blink": "Durum de\u011fi\u015fikli\u011fi g\u00f6nderilirken panelin LED'ini yan\u0131p s\u00f6nd\u00fcr", + "discovery": "A\u011f\u0131n\u0131zdaki ke\u015fif isteklerine yan\u0131t verin", "override_api_host": "Varsay\u0131lan Home Assistant API ana bilgisayar paneli URL'sini ge\u00e7ersiz k\u0131l" - } + }, + "description": "L\u00fctfen paneliniz i\u00e7in istedi\u011finiz davran\u0131\u015f\u0131 se\u00e7in", + "title": "\u00c7e\u015fitli Yap\u0131land\u0131rma" + }, + "options_switch": { + "data": { + "activation": "A\u00e7\u0131kken \u00e7\u0131kt\u0131", + "momentary": "Darbe s\u00fcresi (ms) (iste\u011fe ba\u011fl\u0131)", + "more_states": "Bu b\u00f6lge i\u00e7in ek durumlar yap\u0131land\u0131r\u0131n", + "name": "Ad (iste\u011fe ba\u011fl\u0131)", + "pause": "Darbeler aras\u0131nda duraklama (ms) (iste\u011fe ba\u011fl\u0131)", + "repeat": "Tekrarlanacak zamanlar (-1=sonsuz) (iste\u011fe ba\u011fl\u0131)" + }, + "description": "{zone} se\u00e7enekleri: durum {state}", + "title": "De\u011fi\u015ftirilebilir \u00c7\u0131k\u0131\u015f\u0131 Yap\u0131land\u0131r" } } } diff --git a/homeassistant/components/kostal_plenticore/translations/tr.json b/homeassistant/components/kostal_plenticore/translations/tr.json new file mode 100644 index 00000000000..573047e61b5 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "password": "Parola" + } + } + } + }, + "title": "Kostal Plenticore Solar \u0130nverter" +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json index ac79bb4746f..fa3decd322e 100644 --- a/homeassistant/components/kraken/translations/tr.json +++ b/homeassistant/components/kraken/translations/tr.json @@ -1,10 +1,28 @@ { "config": { + "abort": { + "already_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, "step": { "user": { "data": { "one": "Bo\u015f", "other": "" + }, + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131", + "tracked_asset_pairs": "\u0130zlenen Varl\u0131k \u00c7iftleri" } } } diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index e1e57b39737..98c52010cb8 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -4,6 +4,9 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmedik hata" }, + "create_entry": { + "default": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n." + }, "error": { "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", @@ -15,7 +18,9 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n.\n Bunu hesap eklemeden \u00f6nce yapmak isteyebilirsiniz.", + "title": "Life360 Hesap Bilgileri" } } } diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index fc7532a1e34..ca4cfa92020 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/light/translations/tr.json b/homeassistant/components/light/translations/tr.json index 6fa71e8f339..21de5074bbd 100644 --- a/homeassistant/components/light/translations/tr.json +++ b/homeassistant/components/light/translations/tr.json @@ -1,4 +1,22 @@ { + "device_automation": { + "action_type": { + "brightness_decrease": "{entity_name} parlakl\u0131\u011f\u0131n\u0131 azalt", + "brightness_increase": "{entity_name} parlakl\u0131\u011f\u0131n\u0131 art\u0131r\u0131n", + "flash": "Fla\u015f {entity_name}", + "toggle": "{entity_name} de\u011fi\u015ftir", + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "condition_type": { + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/litejet/translations/tr.json b/homeassistant/components/litejet/translations/tr.json index de4ea12cb6f..55bc5a35195 100644 --- a/homeassistant/components/litejet/translations/tr.json +++ b/homeassistant/components/litejet/translations/tr.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "open_failed": "Belirtilen seri ba\u011flant\u0131 noktas\u0131 a\u00e7\u0131lam\u0131yor." + }, "step": { "user": { + "data": { + "port": "Port" + }, + "description": "LiteJet'in RS232-2 ba\u011flant\u0131 noktas\u0131n\u0131 bilgisayar\u0131n\u0131za ba\u011flay\u0131n ve seri ba\u011flant\u0131 noktas\u0131 ayg\u0131t\u0131n\u0131n yolunu girin. \n\n LiteJet MCP 19.2 K baud, 8 veri biti, 1 durdurma biti, e\u015fliksiz ve her yan\u0131ttan sonra bir 'CR' iletecek \u015fekilde yap\u0131land\u0131r\u0131lmal\u0131d\u0131r.", "title": "LiteJet'e Ba\u011flan\u0131n" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Varsay\u0131lan Ge\u00e7i\u015f (saniye)" + }, + "title": "LiteJet'i yap\u0131land\u0131r\u0131n" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json index 84adcdf8225..906abd1b2e5 100644 --- a/homeassistant/components/locative/translations/tr.json +++ b/homeassistant/components/locative/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Konumlar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Locative uygulamas\u0131nda webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Konum Belirleyici Webhook'u ayarlay\u0131n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lock/translations/tr.json b/homeassistant/components/lock/translations/tr.json index ea6ff1a157d..a6e12ea5a98 100644 --- a/homeassistant/components/lock/translations/tr.json +++ b/homeassistant/components/lock/translations/tr.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "lock": "{entity_name} kilitle", + "open": "{entity_name} a\u00e7\u0131n", + "unlock": "{entity_name} kilidini a\u00e7\u0131" + }, + "condition_type": { + "is_locked": "{entity_name} kilitli", + "is_unlocked": "{entity_name} kilidi a\u00e7\u0131ld\u0131" + }, "trigger_type": { "locked": "{entity_name} kilitlendi", "unlocked": "{entity_name} kilidi a\u00e7\u0131ld\u0131" diff --git a/homeassistant/components/logi_circle/translations/tr.json b/homeassistant/components/logi_circle/translations/tr.json index 54fdecd7dec..5073dad1b62 100644 --- a/homeassistant/components/logi_circle/translations/tr.json +++ b/homeassistant/components/logi_circle/translations/tr.json @@ -1,14 +1,27 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "external_error": "Ba\u015fka bir ak\u0131\u015ftan \u00f6zel durum olu\u015ftu.", + "external_setup": "Logi Circle, ba\u015fka bir ak\u0131\u015ftan ba\u015far\u0131yla yap\u0131land\u0131r\u0131ld\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." }, "error": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "follow_link": "L\u00fctfen ba\u011flant\u0131y\u0131 takip edin ve G\u00f6nder'e basmadan \u00f6nce kimli\u011finizi do\u011frulay\u0131n.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { "auth": { - "description": "L\u00fctfen a\u015fa\u011f\u0131daki ba\u011flant\u0131y\u0131 takip edin ve Logi Circle hesab\u0131n\u0131za eri\u015fimi **Kabul** edin, ard\u0131ndan geri d\u00f6n\u00fcn ve a\u015fa\u011f\u0131daki **G\u00f6nder**'e bas\u0131n. \n\n [Ba\u011flant\u0131]( {authorization_url} )" + "description": "L\u00fctfen a\u015fa\u011f\u0131daki ba\u011flant\u0131y\u0131 takip edin ve Logi Circle hesab\u0131n\u0131za eri\u015fimi **Kabul** edin, ard\u0131ndan geri d\u00f6n\u00fcn ve a\u015fa\u011f\u0131daki **G\u00f6nder**'e bas\u0131n. \n\n [Ba\u011flant\u0131]( {authorization_url} )", + "title": "Logi Circle ile kimlik do\u011frulamas\u0131" + }, + "user": { + "data": { + "flow_impl": "Sa\u011flay\u0131c\u0131" + }, + "description": "Logi Circle ile kimlik do\u011frulamas\u0131 yapmak istedi\u011finiz kimlik do\u011frulama sa\u011flay\u0131c\u0131s\u0131n\u0131 se\u00e7in.", + "title": "Kimlik Do\u011frulama Sa\u011flay\u0131c\u0131s\u0131" } } } diff --git a/homeassistant/components/lookin/translations/tr.json b/homeassistant/components/lookin/translations/tr.json index d31859f55ab..b1751d5d413 100644 --- a/homeassistant/components/lookin/translations/tr.json +++ b/homeassistant/components/lookin/translations/tr.json @@ -1,9 +1,30 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, "flow_title": "{name} ({host})", "step": { + "device_name": { + "data": { + "name": "Ad" + } + }, "discovery_confirm": { "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "ip_address": "IP Adresi" + } } } } diff --git a/homeassistant/components/luftdaten/translations/tr.json b/homeassistant/components/luftdaten/translations/tr.json index 04565de3d28..decde0caff5 100644 --- a/homeassistant/components/luftdaten/translations/tr.json +++ b/homeassistant/components/luftdaten/translations/tr.json @@ -2,7 +2,17 @@ "config": { "error": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_sensor": "Sens\u00f6r mevcut de\u011fil veya ge\u00e7ersiz" + }, + "step": { + "user": { + "data": { + "show_on_map": "Haritada g\u00f6ster", + "station_id": "Luftdaten Sens\u00f6r Kimli\u011fi" + }, + "title": "Luftdaten'i tan\u0131mlay\u0131n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json index 765c28565cd..bdee23411b8 100644 --- a/homeassistant/components/lyric/translations/tr.json +++ b/homeassistant/components/lyric/translations/tr.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Yetki URL'si olu\u015fturulurken zaman a\u015f\u0131m\u0131 olu\u015ftu.", - "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "create_entry": { "default": "Ba\u015far\u0131yla do\u011fruland\u0131" @@ -12,7 +13,8 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" }, "reauth_confirm": { - "description": "Lyric entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor." + "description": "Lyric entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" } } } diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json index 84adcdf8225..3918614af2e 100644 --- a/homeassistant/components/mailgun/translations/tr.json +++ b/homeassistant/components/mailgun/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Webhooks with Mailgun]( {mailgun_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131 hakk\u0131nda [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "Mailgun'u kurmak istedi\u011finizden emin misiniz?", + "title": "Mailgun Webhook'u kurun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/tr.json b/homeassistant/components/mazda/translations/tr.json new file mode 100644 index 00000000000..6c574d1de0b --- /dev/null +++ b/homeassistant/components/mazda/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "account_locked": "Hesap kilitlendi. L\u00fctfen daha sonra tekrar deneyiniz.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola", + "region": "B\u00f6lge" + }, + "description": "L\u00fctfen MyMazda mobil uygulamas\u0131na giri\u015f yapmak i\u00e7in kulland\u0131\u011f\u0131n\u0131z e-posta adresini ve \u015fifreyi giriniz.", + "title": "Mazda Connected Services - Hesap Ekle" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index 5c175a50324..0e9eb8eab55 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -2,7 +2,10 @@ "device_automation": { "condition_type": { "is_idle": "{entity_name} bo\u015fta", - "is_off": "{entity_name} kapal\u0131" + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k", + "is_paused": "{entity_name} duraklat\u0131ld\u0131", + "is_playing": "{entity_name} oynat\u0131l\u0131yor" }, "trigger_type": { "idle": "{entity_name} bo\u015fta", diff --git a/homeassistant/components/melcloud/translations/tr.json b/homeassistant/components/melcloud/translations/tr.json index 6bce50f3de6..dd03f27f953 100644 --- a/homeassistant/components/melcloud/translations/tr.json +++ b/homeassistant/components/melcloud/translations/tr.json @@ -13,7 +13,9 @@ "data": { "password": "Parola", "username": "E-posta" - } + }, + "description": "MELCloud hesab\u0131n\u0131z\u0131 kullanarak ba\u011flan\u0131n.", + "title": "MELCloud'a ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/met/translations/tr.json b/homeassistant/components/met/translations/tr.json index d256711728c..5d91ce78b82 100644 --- a/homeassistant/components/met/translations/tr.json +++ b/homeassistant/components/met/translations/tr.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "no_home": "Ev Asistan\u0131 yap\u0131land\u0131rmas\u0131nda ev koordinatlar\u0131 ayarlanmiyor" + }, "error": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "step": { "user": { "data": { + "elevation": "Y\u00fckseklik", "latitude": "Enlem", - "longitude": "Boylam" - } + "longitude": "Boylam", + "name": "Ad" + }, + "description": "Meteoroloji Enstit\u00fcs\u00fc", + "title": "Konum" } } } diff --git a/homeassistant/components/met_eireann/translations/tr.json b/homeassistant/components/met_eireann/translations/tr.json new file mode 100644 index 00000000000..689a539e885 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "elevation": "Y\u00fckseklik", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, + "description": "Met \u00c9ireann Public Weather Forecast API'sinden gelen hava durumu verilerini kullanmak i\u00e7in konumunuzu girin", + "title": "Konum" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json index 59c3886a900..4793dbb4fe7 100644 --- a/homeassistant/components/meteo_france/translations/tr.json +++ b/homeassistant/components/meteo_france/translations/tr.json @@ -8,10 +8,18 @@ "empty": "\u015eehir aramas\u0131nda sonu\u00e7 yok: l\u00fctfen \u015fehir alan\u0131n\u0131 kontrol edin" }, "step": { + "cities": { + "data": { + "city": "\u015eehir" + }, + "description": "Listeden \u015fehrinizi se\u00e7in", + "title": "M\u00e9t\u00e9o-Fransa" + }, "user": { "data": { "city": "\u015eehir" }, + "description": "Posta kodunu (yaln\u0131zca Fransa i\u00e7in, \u00f6nerilir) veya \u015fehir ad\u0131n\u0131 girin", "title": "M\u00e9t\u00e9o-Fransa" } } diff --git a/homeassistant/components/meteoclimatic/translations/tr.json b/homeassistant/components/meteoclimatic/translations/tr.json new file mode 100644 index 00000000000..a56fe984e2e --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "error": { + "not_found": "A\u011fda cihaz bulunamad\u0131" + }, + "step": { + "user": { + "data": { + "code": "\u0130stasyon kodu" + }, + "description": "Meteoclimatic istasyon kodunu girin (\u00f6rne\u011fin, ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/tr.json b/homeassistant/components/metoffice/translations/tr.json index 55064a139ef..0e1891ab77b 100644 --- a/homeassistant/components/metoffice/translations/tr.json +++ b/homeassistant/components/metoffice/translations/tr.json @@ -14,7 +14,8 @@ "latitude": "Enlem", "longitude": "Boylam" }, - "description": "Enlem ve boylam, en yak\u0131n hava istasyonunu bulmak i\u00e7in kullan\u0131lacakt\u0131r." + "description": "Enlem ve boylam, en yak\u0131n hava istasyonunu bulmak i\u00e7in kullan\u0131lacakt\u0131r.", + "title": "\u0130ngiltere Met Office'e ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json index cffcc65151c..aa8bfcf4d0a 100644 --- a/homeassistant/components/mikrotik/translations/tr.json +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -5,15 +5,30 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "name_exists": "Bu ad zaten var" }, "step": { "user": { "data": { "host": "Ana Bilgisayar", + "name": "Ad", "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL kullan" + }, + "title": "Mikrotik Router'\u0131 kurun" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP pingini etkinle\u015ftir", + "detection_time": "Ev aral\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun", + "force_dhcp": "DHCP kullanarak taramay\u0131 zorla" } } } diff --git a/homeassistant/components/mobile_app/translations/tr.json b/homeassistant/components/mobile_app/translations/tr.json index ed278186c16..af35741315f 100644 --- a/homeassistant/components/mobile_app/translations/tr.json +++ b/homeassistant/components/mobile_app/translations/tr.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "install_app": "Home Assistant ile entegrasyonu ayarlamak i\u00e7in mobil uygulamay\u0131 a\u00e7\u0131n. Uyumlu uygulamalar\u0131n listesi i\u00e7in [belgelere]( {apps_url}" + }, + "step": { + "confirm": { + "description": "Mobil Uygulama bile\u015fenini kurmak istiyor musunuz?" + } + } + }, "device_automation": { "action_type": { "notify": "Bildirim g\u00f6nder" diff --git a/homeassistant/components/modem_callerid/translations/tr.json b/homeassistant/components/modem_callerid/translations/tr.json index 1faa8c56406..eec011d8c6f 100644 --- a/homeassistant/components/modem_callerid/translations/tr.json +++ b/homeassistant/components/modem_callerid/translations/tr.json @@ -1,14 +1,24 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_devices_found": "Kalan cihaz bulunamad\u0131" }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "usb_confirm": { "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir.", "title": "Telefon Modemi" }, "user": { + "data": { + "name": "Ad", + "port": "Port" + }, + "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir.", "title": "Telefon Modemi" } } diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json index 41246299cb1..aa17461c063 100644 --- a/homeassistant/components/modern_forms/translations/tr.json +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -1,6 +1,23 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "description": "Modern Forms fan\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." + }, "zeroconf_confirm": { "description": "'{name}' adl\u0131 Modern Formlar fan\u0131n\u0131 Ev Asistan\u0131'na eklemek istiyor musunuz?", "title": "Ke\u015ffedilen Modern Formlar fan cihaz\u0131" diff --git a/homeassistant/components/monoprice/translations/tr.json b/homeassistant/components/monoprice/translations/tr.json index 7c622a3cb4a..bb9728f18a7 100644 --- a/homeassistant/components/monoprice/translations/tr.json +++ b/homeassistant/components/monoprice/translations/tr.json @@ -21,5 +21,20 @@ "title": "Cihaza ba\u011flan\u0131n" } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Kaynak #1 ad\u0131", + "source_2": "Kaynak #2 ad\u0131", + "source_3": "Kaynak #3 ad\u0131", + "source_4": "Kaynak #4 ad\u0131", + "source_5": "Kaynak #5 ad\u0131", + "source_6": "Kaynak #6 ad\u0131" + }, + "title": "Kaynaklar\u0131 yap\u0131land\u0131r" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.tr.json b/homeassistant/components/moon/translations/sensor.tr.json new file mode 100644 index 00000000000..487f1de9d85 --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.tr.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "\u0130lk D\u00f6rd\u00fcn", + "full_moon": "Dolunay", + "last_quarter": "Son D\u00f6rd\u00fcn", + "new_moon": "Yeni Ay", + "waning_crescent": "Azalan Hilal", + "waning_gibbous": "\u015ei\u015fkin Ay", + "waxing_crescent": "Hilal", + "waxing_gibbous": "\u015ei\u015fkin Ay" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index e3289f1c3be..d45d8a2890e 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -6,6 +6,7 @@ "connection_error": "Ba\u011flanma hatas\u0131" }, "error": { + "discovery_error": "Motion Gateway bulunamad\u0131", "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc" }, "flow_title": "Hareketli Panjurlar", @@ -14,12 +15,15 @@ "data": { "api_key": "API Anahtar\u0131", "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc" - } + }, + "description": "16 karakterlik API Anahtar\u0131na ihtiyac\u0131n\u0131z olacak, talimatlar i\u00e7in https://www.home-assistant.io/integrations/motion_blinds/#retriving-the-key adresine bak\u0131n.", + "title": "Hareketli Panjurlar" }, "select": { "data": { "select_ip": "\u0130p Adresi" }, + "description": "Motion Gateway'e ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n", "title": "Ba\u011flamak istedi\u011finiz Hareket A\u011f Ge\u00e7idini se\u00e7in" }, "user": { diff --git a/homeassistant/components/motioneye/translations/tr.json b/homeassistant/components/motioneye/translations/tr.json index 4fed79615aa..a2a909a062b 100644 --- a/homeassistant/components/motioneye/translations/tr.json +++ b/homeassistant/components/motioneye/translations/tr.json @@ -1,14 +1,38 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, "error": { - "invalid_url": "Ge\u00e7ersiz URL" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_url": "Ge\u00e7ersiz URL", + "unknown": "Beklenmeyen hata" + }, + "step": { + "hassio_confirm": { + "description": "{addon} taraf\u0131ndan sa\u011flanan motionEye hizmetine ba\u011flanmak i\u00e7in Home Assistant'\u0131 yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla motionEye" + }, + "user": { + "data": { + "admin_password": "Y\u00f6netici Parola", + "admin_username": "Y\u00f6netici Kullan\u0131c\u0131 Ad\u0131", + "surveillance_password": "G\u00f6zetim Parola", + "surveillance_username": "G\u00f6zetim Kullan\u0131c\u0131 Ad\u0131", + "url": "URL" + } + } } }, "options": { "step": { "init": { "data": { - "stream_url_template": "Ak\u0131\u015f URL \u015fablonu" + "stream_url_template": "Ak\u0131\u015f URL \u015fablonu", + "webhook_set": "Olaylar\u0131 Home Assistant'a bildirmek i\u00e7in motionEye webhooklar\u0131n\u0131 yap\u0131land\u0131r\u0131n", + "webhook_set_overwrite": "Tan\u0131nmayan webhooklar\u0131n \u00fczerine yaz" } } } diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 71f8e2c1055..e840c3aa2e1 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { @@ -9,25 +10,38 @@ "step": { "broker": { "data": { + "broker": "Broker", + "discovery": "Ke\u015ffetmeyi etkinle\u015ftir", "password": "Parola", - "port": "Port" - } + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin." }, "hassio_confirm": { "data": { "discovery": "Ke\u015ffetmeyi etkinle\u015ftir" - } + }, + "description": "{addon} taraf\u0131ndan sa\u011flanan MQTT arac\u0131s\u0131na ba\u011flanacak \u015fekilde yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla MQTT Broker" } } }, "device_automation": { "trigger_subtype": { + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "button_5": "Be\u015finci d\u00fc\u011fme", + "button_6": "Alt\u0131nc\u0131 d\u00fc\u011fme", "turn_off": "Kapat", "turn_on": "A\u00e7" }, "trigger_type": { "button_double_press": "\" {subtype} \" \u00e7ift t\u0131kland\u0131", "button_long_press": "\" {subtype} \" s\u00fcrekli olarak bas\u0131ld\u0131", + "button_long_release": "\"{alt t\u00fcr}\" uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", "button_quadruple_press": "\" {subtype} \" d\u00f6rt kez t\u0131kland\u0131", "button_quintuple_press": "\" {subtype} \" be\u015fli t\u0131kland\u0131", "button_short_press": "\" {subtype} \" bas\u0131ld\u0131", @@ -37,23 +51,37 @@ }, "options": { "error": { + "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", + "bad_will": "Ge\u00e7ersiz olacak konu.", "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "broker": { "data": { + "broker": "Broker", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin.", + "title": "Broker se\u00e7enekleri" }, "options": { "data": { + "birth_enable": "Do\u011fum mesaj\u0131n\u0131 etkinle\u015ftir", + "birth_payload": "Do\u011fum mesaj\u0131 y\u00fckl\u00fc", + "birth_qos": "Do\u011fum mesaj\u0131 QoS", + "birth_retain": "Do\u011fum mesaj\u0131 saklama", + "birth_topic": "Do\u011fum mesaj\u0131 konusu", "discovery": "Ke\u015ffetmeyi etkinle\u015ftir", + "will_enable": "Etkinle\u015ftir iletisi", + "will_payload": "\u0130leti y\u00fckl\u00fc", + "will_qos": "QoS mesaj\u0131 g\u00f6nderecek", "will_retain": "Mesaj korunacak m\u0131", "will_topic": "Mesaj konusu olacak" }, - "description": "Ke\u015fif - Ke\u015fif etkinle\u015ftirilirse (\u00f6nerilir), Home Assistant, yap\u0131land\u0131rmalar\u0131n\u0131 MQTT arac\u0131s\u0131nda yay\u0131nlayan cihazlar\u0131 ve varl\u0131klar\u0131 otomatik olarak ke\u015ffeder. Ke\u015fif devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, t\u00fcm yap\u0131land\u0131rma manuel olarak yap\u0131lmal\u0131d\u0131r.\n Do\u011fum mesaj\u0131 - Do\u011fum mesaj\u0131, Home Assistant (yeniden) MQTT arac\u0131s\u0131na her ba\u011fland\u0131\u011f\u0131nda g\u00f6nderilir.\n Will mesaj\u0131 - Will mesaj\u0131, Home Assistant arac\u0131yla olan ba\u011flant\u0131s\u0131n\u0131 her kaybetti\u011finde, hem temizlik durumunda (\u00f6rn. Home Assistant kapan\u0131yor) hem de kirli bir durumda (\u00f6rn. ba\u011flant\u0131y\u0131 kes." + "description": "Ke\u015fif - Ke\u015fif etkinle\u015ftirilirse (\u00f6nerilir), Home Assistant, yap\u0131land\u0131rmalar\u0131n\u0131 MQTT arac\u0131s\u0131nda yay\u0131nlayan cihazlar\u0131 ve varl\u0131klar\u0131 otomatik olarak ke\u015ffeder. Ke\u015fif devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, t\u00fcm yap\u0131land\u0131rma manuel olarak yap\u0131lmal\u0131d\u0131r.\n Do\u011fum mesaj\u0131 - Do\u011fum mesaj\u0131, Home Assistant (yeniden) MQTT arac\u0131s\u0131na her ba\u011fland\u0131\u011f\u0131nda g\u00f6nderilir.\n Will mesaj\u0131 - Will mesaj\u0131, Home Assistant arac\u0131yla olan ba\u011flant\u0131s\u0131n\u0131 her kaybetti\u011finde, hem temizlik durumunda (\u00f6rn. Home Assistant kapan\u0131yor) hem de kirli bir durumda (\u00f6rn. ba\u011flant\u0131y\u0131 kes.", + "title": "MQTT se\u00e7enekleri" } } } diff --git a/homeassistant/components/mullvad/translations/tr.json b/homeassistant/components/mullvad/translations/tr.json new file mode 100644 index 00000000000..6bbfbca32ce --- /dev/null +++ b/homeassistant/components/mullvad/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "description": "Mullvad VPN entegrasyonunu ayarlad\u0131n\u0131z m\u0131?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/tr.json b/homeassistant/components/mutesync/translations/tr.json new file mode 100644 index 00000000000..e45abded24b --- /dev/null +++ b/homeassistant/components/mutesync/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Se\u00e7enekler > Kimlik Do\u011frulama'da kimlik do\u011frulamay\u0131 etkinle\u015ftirin", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/tr.json b/homeassistant/components/myq/translations/tr.json index 3cbaf7169ee..7b2a6ec710f 100644 --- a/homeassistant/components/myq/translations/tr.json +++ b/homeassistant/components/myq/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -10,6 +11,9 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "Parola" + }, "description": "{username} i\u00e7in \u015fifre art\u0131k ge\u00e7erli de\u011fil.", "title": "MyQ Hesab\u0131n\u0131z\u0131 yeniden do\u011frulay\u0131n" }, diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index c83997435a2..4ea99e6cdc7 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -1,14 +1,68 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "duplicate_persistence_file": "Kal\u0131c\u0131l\u0131k dosyas\u0131 zaten kullan\u0131mda", + "duplicate_topic": "Konu zaten kullan\u0131l\u0131yor", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_device": "Ge\u00e7ersiz cihaz", + "invalid_ip": "Ge\u00e7ersiz IP adresi", + "invalid_persistence_file": "Ge\u00e7ersiz kal\u0131c\u0131l\u0131k dosyas\u0131", + "invalid_port": "Ge\u00e7ersiz ba\u011flant\u0131 noktas\u0131 numaras\u0131", + "invalid_publish_topic": "Ge\u00e7ersiz yay\u0131nlama konusu", + "invalid_serial": "Ge\u00e7ersiz seri ba\u011flant\u0131 noktas\u0131", + "invalid_subscribe_topic": "Ge\u00e7ersiz abone konusu", + "invalid_version": "Ge\u00e7ersiz MySensors s\u00fcr\u00fcm\u00fc", + "not_a_number": "L\u00fctfen bir numara giriniz", + "port_out_of_range": "Port numaras\u0131 en az 1, en fazla 65535 olmal\u0131d\u0131r", + "same_topic": "Abone olma ve yay\u0131nlama konular\u0131 ayn\u0131", + "unknown": "Beklenmeyen hata" + }, "error": { - "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "duplicate_persistence_file": "Kal\u0131c\u0131l\u0131k dosyas\u0131 zaten kullan\u0131mda", + "duplicate_topic": "Konu zaten kullan\u0131l\u0131yor", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_device": "Ge\u00e7ersiz cihaz", + "invalid_ip": "Ge\u00e7ersiz IP adresi", + "invalid_persistence_file": "Ge\u00e7ersiz kal\u0131c\u0131l\u0131k dosyas\u0131", + "invalid_port": "Ge\u00e7ersiz ba\u011flant\u0131 noktas\u0131 numaras\u0131", + "invalid_publish_topic": "Ge\u00e7ersiz yay\u0131nlama konusu", + "invalid_serial": "Ge\u00e7ersiz seri ba\u011flant\u0131 noktas\u0131", + "invalid_subscribe_topic": "Ge\u00e7ersiz abone konusu", + "invalid_version": "Ge\u00e7ersiz MySensors s\u00fcr\u00fcm\u00fc", + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", + "not_a_number": "L\u00fctfen bir numara giriniz", + "port_out_of_range": "Port numaras\u0131 en az 1, en fazla 65535 olmal\u0131d\u0131r", + "same_topic": "Abone olma ve yay\u0131nlama konular\u0131 ayn\u0131d\u0131r", + "unknown": "Beklenmeyen hata" }, "step": { "gw_mqtt": { + "data": { + "persistence_file": "kal\u0131c\u0131l\u0131k dosyas\u0131 (otomatik olu\u015fturmak i\u00e7in bo\u015f b\u0131rak\u0131n)", + "retain": "mqtt tutma", + "topic_in_prefix": "girdi konular\u0131 i\u00e7in \u00f6nek (topic_in_prefix)", + "topic_out_prefix": "\u00e7\u0131kt\u0131 konular\u0131 i\u00e7in \u00f6nek (topic_out_prefix)", + "version": "MySensors s\u00fcr\u00fcm\u00fc" + }, "description": "MQTT a\u011f ge\u00e7idi kurulumu" }, + "gw_serial": { + "data": { + "baud_rate": "baud h\u0131z\u0131", + "device": "Seri ba\u011flant\u0131 noktas\u0131", + "persistence_file": "kal\u0131c\u0131l\u0131k dosyas\u0131 (otomatik olu\u015fturmak i\u00e7in bo\u015f b\u0131rak\u0131n)", + "version": "MySensors s\u00fcr\u00fcm\u00fcne" + }, + "description": "Seri a\u011f ge\u00e7idi kurulumu" + }, "gw_tcp": { "data": { + "device": "A\u011f ge\u00e7idinin IP adresini", + "persistence_file": "kal\u0131c\u0131l\u0131k dosyas\u0131 (otomatik olu\u015fturmak i\u00e7in bo\u015f b\u0131rak\u0131n)", "tcp_port": "port", "version": "MySensors s\u00fcr\u00fcm\u00fc" }, diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json new file mode 100644 index 00000000000..1c5cadbdf0b --- /dev/null +++ b/homeassistant/components/nam/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "device_unsupported": "Cihaz desteklenmiyor." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "{host} Nettigo Air Monitor'\u00fc kurmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "description": "Nettigo Air Monitor entegrasyonunu kurun." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json index 2b23e027d22..2510264f599 100644 --- a/homeassistant/components/nanoleaf/translations/tr.json +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -1,10 +1,27 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_allowing_new_tokens": "Nanoleaf yeni anahtarlara izin vermiyor, yukar\u0131daki talimatlar\u0131 izleyin.", + "unknown": "Beklenmeyen hata" + }, "flow_title": "{name}", "step": { "link": { "description": "Nanoleaf'inizdeki g\u00fc\u00e7 d\u00fc\u011fmesini d\u00fc\u011fme LED'leri yan\u0131p s\u00f6nmeye ba\u015flayana kadar 5 saniye bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde **G\u00d6NDER**'e t\u0131klay\u0131n.", "title": "Ba\u011flant\u0131 Nanoleaf" + }, + "user": { + "data": { + "host": "Ana bilgisayar" + } } } } diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 18fa4749d88..3f71eb8ee6d 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -2,12 +2,22 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + }, + "reauth_confirm": { + "title": "Kuruluma ba\u015flamak ister misiniz?" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 98506684dbb..5f210f01dc4 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -18,6 +18,9 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "auth": { + "title": "Po\u0142\u0105cz z kontem Google" + }, "init": { "data": { "flow_impl": "Dostawca" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index b36d5dc483d..11dd05897fd 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -1,11 +1,20 @@ { "config": { "abort": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "error": { + "internal_error": "Kod do\u011frularken i\u00e7 hata olu\u015ftu", + "invalid_pin": "Ge\u00e7ersiz PIN Kodu", + "timeout": "Zaman a\u015f\u0131m\u0131 do\u011frulama kodu", "unknown": "Beklenmeyen hata" }, "step": { @@ -15,6 +24,27 @@ }, "description": "Google hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [hesab\u0131n\u0131z\u0131 yetkilendirin]( {url} ). \n\n Yetkilendirmeden sonra, sa\u011flanan Auth Token kodunu a\u015fa\u011f\u0131ya kopyalay\u0131p yap\u0131\u015ft\u0131r\u0131n.", "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "init": { + "data": { + "flow_impl": "Sa\u011flay\u0131c\u0131" + }, + "description": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7", + "title": "Kimlik Do\u011frulama Sa\u011flay\u0131c\u0131s\u0131" + }, + "link": { + "data": { + "code": "PIN Kodu" + }, + "description": "Nest hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [hesab\u0131n\u0131z\u0131 yetkilendirin]( {url} ). \n\n Yetkilendirmeden sonra, a\u015fa\u011f\u0131da verilen PIN kodunu kopyalay\u0131p yap\u0131\u015ft\u0131r\u0131n.", + "title": "Nest Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Nest entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" } } }, diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json index 3c6302ec6c9..d40de61e36a 100644 --- a/homeassistant/components/netatmo/translations/tr.json +++ b/homeassistant/components/netatmo/translations/tr.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, "reauth_confirm": { - "description": "Netatmo entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor" + "description": "Netatmo entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" } } }, @@ -21,8 +32,14 @@ "cancel_set_point": "{entity_name} zamanlamas\u0131na devam etti", "human": "{entity_name} bir insan alg\u0131lad\u0131", "movement": "{entity_name} hareket alg\u0131lad\u0131", + "outdoor": "{entity_name} bir d\u0131\u015f mekan etkinli\u011fi alg\u0131lad\u0131", + "person": "{entity_name} bir ki\u015fi tespit etti", + "person_away": "{entity_name} bir ki\u015finin ayr\u0131ld\u0131\u011f\u0131n\u0131 tespit etti", + "set_point": "{entity_name} hedef s\u0131cakl\u0131\u011f\u0131 manuel olarak ayarland\u0131", + "therm_mode": "{entity_name} , \" {subtype} \" olarak de\u011fi\u015ftirildi", "turned_off": "{entity_name} kapat\u0131ld\u0131", - "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131", + "vehicle": "{entity_name} bir ara\u00e7 alg\u0131lad\u0131" } }, "options": { diff --git a/homeassistant/components/netgear/translations/tr.json b/homeassistant/components/netgear/translations/tr.json index 899657b6edf..fbdb76ce1db 100644 --- a/homeassistant/components/netgear/translations/tr.json +++ b/homeassistant/components/netgear/translations/tr.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { "config": "Ba\u011flant\u0131 veya oturum a\u00e7ma hatas\u0131: l\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 kontrol edin" }, "step": { "user": { + "data": { + "host": "Ana bilgisayar (\u0130ste\u011fe ba\u011fl\u0131)", + "password": "Parola", + "port": "Port (\u0130ste\u011fe ba\u011fl\u0131)", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "username": "Kullan\u0131c\u0131 Ad\u0131 (\u0130ste\u011fe ba\u011fl\u0131)" + }, "description": "Varsay\u0131lan ana bilgisayar: {host}\n Varsay\u0131lan ba\u011flant\u0131 noktas\u0131: {port}\n Varsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}", "title": "Netgear" } @@ -13,6 +23,9 @@ "options": { "step": { "init": { + "data": { + "consider_home": "Ev s\u00fcresini g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)" + }, "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", "title": "Netgear" } diff --git a/homeassistant/components/nexia/translations/tr.json b/homeassistant/components/nexia/translations/tr.json index 47f3d931c46..8918bcd8f5e 100644 --- a/homeassistant/components/nexia/translations/tr.json +++ b/homeassistant/components/nexia/translations/tr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marka", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/nfandroidtv/translations/tr.json b/homeassistant/components/nfandroidtv/translations/tr.json index f8411bc1ea0..01bc2be90c1 100644 --- a/homeassistant/components/nfandroidtv/translations/tr.json +++ b/homeassistant/components/nfandroidtv/translations/tr.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { + "data": { + "host": "Ana bilgisayar", + "name": "Ad" + }, "description": "Bu entegrasyon, Android TV i\u00e7in Bildirimler uygulamas\u0131n\u0131 gerektirir. \n\n Android TV i\u00e7in: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n Fire TV i\u00e7in: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Y\u00f6nlendiricinizde DHCP rezervasyonu (y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n) veya cihazda statik bir IP adresi ayarlamal\u0131s\u0131n\u0131z. Aksi takdirde, cihaz sonunda kullan\u0131lamaz hale gelecektir.", "title": "Android TV / Fire TV i\u00e7in Bildirimler" } diff --git a/homeassistant/components/nightscout/translations/bg.json b/homeassistant/components/nightscout/translations/bg.json index 93e70be433e..6685ae35a2f 100644 --- a/homeassistant/components/nightscout/translations/bg.json +++ b/homeassistant/components/nightscout/translations/bg.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447" + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" } } } diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json index 95f36a4d124..91f5c8f6a8e 100644 --- a/homeassistant/components/nightscout/translations/tr.json +++ b/homeassistant/components/nightscout/translations/tr.json @@ -14,7 +14,9 @@ "data": { "api_key": "API Anahtar\u0131", "url": "URL" - } + }, + "description": "- URL: Nightcout \u00f6rne\u011finizin adresi. \u00d6rnek: https://myhomeassistant.duckdns.org:5423\n - API Anahtar\u0131 (iste\u011fe ba\u011fl\u0131): Yaln\u0131zca \u00f6rne\u011finiz korunuyorsa kullan\u0131n (auth_default_roles != okunabilir).", + "title": "Nightscout sunucu bilgilerinizi girin." } } } diff --git a/homeassistant/components/nmap_tracker/translations/tr.json b/homeassistant/components/nmap_tracker/translations/tr.json index a21bf3780a8..2d277e979b3 100644 --- a/homeassistant/components/nmap_tracker/translations/tr.json +++ b/homeassistant/components/nmap_tracker/translations/tr.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_hosts": "Ge\u00e7ersiz Ana Bilgisayarlar" + }, "step": { "user": { "data": { + "exclude": "Taraman\u0131n d\u0131\u015f\u0131nda tutulacak a\u011f adresleri (virg\u00fcl ayr\u0131lm\u0131\u015f)", "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri" @@ -12,12 +19,21 @@ } }, "options": { + "error": { + "invalid_hosts": "Ge\u00e7ersiz Ana Bilgisayarlar" + }, "step": { "init": { "data": { + "consider_home": "Bir cihaz izleyicisi g\u00f6r\u00fclmedikten sonra evde de\u011fil olarak i\u015faretlenene kadar beklemek i\u00e7in saniyeler kald\u0131.", + "exclude": "Taraman\u0131n d\u0131\u015f\u0131nda tutulacak a\u011f adresleri (virg\u00fcl ayr\u0131lm\u0131\u015f)", + "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", + "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "interval_seconds": "Tarama aral\u0131\u011f\u0131", + "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri", "track_new_devices": "Yeni cihazlar\u0131 izle" - } + }, + "description": "Nmap taraf\u0131ndan taranacak ana bilgisayarlar\u0131 yap\u0131land\u0131r\u0131n. A\u011f adresi ve hari\u00e7 tutulanlar, IP Adresleri (192.168.1.1), IP A\u011flar\u0131 (192.168.0.0/24) veya IP Aral\u0131klar\u0131 (192.168.1.0-32) olabilir." } } }, diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index 2f6da34d2ed..4bc533a7f23 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -1,24 +1,28 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "no_devices": "Hesapta cihaz bulunamad\u0131" + "no_devices": "Hesapta cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" }, "step": { "reauth_confirm": { "data": { "password": "\u015eifre" }, - "description": "{username} \u015fifresini tekrar girin." + "description": "{username} \u015fifresini tekrar girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Bilgilerinizi doldurun" } } } diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json index 1947fc31e63..ee240b2c242 100644 --- a/homeassistant/components/nuheat/translations/tr.json +++ b/homeassistant/components/nuheat/translations/tr.json @@ -16,7 +16,8 @@ "serial_number": "Termostat\u0131n seri numaras\u0131.", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "https://MyNuHeat.com'da oturum a\u00e7\u0131p termostat(lar)\u0131n\u0131z\u0131 se\u00e7erek termostat\u0131n\u0131z\u0131n say\u0131sal seri numaras\u0131n\u0131 veya kimli\u011fini alman\u0131z gerekecektir." + "description": "https://MyNuHeat.com'da oturum a\u00e7\u0131p termostat(lar)\u0131n\u0131z\u0131 se\u00e7erek termostat\u0131n\u0131z\u0131n say\u0131sal seri numaras\u0131n\u0131 veya kimli\u011fini alman\u0131z gerekecektir.", + "title": "NuHeat'e ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json index ba6a496fa4c..801d9efcbd0 100644 --- a/homeassistant/components/nuki/translations/tr.json +++ b/homeassistant/components/nuki/translations/tr.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "token": "Eri\u015fim Anahtar\u0131" + }, + "description": "Nuki entegrasyonunun k\u00f6pr\u00fcn\u00fczle yeniden kimlik do\u011frulamas\u0131 yapmas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "host": "Ana Bilgisayar", diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json index b383d765619..d000b142c1f 100644 --- a/homeassistant/components/nut/translations/tr.json +++ b/homeassistant/components/nut/translations/tr.json @@ -11,12 +11,15 @@ "resources": { "data": { "resources": "Kaynaklar" - } + }, + "title": "\u0130zlenecek Kaynaklar\u0131 Se\u00e7in" }, "ups": { "data": { - "alias": "Takma ad" - } + "alias": "Takma ad", + "resources": "Kaynaklar" + }, + "title": "\u0130zlenecek UPS'i Se\u00e7in" }, "user": { "data": { @@ -24,15 +27,21 @@ "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "NUT sunucusuna ba\u011flan\u0131n" } } }, "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "init": { "data": { - "resources": "Kaynaklar" + "resources": "Kaynaklar", + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" }, "description": "Sens\u00f6r Kaynaklar\u0131'n\u0131 se\u00e7in." } diff --git a/homeassistant/components/nws/translations/tr.json b/homeassistant/components/nws/translations/tr.json index 8f51593aedb..a31bade53f3 100644 --- a/homeassistant/components/nws/translations/tr.json +++ b/homeassistant/components/nws/translations/tr.json @@ -12,9 +12,11 @@ "data": { "api_key": "API Anahtar\u0131", "latitude": "Enlem", - "longitude": "Boylam" + "longitude": "Boylam", + "station": "METAR istasyon kodu" }, - "description": "Bir METAR istasyon kodu belirtilmezse, en yak\u0131n istasyonu bulmak i\u00e7in enlem ve boylam kullan\u0131lacakt\u0131r. \u015eimdilik bir API Anahtar\u0131 herhangi bir \u015fey olabilir. Ge\u00e7erli bir e-posta adresi kullanman\u0131z tavsiye edilir." + "description": "Bir METAR istasyon kodu belirtilmezse, en yak\u0131n istasyonu bulmak i\u00e7in enlem ve boylam kullan\u0131lacakt\u0131r. \u015eimdilik bir API Anahtar\u0131 herhangi bir \u015fey olabilir. Ge\u00e7erli bir e-posta adresi kullanman\u0131z tavsiye edilir.", + "title": "Ulusal Hava Durumu Servisine ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/nzbget/translations/tr.json b/homeassistant/components/nzbget/translations/tr.json index 63b6c489018..27b101fb59a 100644 --- a/homeassistant/components/nzbget/translations/tr.json +++ b/homeassistant/components/nzbget/translations/tr.json @@ -7,13 +7,19 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { "user": { "data": { + "host": "Ana bilgisayar", + "name": "Ad", "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "title": "NZBGet'e ba\u011flan\u0131n" } } }, diff --git a/homeassistant/components/octoprint/translations/tr.json b/homeassistant/components/octoprint/translations/tr.json index 769f9e2504f..6e76ae6d8fd 100644 --- a/homeassistant/components/octoprint/translations/tr.json +++ b/homeassistant/components/octoprint/translations/tr.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "auth_failed": "Uygulama API anahtar\u0131 al\u0131namad\u0131" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "auth_failed": "Uygulama API anahtar\u0131 al\u0131namad\u0131", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" }, "flow_title": "OctoPrint Yaz\u0131c\u0131: {host}", "progress": { @@ -10,6 +17,7 @@ "step": { "user": { "data": { + "host": "Ana bilgisayar", "path": "Uygulama Yolu", "port": "Ba\u011flant\u0131 Noktas\u0131 Numaras\u0131", "ssl": "SSL Kullan", diff --git a/homeassistant/components/omnilogic/translations/tr.json b/homeassistant/components/omnilogic/translations/tr.json index a2939122ef6..73fdc1d4d86 100644 --- a/homeassistant/components/omnilogic/translations/tr.json +++ b/homeassistant/components/omnilogic/translations/tr.json @@ -21,7 +21,8 @@ "step": { "init": { "data": { - "ph_offset": "pH ofseti (pozitif veya negatif)" + "ph_offset": "pH ofseti (pozitif veya negatif)", + "polling_interval": "Kontrol etme aral\u0131\u011f\u0131 (saniye cinsinden)" } } } diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json index 6a0084d0a96..693cfe01a9e 100644 --- a/homeassistant/components/ondilo_ico/translations/tr.json +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index f59da2ab7e7..ed0ea6fcaf2 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -12,12 +12,14 @@ "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" }, "user": { "data": { "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" - } + }, + "title": "1-Wire'\u0131 kurun" } } } diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 73a89ad5a85..b1248f4d578 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_h264": "Kullan\u0131labilir H264 ak\u0131\u015f\u0131 yok. Cihaz\u0131n\u0131zdaki profil yap\u0131land\u0131rmas\u0131n\u0131 kontrol edin.", + "no_mac": "ONVIF cihaz\u0131 i\u00e7in benzersiz kimlik yap\u0131land\u0131r\u0131lamad\u0131.", + "onvif_error": "ONVIF cihaz\u0131 ayarlan\u0131rken hata olu\u015ftu. Daha fazla bilgi i\u00e7in g\u00fcnl\u00fckleri kontrol edin." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" @@ -12,15 +15,24 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Kimlik do\u011frulamay\u0131 yap\u0131land\u0131r" }, "configure": { + "data": { + "host": "Ana bilgisayar", + "name": "Ad", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" }, "configure_profile": { "data": { "include": "Kamera varl\u0131\u011f\u0131 olu\u015ftur" }, + "description": "{profile} i\u00e7in {resolution} \u00e7\u00f6z\u00fcn\u00fcrl\u00fckte kamera varl\u0131\u011f\u0131 olu\u015fturulsun mu?", "title": "Profilleri Yap\u0131land\u0131r" }, "device": { diff --git a/homeassistant/components/opengarage/translations/tr.json b/homeassistant/components/opengarage/translations/tr.json index cd800abed1d..fcbd46ed8f6 100644 --- a/homeassistant/components/opengarage/translations/tr.json +++ b/homeassistant/components/opengarage/translations/tr.json @@ -5,13 +5,16 @@ }, "error": { "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, "step": { "user": { "data": { "device_key": "Cihaz Anahtar\u0131", - "port": "Port" + "host": "Ana bilgisayar", + "port": "Port", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } } diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json index 507b71ede5b..a969927ebe2 100644 --- a/homeassistant/components/opentherm_gw/translations/tr.json +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -2,15 +2,31 @@ "config": { "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "id_exists": "A\u011f ge\u00e7idi kimli\u011fi zaten var" }, "step": { "init": { "data": { - "device": "Yol veya URL" + "device": "Yol veya URL", + "id": "ID", + "name": "Ad" }, "title": "OpenTherm A\u011f Ge\u00e7idi" } } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Zemin S\u0131cakl\u0131\u011f\u0131", + "read_precision": "Hassas Okuma", + "set_precision": "Hassasiyeti Ayarla", + "temporary_override_mode": "Ge\u00e7ici Ayar Noktas\u0131 Ge\u00e7ersiz K\u0131lma Modu" + }, + "description": "OpenTherm A\u011f Ge\u00e7idi i\u00e7in Se\u00e7enekler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json index aef4a5ef8ee..d5caa40721a 100644 --- a/homeassistant/components/openuv/translations/tr.json +++ b/homeassistant/components/openuv/translations/tr.json @@ -10,15 +10,21 @@ "user": { "data": { "api_key": "API Anahtar\u0131", + "elevation": "Y\u00fckseklik", "latitude": "Enlem", "longitude": "Boylam" - } + }, + "title": "Bilgilerinizi doldurun" } } }, "options": { "step": { "init": { + "data": { + "from_window": "Koruma penceresi i\u00e7in ba\u015flang\u0131\u00e7 UV indeksi", + "to_window": "Koruma penceresi i\u00e7in UV indeksini sonland\u0131rma" + }, "title": "OpenUV'yi yap\u0131land\u0131r\u0131n" } } diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index 714daac3253..6c832b707e1 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -18,7 +18,9 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Enerji kullan\u0131m\u0131n\u0131za eri\u015fmek i\u00e7in bir OVO Energy \u00f6rne\u011fi ayarlay\u0131n.", + "title": "OVO Enerji Hesab\u0131 Ekle" } } } diff --git a/homeassistant/components/owntracks/translations/tr.json b/homeassistant/components/owntracks/translations/tr.json index a152eb19468..944cd176580 100644 --- a/homeassistant/components/owntracks/translations/tr.json +++ b/homeassistant/components/owntracks/translations/tr.json @@ -2,6 +2,15 @@ "config": { "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "create_entry": { + "default": "\n\n Android'de [OwnTracks uygulamas\u0131n\u0131]( {android_url} ) a\u00e7\u0131n, tercihler - > ba\u011flant\u0131'ya gidin. A\u015fa\u011f\u0131daki ayarlar\u0131 de\u011fi\u015ftirin:\n - Mod: \u00d6zel HTTP\n - Ana makine: {webhook_url}\n - Kimlik:\n - Kullan\u0131c\u0131 ad\u0131: `' ''\n - Cihaz Kimli\u011fi: `' '` \n\n iOS'ta [OwnTracks uygulamas\u0131n\u0131]( {ios_url} ) a\u00e7\u0131n, sol \u00fcstteki (i) simgesine - > ayarlara dokunun. A\u015fa\u011f\u0131daki ayarlar\u0131 de\u011fi\u015ftirin:\n - Mod: HTTP\n - URL: {webhook_url}\n - Kimlik do\u011frulamay\u0131 a\u00e7\n - Kullan\u0131c\u0131 Kimli\u011fi: `' '' \n\n {secret}\n\n Daha fazla bilgi i\u00e7in [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "OwnTracks'i kurmak istedi\u011finizden emin misiniz?", + "title": "OwnTracks'i kurun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json index 99eda8b8311..e9e8643fa94 100644 --- a/homeassistant/components/ozw/translations/tr.json +++ b/homeassistant/components/ozw/translations/tr.json @@ -9,10 +9,16 @@ "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "addon_start_failed": "OpenZWave eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin." + }, "progress": { "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." }, "step": { + "hassio_confirm": { + "title": "OpenZWave eklentisi ile OpenZWave entegrasyonunu kurun" + }, "install_addon": { "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" }, @@ -25,8 +31,10 @@ }, "start_addon": { "data": { - "network_key": "A\u011f Anahtar\u0131" - } + "network_key": "A\u011f Anahtar\u0131", + "usb_path": "USB Cihaz Yolu" + }, + "title": "OpenZWave eklenti yap\u0131land\u0131rmas\u0131n\u0131 girin" } } } diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index acdccc57615..f445e82b585 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -1,7 +1,15 @@ { "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { + "data": { + "host": "Ana bilgisayar", + "name": "Ad" + }, "description": "Home Assistant ile entegre etmek i\u00e7in P1 Monitor'\u00fc kurun." } } diff --git a/homeassistant/components/panasonic_viera/translations/tr.json b/homeassistant/components/panasonic_viera/translations/tr.json index f221ec5a3b4..a668bb37b3e 100644 --- a/homeassistant/components/panasonic_viera/translations/tr.json +++ b/homeassistant/components/panasonic_viera/translations/tr.json @@ -6,13 +6,23 @@ "unknown": "Beklenmeyen hata" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_pin_code": "Girdi\u011finiz PIN Kodu ge\u00e7ersiz" }, "step": { + "pairing": { + "data": { + "pin": "PIN Kodu" + }, + "description": "TV'nizde g\u00f6r\u00fcnt\u00fclenen PIN Kodu kodunu girin", + "title": "E\u015fle\u015ftirme" + }, "user": { "data": { - "host": "\u0130p Adresi" + "host": "\u0130p Adresi", + "name": "Ad" }, + "description": "Panasonic Viera TV'nizin IP Adresi 'ni girin", "title": "TV'nizi kurun" } } diff --git a/homeassistant/components/philips_js/translations/tr.json b/homeassistant/components/philips_js/translations/tr.json index aed6030c02a..884c74b0ffa 100644 --- a/homeassistant/components/philips_js/translations/tr.json +++ b/homeassistant/components/philips_js/translations/tr.json @@ -1,8 +1,41 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_pin": "Ge\u00e7ersiz PIN", + "pairing_failure": "E\u015fle\u015ftirilemiyor: {error_id}", + "unknown": "Beklenmeyen hata" + }, "step": { "pair": { - "description": "TV'nizde g\u00f6r\u00fcnt\u00fclenen PIN'i girin" + "data": { + "pin": "PIN Kodu" + }, + "description": "TV'nizde g\u00f6r\u00fcnt\u00fclenen PIN'i girin", + "title": "E\u015fle\u015ftir" + }, + "user": { + "data": { + "api_version": "API S\u00fcr\u00fcm\u00fc", + "host": "Ana bilgisayar" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Cihaz\u0131n a\u00e7\u0131lmas\u0131 isteniyor" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Veri bildirim hizmetinin kullan\u0131m\u0131na izin ver." + } } } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index e2fff8d904b..c85971927a1 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -19,7 +19,9 @@ "location": "Konum", "name": "\u0130sim", "port": "Port", - "statistics_only": "Yaln\u0131zca \u0130statistikler" + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "statistics_only": "Yaln\u0131zca \u0130statistikler", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } } diff --git a/homeassistant/components/picnic/translations/tr.json b/homeassistant/components/picnic/translations/tr.json index 9ffb10e1b88..242b4ae4e6a 100644 --- a/homeassistant/components/picnic/translations/tr.json +++ b/homeassistant/components/picnic/translations/tr.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { - "country_code": "\u00dclke kodu" + "country_code": "\u00dclke kodu", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" } } } diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json index 1f21b08ec81..579617127ac 100644 --- a/homeassistant/components/plaato/translations/tr.json +++ b/homeassistant/components/plaato/translations/tr.json @@ -5,23 +5,33 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, + "create_entry": { + "default": "Plaato {device_type} ad\u0131 ** ile {device_name} ** ba\u015far\u0131yla kurulum oldu!" + }, "error": { + "invalid_webhook_device": "Webhook veri g\u00f6ndermeyi desteklemeyen bir cihaz se\u00e7tiniz. Yaln\u0131zca Airlock i\u00e7in kullan\u0131labilir", + "no_api_method": "Bir kimlik do\u011frulama anahtar\u0131 eklemeniz veya webhook se\u00e7meniz gerekiyor", "no_auth_token": "Bir kimlik do\u011frulama jetonu eklemeniz gerekiyor" }, "step": { "api_method": { "data": { + "token": "Yetkilendirme Anahtar\u0131'\u0131n\u0131 buraya yap\u0131\u015ft\u0131r\u0131n", "use_webhook": "Webhook kullan" }, + "description": "API'yi sorgulayabilmek i\u00e7in, [bu](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) talimatlar\u0131 izleyerek elde edilebilecek bir \"auth_token\" gereklidir. \n\n Se\u00e7ilen cihaz: ** {device_type} ** \n\n Yerle\u015fik webhook y\u00f6ntemini kullanmay\u0131 tercih ediyorsan\u0131z (yaln\u0131zca Airlock) l\u00fctfen a\u015fa\u011f\u0131daki kutuyu i\u015faretleyin ve Yetkilendirme Anahtar\u0131n\u0131 bo\u015f b\u0131rak\u0131n", "title": "API y\u00f6ntemini se\u00e7in" }, "user": { "data": { "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n", "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc" - } + }, + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Plaato cihazlar\u0131n\u0131 kurun" }, "webhook": { + "description": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Plaato Airlock'ta webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}", "title": "Webhook kullanmak i\u00e7in" } } @@ -36,6 +46,7 @@ "title": "Plaato i\u00e7in se\u00e7enekler" }, "webhook": { + "description": "Webhook bilgisi: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n", "title": "Plaato Airlock i\u00e7in se\u00e7enekler" } } diff --git a/homeassistant/components/plex/translations/tr.json b/homeassistant/components/plex/translations/tr.json index 93f8cc85eae..c052006687f 100644 --- a/homeassistant/components/plex/translations/tr.json +++ b/homeassistant/components/plex/translations/tr.json @@ -1,19 +1,41 @@ { "config": { "abort": { + "all_configured": "T\u00fcm ba\u011flant\u0131l\u0131 sunucular zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_configured": "Bu Plex sunucusu zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "token_request_timeout": "Anahtar alma zaman a\u015f\u0131m\u0131na u\u011frad\u0131", "unknown": "Beklenmeyen hata" }, + "error": { + "faulty_credentials": "Yetkilendirme ba\u015far\u0131s\u0131z oldu, Token'\u0131 do\u011frulay\u0131n", + "host_or_token": "Ana Bilgisayar veya Anahtardan az birini sa\u011flamal\u0131d\u0131r", + "no_servers": "Plex hesab\u0131na ba\u011fl\u0131 sunucu yok", + "not_found": "Plex sunucusu bulunamad\u0131", + "ssl_error": "SSL sertifikas\u0131 sorunu" + }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { "host": "Ana Bilgisayar", - "port": "Port" - } + "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "token": "Anahtar (opsiyonel)", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "title": "Manuel Plex Yap\u0131land\u0131rmas\u0131" + }, + "select_server": { + "data": { + "server": "Sunucu" + }, + "description": "Birden fazla sunucu mevcut, birini se\u00e7in:", + "title": "Plex sunucusunu se\u00e7in" }, "user": { + "description": "Bir Plex sunucusunu ba\u011flamak i\u00e7in [plex.tv](https://plex.tv) ile devam edin.", "title": "Plex Medya Sunucusu" }, "user_advanced": { @@ -23,5 +45,18 @@ "title": "Plex Medya Sunucusu" } } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "Yeni y\u00f6netilen/payla\u015f\u0131lan kullan\u0131c\u0131lar\u0131 yoksay", + "ignore_plex_web_clients": "Plex Web istemcilerini yoksay", + "monitored_users": "\u0130zlenen kullan\u0131c\u0131lar", + "use_episode_art": "B\u00f6l\u00fcm resmini kullan" + }, + "description": "Plex Medya Oynat\u0131c\u0131lar i\u00e7in Se\u00e7enekler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index 6f09f1e8fe1..e8d5da74484 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -11,7 +11,11 @@ "flow_title": "Smile: {name}", "step": { "user": { - "description": "\u00dcr\u00fcn:" + "data": { + "flow_type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + }, + "description": "\u00dcr\u00fcn:", + "title": "Plugwise tipi" }, "user_gateway": { "data": { @@ -20,13 +24,17 @@ "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" }, - "description": "L\u00fctfen girin" + "description": "L\u00fctfen girin", + "title": "Smile'a Ba\u011flan\u0131n" } } }, "options": { "step": { "init": { + "data": { + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" + }, "description": "Plugwise Se\u00e7eneklerini Ayarlay\u0131n" } } diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json index 5a4849fad07..d3258722848 100644 --- a/homeassistant/components/point/translations/tr.json +++ b/homeassistant/components/point/translations/tr.json @@ -2,10 +2,30 @@ "config": { "abort": { "already_setup": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "external_setup": "Nokta, ba\u015fka bir ak\u0131\u015ftan ba\u015far\u0131yla yap\u0131land\u0131r\u0131ld\u0131.", + "no_flows": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "error": { + "follow_link": "L\u00fctfen ba\u011flant\u0131y\u0131 takip edin ve G\u00f6nder'e basmadan \u00f6nce kimlik do\u011frulamas\u0131 yap\u0131n", "no_token": "Eri\u015fim Belirteci" + }, + "step": { + "auth": { + "description": "L\u00fctfen a\u015fa\u011f\u0131daki ba\u011flant\u0131y\u0131 takip edin ve Minut hesab\u0131n\u0131za eri\u015fimi **Kabul** edin, ard\u0131ndan geri d\u00f6n\u00fcn ve a\u015fa\u011f\u0131daki **G\u00f6nder**'e bas\u0131n. \n\n [Ba\u011flant\u0131]( {authorization_url} )", + "title": "Kimlik Do\u011frulama Noktas\u0131" + }, + "user": { + "data": { + "flow_impl": "Sa\u011flay\u0131c\u0131" + }, + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/tr.json b/homeassistant/components/poolsense/translations/tr.json index 1e2e9d0c5b8..e1b4f150e92 100644 --- a/homeassistant/components/poolsense/translations/tr.json +++ b/homeassistant/components/poolsense/translations/tr.json @@ -11,7 +11,9 @@ "data": { "email": "E-posta", "password": "Parola" - } + }, + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "PoolSense" } } } diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json index dd09a83a78c..0ebaca3b99c 100644 --- a/homeassistant/components/powerwall/translations/tr.json +++ b/homeassistant/components/powerwall/translations/tr.json @@ -1,18 +1,24 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata", + "wrong_version": "G\u00fc\u00e7 duvar\u0131n\u0131z desteklenmeyen bir yaz\u0131l\u0131m s\u00fcr\u00fcm\u00fc kullan\u0131yor. \u00c7\u00f6z\u00fclebilmesi i\u00e7in l\u00fctfen bu sorunu y\u00fckseltmeyi veya bildirmeyi d\u00fc\u015f\u00fcn\u00fcn." }, "flow_title": "Tesla Powerwall ( {ip_address} )", "step": { "user": { "data": { - "ip_address": "\u0130p Adresi" - } + "ip_address": "\u0130p Adresi", + "password": "Parola" + }, + "description": "Parola genellikle Backup Gateway i\u00e7in seri numaras\u0131n\u0131n son 5 karakteridir ve Tesla uygulamas\u0131nda veya Backup Gateway 2 i\u00e7in kap\u0131n\u0131n i\u00e7inde bulunan parolan\u0131n son 5 karakterinde bulunabilir.", + "title": "Powerwall'a ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json index a152eb19468..48ce4808c04 100644 --- a/homeassistant/components/profiler/translations/tr.json +++ b/homeassistant/components/profiler/translations/tr.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/tr.json b/homeassistant/components/prosegur/translations/tr.json index f3fa57430ea..9b14241980a 100644 --- a/homeassistant/components/prosegur/translations/tr.json +++ b/homeassistant/components/prosegur/translations/tr.json @@ -1,9 +1,27 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { + "reauth_confirm": { + "data": { + "description": "Prosegur hesab\u0131yla yeniden kimlik do\u011frulamas\u0131 yap\u0131n.", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { - "country": "\u00dclke" + "country": "\u00dclke", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" } } } diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index 535c9467fb0..895491bfe4b 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -1,26 +1,40 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "credential_error": "Kimlik bilgileri al\u0131n\u0131rken hata olu\u015ftu.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "port_987_bind_error": "987 numaral\u0131 ba\u011flant\u0131 noktas\u0131na ba\u011flanamad\u0131. Ek bilgi i\u00e7in [belgelere](https://www.home-assistant.io/components/ps4/) bak\u0131n.", + "port_997_bind_error": "997 numaral\u0131 ba\u011flant\u0131 noktas\u0131na ba\u011flanamad\u0131. Ek bilgi i\u00e7in [belgelere](https://www.home-assistant.io/components/ps4/) bak\u0131n." }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "credential_timeout": "Kimlik bilgisi hizmeti zaman a\u015f\u0131m\u0131na u\u011frad\u0131. Yeniden ba\u015flatmak i\u00e7in g\u00f6nder'e bas\u0131n.", + "login_failed": "PlayStation 4 ile e\u015fle\u015ftirilemedi. PIN Kodu nin do\u011fru oldu\u011funu do\u011frulay\u0131n.", + "no_ipaddress": "Yap\u0131land\u0131rmak istedi\u011finiz PlayStation 4'\u00fcn IP Adresi kodunu girin." }, "step": { "creds": { + "description": "Kimlik bilgileri gerekli. \"G\u00f6nder\"e bas\u0131n ve ard\u0131ndan PS4 2. Ekran Uygulamas\u0131nda cihazlar\u0131 yenileyin ve devam etmek i\u00e7in \"Home-Asistan\" cihaz\u0131n\u0131 se\u00e7in.", "title": "PlayStation 4" }, "link": { "data": { + "code": "PIN Kodu", "ip_address": "\u0130p Adresi", + "name": "Ad", "region": "B\u00f6lge" }, + "description": "PlayStation 4 bilgilerinizi girin. PIN Kodu i\u00e7in PlayStation 4 konsolunuzda 'Ayarlar'a gidin. Ard\u0131ndan \"Mobil Uygulama Ba\u011flant\u0131 Ayarlar\u0131\"na gidin ve \"Cihaz Ekle\"yi se\u00e7in. PIN Kodu kodunu girin. Ek bilgi i\u00e7in [belgelere](https://www.home-assistant.io/components/ps4/) bak\u0131n.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n)." - } + "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n).", + "mode": "Yap\u0131land\u0131rma Modu" + }, + "description": "Yap\u0131land\u0131rma i\u00e7in modu se\u00e7in. IP Adresi alan\u0131, Otomatik Ke\u015fif se\u00e7ildi\u011finde cihazlar otomatik olarak ke\u015ffedilece\u011finden bo\u015f b\u0131rak\u0131labilir.", + "title": "PlayStation 4" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json index fc6f4973da0..5d27e2bb033 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -7,8 +7,11 @@ "user": { "data": { "name": "Sens\u00f6r Ad\u0131", - "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)" + "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", + "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)", + "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" }, + "description": "Bu sens\u00f6r, \u0130spanya'da [saatlik elektrik fiyatland\u0131rmas\u0131 (PVPC)](https://www.esios.ree.es/es/pvpc) almak i\u00e7in resmi API'yi kullan\u0131r.\n Daha kesin a\u00e7\u0131klama i\u00e7in [entegrasyon belgelerini](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) ziyaret edin.", "title": "Sens\u00f6r kurulumu" } } @@ -18,6 +21,7 @@ "init": { "data": { "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", + "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)", "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" }, "description": "Bu sens\u00f6r, \u0130spanya'da [saatlik elektrik fiyatland\u0131rmas\u0131 (PVPC)](https://www.esios.ree.es/es/pvpc) almak i\u00e7in resmi API'yi kullan\u0131r.\n Daha kesin a\u00e7\u0131klama i\u00e7in [entegrasyon belgelerini](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) ziyaret edin.", diff --git a/homeassistant/components/rachio/translations/tr.json b/homeassistant/components/rachio/translations/tr.json index 8bbc4eb1e49..a6c9de15318 100644 --- a/homeassistant/components/rachio/translations/tr.json +++ b/homeassistant/components/rachio/translations/tr.json @@ -12,6 +12,17 @@ "user": { "data": { "api_key": "API Anahtar\u0131" + }, + "description": "https://app.rach.io/ adresinden API Anahtar\u0131na ihtiyac\u0131n\u0131z olacak. Ayarlar'a gidin, ard\u0131ndan 'API ANAHTARI AL' se\u00e7ene\u011fini t\u0131klay\u0131n.", + "title": "Rachio cihaz\u0131n\u0131za ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Bir b\u00f6lge anahtar\u0131 etkinle\u015ftirilirken \u00e7al\u0131\u015ft\u0131r\u0131lacak dakika cinsinden s\u00fcre" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/tr.json b/homeassistant/components/rainforest_eagle/translations/tr.json index 20704f9583d..39886e8669d 100644 --- a/homeassistant/components/rainforest_eagle/translations/tr.json +++ b/homeassistant/components/rainforest_eagle/translations/tr.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { "cloud_id": "Bulut kimli\u011fi", + "host": "Ana bilgisayar", "install_code": "Y\u00fckleme kodu" } } diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index 80cfc05e568..4a45215a737 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -6,13 +6,15 @@ "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "{ip}", "step": { "user": { "data": { "ip_address": "Ana makine ad\u0131 veya IP adresi", "password": "Parola", "port": "Port" - } + }, + "title": "Bilgilerinizi doldurun" } } }, diff --git a/homeassistant/components/rdw/translations/he.json b/homeassistant/components/rdw/translations/he.json new file mode 100644 index 00000000000..067d7208d62 --- /dev/null +++ b/homeassistant/components/rdw/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/hu.json b/homeassistant/components/rdw/translations/hu.json new file mode 100644 index 00000000000..52d3e6dac59 --- /dev/null +++ b/homeassistant/components/rdw/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown_license_plate": "Ismeretlen rendsz\u00e1m" + }, + "step": { + "user": { + "data": { + "license_plate": "Rendsz\u00e1m" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/pl.json b/homeassistant/components/rdw/translations/pl.json new file mode 100644 index 00000000000..93684f8dfd3 --- /dev/null +++ b/homeassistant/components/rdw/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/ru.json b/homeassistant/components/rdw/translations/ru.json new file mode 100644 index 00000000000..a885bf3067e --- /dev/null +++ b/homeassistant/components/rdw/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown_license_plate": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440\u043d\u043e\u0439 \u0437\u043d\u0430\u043a." + }, + "step": { + "user": { + "data": { + "license_plate": "\u041d\u043e\u043c\u0435\u0440\u043d\u043e\u0439 \u0437\u043d\u0430\u043a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/tr.json b/homeassistant/components/rdw/translations/tr.json index 14835007351..c47dadfafa4 100644 --- a/homeassistant/components/rdw/translations/tr.json +++ b/homeassistant/components/rdw/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown_license_plate": "Bilinmeyen plaka" }, "step": { diff --git a/homeassistant/components/rdw/translations/zh-Hant.json b/homeassistant/components/rdw/translations/zh-Hant.json new file mode 100644 index 00000000000..fdf54dd247a --- /dev/null +++ b/homeassistant/components/rdw/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown_license_plate": "\u672a\u77e5\u8eca\u724c" + }, + "step": { + "user": { + "data": { + "license_plate": "\u8eca\u724c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/tr.json b/homeassistant/components/recollect_waste/translations/tr.json index 3e21df87207..084edd95657 100644 --- a/homeassistant/components/recollect_waste/translations/tr.json +++ b/homeassistant/components/recollect_waste/translations/tr.json @@ -3,12 +3,26 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "invalid_place_or_service_id": "Ge\u00e7ersiz Yer veya Hizmet Kimli\u011fi" + }, "step": { "user": { "data": { + "place_id": "Yer kimli\u011fi", "service_id": "Hizmet Kimli\u011fi" } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Al\u0131m t\u00fcrleri i\u00e7in kolay adlar kullan\u0131n (m\u00fcmk\u00fcn oldu\u011funda)" + }, + "title": "At\u0131k Geri Toplama Yap\u0131land\u0131r" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/tr.json b/homeassistant/components/renault/translations/tr.json index ddb98324782..42de45d5cd2 100644 --- a/homeassistant/components/renault/translations/tr.json +++ b/homeassistant/components/renault/translations/tr.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "kamereon_no_account": "Kamereon hesab\u0131 bulunamad\u0131" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "kamereon_no_account": "Kamereon hesab\u0131 bulunamad\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_credentials": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { "kamereon": { @@ -14,11 +19,14 @@ "data": { "password": "\u015eifre" }, - "description": "{username} i\u00e7in \u015fifrenizi g\u00fcncelleyin" + "description": "{username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { - "locale": "Yerel ayar" + "locale": "Yerel ayar", + "password": "Parola", + "username": "E-posta" }, "title": "Renault kimlik bilgilerini ayarla" } diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json index 8627e635483..2d720196874 100644 --- a/homeassistant/components/rfxtrx/translations/tr.json +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -5,14 +5,19 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "one": "Bo\u015f", + "other": "Bo\u015f" }, "step": { + "one": "Bo\u015f", + "other": "Bo\u015f", "setup_network": { "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "title": "Ba\u011flant\u0131 adresini se\u00e7in" }, "setup_serial": { "data": { @@ -21,6 +26,9 @@ "title": "Cihaz" }, "setup_serial_manual_path": { + "data": { + "device": "USB Cihaz Yolu" + }, "title": "Yol" }, "user": { @@ -41,23 +49,42 @@ "status": "Al\u0131nan durum: {subtype}" } }, + "one": "Bo\u015f", "options": { "error": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_event_code": "Ge\u00e7ersiz etkinlik kodunu", + "invalid_input_2262_off": "Komut kapatma i\u00e7in ge\u00e7ersiz giri\u015f", + "invalid_input_2262_on": "A\u00e7ma komutu i\u00e7in ge\u00e7ersiz giri\u015f", + "invalid_input_off_delay": "Kapanma gecikmesi i\u00e7in ge\u00e7ersiz giri\u015f", "unknown": "Beklenmeyen hata" }, "step": { + "prompt_options": { + "data": { + "automatic_add": "Otomatik eklemeyi etkinle\u015ftir", + "debug": "Hata ay\u0131klamay\u0131 etkinle\u015ftir", + "device": "Yap\u0131land\u0131rmak i\u00e7in cihaz\u0131 se\u00e7in", + "event_code": "Eklemek i\u00e7in etkinlik kodunu girin", + "remove_device": "Silinecek cihaz\u0131 se\u00e7in" + }, + "title": "Rfxtrx Se\u00e7enekleri" + }, "set_device_options": { "data": { + "command_off": "Komut kapatma i\u00e7in veri bitleri de\u011feri", + "command_on": "Komut i\u00e7in veri bitleri de\u011feri", "data_bit": "Veri biti say\u0131s\u0131", "fire_event": "Cihaz etkinli\u011fini etkinle\u015ftir", "off_delay": "Kapanma gecikmesi", "off_delay_enabled": "Kapatma gecikmesini etkinle\u015ftir", "replace_device": "De\u011fi\u015ftirilecek cihaz\u0131 se\u00e7in", + "signal_repetitions": "Sinyal tekrar\u0131 say\u0131s\u0131", "venetian_blind_mode": "Jaluzi modu" }, "title": "Cihaz se\u00e7eneklerini yap\u0131land\u0131r\u0131n" } } - } + }, + "other": "Bo\u015f" } \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/pl.json b/homeassistant/components/ridwell/translations/pl.json index b8b737c37a3..e052fdaa9a4 100644 --- a/homeassistant/components/ridwell/translations/pl.json +++ b/homeassistant/components/ridwell/translations/pl.json @@ -1,6 +1,11 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/ridwell/translations/tr.json b/homeassistant/components/ridwell/translations/tr.json index 0062ba735c8..d9ab1290edb 100644 --- a/homeassistant/components/ridwell/translations/tr.json +++ b/homeassistant/components/ridwell/translations/tr.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "reauth_confirm": { "data": { "password": "\u015eifre" }, - "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:" + "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { diff --git a/homeassistant/components/ring/translations/tr.json b/homeassistant/components/ring/translations/tr.json index caba385d7fa..690d60e8e09 100644 --- a/homeassistant/components/ring/translations/tr.json +++ b/homeassistant/components/ring/translations/tr.json @@ -8,11 +8,18 @@ "unknown": "Beklenmeyen hata" }, "step": { + "2fa": { + "data": { + "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" + }, + "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Ring hesab\u0131yla oturum a\u00e7\u0131n" } } } diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json index 280de17f77d..168fc621e05 100644 --- a/homeassistant/components/risco/translations/tr.json +++ b/homeassistant/components/risco/translations/tr.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Parola", + "pin": "PIN Kodu", "username": "Kullan\u0131c\u0131 Ad\u0131" } } @@ -26,9 +27,15 @@ "armed_home": "Evde Modu Aktif", "armed_night": "Gece Modu Aktif" }, - "description": "Home Assistant alarm\u0131n\u0131 kurarken Risco alarm\u0131n\u0131z\u0131 hangi duruma ayarlayaca\u011f\u0131n\u0131z\u0131 se\u00e7in" + "description": "Home Assistant alarm\u0131n\u0131 kurarken Risco alarm\u0131n\u0131z\u0131 hangi duruma ayarlayaca\u011f\u0131n\u0131z\u0131 se\u00e7in", + "title": "Home Assistant eyaletlerini Risco eyaletleriyle e\u015fleyin" }, "init": { + "data": { + "code_arm_required": "Devreye almak i\u00e7in PIN Kodu gerektir", + "code_disarm_required": "Devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in PIN Kodu", + "scan_interval": "Risco'ya ne s\u0131kl\u0131kla anket yap\u0131l\u0131r (saniye cinsinden)" + }, "title": "Se\u00e7enekleri yap\u0131land\u0131r\u0131n" }, "risco_to_ha": { @@ -36,8 +43,12 @@ "A": "Grup A", "B": "Grup B", "C": "Grup C", - "D": "Grup D" - } + "D": "Grup D", + "arm": "Aktif (UZAKTA)", + "partial_arm": "K\u0131smen Aktif (KAL)" + }, + "description": "Risco taraf\u0131ndan bildirilen her durum i\u00e7in Home Assistant alarm\u0131n\u0131z\u0131n hangi durumu bildirece\u011fini se\u00e7in", + "title": "Risco eyaletlerini Home Assistant eyaletleriyle e\u015fleyin" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/tr.json b/homeassistant/components/rituals_perfume_genie/translations/tr.json new file mode 100644 index 00000000000..ee1316315ec --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "title": "Rituals hesab\u0131n\u0131za ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json index 0dca1a028b2..23b33b69efe 100644 --- a/homeassistant/components/roku/translations/tr.json +++ b/homeassistant/components/roku/translations/tr.json @@ -2,23 +2,35 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { "discovery_confirm": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, "description": "{name} kurmak istiyor musunuz?", "title": "Roku" }, "ssdp_confirm": { - "description": "{name} kurmak istiyor musunuz?" + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, + "description": "{name} kurmak istiyor musunuz?", + "title": "Roku" }, "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Roku bilgilerinizi girin." } } } diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index 3d85144c188..a3a7ffc9f17 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "not_irobot_device": "Bulunan cihaz bir iRobot cihaz\u0131 de\u011fil" + "not_irobot_device": "Bulunan cihaz bir iRobot cihaz\u0131 de\u011fil", + "short_blid": "BLID kesildi" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" @@ -33,10 +34,12 @@ "blid": "BLID", "host": "Ana Bilgisayar" }, + "description": "A\u011f\u0131n\u0131zda Roomba veya Braava bulunamad\u0131.", "title": "Cihaza manuel olarak ba\u011flan\u0131n" }, "user": { "data": { + "blid": "BLID", "continuous": "S\u00fcrekli", "delay": "Gecikme", "host": "Ana Bilgisayar", diff --git a/homeassistant/components/roon/translations/bg.json b/homeassistant/components/roon/translations/bg.json new file mode 100644 index 00000000000..36b9a0b4bff --- /dev/null +++ b/homeassistant/components/roon/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index 94e452e48bb..75fedf6383e 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -15,7 +15,8 @@ "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Roon sunucusu bulunamad\u0131, l\u00fctfen Ana Bilgisayar Ad\u0131n\u0131z\u0131 veya IP'nizi girin." } } } diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json index f1dfcf16667..5ad414a1be8 100644 --- a/homeassistant/components/rpi_power/translations/tr.json +++ b/homeassistant/components/rpi_power/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "Bu bile\u015fen i\u00e7in gereken sistem s\u0131n\u0131f\u0131n\u0131 bulam\u0131yorum, \u00e7ekirde\u011finizin yeni oldu\u011fundan ve donan\u0131m\u0131n desteklendi\u011finden emin olun", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/samsungtv/translations/da.json b/homeassistant/components/samsungtv/translations/da.json index 9ca829b7679..d41a62c5e7b 100644 --- a/homeassistant/components/samsungtv/translations/da.json +++ b/homeassistant/components/samsungtv/translations/da.json @@ -9,7 +9,7 @@ "flow_title": "Samsung-tv: {model}", "step": { "confirm": { - "description": "Vil du konfigurere Samsung-tv {model}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", + "description": "Vil du konfigurere Samsung-tv {device}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", "title": "Samsung-tv" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/es-419.json b/homeassistant/components/samsungtv/translations/es-419.json index dfe7793a235..179965fda80 100644 --- a/homeassistant/components/samsungtv/translations/es-419.json +++ b/homeassistant/components/samsungtv/translations/es-419.json @@ -9,7 +9,7 @@ "flow_title": "Televisi\u00f3n Samsung: {model}", "step": { "confirm": { - "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {model}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 75b6bab6676..03d6eb2b3c6 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -17,7 +17,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", + "description": "Voulez vous installer la TV {device} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", "title": "TV Samsung" }, "reauth_confirm": { diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 7efb88bf7eb..4f62bbf6237 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -10,7 +10,7 @@ "flow_title": "\uc0bc\uc131 TV: {model}", "step": { "confirm": { - "description": "{model} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4.", + "description": "{device} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4.", "title": "\uc0bc\uc131 TV" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/lb.json b/homeassistant/components/samsungtv/translations/lb.json index 110b063b6e8..0740ab119a4 100644 --- a/homeassistant/components/samsungtv/translations/lb.json +++ b/homeassistant/components/samsungtv/translations/lb.json @@ -10,7 +10,7 @@ "flow_title": "Samsnung TV:{model}", "step": { "confirm": { - "description": "W\u00ebllt dir de Samsung TV {model} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun fir d\u00ebse TV g\u00ebtt iwwerschriwwen.", + "description": "W\u00ebllt dir de Samsung TV {device} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun fir d\u00ebse TV g\u00ebtt iwwerschriwwen.", "title": "Samsnung TV" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/sl.json b/homeassistant/components/samsungtv/translations/sl.json index 1a94ce59833..41536f72b16 100644 --- a/homeassistant/components/samsungtv/translations/sl.json +++ b/homeassistant/components/samsungtv/translations/sl.json @@ -9,7 +9,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.", + "description": "Vnesite podatke o televizorju Samsung {device}. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 3558559c4b6..835bb5e5c9b 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -9,7 +9,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vill du st\u00e4lla in Samsung TV {model}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver.", + "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index f2d6fc7e978..8bb79aa801c 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -5,12 +5,19 @@ "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", "cannot_connect": "Ba\u011flanma hatas\u0131", + "id_missing": "Bu Samsung cihaz\u0131n\u0131n Seri Numaras\u0131 yok.", "missing_config_entry": "Bu Samsung cihaz\u0131nda bir yap\u0131land\u0131rma giri\u015fi yok.", - "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." + "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et." }, "flow_title": "Samsung TV: {model}", "step": { "confirm": { + "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz.", "title": "Samsung TV" }, "reauth_confirm": { diff --git a/homeassistant/components/samsungtv/translations/uk.json b/homeassistant/components/samsungtv/translations/uk.json index 83bb18e76f1..f6aa504ccc8 100644 --- a/homeassistant/components/samsungtv/translations/uk.json +++ b/homeassistant/components/samsungtv/translations/uk.json @@ -10,7 +10,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {model}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.", + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {device}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.", "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung" }, "user": { diff --git a/homeassistant/components/screenlogic/translations/tr.json b/homeassistant/components/screenlogic/translations/tr.json new file mode 100644 index 00000000000..c56eac9b02d --- /dev/null +++ b/homeassistant/components/screenlogic/translations/tr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP Adresi", + "port": "Port" + }, + "description": "ScreenLogic Gateway bilgilerinizi girin.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "A\u011f ge\u00e7idi" + }, + "description": "A\u015fa\u011f\u0131daki ScreenLogic a\u011f ge\u00e7itleri ke\u015ffedildi. L\u00fctfen yap\u0131land\u0131rmak i\u00e7in birini se\u00e7in veya bir ScreenLogic a\u011f ge\u00e7idini manuel olarak yap\u0131land\u0131rmay\u0131 se\u00e7in.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Taramalar aras\u0131ndaki saniyeler" + }, + "description": "{gateway_name} i\u00e7in ayarlar\u0131 belirtin", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.tr.json b/homeassistant/components/season/translations/sensor.tr.json index c70e7cc881e..ba08a57a363 100644 --- a/homeassistant/components/season/translations/sensor.tr.json +++ b/homeassistant/components/season/translations/sensor.tr.json @@ -5,6 +5,12 @@ "spring": "\u0130lkbahar", "summer": "Yaz", "winter": "K\u0131\u015f" + }, + "season__season__": { + "autumn": "Sonbahar", + "spring": "\u0130lkbahar", + "summer": "Yaz", + "winter": "K\u0131\u015f" } } } \ No newline at end of file diff --git a/homeassistant/components/select/translations/tr.json b/homeassistant/components/select/translations/tr.json new file mode 100644 index 00000000000..466770a2681 --- /dev/null +++ b/homeassistant/components/select/translations/tr.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "{entity_name} se\u00e7ene\u011fini de\u011fi\u015ftirin" + }, + "condition_type": { + "selected_option": "Ge\u00e7erli {entity_name} se\u00e7ili se\u00e7enek" + }, + "trigger_type": { + "current_option_changed": "{entity_name} se\u00e7ene\u011fi de\u011fi\u015fti" + } + }, + "title": "Se\u00e7" +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json index 7eee55f5724..3261bbec3b9 100644 --- a/homeassistant/components/sense/translations/tr.json +++ b/homeassistant/components/sense/translations/tr.json @@ -14,7 +14,8 @@ "email": "E-posta", "password": "Parola", "timeout": "Zaman a\u015f\u0131m\u0131" - } + }, + "title": "Sense Enerji Monit\u00f6r\u00fcn\u00fcze ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 7dd30efed8f..1b500a1f478 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -1,16 +1,29 @@ { "device_automation": { "condition_type": { + "is_battery_level": "Mevcut {entity_name} pil seviyesi", "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", "is_energy": "Mevcut {entity_name} enerjisi", "is_gas": "Mevcut {entity_name} gaz\u0131", + "is_humidity": "Ge\u00e7erli {entity_name} nem oran\u0131", + "is_illuminance": "Mevcut {entity_name} ayd\u0131nlatma d\u00fczeyi", + "is_nitrogen_dioxide": "Mevcut {entity_name} nitrojen dioksit konsantrasyon seviyesi", + "is_nitrogen_monoxide": "Mevcut {entity_name} nitrojen monoksit konsantrasyon seviyesi", + "is_nitrous_oxide": "Ge\u00e7erli {entity_name} azot oksit konsantrasyon seviyesi", + "is_ozone": "Mevcut {entity_name} ozon konsantrasyon seviyesi", + "is_pm1": "Mevcut {entity_name} PM1 konsantrasyon seviyesi", + "is_pm10": "Mevcut {entity_name} PM10 konsantrasyon seviyesi", + "is_pm25": "Mevcut {entity_name} PM2.5 konsantrasyon seviyesi", + "is_power": "Mevcut {entity_name} g\u00fcc\u00fc", "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", + "is_pressure": "Ge\u00e7erli {entity_name} bas\u0131nc\u0131", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", "is_value": "Mevcut {entity_name} de\u011feri", + "is_volatile_organic_compounds": "Mevcut {entity_name} u\u00e7ucu organik bile\u015fik konsantrasyon seviyesi", "is_voltage": "Mevcut {entity_name} voltaj\u0131" }, "trigger_type": { @@ -36,6 +49,7 @@ "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", + "volatile_organic_compounds": "{entity_name} u\u00e7ucu organik bile\u015fik konsantrasyonu de\u011fi\u015fiklikleri", "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" } }, diff --git a/homeassistant/components/sentry/translations/bg.json b/homeassistant/components/sentry/translations/bg.json new file mode 100644 index 00000000000..fcb2c982fdb --- /dev/null +++ b/homeassistant/components/sentry/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json index 4dab23fbd94..fa3f39e9cde 100644 --- a/homeassistant/components/sentry/translations/tr.json +++ b/homeassistant/components/sentry/translations/tr.json @@ -11,7 +11,9 @@ "user": { "data": { "dsn": "DSN" - } + }, + "description": "Sentry DSN'nizi girin", + "title": "Sentry" } } }, @@ -19,7 +21,14 @@ "step": { "init": { "data": { - "environment": "Ortam\u0131n iste\u011fe ba\u011fl\u0131 ad\u0131." + "environment": "Ortam\u0131n iste\u011fe ba\u011fl\u0131 ad\u0131.", + "event_custom_components": "\u00d6zel bile\u015fenlerden olay g\u00f6nder", + "event_handled": "\u0130\u015flenen etkinlikleri g\u00f6nder", + "event_third_party_packages": "\u00dc\u00e7\u00fcnc\u00fc taraf paketlerden etkinlik g\u00f6nder", + "logging_event_level": "G\u00fcnl\u00fck seviyesi Sentry, a\u015fa\u011f\u0131dakiler i\u00e7in bir olay kaydedecektir:", + "logging_level": "G\u00fcnl\u00fck seviyesi Sentry, g\u00fcnl\u00fckleri i\u00e7erik par\u00e7alar\u0131 olarak kaydeder.", + "tracing": "Performans izlemeyi etkinle\u015ftir", + "tracing_sample_rate": "\u00d6rnekleme h\u0131z\u0131n\u0131n izlenmesi; 0.0 ile 1.0 aras\u0131nda (1.0 = %100)" } } } diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index a89379ef0d6..2ce8d740685 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unsupported_firmware": "Cihaz, desteklenmeyen bir versiyon s\u00fcr\u00fcm\u00fc kullan\u0131yor." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -10,6 +11,9 @@ }, "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "{model} 'i {host} kurmak istiyor musunuz? \n\n Parola korumal\u0131 pille \u00e7al\u0131\u015fan cihazlar, kuruluma devam etmeden \u00f6nce uyand\u0131r\u0131lmal\u0131d\u0131r.\n Parola korumal\u0131 olmayan pille \u00e7al\u0131\u015fan cihazlar, cihaz uyand\u0131\u011f\u0131nda eklenecektir, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 manuel olarak uyand\u0131rabilir veya cihazdan bir sonraki veri g\u00fcncellemesini bekleyebilirsiniz." + }, "credentials": { "data": { "password": "Parola", @@ -29,13 +33,20 @@ "button": "D\u00fc\u011fme", "button1": "\u0130lk d\u00fc\u011fme", "button2": "\u0130kinci d\u00fc\u011fme", - "button3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme" + "button3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme" }, "trigger_type": { + "btn_down": "{subtype} a\u015fa\u011f\u0131 d\u00fc\u011fme", + "btn_up": "{subtype} d\u00fc\u011fmesi yukar\u0131", "double": "{subtype} \u00e7ift t\u0131kland\u0131", + "double_push": "{subtype} \u00e7ift basma", "long": "{subtype} uzun t\u0131kland\u0131", + "long_push": "{subtype} uzun basma", "long_single": "{subtype} uzun t\u0131kland\u0131 ve ard\u0131ndan tek t\u0131kland\u0131", "single": "{subtype} tek t\u0131kland\u0131", + "single_long": "{subtype} tek t\u0131kland\u0131 ve ard\u0131ndan uzun t\u0131kland\u0131", + "single_push": "{subtype} tek basma", "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131" } } diff --git a/homeassistant/components/sia/translations/tr.json b/homeassistant/components/sia/translations/tr.json index c67fcc6698b..8d5a1d9dbcb 100644 --- a/homeassistant/components/sia/translations/tr.json +++ b/homeassistant/components/sia/translations/tr.json @@ -6,10 +6,18 @@ "invalid_key_format": "Anahtar onalt\u0131l\u0131k bir de\u011fer de\u011fildir, l\u00fctfen yaln\u0131zca 0-9 ve AF kullan\u0131n.", "invalid_key_length": "Anahtar do\u011fru uzunlukta de\u011fil, 16, 24 veya 32 onalt\u0131l\u0131k karakterden olu\u015fmal\u0131d\u0131r.", "invalid_ping": "Ping aral\u0131\u011f\u0131 1 ile 1440 dakika aras\u0131nda olmal\u0131d\u0131r.", - "invalid_zones": "En az 1 b\u00f6lge olmas\u0131 gerekir." + "invalid_zones": "En az 1 b\u00f6lge olmas\u0131 gerekir.", + "unknown": "Beklenmeyen hata" }, "step": { "additional_account": { + "data": { + "account": "Hesap Kimli\u011fi", + "additional_account": "Ek hesaplar", + "encryption_key": "\u015eifreleme anahtar\u0131", + "ping_interval": "Ping Aral\u0131\u011f\u0131 (dk)", + "zones": "Hesab\u0131n b\u00f6lge say\u0131s\u0131" + }, "title": "Ge\u00e7erli ba\u011flant\u0131 noktas\u0131na ba\u015fka bir hesap ekleyin." }, "user": { @@ -18,17 +26,25 @@ "additional_account": "Ek hesaplar", "encryption_key": "\u015eifreleme anahtar\u0131", "ping_interval": "Ping Aral\u0131\u011f\u0131 (dk)", - "protocol": "Protokol" - } + "port": "Port", + "protocol": "Protokol", + "zones": "Hesab\u0131n b\u00f6lge say\u0131s\u0131" + }, + "title": "SIA tabanl\u0131 alarm sistemleri i\u00e7in ba\u011flant\u0131 olu\u015fturun." } } }, "options": { "step": { "options": { + "data": { + "ignore_timestamps": "SIA olaylar\u0131n\u0131n zaman damgas\u0131 kontrol\u00fcn\u00fc yoksay", + "zones": "Hesab\u0131n b\u00f6lge say\u0131s\u0131" + }, "description": "Hesap i\u00e7in se\u00e7enekleri ayarlay\u0131n: {account}", "title": "SIA Kurulumu i\u00e7in se\u00e7enekler." } } - } + }, + "title": "SIA Alarm Sistemleri" } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 2a7936a0158..61a01c31292 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -6,6 +6,7 @@ "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, "error": { + "identifier_exists": "Hesap zaten kay\u0131tl\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor", "unknown": "Beklenmeyen hata" @@ -31,10 +32,22 @@ }, "user": { "data": { + "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)", "password": "Parola", "username": "E-posta adresi" }, - "description": "SimpliSafe, 2021'den itibaren web uygulamas\u0131 \u00fczerinden yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle, bu s\u00fcrecin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](yetkilendirme kodu http://home assistant.io/integrations/simplisafe#getting) okudu\u011funuzdan emin olun.\n\nHaz\u0131r oldu\u011funuzda, SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]({url}) t\u0131klat\u0131n. \u0130\u015flem tamamland\u0131\u011f\u0131nda, buraya d\u00f6n\u00fcn ve G\u00f6nder'i t\u0131klat\u0131n." + "description": "SimpliSafe, 2021'den itibaren web uygulamas\u0131 \u00fczerinden yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle, bu s\u00fcrecin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](yetkilendirme kodu http://home assistant.io/integrations/simplisafe#getting) okudu\u011funuzdan emin olun.\n\nHaz\u0131r oldu\u011funuzda, SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]({url}) t\u0131klat\u0131n. \u0130\u015flem tamamland\u0131\u011f\u0131nda, buraya d\u00f6n\u00fcn ve G\u00f6nder'i t\u0131klat\u0131n.", + "title": "Bilgilerinizi doldurun." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)" + }, + "title": "SimpliSafe'i yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/sma/translations/tr.json b/homeassistant/components/sma/translations/tr.json index 852cfb66adb..dec1abfeaac 100644 --- a/homeassistant/components/sma/translations/tr.json +++ b/homeassistant/components/sma/translations/tr.json @@ -1,12 +1,23 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, "error": { - "cannot_retrieve_device_info": "Ba\u015far\u0131yla ba\u011fland\u0131, ancak cihaz bilgileri al\u0131namad\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "cannot_retrieve_device_info": "Ba\u015far\u0131yla ba\u011fland\u0131, ancak cihaz bilgileri al\u0131namad\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, "step": { "user": { "data": { - "group": "Grup" + "group": "Grup", + "host": "Ana bilgisayar", + "password": "Parola", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, "description": "SMA cihaz bilgilerinizi girin.", "title": "SMA Solar'\u0131 kurun" diff --git a/homeassistant/components/smappee/translations/bg.json b/homeassistant/components/smappee/translations/bg.json index 83b32fc7c85..9173bdc0bc7 100644 --- a/homeassistant/components/smappee/translations/bg.json +++ b/homeassistant/components/smappee/translations/bg.json @@ -3,7 +3,13 @@ "abort": { "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." }, + "flow_title": "{name}", "step": { + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" } diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json index 4ba8a4da9a6..79b4a82b220 100644 --- a/homeassistant/components/smappee/translations/tr.json +++ b/homeassistant/components/smappee/translations/tr.json @@ -3,22 +3,31 @@ "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_configured_local_device": "Yerel ayg\u0131t (lar) zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. L\u00fctfen bir bulut cihaz\u0131n\u0131 yap\u0131land\u0131rmadan \u00f6nce bunlar\u0131 kald\u0131r\u0131n.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_mdns": "Smappee entegrasyonu i\u00e7in desteklenmeyen cihaz." + "invalid_mdns": "Smappee entegrasyonu i\u00e7in desteklenmeyen cihaz.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { "environment": { "data": { "environment": "\u00c7evre" - } + }, + "description": "Smappee'nizi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." }, "local": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Smappee yerel entegrasyonunu ba\u015flatmak i\u00e7in ana bilgisayar\u0131 girin" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, "zeroconf_confirm": { + "description": "Seri numaras\u0131 ` {serialnumber} ` olan Smappee cihaz\u0131n\u0131 Home Assistant'a eklemek istiyor musunuz?", "title": "Smappee cihaz\u0131 bulundu" } } diff --git a/homeassistant/components/smart_meter_texas/translations/bg.json b/homeassistant/components/smart_meter_texas/translations/bg.json index 2ac8a444100..be758888427 100644 --- a/homeassistant/components/smart_meter_texas/translations/bg.json +++ b/homeassistant/components/smart_meter_texas/translations/bg.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json index 98da6384f8d..699967ce8ee 100644 --- a/homeassistant/components/smarthab/translations/tr.json +++ b/homeassistant/components/smarthab/translations/tr.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "service": "SmartHab'a ula\u015fmaya \u00e7al\u0131\u015f\u0131rken hata olu\u015ftu. Servis kapal\u0131 olabilir. Ba\u011flant\u0131n\u0131z\u0131 kontrol edin.", "unknown": "Beklenmeyen hata" }, "step": { @@ -10,6 +11,7 @@ "email": "E-posta", "password": "Parola" }, + "description": "Teknik nedenlerle, Ev Asistan\u0131 kurulumunuza \u00f6zel ikincil bir hesap kulland\u0131\u011f\u0131n\u0131zdan emin olun. SmartHab uygulamas\u0131ndan bir tane olu\u015fturabilirsiniz.", "title": "SmartHab'\u0131 kurun" } } diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json index 5f96ada17ba..1f6a5117900 100644 --- a/homeassistant/components/smartthings/translations/tr.json +++ b/homeassistant/components/smartthings/translations/tr.json @@ -1,17 +1,37 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant, SmartThings'ten g\u00fcncellemeleri almak i\u00e7in do\u011fru \u015fekilde yap\u0131land\u0131r\u0131lmam\u0131\u015f. Webhook URL'si ge\u00e7ersiz:\n > {webhook_url} \n\n L\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 [talimatlara]( {component_url} ) g\u00f6re g\u00fcncelleyin, Home Assistant'\u0131 yeniden ba\u015flat\u0131n ve tekrar deneyin.", + "no_available_locations": "Home Assistant'ta kurulacak kullan\u0131labilir SmartThings Locations yok." + }, "error": { + "app_setup_error": "SmartApp kurulamad\u0131. L\u00fctfen tekrar deneyin.", + "token_forbidden": "Anahtar, gerekli OAuth kapsam\u0131na sahip de\u011fil.", + "token_invalid_format": "Anahtar UID/GUID bi\u00e7iminde olmal\u0131d\u0131r", + "token_unauthorized": "Anahtar art\u0131k ge\u00e7ersiz veya yetkilendirilmemi\u015f.", "webhook_error": "SmartThings, webhook URL'sini do\u011frulayamad\u0131. L\u00fctfen webhook URL'sinin internetten eri\u015filebilir oldu\u011fundan emin olun ve tekrar deneyin." }, "step": { + "authorize": { + "title": "Home Asistan\u0131'n\u0131 Yetkilendir" + }, "pat": { "data": { "access_token": "Eri\u015fim Belirteci" - } + }, + "description": "L\u00fctfen [talimatlar]( {component_url} ) daki gibi olu\u015fturulmu\u015f bir SmartThings [Ki\u015fisel Eri\u015fim Anahtar\u0131]( {token_url} } ) girin. Bu, SmartThings hesab\u0131n\u0131zda Home Assistant entegrasyonunu olu\u015fturmak i\u00e7in kullan\u0131lacakt\u0131r.", + "title": "Ki\u015fisel Eri\u015fim Anahtar\u0131 Girin" }, "select_location": { + "data": { + "location_id": "Konum" + }, "description": "L\u00fctfen Home Assistant'a eklemek istedi\u011finiz SmartThings Konumunu se\u00e7in. Ard\u0131ndan yeni bir pencere a\u00e7aca\u011f\u0131z ve sizden oturum a\u00e7man\u0131z\u0131 ve Home Assistant entegrasyonunun se\u00e7ilen konuma y\u00fcklenmesine izin vermenizi isteyece\u011fiz.", "title": "Konum Se\u00e7in" + }, + "user": { + "description": "SmartThings, \u015fu adreste Home Assistant'a an\u0131nda iletme g\u00fcncellemeleri g\u00f6nderecek \u015fekilde yap\u0131land\u0131r\u0131lacakt\u0131r:\n > {webhook_url} \n\n Bu do\u011fru de\u011filse, l\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 g\u00fcncelleyin, Home Assistant'\u0131 yeniden ba\u015flat\u0131n ve tekrar deneyin.", + "title": "Geri \u00c7a\u011f\u0131rma URL'sini Onayla" } } } diff --git a/homeassistant/components/smarttub/translations/tr.json b/homeassistant/components/smarttub/translations/tr.json index e3dac83de80..8917c5903df 100644 --- a/homeassistant/components/smarttub/translations/tr.json +++ b/homeassistant/components/smarttub/translations/tr.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "reauth_confirm": { - "description": "SmartTub entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor" + "description": "SmartTub entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "description": "Oturum a\u00e7mak i\u00e7in SmartTub e-posta adresinizi ve \u015fifrenizi girin", "title": "Oturum a\u00e7ma" } } diff --git a/homeassistant/components/smhi/translations/tr.json b/homeassistant/components/smhi/translations/tr.json index 1cf772ce6fa..380a0da053e 100644 --- a/homeassistant/components/smhi/translations/tr.json +++ b/homeassistant/components/smhi/translations/tr.json @@ -8,7 +8,8 @@ "user": { "data": { "latitude": "Enlem", - "longitude": "Boylam" + "longitude": "Boylam", + "name": "Ad" }, "title": "\u0130sve\u00e7'teki konum" } diff --git a/homeassistant/components/sms/translations/tr.json b/homeassistant/components/sms/translations/tr.json index 1ef2efb8121..0488390beed 100644 --- a/homeassistant/components/sms/translations/tr.json +++ b/homeassistant/components/sms/translations/tr.json @@ -10,6 +10,9 @@ }, "step": { "user": { + "data": { + "device": "Cihaz" + }, "title": "Modeme ba\u011flan\u0131n" } } diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json index b8159be58b4..248a546d2b2 100644 --- a/homeassistant/components/solaredge/translations/tr.json +++ b/homeassistant/components/solaredge/translations/tr.json @@ -12,8 +12,11 @@ "step": { "user": { "data": { - "api_key": "API Anahtar\u0131" - } + "api_key": "API Anahtar\u0131", + "name": "Bu kurulumun ad\u0131", + "site_id": "SolarEdge site kimli\u011fi" + }, + "title": "Bu kurulum i\u00e7in API parametrelerini tan\u0131mlay\u0131n" } } } diff --git a/homeassistant/components/solarlog/translations/tr.json b/homeassistant/components/solarlog/translations/tr.json index a11d3815eed..7f69014887b 100644 --- a/homeassistant/components/solarlog/translations/tr.json +++ b/homeassistant/components/solarlog/translations/tr.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar" - } + "host": "Ana Bilgisayar", + "name": "Solar-Log sens\u00f6rleriniz i\u00e7in kullan\u0131lacak \u00f6nek" + }, + "title": "Solar-Log ba\u011flant\u0131n\u0131z\u0131 tan\u0131mlay\u0131n" } } } diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json index 21a477c75a7..39d6bae6b7a 100644 --- a/homeassistant/components/soma/translations/tr.json +++ b/homeassistant/components/soma/translations/tr.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "already_setup": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "connection_error": "Ba\u011flanma hatas\u0131", + "missing_configuration": "Soma bile\u015feni yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "result_error": "SOMA Connect hata durumuyla yan\u0131t verdi." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, "step": { "user": { "data": { "host": "Ana Bilgisayar", "port": "Port" - } + }, + "description": "L\u00fctfen SOMA Connect'inizin ba\u011flant\u0131 ayarlar\u0131n\u0131 girin.", + "title": "SOMA Ba\u011flant\u0131s\u0131" } } } diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json index a152eb19468..b3b645cd52d 100644 --- a/homeassistant/components/somfy/translations/tr.json +++ b/homeassistant/components/somfy/translations/tr.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json index eadf0100045..d1e961cb2b9 100644 --- a/homeassistant/components/sonarr/translations/tr.json +++ b/homeassistant/components/sonarr/translations/tr.json @@ -9,12 +9,30 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "{name}", "step": { + "reauth_confirm": { + "description": "Sonarr entegrasyonunun, \u015fu adreste bar\u0131nd\u0131r\u0131lan Sonarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekir: {host}", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "api_key": "API Anahtar\u0131", + "base_path": "API yolu", "host": "Ana Bilgisayar", - "port": "Port" + "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "G\u00f6r\u00fcnt\u00fclenecek gelecek g\u00fcn say\u0131s\u0131", + "wanted_max_items": "G\u00f6r\u00fcnt\u00fclenecek maksimum istenen \u00f6\u011fe say\u0131s\u0131" } } } diff --git a/homeassistant/components/songpal/translations/tr.json b/homeassistant/components/songpal/translations/tr.json index ab90d4b1067..e1e9c0a4140 100644 --- a/homeassistant/components/songpal/translations/tr.json +++ b/homeassistant/components/songpal/translations/tr.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "not_songpal_device": "Songpal cihaz\u0131 de\u011fil" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name} ({host})", "step": { + "init": { + "description": "{name} ( {host} ) kurmak istiyor musunuz?" + }, "user": { "data": { "endpoint": "Biti\u015f noktas\u0131" diff --git a/homeassistant/components/sonos/translations/tr.json b/homeassistant/components/sonos/translations/tr.json index 5fae97936e4..7ebd593bac0 100644 --- a/homeassistant/components/sonos/translations/tr.json +++ b/homeassistant/components/sonos/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "not_sonos_device": "Bulunan cihaz bir Sonos cihaz\u0131 de\u011fil", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, diff --git a/homeassistant/components/spider/translations/tr.json b/homeassistant/components/spider/translations/tr.json index 9bcc6bb1c41..9d0729ffba4 100644 --- a/homeassistant/components/spider/translations/tr.json +++ b/homeassistant/components/spider/translations/tr.json @@ -12,7 +12,8 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "mijn.ithodaalderop.nl hesab\u0131yla oturum a\u00e7\u0131n" } } } diff --git a/homeassistant/components/spotify/translations/tr.json b/homeassistant/components/spotify/translations/tr.json index c543f155e4d..32c21899d52 100644 --- a/homeassistant/components/spotify/translations/tr.json +++ b/homeassistant/components/spotify/translations/tr.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_timeout": "Kimlik do\u011frulama URL'sini olu\u015ftururken zaman a\u015f\u0131m\u0131 ger\u00e7ekle\u015fti.", - "missing_configuration": "Spotify entegrasyonu ayarlanmam\u0131\u015f. L\u00fctfen dok\u00fcmentasyonu takip et." + "missing_configuration": "Spotify entegrasyonu ayarlanmam\u0131\u015f. L\u00fctfen dok\u00fcmentasyonu takip et.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "reauth_account_mismatch": "Kimli\u011fi do\u011frulanm\u0131\u015f Spotify hesab\u0131, yeniden kimlik do\u011frulamas\u0131 gereken hesapla e\u015fle\u015fmiyor." }, "create_entry": { "default": "Spotify ile kimlik ba\u015far\u0131yla do\u011fruland\u0131." @@ -10,6 +12,10 @@ "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Spotify entegrasyonunun \u015fu hesap i\u00e7in Spotify ile yeniden kimlik do\u011frulamas\u0131 yapmas\u0131 gerekiyor: {account}", + "title": "Entegrasyonu Yeniden Do\u011frula" } } }, diff --git a/homeassistant/components/squeezebox/translations/tr.json b/homeassistant/components/squeezebox/translations/tr.json index ff249aafa14..7fa98585507 100644 --- a/homeassistant/components/squeezebox/translations/tr.json +++ b/homeassistant/components/squeezebox/translations/tr.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_server_found": "LMS sunucusu bulunamad\u0131." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_server_found": "Sunucu otomatik olarak ke\u015ffedilemedi.", "unknown": "Beklenmeyen hata" }, + "flow_title": "{host}", "step": { "edit": { "data": { @@ -15,7 +18,8 @@ "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Ba\u011flant\u0131 bilgilerini d\u00fczenle" }, "user": { "data": { diff --git a/homeassistant/components/starline/translations/tr.json b/homeassistant/components/starline/translations/tr.json index 9d52f589e98..b89c9ae587e 100644 --- a/homeassistant/components/starline/translations/tr.json +++ b/homeassistant/components/starline/translations/tr.json @@ -1,10 +1,17 @@ { "config": { "error": { + "error_auth_app": "Yanl\u0131\u015f uygulama kimli\u011fi veya anahtar\u0131", + "error_auth_mfa": "Yanl\u0131\u015f kod", "error_auth_user": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131 ya da parola" }, "step": { "auth_app": { + "data": { + "app_id": "Uygulama Kimli\u011fi", + "app_secret": "Gizli" + }, + "description": "[StarLine geli\u015ftirici hesab\u0131ndan](https://my.starline.ru/developer) uygulama kimli\u011fi ve gizli kod", "title": "Uygulama kimlik bilgileri" }, "auth_captcha": { @@ -26,7 +33,8 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "StarLine hesab\u0131 e-postas\u0131 ve parolas\u0131" + "description": "StarLine hesab\u0131 e-postas\u0131 ve parolas\u0131", + "title": "Kullan\u0131c\u0131 kimlik bilgilerini" } } } diff --git a/homeassistant/components/stookalert/translations/tr.json b/homeassistant/components/stookalert/translations/tr.json index 717f6d72b94..7cbf0e58fdc 100644 --- a/homeassistant/components/stookalert/translations/tr.json +++ b/homeassistant/components/stookalert/translations/tr.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "province": "\u0130l" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/tr.json b/homeassistant/components/subaru/translations/tr.json index dd2bd5a4a29..5724bd4f2c3 100644 --- a/homeassistant/components/subaru/translations/tr.json +++ b/homeassistant/components/subaru/translations/tr.json @@ -1,14 +1,43 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "bad_pin_format": "PIN 4 haneli olmal\u0131d\u0131r", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "incorrect_pin": "Yanl\u0131\u015f PIN", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "pin": { "data": { "pin": "PIN" - } + }, + "description": "L\u00fctfen MySubaru PIN'inizi girin\n NOT: Hesaptaki t\u00fcm ara\u00e7lar ayn\u0131 PIN'e sahip olmal\u0131d\u0131r", + "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" }, "user": { + "data": { + "country": "\u00dclkeyi se\u00e7", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen MySubaru kimlik bilgilerinizi girin\n NOT: \u0130lk kurulum 30 saniye kadar s\u00fcrebilir", "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" } } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Ara\u00e7 yoklamay\u0131 etkinle\u015ftir" + }, + "description": "Etkinle\u015ftirildi\u011finde, ara\u00e7 yoklama, yeni sens\u00f6r verileri elde etmek i\u00e7in her 2 saatte bir arac\u0131n\u0131za bir uzaktan komut g\u00f6nderir. Ara\u00e7 sorgulamas\u0131 olmadan, yeni sens\u00f6r verileri yaln\u0131zca ara\u00e7 otomatik olarak veri g\u00f6nderdi\u011finde al\u0131n\u0131r (normalde motor kapat\u0131ld\u0131ktan sonra).", + "title": "Subaru Starlink Se\u00e7enekleri" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/tr.json b/homeassistant/components/surepetcare/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/tr.json b/homeassistant/components/switch/translations/tr.json index 0bbe4d5abf6..824b01c3b02 100644 --- a/homeassistant/components/switch/translations/tr.json +++ b/homeassistant/components/switch/translations/tr.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "{entity_name} de\u011fi\u015ftir", + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "condition_type": { + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 23262ea8ed4..15e64b86e5a 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -8,7 +8,11 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "few": "Puste", + "many": "Pustych", + "one": "Pusty", + "other": "" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index f108af331e3..75ec7055c6a 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -1,11 +1,14 @@ { "config": { "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", - "switchbot_unsupported_type": "Desteklenmeyen Switchbot T\u00fcr\u00fc." + "switchbot_unsupported_type": "Desteklenmeyen Switchbot T\u00fcr\u00fc.", + "unknown": "Beklenmeyen hata" }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", "one": "Bo\u015f", "other": "Bo\u015f" }, @@ -13,7 +16,22 @@ "step": { "user": { "data": { - "mac": "Cihaz MAC adresi" + "mac": "Cihaz MAC adresi", + "name": "Ad", + "password": "Parola" + }, + "title": "Switchbot cihaz\u0131n\u0131 kurun" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Yeniden deneme say\u0131s\u0131", + "retry_timeout": "Yeniden denemeler aras\u0131ndaki zaman a\u015f\u0131m\u0131", + "scan_timeout": "Reklam verilerinin taranmas\u0131 ne kadar s\u00fcrer?", + "update_time": "G\u00fcncellemeler aras\u0131ndaki s\u00fcre (saniye)" } } } diff --git a/homeassistant/components/switcher_kis/translations/tr.json b/homeassistant/components/switcher_kis/translations/tr.json new file mode 100644 index 00000000000..3df15466f03 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/tr.json b/homeassistant/components/syncthing/translations/tr.json index c46a8154d6f..eeeb7e02c3c 100644 --- a/homeassistant/components/syncthing/translations/tr.json +++ b/homeassistant/components/syncthing/translations/tr.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "user": { "data": { "title": "Senkronizasyon entegrasyonunu kurun", - "token": "Anahtar" + "token": "Anahtar", + "url": "URL", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } } diff --git a/homeassistant/components/syncthru/translations/tr.json b/homeassistant/components/syncthru/translations/tr.json index 942457958f8..f1c9117e684 100644 --- a/homeassistant/components/syncthru/translations/tr.json +++ b/homeassistant/components/syncthru/translations/tr.json @@ -3,9 +3,16 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "invalid_url": "Ge\u00e7ersiz URL", + "syncthru_not_supported": "Cihaz SyncThru'yu desteklemiyor", + "unknown_state": "Yaz\u0131c\u0131 durumu bilinmiyor, URL'yi ve a\u011f ba\u011flant\u0131s\u0131n\u0131 do\u011frulay\u0131n" + }, + "flow_title": "{name}", "step": { "confirm": { "data": { + "name": "Ad", "url": "Web aray\u00fcz\u00fc URL'si" } }, diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index 0b730d77629..2941e260c93 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -2,41 +2,71 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "reconfigure_successful": "Yeniden yap\u0131land\u0131rma ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "missing_data": "Eksik veriler: l\u00fctfen daha sonra veya ba\u015fka bir yap\u0131land\u0131rmay\u0131 yeniden deneyin", + "otp_failed": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, yeni bir parolayla yeniden deneyin", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "Kod" + }, + "title": "Synology DSM: iki ad\u0131ml\u0131 kimlik do\u011frulama" + }, "link": { "data": { "password": "Parola", "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" - } + }, + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "title": "Synology DSM" }, "reauth": { - "description": "Sebep: {details}" + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Sebep: {details}", + "title": "Synology DSM Entegrasyonu Yeniden Do\u011frula" }, "reauth_confirm": { "data": { "password": "\u015eifre", "username": "Kullan\u0131c\u0131 ad\u0131" - } + }, + "title": "Synology DSM Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { "host": "Ana Bilgisayar", "password": "Parola", "port": "Port", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" }, "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Taramalar aras\u0131ndaki dakika", + "timeout": "Zaman a\u015f\u0131m\u0131 (saniye)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/tr.json b/homeassistant/components/system_bridge/translations/tr.json new file mode 100644 index 00000000000..9c269e6e5fb --- /dev/null +++ b/homeassistant/components/system_bridge/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "{name} i\u00e7in yap\u0131land\u0131rman\u0131zda belirledi\u011finiz API Anahtar\u0131n\u0131 girin." + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana bilgisayar", + "port": "Port" + }, + "description": "L\u00fctfen ba\u011flant\u0131 bilgilerinizi giriniz." + } + } + }, + "title": "Sistem K\u00f6pr\u00fcs\u00fc" +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/tr.json b/homeassistant/components/tado/translations/tr.json index 09ffbf8a7d1..30fc86b8e6a 100644 --- a/homeassistant/components/tado/translations/tr.json +++ b/homeassistant/components/tado/translations/tr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_homes": "Bu Tado hesab\u0131na ba\u011fl\u0131 ev yok.", "unknown": "Beklenmeyen hata" }, "step": { @@ -13,7 +14,8 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Tado hesab\u0131n\u0131za ba\u011flan\u0131n" } } }, @@ -22,7 +24,9 @@ "init": { "data": { "fallback": "Geri d\u00f6n\u00fc\u015f modunu etkinle\u015ftirin." - } + }, + "description": "Geri d\u00f6n\u00fc\u015f modu, bir b\u00f6lgeyi manuel olarak ayarlad\u0131ktan sonra bir sonraki program anahtar\u0131nda Ak\u0131ll\u0131 Programa ge\u00e7ecektir.", + "title": "Tado se\u00e7eneklerini ayarlay\u0131n." } } } diff --git a/homeassistant/components/tag/translations/tr.json b/homeassistant/components/tag/translations/tr.json new file mode 100644 index 00000000000..52000386163 --- /dev/null +++ b/homeassistant/components/tag/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Etiket" +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json index a559d0911ee..95e4e41b11e 100644 --- a/homeassistant/components/tasmota/translations/tr.json +++ b/homeassistant/components/tasmota/translations/tr.json @@ -3,8 +3,14 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "invalid_discovery_topic": "Ge\u00e7ersiz ba\u015fl\u0131k ke\u015ffi" + }, "step": { "config": { + "data": { + "discovery_prefix": "Ba\u015fl\u0131k Ke\u015ffet" + }, "description": "L\u00fctfen Tasmota yap\u0131land\u0131rmas\u0131n\u0131 girin.", "title": "Tasmota" }, diff --git a/homeassistant/components/tellduslive/translations/tr.json b/homeassistant/components/tellduslive/translations/tr.json index 300fad68391..7b462a4b61d 100644 --- a/homeassistant/components/tellduslive/translations/tr.json +++ b/homeassistant/components/tellduslive/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", "unknown": "Beklenmeyen hata", "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." }, @@ -9,10 +10,16 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "auth": { + "description": "TelldusLive hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in:\n 1. A\u015fa\u011f\u0131daki ba\u011flant\u0131ya t\u0131klay\u0131n\n 2. Telldus Live'a giri\u015f yap\u0131n\n 3. ** {app_name} **'yi yetkilendirin (**Evet**'i t\u0131klay\u0131n).\n 4. Buraya geri d\u00f6n\u00fcn ve **G\u00d6NDER**'e t\u0131klay\u0131n. \n\n [TelldusLive hesab\u0131n\u0131 ba\u011fla]( {auth_url} )", + "title": "TelldusLive'a kar\u015f\u0131 kimlik do\u011frulamas\u0131" + }, "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Bo\u015f", + "title": "Biti\u015f noktas\u0131n\u0131 se\u00e7in." } } } diff --git a/homeassistant/components/tibber/translations/tr.json b/homeassistant/components/tibber/translations/tr.json index 5f8e72986b2..a34aab70d6c 100644 --- a/homeassistant/components/tibber/translations/tr.json +++ b/homeassistant/components/tibber/translations/tr.json @@ -5,13 +5,16 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "timeout": "Tibber'a ba\u011flan\u0131rken zaman a\u015f\u0131m\u0131" }, "step": { "user": { "data": { "access_token": "Eri\u015fim Belirteci" - } + }, + "description": "https://developer.tibber.com/settings/accesstoken adresinden eri\u015fim anahtar\u0131n\u0131z\u0131 girin", + "title": "Tibber" } } } diff --git a/homeassistant/components/tile/translations/tr.json b/homeassistant/components/tile/translations/tr.json index 8a04e2f4bbf..793f7fd5fd7 100644 --- a/homeassistant/components/tile/translations/tr.json +++ b/homeassistant/components/tile/translations/tr.json @@ -21,7 +21,8 @@ "init": { "data": { "show_inactive": "Etkin Olmayan Karolar\u0131 G\u00f6ster" - } + }, + "title": "Tile'yi Yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/toon/translations/tr.json b/homeassistant/components/toon/translations/tr.json index a52d914241d..5964dea6ba5 100644 --- a/homeassistant/components/toon/translations/tr.json +++ b/homeassistant/components/toon/translations/tr.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Se\u00e7ilen anla\u015fma zaten yap\u0131land\u0131r\u0131lm\u0131\u015f.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_agreements": "Bu hesapta Toon ekran\u0131 yok.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." }, "step": { diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json index f941db5ab89..3b3f8eabe5e 100644 --- a/homeassistant/components/totalconnect/translations/tr.json +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -1,17 +1,32 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "usercode": "Kullan\u0131c\u0131 kodu bu konumdaki kullan\u0131c\u0131 i\u00e7in ge\u00e7erli de\u011fil" }, "step": { + "locations": { + "data": { + "location": "Konum", + "usercode": "Kullan\u0131c\u0131 kodu" + }, + "description": "Bu kullan\u0131c\u0131n\u0131n kullan\u0131c\u0131 kodunu {location_id} konumuna girin", + "title": "Konum Kullan\u0131c\u0131 Kodlar\u0131" + }, + "reauth_confirm": { + "description": "Total Connect'in hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "Toplam Ba\u011flant\u0131" } } } diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index a8834ae4c92..5e4dca23c37 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "flow_title": "{name} {model} ({host})", "step": { "confirm": { diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index 9a2b1a119cd..c16dc8c09d2 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -4,8 +4,12 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, + "create_entry": { + "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Traccar'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n \u015eu URL'yi kullan\u0131n: ` {webhook_url} ` \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + }, "step": { "user": { + "description": "Traccar'\u0131 kurmak istedi\u011finizden emin misiniz?", "title": "Traccar'\u0131 kur" } } diff --git a/homeassistant/components/tractive/translations/tr.json b/homeassistant/components/tractive/translations/tr.json new file mode 100644 index 00000000000..270c80ca0a4 --- /dev/null +++ b/homeassistant/components/tractive/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_failed_existing": "Yap\u0131land\u0131rma giri\u015fi g\u00fcncellenemedi, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/tr.json b/homeassistant/components/tradfri/translations/tr.json index e4483536b12..26db68abc5c 100644 --- a/homeassistant/components/tradfri/translations/tr.json +++ b/homeassistant/components/tradfri/translations/tr.json @@ -5,13 +5,19 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_authenticate": "Kimlik do\u011frulamas\u0131 yap\u0131lam\u0131yor, A\u011f Ge\u00e7idi, \u00f6rne\u011fin Homekit gibi ba\u015fka bir sunucuyla e\u015fle\u015ftirilmi\u015f mi?", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_key": "Sa\u011flanan anahtarla kay\u0131t olunamad\u0131. Bu durum devam ederse, a\u011f ge\u00e7idini yeniden ba\u015flatmay\u0131 deneyin.", + "timeout": "Kodun do\u011frulanmas\u0131 zaman a\u015f\u0131m\u0131na u\u011frad\u0131." }, "step": { "auth": { "data": { - "host": "Ana Bilgisayar" - } + "host": "Ana Bilgisayar", + "security_code": "G\u00fcvenlik Kodu" + }, + "description": "G\u00fcvenlik kodunu a\u011f ge\u00e7idinizin arkas\u0131nda bulabilirsiniz.", + "title": "G\u00fcvenlik kodunu gir" } } } diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index cffcc65151c..94ba7572e03 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -5,16 +5,31 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "name_exists": "Ad zaten var" }, "step": { "user": { "data": { "host": "Ana Bilgisayar", + "name": "Ad", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "\u0130letim \u0130stemcisi Kurulumu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "Limit", + "order": "Sipari\u015f", + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" + }, + "title": "\u0130letim se\u00e7eneklerini yap\u0131land\u0131r\u0131n" } } } diff --git a/homeassistant/components/tuya/translations/select.pl.json b/homeassistant/components/tuya/translations/select.pl.json new file mode 100644 index 00000000000..36bc447f766 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.pl.json @@ -0,0 +1,25 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "1": "Wy\u0142\u0105czony", + "2": "W\u0142\u0105czony" + }, + "tuya__led_type": { + "halogen": "Halogen", + "led": "LED" + }, + "tuya__motion_sensitivity": { + "0": "Niska czu\u0142o\u015b\u0107", + "1": "\u015arednia czu\u0142o\u015b\u0107", + "2": "Wysoka czu\u0142o\u015b\u0107" + }, + "tuya__relay_status": { + "last": "Zapami\u0119taj ostatni stan", + "memory": "Zapami\u0119taj ostatni stan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.pl.json b/homeassistant/components/tuya/translations/sensor.pl.json new file mode 100644 index 00000000000..68556f9f757 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.pl.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Temperatura wrzenia", + "cooling": "Ch\u0142odzenie", + "heating": "Ogrzewanie", + "heating_temp": "Temperatura ogrzewania", + "reserve_1": "Rezerwa 1", + "reserve_2": "Rezerwa 2", + "reserve_3": "Rezerwa 3", + "standby": "Tryb czuwania", + "warm": "Utrzymanie ciep\u0142a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 9de7fea55d4..fcd7cf3b2e4 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -14,6 +14,7 @@ "login": { "data": { "access_id": "Eri\u015fim Kimli\u011fi", + "access_secret": "Eri\u015fim Anahtar\u0131", "country_code": "\u00dclke Kodu", "endpoint": "Kullan\u0131labilirlik Alan\u0131", "password": "\u015eifre", @@ -25,6 +26,8 @@ }, "user": { "data": { + "access_id": "Tuya IoT Eri\u015fim Kimli\u011fi", + "access_secret": "Tuya IoT Eri\u015fim Anahtar\u0131", "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", "password": "Parola", "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", @@ -50,12 +53,15 @@ "device": { "data": { "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", + "curr_temp_divider": "Mevcut S\u0131cakl\u0131k de\u011feri b\u00f6l\u00fcc\u00fc (0 = varsay\u0131lan\u0131 kullan)", "max_kelvin": "Kelvin'de desteklenen maksimum renk s\u0131cakl\u0131\u011f\u0131", "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", + "set_temp_divided": "Ayarlanan s\u0131cakl\u0131k komutu i\u00e7in b\u00f6l\u00fcnm\u00fc\u015f S\u0131cakl\u0131k de\u011ferini kullan\u0131n", "support_color": "Vurgu rengi", "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", + "temp_step_override": "Hedef S\u0131cakl\u0131k ad\u0131m\u0131", "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" }, diff --git a/homeassistant/components/twentemilieu/translations/tr.json b/homeassistant/components/twentemilieu/translations/tr.json index 590aec1894c..363128d9c1e 100644 --- a/homeassistant/components/twentemilieu/translations/tr.json +++ b/homeassistant/components/twentemilieu/translations/tr.json @@ -4,7 +4,19 @@ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_address": "Adres Twente Milieu hizmet b\u00f6lgesinde bulunamad\u0131." + }, + "step": { + "user": { + "data": { + "house_letter": "Ev mektubu/ek", + "house_number": "Ev numaras\u0131", + "post_code": "Posta kodu" + }, + "description": "Adresinizde at\u0131k toplama bilgileri sa\u011flayan Twente Milieu'yu kurun.", + "title": "Twente Milieu" + } } } } \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json index 84adcdf8225..ef684cbc92c 100644 --- a/homeassistant/components/twilio/translations/tr.json +++ b/homeassistant/components/twilio/translations/tr.json @@ -3,6 +3,15 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "create_entry": { + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Twilio ile Webhook]( {twilio_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: application/x-www-form-urlencoded \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131 hakk\u0131nda [belgelere]( {docs_url}" + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Twilio Webhook'u kurun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index c39fa08217a..700f70def14 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -17,8 +17,11 @@ "host": "Ana Bilgisayar", "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 ad\u0131" - } + "site": "Site Kimli\u011fi", + "username": "Kullan\u0131c\u0131 ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "title": "UniFi Controller'\u0131 kurun" } } }, @@ -26,8 +29,46 @@ "step": { "client_control": { "data": { - "dpi_restrictions": "DPI k\u0131s\u0131tlama gruplar\u0131n\u0131n kontrol\u00fcne izin ver" + "block_client": "A\u011f eri\u015fimi denetimli istemciler", + "dpi_restrictions": "DPI k\u0131s\u0131tlama gruplar\u0131n\u0131n kontrol\u00fcne izin ver", + "poe_clients": "\u0130stemcilerin POE denetimine izin ver" + }, + "description": "\u0130stemci denetimlerini yap\u0131land\u0131r\u0131n \n\n A\u011f eri\u015fimini denetlemek istedi\u011finiz seri numaralar\u0131 i\u00e7in anahtarlar olu\u015fturun.", + "title": "UniFi se\u00e7enekleri 2/3" + }, + "device_tracker": { + "data": { + "detection_time": "Son g\u00f6r\u00fclmeden uzakta say\u0131lana kadar ge\u00e7en saniye cinsinden s\u00fcre", + "ignore_wired_bug": "UniFi kablolu hata mant\u0131\u011f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "ssid_filter": "Kablosuz istemcileri izlemek i\u00e7in SSID'leri se\u00e7in", + "track_clients": "A\u011f istemcilerini takip edin", + "track_devices": "A\u011f cihazlar\u0131n\u0131 takip edin (Ubiquiti cihazlar\u0131)", + "track_wired_clients": "Kablolu a\u011f istemcilerini dahil et" + }, + "description": "Cihaz izlemeyi yap\u0131land\u0131r", + "title": "UniFi se\u00e7enekleri 1/3" + }, + "init": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" } + }, + "simple_options": { + "data": { + "block_client": "A\u011f eri\u015fimi denetimli istemciler", + "track_clients": "A\u011f istemcilerini takip edin", + "track_devices": "A\u011f cihazlar\u0131n\u0131 takip edin (Ubiquiti cihazlar\u0131)" + }, + "description": "UniFi entegrasyonunu yap\u0131land\u0131r\u0131n" + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "A\u011f istemcileri i\u00e7in bant geni\u015fli\u011fi kullan\u0131m sens\u00f6rleri", + "allow_uptime_sensors": "A\u011f istemcileri i\u00e7in \u00e7al\u0131\u015fma s\u00fcresi sens\u00f6rleri" + }, + "description": "\u0130statistik sens\u00f6rlerini yap\u0131land\u0131r\u0131n", + "title": "UniFi se\u00e7enekleri 3/3" } } } diff --git a/homeassistant/components/upb/translations/tr.json b/homeassistant/components/upb/translations/tr.json index b70e56b88da..0fd1504e32b 100644 --- a/homeassistant/components/upb/translations/tr.json +++ b/homeassistant/components/upb/translations/tr.json @@ -10,7 +10,13 @@ }, "step": { "user": { - "description": "Bir Evrensel Elektrik Hatt\u0131 Veriyolu Elektrik Hatt\u0131 Aray\u00fcz Mod\u00fcl\u00fc (UPB PIM) ba\u011flay\u0131n. Adres dizesi 'tcp' i\u00e7in 'adres[:port]' bi\u00e7iminde olmal\u0131d\u0131r. Ba\u011flant\u0131 noktas\u0131 iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 2101'dir. \u00d6rnek: '192.168.1.42'. Seri protokol i\u00e7in adres 'tty[:baud]' bi\u00e7iminde olmal\u0131d\u0131r. Baud iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 4800'd\u00fcr. \u00d6rnek: '/dev/ttyS1'." + "data": { + "address": "Adres (yukar\u0131daki a\u00e7\u0131klamaya bak\u0131n)", + "file_path": "UPSStart UPB d\u0131\u015fa aktarma dosyas\u0131n\u0131n yolu ve ad\u0131.", + "protocol": "Protokol" + }, + "description": "Bir Evrensel Elektrik Hatt\u0131 Veriyolu Elektrik Hatt\u0131 Aray\u00fcz Mod\u00fcl\u00fc (UPB PIM) ba\u011flay\u0131n. Adres dizesi 'tcp' i\u00e7in 'adres[:port]' bi\u00e7iminde olmal\u0131d\u0131r. Ba\u011flant\u0131 noktas\u0131 iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 2101'dir. \u00d6rnek: '192.168.1.42'. Seri protokol i\u00e7in adres 'tty[:baud]' bi\u00e7iminde olmal\u0131d\u0131r. Baud iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 4800'd\u00fcr. \u00d6rnek: '/dev/ttyS1'.", + "title": "UPB PIM'e ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/upcloud/translations/tr.json b/homeassistant/components/upcloud/translations/tr.json index f1840698493..1e0f7a4bc4d 100644 --- a/homeassistant/components/upcloud/translations/tr.json +++ b/homeassistant/components/upcloud/translations/tr.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Saniye cinsinden g\u00fcncelleme aral\u0131\u011f\u0131, minimum 30" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json index 2715f66e090..ec26f90fa41 100644 --- a/homeassistant/components/upnp/translations/tr.json +++ b/homeassistant/components/upnp/translations/tr.json @@ -1,19 +1,39 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "incomplete_discovery": "Tamamlanmam\u0131\u015f tarama", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "one": "Bo\u015f", + "other": "Bo\u015f" }, "flow_title": "UPnP / IGD: {name}", "step": { + "init": { + "one": "Bo\u015f", + "other": "" + }, "ssdp_confirm": { "description": "Bu UPnP / IGD cihaz\u0131n\u0131 kurmak istiyor musunuz?" }, "user": { "data": { "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)", + "unique_id": "Cihaz", "usn": "Cihaz" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/tr.json b/homeassistant/components/uptimerobot/translations/tr.json index 007b33db388..c209afdfd8e 100644 --- a/homeassistant/components/uptimerobot/translations/tr.json +++ b/homeassistant/components/uptimerobot/translations/tr.json @@ -1,16 +1,29 @@ { "config": { "abort": { - "reauth_failed_existing": "Yap\u0131land\u0131rma giri\u015fi g\u00fcncellenemedi, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_failed_existing": "Yap\u0131land\u0131rma giri\u015fi g\u00fcncellenemedi, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" }, "error": { - "reauth_failed_matching_account": "Sa\u011flad\u0131\u011f\u0131n\u0131z API anahtar\u0131, mevcut yap\u0131land\u0131rman\u0131n hesap kimli\u011fiyle e\u015fle\u015fmiyor." + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "reauth_failed_matching_account": "Sa\u011flad\u0131\u011f\u0131n\u0131z API anahtar\u0131, mevcut yap\u0131land\u0131rman\u0131n hesap kimli\u011fiyle e\u015fle\u015fmiyor.", + "unknown": "Beklenmeyen hata" }, "step": { "reauth_confirm": { - "description": "UptimeRobot'tan yeni bir salt okunur API anahtar\u0131 sa\u011flaman\u0131z gerekiyor" + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "UptimeRobot'tan yeni bir salt okunur API anahtar\u0131 sa\u011flaman\u0131z gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { + "data": { + "api_key": "API Anahtar\u0131" + }, "description": "UptimeRobot'tan salt okunur bir API anahtar\u0131 sa\u011flaman\u0131z gerekiyor" } } diff --git a/homeassistant/components/vacuum/translations/tr.json b/homeassistant/components/vacuum/translations/tr.json index 7d127417cb5..955dfb03a5f 100644 --- a/homeassistant/components/vacuum/translations/tr.json +++ b/homeassistant/components/vacuum/translations/tr.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "{entity_name} \u00f6\u011fesini temizle", + "dock": "{entity_name} dock'a d\u00f6ns\u00fcn" + }, + "condition_type": { + "is_cleaning": "{entity_name} temizleniyor", + "is_docked": "{entity_name} yerle\u015ftirildi" + }, + "trigger_type": { + "cleaning": "{entity_name} temizlemeye ba\u015flad\u0131", + "docked": "{entity_name} yerle\u015ftirildi" + } + }, "state": { "_": { "cleaning": "Temizleniyor", diff --git a/homeassistant/components/velbus/translations/tr.json b/homeassistant/components/velbus/translations/tr.json index e7ee4ea7157..001b2c1d5eb 100644 --- a/homeassistant/components/velbus/translations/tr.json +++ b/homeassistant/components/velbus/translations/tr.json @@ -6,6 +6,15 @@ "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "name": "Bu velbus ba\u011flant\u0131s\u0131n\u0131n ad\u0131", + "port": "Ba\u011flant\u0131 dizesi" + }, + "title": "Velbus ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc tan\u0131mlay\u0131n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pl.json b/homeassistant/components/venstar/translations/pl.json index c931afdae8d..34af592d520 100644 --- a/homeassistant/components/venstar/translations/pl.json +++ b/homeassistant/components/venstar/translations/pl.json @@ -4,7 +4,17 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Has\u0142o", + "pin": "Kod PIN" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json index 9efa5c3c214..a4493886577 100644 --- a/homeassistant/components/venstar/translations/tr.json +++ b/homeassistant/components/venstar/translations/tr.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, "step": { "user": { "data": { "host": "Sunucu", "password": "\u015eifre", "pin": "PIN Kodu", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 ad\u0131" }, "title": "Venstar Termostat'a ba\u011flan\u0131n" diff --git a/homeassistant/components/vera/translations/tr.json b/homeassistant/components/vera/translations/tr.json index 35e81599bb1..fa66c1c9806 100644 --- a/homeassistant/components/vera/translations/tr.json +++ b/homeassistant/components/vera/translations/tr.json @@ -2,11 +2,27 @@ "config": { "abort": { "cannot_connect": "{base_url} url'si ile denetleyiciye ba\u011flan\u0131lamad\u0131" + }, + "step": { + "user": { + "data": { + "exclude": "Home Asistan\u0131'ndan hari\u00e7 tutulacak Vera cihaz kimlikleri.", + "lights": "Vera, Home Assistant'ta \u0131\u015f\u0131k gibi davranmak i\u00e7in cihaz kimliklerini de\u011fi\u015ftirir.", + "vera_controller_url": "Denetleyici URL'si" + }, + "description": "A\u015fa\u011f\u0131da bir Vera denetleyici URL'si sa\u011flay\u0131n. \u015eu \u015fekilde g\u00f6r\u00fcnmelidir: http://192.168.1.161:3480.", + "title": "Vera denetleyicisini kurun" + } } }, "options": { "step": { "init": { + "data": { + "exclude": "Home Asistan\u0131'ndan hari\u00e7 tutulacak Vera cihaz kimlikleri.", + "lights": "Vera, Home Assistant'ta \u0131\u015f\u0131k gibi davranmak i\u00e7in cihaz kimliklerini de\u011fi\u015ftirir." + }, + "description": "\u0130ste\u011fe ba\u011fl\u0131 parametrelerle ilgili ayr\u0131nt\u0131lar i\u00e7in vera belgelerine bak\u0131n: https://www.home-assistant.io/integrations/vera/. Not: Buradaki herhangi bir de\u011fi\u015fiklik, ev asistan\u0131 sunucusunun yeniden ba\u015flat\u0131lmas\u0131n\u0131 gerektirecektir. De\u011ferleri temizlemek i\u00e7in bir bo\u015fluk b\u0131rak\u0131n.", "title": "Vera denetleyici se\u00e7enekleri" } } diff --git a/homeassistant/components/verisure/translations/tr.json b/homeassistant/components/verisure/translations/tr.json index 3396fdd0692..88a0d943872 100644 --- a/homeassistant/components/verisure/translations/tr.json +++ b/homeassistant/components/verisure/translations/tr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "installation": { "data": { @@ -9,12 +17,16 @@ }, "reauth_confirm": { "data": { - "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." + "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n.", + "email": "E-posta", + "password": "Parola" } }, "user": { "data": { - "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n." + "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n.", + "email": "E-posta", + "password": "Parola" } } } @@ -26,7 +38,8 @@ "step": { "init": { "data": { - "lock_code_digits": "Kilitler i\u00e7in PIN kodundaki hane say\u0131s\u0131" + "lock_code_digits": "Kilitler i\u00e7in PIN kodundaki hane say\u0131s\u0131", + "lock_default_code": "Kilitler i\u00e7in varsay\u0131lan PIN kodu, kod verilmezse kullan\u0131lacakt\u0131r." } } } diff --git a/homeassistant/components/vilfo/translations/tr.json b/homeassistant/components/vilfo/translations/tr.json index dc66041e35a..2e27132afb6 100644 --- a/homeassistant/components/vilfo/translations/tr.json +++ b/homeassistant/components/vilfo/translations/tr.json @@ -13,7 +13,9 @@ "data": { "access_token": "Eri\u015fim Belirteci", "host": "Ana Bilgisayar" - } + }, + "description": "Vilfo Router entegrasyonunu ayarlay\u0131n. Vilfo Router ana bilgisayar ad\u0131n\u0131za/IP'nize ve bir API eri\u015fim anahtar\u0131na ihtiyac\u0131n\u0131z var. Bu entegrasyon ve bu ayr\u0131nt\u0131lar\u0131n nas\u0131l al\u0131naca\u011f\u0131 hakk\u0131nda ek bilgi i\u00e7in \u015fu adresi ziyaret edin: https://www.home-assistant.io/integrations/vilfo", + "title": "Vilfo Router'a ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index b976471b56a..332340ec0e0 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -2,18 +2,52 @@ "config": { "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "updated_entry": "Bu giri\u015f zaten kuruldu ancak konfig\u00fcrasyonda tan\u0131mlanan ad, uygulamalar ve/veya se\u00e7enekler daha \u00f6nce i\u00e7e aktar\u0131lan konfig\u00fcrasyonla e\u015fle\u015fmiyor, bu nedenle konfig\u00fcrasyon giri\u015fi buna g\u00f6re g\u00fcncellendi." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "complete_pairing_failed": "E\u015fle\u015ftirme tamamlanamad\u0131. Yeniden g\u00f6ndermeden \u00f6nce, verdi\u011finiz PIN'in do\u011fru oldu\u011fundan ve TV'ye g\u00fc\u00e7 verildi\u011finden ve a\u011fa ba\u011fl\u0131 oldu\u011fundan emin olun." + "complete_pairing_failed": "E\u015fle\u015ftirme tamamlanamad\u0131. Yeniden g\u00f6ndermeden \u00f6nce, verdi\u011finiz PIN'in do\u011fru oldu\u011fundan ve TV'ye g\u00fc\u00e7 verildi\u011finden ve a\u011fa ba\u011fl\u0131 oldu\u011fundan emin olun.", + "existing_config_entry_found": "Ayn\u0131 seri numaras\u0131na sahip mevcut bir VIZIO SmartCast Cihaz\u0131 yap\u0131land\u0131rma giri\u015fi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Bunu konfig\u00fcre etmek i\u00e7in mevcut giri\u015fi silmelisiniz." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN Kodu" + }, + "description": "TV'niz bir kod g\u00f6steriyor olmal\u0131d\u0131r. Bu kodu forma girin ve ard\u0131ndan e\u015fle\u015ftirmeyi tamamlamak i\u00e7in bir sonraki ad\u0131ma ge\u00e7in.", + "title": "E\u015fle\u015ftirme \u0130\u015flemini Tamamlay\u0131n" + }, + "pairing_complete": { + "description": "VIZIO SmartCast Cihaz\u0131 art\u0131k Home Assistant'a ba\u011fl\u0131.", + "title": "E\u015fle\u015ftirme Tamamland\u0131" + }, + "pairing_complete_import": { + "description": "VIZIO SmartCast Cihaz\u0131 art\u0131k Home Assistant'a ba\u011fl\u0131. \n\n Eri\u015fim Anahtar\u0131 de\u011feriniz '** {access_token} **'.", + "title": "E\u015fle\u015ftirme Tamamland\u0131" + }, "user": { "data": { "access_token": "Eri\u015fim Belirteci", - "host": "Ana Bilgisayar" - } + "device_class": "Cihaz tipi", + "host": "Ana Bilgisayar", + "name": "Ad" + }, + "description": "Eri\u015fim Anahtar\u0131 yaln\u0131zca TV'ler i\u00e7in gereklidir. Bir TV yap\u0131land\u0131r\u0131yorsan\u0131z ve hen\u00fcz bir Eri\u015fim Anahtar\u0131 , e\u015fle\u015ftirme i\u015fleminden ge\u00e7mek i\u00e7in bo\u015f b\u0131rak\u0131n.", + "title": "VIZIO SmartCast Cihaz\u0131" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "Dahil veya Hari\u00e7 Tutulacak Uygulamalar", + "include_or_exclude": "Uygulamalar Dahil mi yoksa, Hari\u00e7 tutulsun mu?", + "volume_step": "Birim Ad\u0131m Boyutu" + }, + "description": "Smart TV'niz varsa, kaynak listenize hangi uygulamalar\u0131n dahil edilece\u011fini veya hari\u00e7 tutulaca\u011f\u0131n\u0131 se\u00e7erek iste\u011fe ba\u011fl\u0131 olarak kaynak listenizi filtreleyebilirsiniz.", + "title": "VIZIO SmartCast Cihaz\u0131 Se\u00e7eneklerini g\u00fcncelleyin" } } } diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json index aeee8ca6eec..93bd4a290e7 100644 --- a/homeassistant/components/vlc_telnet/translations/tr.json +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -2,11 +2,17 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "reauth_successful": "Kimlik do\u011frulama yeniden ba\u015far\u0131l\u0131 oldu" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Kimlik do\u011frulama yeniden ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" }, "error": { - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "{addon} eklentisine ba\u011flanmak istiyor musunuz?" diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json index 249bb17d64e..c954aa35ac2 100644 --- a/homeassistant/components/volumio/translations/tr.json +++ b/homeassistant/components/volumio/translations/tr.json @@ -9,6 +9,10 @@ "unknown": "Beklenmeyen hata" }, "step": { + "discovery_confirm": { + "description": "Home Asistan\u0131'na Volumio ('{name}') eklemek istiyor musunuz?", + "title": "Bulunan Volumio" + }, "user": { "data": { "host": "Ana Bilgisayar", diff --git a/homeassistant/components/wallbox/translations/tr.json b/homeassistant/components/wallbox/translations/tr.json new file mode 100644 index 00000000000..a5efc052b2f --- /dev/null +++ b/homeassistant/components/wallbox/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "station": "\u0130stasyon Seri Numaras\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/tr.json b/homeassistant/components/water_heater/translations/tr.json index 5b5115889ee..c4633d419c3 100644 --- a/homeassistant/components/water_heater/translations/tr.json +++ b/homeassistant/components/water_heater/translations/tr.json @@ -12,6 +12,7 @@ "gas": "Gaz", "heat_pump": "Is\u0131 pompas\u0131", "high_demand": "Y\u00fcksek talep", + "off": "Kapal\u0131", "performance": "Performans" } } diff --git a/homeassistant/components/watttime/translations/tr.json b/homeassistant/components/watttime/translations/tr.json index 00c82748832..4c828b6b5be 100644 --- a/homeassistant/components/watttime/translations/tr.json +++ b/homeassistant/components/watttime/translations/tr.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata", "unknown_coordinates": "Enlem/boylam i\u00e7in veri yok" }, "step": { "coordinates": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + }, "description": "\u0130zlenecek enlem ve boylam\u0131 girin:" }, "location": { @@ -16,7 +26,9 @@ "reauth_confirm": { "data": { "password": "\u015eifre" - } + }, + "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:", + "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/tr.json b/homeassistant/components/waze_travel_time/translations/tr.json index afc6741e97a..71e65efb333 100644 --- a/homeassistant/components/waze_travel_time/translations/tr.json +++ b/homeassistant/components/waze_travel_time/translations/tr.json @@ -1,9 +1,39 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { + "data": { + "destination": "Hedef", + "name": "Ad", + "origin": "Men\u015fei", + "region": "B\u00f6lge" + }, "description": "Ba\u015flang\u0131\u00e7 ve Var\u0131\u015f Yeri i\u00e7in, konumun adresini veya GPS koordinatlar\u0131n\u0131 girin (GPS koordinatlar\u0131 virg\u00fclle ayr\u0131lmal\u0131d\u0131r). Bu bilgiyi kendi durumunda sa\u011flayan bir varl\u0131k kimli\u011fi, enlem ve boylam niteliklerine sahip bir varl\u0131k kimli\u011fi veya b\u00f6lge dostu ad da girebilirsiniz." } } - } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Feribotlardan Ka\u00e7\u0131n\u0131n?", + "avoid_subscription_roads": "Vinyet / Abonelik Gerektiren Yollardan Ka\u00e7\u0131n\u0131n?", + "avoid_toll_roads": "\u00dccretli Yollardan Ka\u00e7\u0131n\u0131n?", + "excl_filter": "Alt dize, Se\u00e7ili Rotan\u0131n A\u00e7\u0131klamas\u0131nda DE\u011e\u0130L", + "incl_filter": "Se\u00e7ili Rotan\u0131n A\u00e7\u0131klamas\u0131nda Alt Dize Olu\u015fturma", + "realtime": "Ger\u00e7ek Zamanl\u0131 Seyahat S\u00fcresi?", + "units": "Birimler", + "vehicle_type": "Ara\u00e7 Tipi" + }, + "description": "'Alt dizi' giri\u015fleri, entegrasyonu belirli bir rotay\u0131 kullanmaya zorlaman\u0131za veya zaman yolculu\u011fu hesaplamas\u0131nda belirli bir rotadan ka\u00e7\u0131nman\u0131za izin verecektir." + } + } + }, + "title": "Waze Seyahat S\u00fcresi" } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/tr.json b/homeassistant/components/whirlpool/translations/tr.json new file mode 100644 index 00000000000..6bb59e7943a --- /dev/null +++ b/homeassistant/components/whirlpool/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json index 26ec2e61e00..281300caee3 100644 --- a/homeassistant/components/wiffi/translations/tr.json +++ b/homeassistant/components/wiffi/translations/tr.json @@ -8,7 +8,8 @@ "user": { "data": { "port": "Port" - } + }, + "title": "WIFI cihazlar\u0131 i\u00e7in TCP sunucusunu kurun" } } }, diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json index 5307276a71d..446504f49d7 100644 --- a/homeassistant/components/wilight/translations/tr.json +++ b/homeassistant/components/wilight/translations/tr.json @@ -1,7 +1,16 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "not_supported_device": "Bu WiLight \u015fu anda desteklenmiyor", + "not_wilight_device": "Bu Cihaz WiLight de\u011fil" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "{name} kurmak istiyor musunuz? \n\n \u015eunlar\u0131 destekler: {components}", + "title": "WiLight" + } } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json index 4e0228708ea..31b2a424071 100644 --- a/homeassistant/components/withings/translations/tr.json +++ b/homeassistant/components/withings/translations/tr.json @@ -1,17 +1,32 @@ { "config": { "abort": { - "already_configured": "Profil i\u00e7in yap\u0131land\u0131rma g\u00fcncellendi." + "already_configured": "Profil i\u00e7in yap\u0131land\u0131rma g\u00fcncellendi.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" + }, + "create_entry": { + "default": "Withings ile ba\u015far\u0131yla do\u011fruland\u0131." }, "error": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "flow_title": "{profile}", "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, "profile": { "data": { "profile": "Profil Ad\u0131" }, + "description": "Bu veriler i\u00e7in benzersiz bir profil ad\u0131 sa\u011flay\u0131n. Genellikle bu, \u00f6nceki ad\u0131mda se\u00e7ti\u011finiz profilin ad\u0131d\u0131r.", "title": "Kullan\u0131c\u0131 profili." + }, + "reauth": { + "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile}", + "title": "Entegrasyonu Yeniden Do\u011frula" } } } diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index f02764c8aba..859e8c91b10 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -16,7 +16,17 @@ "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n." }, "zeroconf_confirm": { - "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?" + "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?", + "title": "Bulunan WLED cihaz\u0131" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "1 LED segmenti ile bile ana \u0131\u015f\u0131\u011f\u0131 koruyun." + } } } } diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json index c276cbfc7eb..2a8827f9bfd 100644 --- a/homeassistant/components/wolflink/translations/sensor.tr.json +++ b/homeassistant/components/wolflink/translations/sensor.tr.json @@ -1,16 +1,70 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Baca gaz\u0131 damperi", + "absenkbetrieb": "Gerileme modu", + "absenkstop": "Gerileme durdurma", + "aktiviert": "Aktif", + "antilegionellenfunktion": "Anti-lejyonella Fonksiyonu", + "at_abschaltung": "OT kapatma", + "at_frostschutz": "OT donma korumas\u0131", + "aus": "Devre d\u0131\u015f\u0131", + "auto": "Otomatik", + "auto_off_cool": "Otomatik Kapal\u0131So\u011futma", + "auto_on_cool": "Otomatik A\u00e7\u0131kSo\u011futma", + "automatik_aus": "Otomatik KAPALI", + "automatik_ein": "Otomatik A\u00c7IK", + "bereit_keine_ladung": "Haz\u0131r, y\u00fcklenmiyor", + "betrieb_ohne_brenner": "Br\u00fcl\u00f6rs\u00fcz \u00e7al\u0131\u015fma", + "cooling": "So\u011futma", + "deaktiviert": "Etkin de\u011fil", + "dhw_prior": "DHWPrior", + "eco": "Eko", + "ein": "Etkin", + "estrichtrocknung": "\u015eap kurutma", + "externe_deaktivierung": "Harici devre d\u0131\u015f\u0131 b\u0131rakma", + "fernschalter_ein": "Uzaktan kumanda etkin", + "frost_heizkreis": "Is\u0131tma devresi donmas\u0131", + "frost_warmwasser": "DHW donma koruma", + "frostschutz": "Donma korumas\u0131", + "gasdruck": "Gaz bas\u0131nc\u0131", "glt_betrieb": "BMS modu", + "gradienten_uberwachung": "Gradyan izleme", "heizbetrieb": "Is\u0131tma modu", + "heizgerat_mit_speicher": "Silindirli kazan", + "heizung": "Is\u0131tma", + "initialisierung": "Ba\u015flatma", + "kalibration": "Kalibrasyon", "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", + "kalibration_warmwasserbetrieb": "DHW kalibrasyonu", + "kaskadenbetrieb": "Kademeli i\u015flem", + "kombibetrieb": "Kombi modu", + "kombigerat": "Kombi", + "kombigerat_mit_solareinbindung": "G\u00fcne\u015f enerjisi entegreli kombi", + "mindest_kombizeit": "Minimum kombi s\u00fcresi", + "nachlauf_heizkreispumpe": "Is\u0131tma devresi pompas\u0131 \u00e7al\u0131\u015fmas\u0131", + "nachspulen": "Temizleme sonras\u0131", + "nur_heizgerat": "Sadece kazan", + "parallelbetrieb": "Paralel mod", + "partymodus": "Parti modu", + "perm_cooling": "PermCooling", + "permanent": "Kal\u0131c\u0131", + "permanentbetrieb": "Kal\u0131c\u0131 mod", "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "rt_abschaltung": "RT kapatma", + "rt_frostschutz": "RT donma korumas\u0131", + "ruhekontakt": "Dinlenme konta\u011f\u0131", + "schornsteinfeger": "Emisyon testi", + "smart_grid": "SmartGrid", "smart_home": "Ak\u0131ll\u0131 Ev", "softstart": "Yumu\u015fak ba\u015flang\u0131\u00e7", "solarbetrieb": "G\u00fcne\u015f modu", "sparbetrieb": "Ekonomi modu", "sparen": "Ekonomi", + "spreizung_hoch": "dT \u00e7ok geni\u015f", + "spreizung_kf": "KF'yi yay\u0131n", "stabilisierung": "Stabilizasyon", "standby": "Bekleme", "start": "Ba\u015flat", @@ -21,8 +75,12 @@ "tpw": "TPW", "urlaubsmodus": "Tatil modu", "ventilprufung": "Valf testi", + "vorspulen": "Giri\u015f durulama", "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW h\u0131zl\u0131 ba\u015flang\u0131\u00e7", "warmwasserbetrieb": "DHW modu", + "warmwassernachlauf": "DHW \u00e7al\u0131\u015fmas\u0131", + "warmwasservorrang": "DHW \u00f6nceli\u011fi", "zunden": "Ate\u015fleme" } } diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json index 6ed28a58c79..aa7dbb1747a 100644 --- a/homeassistant/components/wolflink/translations/tr.json +++ b/homeassistant/components/wolflink/translations/tr.json @@ -9,11 +9,18 @@ "unknown": "Beklenmeyen hata" }, "step": { + "device": { + "data": { + "device_name": "Cihaz" + }, + "title": "WOLF ayg\u0131t\u0131 se\u00e7in" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "title": "KURT SmartSet ba\u011flant\u0131s\u0131" } } } diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json index a152eb19468..3f4025ade5d 100644 --- a/homeassistant/components/xbox/translations/tr.json +++ b/homeassistant/components/xbox/translations/tr.json @@ -1,7 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 24da29417d1..34e16ce7269 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -7,6 +7,7 @@ }, "error": { "discovery_error": "Bir Xiaomi Aqara A\u011f Ge\u00e7idi ke\u015ffedilemedi, HomeAssistant'\u0131 aray\u00fcz olarak \u00e7al\u0131\u015ft\u0131ran cihaz\u0131n IP'sini kullanmay\u0131 deneyin", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi , bkz. https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc", "invalid_key": "Ge\u00e7ersiz a\u011f ge\u00e7idi anahtar\u0131", "invalid_mac": "Ge\u00e7ersiz Mac Adresi" @@ -25,13 +26,17 @@ "key": "A\u011f ge\u00e7idinizin anahtar\u0131", "name": "A\u011f Ge\u00e7idinin Ad\u0131" }, - "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir" + "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir", + "title": "Xiaomi Aqara A\u011f Ge\u00e7idi, iste\u011fe ba\u011fl\u0131 ayarlar" }, "user": { "data": { "host": "\u0130p Adresi (iste\u011fe ba\u011fl\u0131)", + "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc", "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" - } + }, + "description": "Xiaomi Aqara Gateway'inize ba\u011flan\u0131n, IP ve MAC adresleri bo\u015f b\u0131rak\u0131l\u0131rsa otomatik ke\u015fif kullan\u0131l\u0131r", + "title": "Xiaomi Aqara A\u011f Ge\u00e7idi" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 83857a6147a..0d073a291d9 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -3,15 +3,20 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor." + "incomplete_info": "Kurulum cihaz\u0131 i\u00e7in eksik bilgi, ana bilgisayar veya anahtar sa\u011flanmad\u0131.", + "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin", "cloud_login_error": "Xiaomi Miio Cloud'da oturum a\u00e7\u0131lamad\u0131, kimlik bilgilerini kontrol edin.", "cloud_no_devices": "Bu Xiaomi Miio bulut hesab\u0131nda cihaz bulunamad\u0131.", - "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in." + "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in.", + "unknown_device": "Cihaz modeli bilinmiyor, cihaz yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullanarak kurulam\u0131yor.", + "wrong_token": "Sa\u011flama toplam\u0131 hatas\u0131, yanl\u0131\u015f anahtar" }, + "flow_title": "{name}", "step": { "cloud": { "data": { @@ -32,8 +37,13 @@ }, "device": { "data": { - "name": "Cihaz\u0131n ad\u0131" - } + "host": "IP Adresi", + "model": "Cihaz modeli (Opsiyonel)", + "name": "Cihaz\u0131n ad\u0131", + "token": "API Belirteci" + }, + "description": "32 karaktere API Belirteci , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", + "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" }, "gateway": { "data": { @@ -41,11 +51,21 @@ "name": "A\u011f Ge\u00e7idinin Ad\u0131", "token": "API Belirteci" }, + "description": "32 karaktere API Belirteci , bkz. talimatlar i\u00e7in. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" }, "manual": { + "data": { + "host": "IP Adresi", + "token": "API Belirteci" + }, + "description": "32 karaktere API Belirteci , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" }, + "reauth_confirm": { + "description": "Anahtarlar\u0131 g\u00fcncellemek veya eksik bulut kimlik bilgilerini eklemek i\u00e7in Xiaomi Miio entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekir.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "select": { "data": { "select_device": "Miio cihaz\u0131" diff --git a/homeassistant/components/yale_smart_alarm/translations/tr.json b/homeassistant/components/yale_smart_alarm/translations/tr.json index e1780e06fce..6d6b66ca166 100644 --- a/homeassistant/components/yale_smart_alarm/translations/tr.json +++ b/homeassistant/components/yale_smart_alarm/translations/tr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 271a6590b48..37be5646212 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "yxc_control_url_missing": "Kontrol URL'si ssdp a\u00e7\u0131klamas\u0131nda verilmez." }, "error": { @@ -8,7 +9,13 @@ }, "flow_title": "MusicCast: {name}", "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, "user": { + "data": { + "host": "Ana bilgisayar" + }, "description": "Home Assistant ile entegre etmek i\u00e7in MusicCast'i kurun." } } diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index 75c9cdf8f67..f33cf02e3bc 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" @@ -19,7 +20,8 @@ "user": { "data": { "host": "Ana Bilgisayar" - } + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } } }, @@ -28,10 +30,12 @@ "init": { "data": { "model": "Model (Opsiyonel)", + "nightlight_switch": "Gece I\u015f\u0131\u011f\u0131 Anahtar\u0131n\u0131 Kullan", "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet", "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)", "use_music_mode": "M\u00fczik Modunu Etkinle\u015ftir" - } + }, + "description": "Modeli bo\u015f b\u0131rak\u0131rsan\u0131z, otomatik olarak alg\u0131lanacakt\u0131r." } } } diff --git a/homeassistant/components/youless/translations/tr.json b/homeassistant/components/youless/translations/tr.json new file mode 100644 index 00000000000..afa9c9323f2 --- /dev/null +++ b/homeassistant/components/youless/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar", + "name": "Ad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index bc0345f6301..39eb68d8d53 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -1,21 +1,40 @@ { "config": { "abort": { + "not_zha_device": "Bu cihaz bir zha cihaz\u0131 de\u011fil", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "usb_probe_failed": "USB ayg\u0131t\u0131 ara\u015ft\u0131r\u0131lamad\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, "pick_radio": { + "data": { + "radio_type": "Radyo Tipi" + }, + "description": "Zigbee radyonuzun bir t\u00fcr\u00fcn\u00fc se\u00e7in", "title": "Radyo Tipi" }, "port_config": { "data": { + "baudrate": "ba\u011flant\u0131 noktas\u0131 h\u0131z\u0131", + "flow_control": "veri ak\u0131\u015f\u0131 denetimi", "path": "Seri cihaz yolu" }, + "description": "Ba\u011flant\u0131 noktas\u0131na \u00f6zel ayarlar\u0131 girin", "title": "Ayarlar" + }, + "user": { + "data": { + "path": "Seri Cihaz Yolu" + }, + "description": "Zigbee radyo i\u00e7in seri ba\u011flant\u0131 noktas\u0131 se\u00e7in", + "title": "ZHA" } } }, @@ -27,12 +46,67 @@ "title": "Alarm Kontrol Paneli Se\u00e7enekleri" }, "zha_options": { + "consider_unavailable_battery": "Pille \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", + "consider_unavailable_mains": "\u015eebekeyle \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", + "default_light_transition": "Varsay\u0131lan \u0131\u015f\u0131k ge\u00e7i\u015f s\u00fcresi (saniye)", + "enable_identify_on_join": "Cihazlar a\u011fa kat\u0131ld\u0131\u011f\u0131nda tan\u0131mlama efektini etkinle\u015ftir", "title": "Genel Se\u00e7enekler" } }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Uyarmak" + }, + "trigger_subtype": { + "both_buttons": "\u00c7ift d\u00fc\u011fmeler", + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "button_5": "Be\u015finci d\u00fc\u011fme", + "button_6": "Alt\u0131nc\u0131 d\u00fc\u011fme", + "close": "Kapat", + "dim_down": "K\u0131sma", + "dim_up": "A\u00e7ma", + "face_1": "y\u00fcz 1 etkinle\u015ftirilmi\u015f", + "face_2": "y\u00fcz 2 etkinle\u015ftirilmi\u015f", + "face_3": "y\u00fcz 3 etkinle\u015ftirilmi\u015f", + "face_4": "y\u00fcz 4 etkinle\u015ftirilmi\u015f", + "face_5": "y\u00fcz 5 etkinle\u015ftirilmi\u015f", + "face_6": "y\u00fcz 6 etkinle\u015ftirilmi\u015f", + "face_any": "Herhangi bir/belirtilen y\u00fcz(ler) etkinle\u015ftirildi\u011finde", + "left": "Sol", + "open": "A\u00e7\u0131k", + "right": "Sa\u011f", + "turn_off": "Kapat", + "turn_on": "A\u00e7\u0131n" + }, "trigger_type": { - "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131" + "device_dropped": "Cihaz d\u00fc\u015ft\u00fc", + "device_flipped": "Ayg\u0131t \u00e7evrilmi\u015f \"{subtype}\"", + "device_knocked": "Cihaz \" {subtype} \" \u00f6\u011fesini \u00e7ald\u0131", + "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131", + "device_rotated": "Cihaz d\u00f6nd\u00fcr\u00fcld\u00fc \" {subtype} \"", + "device_shaken": "Cihaz salland\u0131", + "device_slid": "Cihaz kayd\u0131rd\u0131 \" {subtype} \"", + "device_tilted": "Cihaz e\u011fik", + "remote_button_alt_double_press": "\" {subtype} \" d\u00fc\u011fmesine \u00e7ift t\u0131kland\u0131 (Alternatif mod)", + "remote_button_alt_long_press": "\" {subtype} \" d\u00fc\u011fmesi s\u00fcrekli bas\u0131l\u0131 (Alternatif mod)", + "remote_button_alt_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131 (Alternatif mod)", + "remote_button_alt_quadruple_press": "\" {subtype} \" d\u00fc\u011fmesi d\u00f6rt kez t\u0131kland\u0131 (Alternatif mod)", + "remote_button_alt_quintuple_press": "\" {subtype} \" d\u00fc\u011fmesi be\u015f kez t\u0131kland\u0131 (Alternatif mod)", + "remote_button_alt_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131 (Alternatif mod)", + "remote_button_alt_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131 (Alternatif mod)", + "remote_button_alt_triple_press": "\" {subtype} \" d\u00fc\u011fmesine \u00fc\u00e7 kez t\u0131kland\u0131 (Alternatif mod)", + "remote_button_double_press": "\" {subtype} \" d\u00fc\u011fmesine \u00e7ift t\u0131kland\u0131", + "remote_button_long_press": "\" {subtype} \" d\u00fc\u011fmesi s\u00fcrekli bas\u0131l\u0131", + "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", + "remote_button_quadruple_press": "\" {subtype} \" d\u00fc\u011fmesi d\u00f6rt kez t\u0131kland\u0131", + "remote_button_quintuple_press": "\" {subtype} \" d\u00fc\u011fmesi be\u015f kez t\u0131kland\u0131", + "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", + "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "remote_button_triple_press": "\" {subtype} \" d\u00fc\u011fmesine \u00fc\u00e7 kez t\u0131kland\u0131" } } } \ No newline at end of file diff --git a/homeassistant/components/zone/translations/tr.json b/homeassistant/components/zone/translations/tr.json index dad65ac92a7..64935f31ae0 100644 --- a/homeassistant/components/zone/translations/tr.json +++ b/homeassistant/components/zone/translations/tr.json @@ -1,12 +1,21 @@ { "config": { + "error": { + "name_exists": "Bu ad zaten var" + }, "step": { "init": { "data": { + "icon": "Simge", "latitude": "Enlem", - "longitude": "Boylam" - } + "longitude": "Boylam", + "name": "Ad", + "passive": "Pasif", + "radius": "Yar\u0131\u00e7ap" + }, + "title": "B\u00f6lge parametrelerini tan\u0131mlay\u0131n" } - } + }, + "title": "B\u00f6lge" } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/tr.json b/homeassistant/components/zoneminder/translations/tr.json index 971f8cc9bd7..0cbd791f831 100644 --- a/homeassistant/components/zoneminder/translations/tr.json +++ b/homeassistant/components/zoneminder/translations/tr.json @@ -3,19 +3,31 @@ "abort": { "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_error": "ZoneMinder sunucusuna ba\u011flan\u0131lamad\u0131.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "create_entry": { + "default": "ZoneMinder sunucusu eklendi." + }, "error": { "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_error": "ZoneMinder sunucusuna ba\u011flan\u0131lamad\u0131.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "Ana Bilgisayar ve Ba\u011flant\u0131 Noktas\u0131 (\u00f6r. 10.10.0.4:8010)", "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } + "path": "ZM Yolu", + "path_zms": "ZMS Yolu", + "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "title": "ZoneMinder Sunucusunu ekleyin." } } } diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json index 383ccc6cc4f..b6afa368b6d 100644 --- a/homeassistant/components/zwave/translations/tr.json +++ b/homeassistant/components/zwave/translations/tr.json @@ -4,11 +4,16 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "option_error": "Z-Wave do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu. USB stickin yolu do\u011fru mu?" + }, "step": { "user": { "data": { - "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)" - } + "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)", + "usb_path": "USB Cihaz Yolu" + }, + "description": "Bu entegrasyon art\u0131k korunmuyor. Yeni kurulumlar i\u00e7in bunun yerine Z-Wave JS kullan\u0131n. \n\n Yap\u0131land\u0131rma de\u011fi\u015fkenleri hakk\u0131nda bilgi i\u00e7in https://www.home-assistant.io/docs/z-wave/installation/ adresine bak\u0131n." } } }, diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index dd00286cb7c..f77b7f1f0f4 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -5,9 +5,12 @@ "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131.", "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "discovery_requires_supervisor": "Tarama, s\u00fcperviz\u00f6r\u00fc gerektirir.", + "not_zwave_device": "Bulunan cihaz bir Z-Wave cihaz\u0131 de\u011fil." }, "error": { "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin.", @@ -15,13 +18,16 @@ "invalid_ws_url": "Ge\u00e7ersiz websocket URL'si", "unknown": "Beklenmeyen hata" }, + "flow_title": "{name}", "progress": { - "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." + "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Z-Wave JS eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." }, "step": { "configure_addon": { "data": { "network_key": "A\u011f Anahtar\u0131", + "s0_legacy_key": "S0 Anahtar\u0131 (Eski)", "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar", @@ -47,6 +53,9 @@ "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" }, + "start_addon": { + "title": "Z-Wave JS eklentisi ba\u015fl\u0131yor." + }, "usb_confirm": { "description": "{name} Z-Wave JS eklentisiyle kurmak istiyor musunuz?" } @@ -59,34 +68,72 @@ "refresh_value": "{entity_name} i\u00e7in de\u011ferleri yenileme", "reset_meter": "{subtype} \u00fczerindeki saya\u00e7lar\u0131 s\u0131f\u0131rla", "set_config_parameter": "{subtype} yap\u0131land\u0131rma parametresinin de\u011ferini ayarlama", - "set_lock_usercode": "{entity_name} \u00fczerinde kullan\u0131c\u0131 kodu ayarlama" + "set_lock_usercode": "{entity_name} \u00fczerinde kullan\u0131c\u0131 kodu ayarlama", + "set_value": "Z-Wave De\u011ferini Ayarla" }, "condition_type": { - "config_parameter": "Yap\u0131land\u0131rma parametresi {subtype} de\u011feri" + "config_parameter": "Yap\u0131land\u0131rma parametresi {subtype} de\u011feri", + "node_status": "D\u00fc\u011f\u00fcm durumu", + "value": "Z-Wave De\u011ferinin mevcut ayar\u0131" }, "trigger_type": { "event.notification.entry_control": "Giri\u015f Kontrol\u00fc bildirimi g\u00f6nderdi", "event.notification.notification": "Bildirim g\u00f6nderdi", + "event.value_notification.basic": "{subtype} \u00fczerinde temel CC etkinli\u011fi", "event.value_notification.central_scene": "{subtype} \u00fczerinde Merkezi Sahne eylemi", - "event.value_notification.scene_activation": "{subtype} \u00fczerinde Sahne Aktivasyonu" + "event.value_notification.scene_activation": "{subtype} \u00fczerinde Sahne Aktivasyonu", + "state.node_status": "D\u00fc\u011f\u00fcm durumu de\u011fi\u015fti", + "zwave_js.value_updated.config_parameter": "{subtype} yap\u0131land\u0131rma parametresinde de\u011fer de\u011fi\u015fikli\u011fi", + "zwave_js.value_updated.value": "Z-Wave JS De\u011ferinde de\u011fer de\u011fi\u015fikli\u011fi" } }, "options": { "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", + "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", + "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", "different_device": "Ba\u011fl\u0131 USB ayg\u0131t\u0131, bu yap\u0131land\u0131rma giri\u015fi i\u00e7in daha \u00f6nce yap\u0131land\u0131r\u0131lanla ayn\u0131 de\u011fil. L\u00fctfen bunun yerine yeni cihaz i\u00e7in yeni bir yap\u0131land\u0131rma giri\u015fi olu\u015fturun." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_ws_url": "Ge\u00e7ersiz websocket URL'si", + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Z-Wave JS eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, "step": { "configure_addon": { "data": { "emulate_hardware": "Donan\u0131m\u0131 Taklit Et", "log_level": "G\u00fcnl\u00fck d\u00fczeyi", "network_key": "A\u011f Anahtar\u0131", + "s0_legacy_key": "S0 Anahtar\u0131 (Eski)", "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", - "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar" + "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar", + "usb_path": "USB Cihaz Yolu" + }, + "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" + }, + "install_addon": { + "title": "Z-Wave JS eklenti kurulumu ba\u015flad\u0131" + }, + "manual": { + "data": { + "url": "URL" } }, "on_supervisor": { + "data": { + "use_addon": "Z-Wave JS Supervisor eklentisini kullan\u0131n" + }, + "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" }, "start_addon": { From 958c588a19bd5577aa10c55679def6114e20faa3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 7 Nov 2021 22:06:22 -0500 Subject: [PATCH 0304/1452] Bump up ZHA dependencies (#59314) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a4cae4686bc..e953e2ef3bc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.5", "zha-quirks==0.0.63", "zigpy-deconz==0.13.0", - "zigpy==0.39.0", + "zigpy==0.41.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.4" diff --git a/requirements_all.txt b/requirements_all.txt index 78daaf173a5..c20364ff914 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2498,7 +2498,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.4 # homeassistant.components.zha -zigpy==0.39.0 +zigpy==0.41.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 272c18b9f4f..3b5ae4b654b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1460,7 +1460,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.4 # homeassistant.components.zha -zigpy==0.39.0 +zigpy==0.41.0 # homeassistant.components.zwave_js zwave-js-server-python==0.31.3 From 977b3cbe98ce2468d9b126c69dd496fbcfd3f157 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Nov 2021 21:13:42 -0600 Subject: [PATCH 0305/1452] Improve support for flux_led pixel/RBM controllers (#59325) * Fetch flux_led effects from library - Each model can have different effects * Improve support for flux_led pixel/RBM controllers - RBM effects 1-100 are now available * empty --- homeassistant/components/flux_led/light.py | 69 +++---------------- .../components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 3 + tests/components/flux_led/conftest.py | 11 +++ tests/components/flux_led/test_light.py | 50 ++++++-------- 7 files changed, 47 insertions(+), 92 deletions(-) create mode 100644 tests/components/flux_led/conftest.py diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index f1fa4ed7dbb..15f9716b52a 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -38,7 +38,6 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE, - EFFECT_COLORLOOP, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_EFFECT, @@ -110,55 +109,8 @@ EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} # Warm-white and Cool-white modes COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 -# List of supported effects which aren't already declared in LIGHT -EFFECT_RED_FADE: Final = "red_fade" -EFFECT_GREEN_FADE: Final = "green_fade" -EFFECT_BLUE_FADE: Final = "blue_fade" -EFFECT_YELLOW_FADE: Final = "yellow_fade" -EFFECT_CYAN_FADE: Final = "cyan_fade" -EFFECT_PURPLE_FADE: Final = "purple_fade" -EFFECT_WHITE_FADE: Final = "white_fade" -EFFECT_RED_GREEN_CROSS_FADE: Final = "rg_cross_fade" -EFFECT_RED_BLUE_CROSS_FADE: Final = "rb_cross_fade" -EFFECT_GREEN_BLUE_CROSS_FADE: Final = "gb_cross_fade" -EFFECT_COLORSTROBE: Final = "colorstrobe" -EFFECT_RED_STROBE: Final = "red_strobe" -EFFECT_GREEN_STROBE: Final = "green_strobe" -EFFECT_BLUE_STROBE: Final = "blue_strobe" -EFFECT_YELLOW_STROBE: Final = "yellow_strobe" -EFFECT_CYAN_STROBE: Final = "cyan_strobe" -EFFECT_PURPLE_STROBE: Final = "purple_strobe" -EFFECT_WHITE_STROBE: Final = "white_strobe" -EFFECT_COLORJUMP: Final = "colorjump" EFFECT_CUSTOM: Final = "custom" -EFFECT_MAP: Final = { - EFFECT_COLORLOOP: 0x25, - EFFECT_RED_FADE: 0x26, - EFFECT_GREEN_FADE: 0x27, - EFFECT_BLUE_FADE: 0x28, - EFFECT_YELLOW_FADE: 0x29, - EFFECT_CYAN_FADE: 0x2A, - EFFECT_PURPLE_FADE: 0x2B, - EFFECT_WHITE_FADE: 0x2C, - EFFECT_RED_GREEN_CROSS_FADE: 0x2D, - EFFECT_RED_BLUE_CROSS_FADE: 0x2E, - EFFECT_GREEN_BLUE_CROSS_FADE: 0x2F, - EFFECT_COLORSTROBE: 0x30, - EFFECT_RED_STROBE: 0x31, - EFFECT_GREEN_STROBE: 0x32, - EFFECT_BLUE_STROBE: 0x33, - EFFECT_YELLOW_STROBE: 0x34, - EFFECT_CYAN_STROBE: 0x35, - EFFECT_PURPLE_STROBE: 0x36, - EFFECT_WHITE_STROBE: 0x37, - EFFECT_COLORJUMP: 0x38, -} -EFFECT_ID_NAME: Final = {v: k for k, v in EFFECT_MAP.items()} -EFFECT_CUSTOM_CODE: Final = 0x60 - -FLUX_EFFECT_LIST: Final = sorted(EFFECT_MAP) + [EFFECT_RANDOM] - SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" CUSTOM_EFFECT_DICT: Final = { @@ -315,9 +267,9 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): } if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): self._attr_supported_features |= SUPPORT_EFFECT - self._attr_effect_list = FLUX_EFFECT_LIST + self._attr_effect_list = [*self._device.effect_list, EFFECT_RANDOM] if custom_effect_colors: - self._attr_effect_list = [*FLUX_EFFECT_LIST, EFFECT_CUSTOM] + self._attr_effect_list.append(EFFECT_CUSTOM) self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition @@ -369,9 +321,10 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): @property def effect(self) -> str | None: """Return the current effect.""" - if (current_mode := self._device.preset_pattern_num) == EFFECT_CUSTOM_CODE: - return EFFECT_CUSTOM - return EFFECT_ID_NAME.get(current_mode) + effect = self._device.effect + if effect is None: + return None + return cast(str, effect) async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" @@ -444,13 +397,9 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): self._custom_effect_transition, ) return - # Effect selection - if effect in EFFECT_MAP: - await self._device.async_set_preset_pattern( - EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED - ) - return - raise ValueError(f"Unknown effect {effect}") + await self._device.async_set_effect(effect, DEFAULT_EFFECT_SPEED) + return + # Handle brightness adjustment in CCT Color Mode if self.color_mode == COLOR_MODE_COLOR_TEMP: await self._device.async_set_white_temp(self._device.color_temp, brightness) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 429465a0f3c..ef87f220a80 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.17"], + "requirements": ["flux_led==0.24.18"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index c20364ff914..f3082ca55cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.17 +flux_led==0.24.18 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b5ae4b654b..8d7adcf59f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.17 +flux_led==0.24.18 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 8f484ac1989..08aefabe17d 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -46,8 +46,10 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.device_type = DeviceType.Bulb bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) + bulb.effect_list = ["some_effect"] bulb.async_set_custom_pattern = AsyncMock() bulb.async_set_preset_pattern = AsyncMock() + bulb.async_set_effect = AsyncMock() bulb.async_set_white_temp = AsyncMock() bulb.async_stop = AsyncMock() bulb.async_update = AsyncMock() @@ -68,6 +70,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.brightness = 128 bulb.model_num = 0x35 + bulb.effect = None bulb.model = "Smart Bulb (0x35)" bulb.version_num = 8 bulb.rgbwcapable = True diff --git a/tests/components/flux_led/conftest.py b/tests/components/flux_led/conftest.py new file mode 100644 index 00000000000..abac297da2d --- /dev/null +++ b/tests/components/flux_led/conftest.py @@ -0,0 +1,11 @@ +"""Tests for the flux_led integration.""" + +import pytest + +from tests.common import mock_device_registry + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6f0ad5aa253..8819852f6b2 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -27,7 +27,6 @@ from homeassistant.components.flux_led.const import ( MODE_AUTO, TRANSITION_JUMP, ) -from homeassistant.components.flux_led.light import EFFECT_CUSTOM_CODE, FLUX_EFFECT_LIST from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -41,6 +40,7 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, + EFFECT_RANDOM, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -202,7 +202,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -253,16 +253,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_preset_pattern.assert_called_with(43, 50) - bulb.async_set_preset_pattern.reset_mock() - - with pytest.raises(ValueError): - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "does not exist"}, - blocking=True, - ) + bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.reset_mock() async def test_rgb_cct_light(hass: HomeAssistant) -> None: @@ -288,7 +280,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -339,8 +331,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_preset_pattern.assert_called_with(43, 50) - bulb.async_set_preset_pattern.reset_mock() + bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.reset_mock() bulb.color_mode = FLUX_COLOR_MODE_CCT bulb.getWhiteTemperature = Mock(return_value=(5000, 128)) bulb.color_temp = 5000 @@ -407,7 +399,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgbw" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] assert attributes[ATTR_RGB_COLOR] == (255, 42, 42) @@ -483,8 +475,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_preset_pattern.assert_called_with(43, 50) - bulb.async_set_preset_pattern.reset_mock() + bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.reset_mock() async def test_rgb_or_w_light(hass: HomeAssistant) -> None: @@ -509,7 +501,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb", "white"] assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) @@ -567,8 +559,8 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_preset_pattern.assert_called_with(43, 50) - bulb.async_set_preset_pattern.reset_mock() + bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -620,7 +612,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgbww" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgbww"] assert attributes[ATTR_HS_COLOR] == (3.237, 94.51) @@ -721,8 +713,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_preset_pattern.assert_called_with(43, 50) - bulb.async_set_preset_pattern.reset_mock() + bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.reset_mock() async def test_white_light(hass: HomeAssistant) -> None: @@ -804,7 +796,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST, "custom"] + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM, "custom"] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -822,11 +814,11 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "custom"}, blocking=True, ) + bulb.effect = "custom" bulb.async_set_custom_pattern.assert_called_with( [[0, 0, 255], [255, 0, 0]], 88, "jump" ) bulb.async_set_custom_pattern.reset_mock() - bulb.preset_pattern_num = EFFECT_CUSTOM_CODE await async_mock_device_turn_on(hass, bulb) state = hass.states.get(entity_id) @@ -840,11 +832,11 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 55, ATTR_EFFECT: "custom"}, blocking=True, ) + bulb.effect = "custom" bulb.async_set_custom_pattern.assert_called_with( [[0, 0, 255], [255, 0, 0]], 88, "jump" ) bulb.async_set_custom_pattern.reset_mock() - bulb.preset_pattern_num = EFFECT_CUSTOM_CODE await async_mock_device_turn_on(hass, bulb) state = hass.states.get(entity_id) @@ -886,7 +878,7 @@ async def test_rgb_light_custom_effects_invalid_colors( attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -915,7 +907,7 @@ async def test_rgb_light_custom_effect_via_service( attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST] + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) From 9241d8073087956ae1f3881d75fc77a137cadc36 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 8 Nov 2021 13:20:01 +1000 Subject: [PATCH 0306/1452] Change zone HVAC mode in Advantage Air (#52816) * Change "on" HVAC mode to auto fixing #48466 * Use HEAT_COOL instead * Add missing HVAC_MODE_HEAT_COOL --- homeassistant/components/advantage_air/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1e6027b8db6..a56273fc10a 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -10,6 +10,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -53,7 +54,7 @@ HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" -ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] +ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL] PARALLEL_UPDATES = 0 @@ -169,7 +170,7 @@ class AdvantageAirZone(AdvantageAirClimateEntity): def hvac_mode(self): """Return the current state as HVAC mode.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: - return HVAC_MODE_FAN_ONLY + return HVAC_MODE_HEAT_COOL return HVAC_MODE_OFF @property From 5151c4d99b11fc67531c6d52ff4e472653795092 Mon Sep 17 00:00:00 2001 From: chriss158 Date: Mon, 8 Nov 2021 11:40:01 +0100 Subject: [PATCH 0307/1452] Add long-term statistics support for homematic sensors (#57396) * Add long-term statistics support for homematic * Refactor cast list to SensorEntityDescription dict Additional: - Gas power, gas energy counter, air pressure and voltage uses long-term-statistics - Gas power, gas energy counter uses device class gas - Voltage uses device class voltage - air pressure uses device class pressure * Refactor expensive loop to separate dictionarys * Use entity description property + fix humidity sensor * Log missing sensor descriptions * Use state class measurement for illumination sensors * Move sensor entity desc missing warning to setup_platform * Set type for hmdevice and homematic to fix mypy error * Use EntityDescription instead of SensorEntityDescription * Update entity.py * fix type * Update climate.py * fix v2 Co-authored-by: Pascal Vizeli --- homeassistant/components/homematic/entity.py | 25 +- homeassistant/components/homematic/sensor.py | 258 ++++++++++++++----- 2 files changed, 213 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 2fb23f707e3..8e83484505b 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -1,11 +1,16 @@ """Homematic base entity.""" +from __future__ import annotations + from abc import abstractmethod from datetime import timedelta import logging +from pyhomematic import HMConnection +from pyhomematic.devicetypes.generic import HMGeneric + from homeassistant.const import ATTR_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from .const import ( ATTR_ADDRESS, @@ -27,7 +32,14 @@ SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) class HMDevice(Entity): """The HomeMatic device base object.""" - def __init__(self, config): + _homematic: HMConnection + _hmdevice: HMGeneric + + def __init__( + self, + config: dict[str, str], + entity_description: EntityDescription | None = None, + ) -> None: """Initialize a generic HomeMatic device.""" self._name = config.get(ATTR_NAME) self._address = config.get(ATTR_ADDRESS) @@ -35,12 +47,13 @@ class HMDevice(Entity): self._channel = config.get(ATTR_CHANNEL) self._state = config.get(ATTR_PARAM) self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None + self._data: dict[str, str] = {} self._connected = False self._available = False - self._channel_map = set() + self._channel_map: set[str] = set() + + if entity_description is not None: + self.entity_description = entity_description # Set parameter to uppercase if self._state: diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 84bb7b4d5a3..19b24fbb3f8 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,15 +1,29 @@ """Support for HomeMatic sensors.""" +from __future__ import annotations + +from copy import copy import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( + ATTR_NAME, CONCENTRATION_PARTS_PER_MILLION, DEGREE, DEVICE_CLASS_CO2, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -24,7 +38,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) -from .const import ATTR_DISCOVER_DEVICES +from .const import ATTR_DISCOVER_DEVICES, ATTR_PARAM from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -45,54 +59,174 @@ HM_STATE_HA_CAST = { "IPLockDLD": {0: None, 1: "locked", 2: "unlocked"}, } -HM_UNIT_HA_CAST = { - "HUMIDITY": PERCENTAGE, - "TEMPERATURE": TEMP_CELSIUS, - "ACTUAL_TEMPERATURE": TEMP_CELSIUS, - "BRIGHTNESS": "#", - "POWER": POWER_WATT, - "CURRENT": ELECTRIC_CURRENT_MILLIAMPERE, - "VOLTAGE": ELECTRIC_POTENTIAL_VOLT, - "ENERGY_COUNTER": ENERGY_WATT_HOUR, - "GAS_POWER": VOLUME_CUBIC_METERS, - "GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, - "IEC_POWER": POWER_WATT, - "IEC_ENERGY_COUNTER": ENERGY_WATT_HOUR, - "LUX": LIGHT_LUX, - "ILLUMINATION": LIGHT_LUX, - "CURRENT_ILLUMINATION": LIGHT_LUX, - "AVERAGE_ILLUMINATION": LIGHT_LUX, - "LOWEST_ILLUMINATION": LIGHT_LUX, - "HIGHEST_ILLUMINATION": LIGHT_LUX, - "RAIN_COUNTER": LENGTH_MILLIMETERS, - "WIND_SPEED": SPEED_KILOMETERS_PER_HOUR, - "WIND_DIRECTION": DEGREE, - "WIND_DIRECTION_RANGE": DEGREE, - "SUNSHINEDURATION": "#", - "AIR_PRESSURE": PRESSURE_HPA, - "FREQUENCY": FREQUENCY_HERTZ, - "VALUE": "#", - "VALVE_STATE": PERCENTAGE, - "CARRIER_SENSE_LEVEL": PERCENTAGE, - "DUTY_CYCLE_LEVEL": PERCENTAGE, - "CONCENTRATION": CONCENTRATION_PARTS_PER_MILLION, + +SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { + "HUMIDITY": SensorEntityDescription( + key="HUMIDITY", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ACTUAL_TEMPERATURE": SensorEntityDescription( + key="ACTUAL_TEMPERATURE", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "TEMPERATURE": SensorEntityDescription( + key="TEMPERATURE", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "LUX": SensorEntityDescription( + key="LUX", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CURRENT_ILLUMINATION": SensorEntityDescription( + key="CURRENT_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ILLUMINATION": SensorEntityDescription( + key="ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "AVERAGE_ILLUMINATION": SensorEntityDescription( + key="AVERAGE_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "LOWEST_ILLUMINATION": SensorEntityDescription( + key="LOWEST_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "HIGHEST_ILLUMINATION": SensorEntityDescription( + key="HIGHEST_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "POWER": SensorEntityDescription( + key="POWER", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "IEC_POWER": SensorEntityDescription( + key="IEC_POWER", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CURRENT": SensorEntityDescription( + key="CURRENT", + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CONCENTRATION": SensorEntityDescription( + key="CONCENTRATION", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ENERGY_COUNTER": SensorEntityDescription( + key="ENERGY_COUNTER", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "IEC_ENERGY_COUNTER": SensorEntityDescription( + key="IEC_ENERGY_COUNTER", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "VOLTAGE": SensorEntityDescription( + key="VOLTAGE", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "GAS_POWER": SensorEntityDescription( + key="GAS_POWER", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_MEASUREMENT, + ), + "GAS_ENERGY_COUNTER": SensorEntityDescription( + key="GAS_ENERGY_COUNTER", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "RAIN_COUNTER": SensorEntityDescription( + key="RAIN_COUNTER", + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + "WIND_SPEED": SensorEntityDescription( + key="WIND_SPEED", + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + icon="mdi:weather-windy", + ), + "WIND_DIRECTION": SensorEntityDescription( + key="WIND_DIRECTION", + native_unit_of_measurement=DEGREE, + ), + "WIND_DIRECTION_RANGE": SensorEntityDescription( + key="WIND_DIRECTION_RANGE", + native_unit_of_measurement=DEGREE, + ), + "SUNSHINEDURATION": SensorEntityDescription( + key="SUNSHINEDURATION", + native_unit_of_measurement="#", + ), + "AIR_PRESSURE": SensorEntityDescription( + key="AIR_PRESSURE", + native_unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "FREQUENCY": SensorEntityDescription( + key="FREQUENCY", + native_unit_of_measurement=FREQUENCY_HERTZ, + ), + "VALUE": SensorEntityDescription( + key="VALUE", + native_unit_of_measurement="#", + ), + "VALVE_STATE": SensorEntityDescription( + key="VALVE_STATE", + native_unit_of_measurement=PERCENTAGE, + ), + "CARRIER_SENSE_LEVEL": SensorEntityDescription( + key="CARRIER_SENSE_LEVEL", + native_unit_of_measurement=PERCENTAGE, + ), + "DUTY_CYCLE_LEVEL": SensorEntityDescription( + key="DUTY_CYCLE_LEVEL", + native_unit_of_measurement=PERCENTAGE, + ), + "BRIGHTNESS": SensorEntityDescription( + key="BRIGHTNESS", + native_unit_of_measurement="#", + icon="mdi:invert-colors", + ), } -HM_DEVICE_CLASS_HA_CAST = { - "HUMIDITY": DEVICE_CLASS_HUMIDITY, - "TEMPERATURE": DEVICE_CLASS_TEMPERATURE, - "ACTUAL_TEMPERATURE": DEVICE_CLASS_TEMPERATURE, - "LUX": DEVICE_CLASS_ILLUMINANCE, - "CURRENT_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "AVERAGE_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "LOWEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "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"} +DEFAULT_SENSOR_DESCRIPTION = SensorEntityDescription( + key="", + entity_registry_enabled_default=True, +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -102,7 +236,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMSensor(conf) + state = conf.get(ATTR_PARAM) + entity_desc = SENSOR_DESCRIPTIONS.get(state) + if entity_desc is None: + name = conf.get(ATTR_NAME) + _LOGGER.warning( + "Sensor (%s) entity description is missing. Sensor state (%s) needs to be maintained", + name, + state, + ) + entity_desc = copy(DEFAULT_SENSOR_DESCRIPTION) + + new_device = HMSensor(conf, entity_desc) devices.append(new_device) add_entities(devices, True) @@ -122,21 +267,6 @@ class HMSensor(HMDevice, SensorEntity): # No cast, return original value return self._hm_get_state() - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return HM_UNIT_HA_CAST.get(self._state) - - @property - def device_class(self): - """Return the device class to use in the frontend, if any.""" - return HM_DEVICE_CLASS_HA_CAST.get(self._state) - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return HM_ICON_HA_CAST.get(self._state) - def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: From 67c2747027f9605010d5f843eb2acdc26baebccd Mon Sep 17 00:00:00 2001 From: Damien Duboeuf Date: Mon, 8 Nov 2021 14:02:18 +0100 Subject: [PATCH 0308/1452] Add MQTT object_id option (#58728) * Add MQTT object_id option * Add MQTT object_id option * Add MQTT object_id option * Add MQTT object_id option - Fix light and vacuum * Add MQTT object_id option - Fix light and vacuum * Add MQTT object_id option - Fix lock * Add MQTT object_id option - Fix device * Add MQTT object_id option - Fix device * Update tests/components/mqtt/test_discovery.py Co-authored-by: Erik Montnemery * Change deprecated method Co-authored-by: Erik Montnemery --- .../components/device_tracker/__init__.py | 1 + .../components/device_tracker/const.py | 1 + .../components/humidifier/__init__.py | 2 + .../components/mqtt/abbreviations.py | 1 + .../components/mqtt/alarm_control_panel.py | 1 + .../components/mqtt/binary_sensor.py | 2 + homeassistant/components/mqtt/camera.py | 1 + homeassistant/components/mqtt/climate.py | 1 + homeassistant/components/mqtt/cover.py | 1 + .../mqtt/device_tracker/schema_discovery.py | 2 + homeassistant/components/mqtt/fan.py | 1 + homeassistant/components/mqtt/humidifier.py | 1 + .../components/mqtt/light/schema_basic.py | 2 + .../components/mqtt/light/schema_json.py | 2 + .../components/mqtt/light/schema_template.py | 2 + homeassistant/components/mqtt/lock.py | 1 + homeassistant/components/mqtt/mixins.py | 28 +++- homeassistant/components/mqtt/number.py | 1 + homeassistant/components/mqtt/scene.py | 25 ++- homeassistant/components/mqtt/select.py | 2 + homeassistant/components/mqtt/sensor.py | 2 + homeassistant/components/mqtt/switch.py | 1 + .../components/mqtt/vacuum/schema_legacy.py | 2 + .../components/mqtt/vacuum/schema_state.py | 2 + homeassistant/components/vacuum/__init__.py | 1 + tests/components/mqtt/test_discovery.py | 152 ++++++++++++++++++ 26 files changed, 232 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index fa537d4af53..035b1923c4c 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -19,6 +19,7 @@ from .const import ( # noqa: F401 CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DOMAIN, + ENTITY_ID_FORMAT, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 09102372db6..216255b9cb6 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -6,6 +6,7 @@ from typing import Final LOGGER: Final = logging.getLogger(__package__) DOMAIN: Final = "device_tracker" +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" PLATFORM_TYPE_LEGACY: Final = "legacy" PLATFORM_TYPE_ENTITY: Final = "entity_platform" diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index d15f70f181a..48a71662be1 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -47,6 +47,8 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) +ENTITY_ID_FORMAT = DOMAIN + ".{}" + DEVICE_CLASSES = [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 155667b00cb..71a8d109c3f 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -97,6 +97,7 @@ ABBREVIATIONS = { "mode_stat_tpl": "mode_state_template", "modes": "modes", "name": "name", + "obj_id": "object_id", "off_dly": "off_delay", "on_cmd_type": "on_command_type", "ops": "options", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 5ac6e5ea89a..ffea92f14c3 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -126,6 +126,7 @@ async def _async_setup_entity( class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" + _entity_id_format = alarm.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 213aabdb006..1928f79032d 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -87,6 +87,8 @@ async def _async_setup_entity( class MqttBinarySensor(MqttEntity, BinarySensorEntity): """Representation a binary sensor that is updated by MQTT.""" + _entity_id_format = binary_sensor.ENTITY_ID_FORMAT + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" self._state = None diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 39457dbd629..a9fdfb96a6c 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -66,6 +66,7 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" + _entity_id_format = camera.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5b186ff9126..b4b1875c4df 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -297,6 +297,7 @@ async def _async_setup_entity( class MqttClimate(MqttEntity, ClimateEntity): """Representation of an MQTT climate device.""" + _entity_id_format = climate.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 859d9811617..c2800cc8239 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -224,6 +224,7 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" + _entity_id_format = cover.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index f962d9208a4..1ccf03423de 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -58,6 +58,8 @@ async def _async_setup_entity( class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" + _entity_id_format = device_tracker.ENTITY_ID_FORMAT + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the tracker.""" self._location_name = None diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f1040825cdf..9d0c954f3ab 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -230,6 +230,7 @@ async def _async_setup_entity( class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" + _entity_id_format = fan.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index e4f578ef94c..a5346c0bf58 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -163,6 +163,7 @@ async def _async_setup_entity( class MqttHumidifier(MqttEntity, HumidifierEntity): """A MQTT humidifier component.""" + _entity_id_format = humidifier.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 1909d7e136b..c0fc65610fd 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -29,6 +29,7 @@ from homeassistant.components.light import ( COLOR_MODE_UNKNOWN, COLOR_MODE_WHITE, COLOR_MODE_XY, + ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -239,6 +240,7 @@ async def async_setup_entity_basic( class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" + _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 412205ea9cf..0ba796df4e7 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -24,6 +24,7 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_XY, + ENTITY_ID_FORMAT, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, @@ -167,6 +168,7 @@ async def async_setup_entity_json( class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" + _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0a19fbb9836..838daab5860 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, + ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -97,6 +98,7 @@ async def async_setup_entity_template( class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" + _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 8de4c4431b9..5410af5d38c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -78,6 +78,7 @@ async def _async_setup_entity( class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" + _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 7cfc00da578..3a374070f2f 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,12 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity +from homeassistant.helpers.entity import ( + ENTITY_CATEGORIES_SCHEMA, + DeviceInfo, + Entity, + async_generate_entity_id, +) from homeassistant.helpers.typing import ConfigType from . import DATA_MQTT, debug_info, publish, subscription @@ -82,6 +87,7 @@ CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" CONF_SUGGESTED_AREA = "suggested_area" CONF_CONFIGURATION_URL = "configuration_url" +CONF_OBJECT_ID = "object_id" MQTT_ATTRIBUTES_BLOCKED = { "assumed_state", @@ -183,6 +189,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + vol.Optional(CONF_OBJECT_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -210,6 +217,14 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): ) +def init_entity_id_from_config(hass, entity, config, entity_id_format): + """Set entity_id from object_id if defined in config.""" + if CONF_OBJECT_ID in config: + entity.entity_id = async_generate_entity_id( + entity_id_format, config[CONF_OBJECT_ID], None, hass + ) + + class MqttAttributes(Entity): """Mixin used for platforms that support JSON attributes.""" @@ -588,6 +603,8 @@ class MqttEntity( ): """Representation of an MQTT entity.""" + _entity_id_format: str + def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Entity.""" self.hass = hass @@ -598,12 +615,21 @@ class MqttEntity( # Load config self._setup_from_config(self._config) + # Initialize entity_id from config + self._init_entity_id() + # Initialize mixin classes MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry) + def _init_entity_id(self): + """Set entity_id from object_id if defined in config.""" + init_entity_id_from_config( + self.hass, self, self._config, self._entity_id_format + ) + async def async_added_to_hass(self): """Subscribe mqtt events.""" await super().async_added_to_hass() diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 6d0163abe32..29f6b4bad21 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -114,6 +114,7 @@ async def _async_setup_entity( class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """representation of an MQTT number.""" + _entity_id_format = number.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c2b201e20e6..67b757e5e8a 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -15,10 +15,12 @@ from . import PLATFORMS from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN from .mixins import ( + CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, async_setup_entry_helper, + init_entity_id_from_config, ) DEFAULT_NAME = "MQTT Scene" @@ -32,6 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_ON): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_OBJECT_ID): cv.string, } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -43,22 +46,22 @@ async def async_setup_platform( ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(async_add_entities, config) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT scene dynamically through MQTT discovery.""" setup = functools.partial( - _async_setup_entity, async_add_entities, config_entry=config_entry + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) await async_setup_entry_helper(hass, scene.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( - async_add_entities, config, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT scene.""" - async_add_entities([MqttScene(config, config_entry, discovery_data)]) + async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) class MqttScene( @@ -68,8 +71,11 @@ class MqttScene( ): """Representation of a scene that can be activated using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + _entity_id_format = scene.DOMAIN + ".{}" + + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT scene.""" + self.hass = hass self._state = False self._sub_state = None @@ -78,9 +84,18 @@ class MqttScene( # Load config self._setup_from_config(config) + # Initialize entity_id from config + self._init_entity_id() + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + def _init_entity_id(self): + """Set entity_id from object_id if defined in config.""" + init_entity_id_from_config( + self.hass, self, self._config, self._entity_id_format + ) + async def async_added_to_hass(self): """Subscribe to MQTT events.""" await super().async_added_to_hass() diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index c43593dfc4b..45304d68079 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -90,6 +90,8 @@ async def _async_setup_entity( class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" + _entity_id_format = select.ENTITY_ID_FORMAT + _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 3881dfee0e8..4d0f610300f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components import sensor from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES_SCHEMA, + ENTITY_ID_FORMAT, STATE_CLASSES_SCHEMA, SensorEntity, ) @@ -132,6 +133,7 @@ async def _async_setup_entity( class MqttSensor(MqttEntity, SensorEntity): """Representation of a sensor that can be updated using MQTT.""" + _entity_id_format = ENTITY_ID_FORMAT _attr_last_reset = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 9cc13ac94bd..8f9178c15f1 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -84,6 +84,7 @@ async def _async_setup_entity( class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" + _entity_id_format = switch.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 63bb47ce21b..b19d4afe23e 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_STATUS, + ENTITY_ID_FORMAT, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, @@ -169,6 +170,7 @@ async def async_setup_entity_legacy( class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" + _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 8c654a526e4..0bfb7289f37 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -4,6 +4,7 @@ import json import voluptuous as vol from homeassistant.components.vacuum import ( + ENTITY_ID_FORMAT, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -149,6 +150,7 @@ async def async_setup_entity_state( class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" + _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 87d8d6f49f8..db186a464fa 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -40,6 +40,7 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" +ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=20) ATTR_BATTERY_ICON = "battery_icon" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 60c3961477b..08eb4b882c5 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -191,6 +191,158 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): assert ("alarm_control_panel", "bla") in hass.data[ALREADY_DISCOVERED] +@pytest.mark.parametrize( + "topic, config, entity_id, name, domain", + [ + ( + "homeassistant/alarm_control_panel/object/bla/config", + '{ "name": "Hello World 1", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "alarm_control_panel.hello_id", + "Hello World 1", + "alarm_control_panel", + ), + ( + "homeassistant/binary_sensor/object/bla/config", + '{ "name": "Hello World 2", "obj_id": "hello_id", "state_topic": "test-topic" }', + "binary_sensor.hello_id", + "Hello World 2", + "binary_sensor", + ), + ( + "homeassistant/camera/object/bla/config", + '{ "name": "Hello World 3", "obj_id": "hello_id", "state_topic": "test-topic", "topic": "test-topic" }', + "camera.hello_id", + "Hello World 3", + "camera", + ), + ( + "homeassistant/climate/object/bla/config", + '{ "name": "Hello World 4", "obj_id": "hello_id", "state_topic": "test-topic" }', + "climate.hello_id", + "Hello World 4", + "climate", + ), + ( + "homeassistant/cover/object/bla/config", + '{ "name": "Hello World 5", "obj_id": "hello_id", "state_topic": "test-topic" }', + "cover.hello_id", + "Hello World 5", + "cover", + ), + ( + "homeassistant/fan/object/bla/config", + '{ "name": "Hello World 6", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "fan.hello_id", + "Hello World 6", + "fan", + ), + ( + "homeassistant/humidifier/object/bla/config", + '{ "name": "Hello World 7", "obj_id": "hello_id", "state_topic": "test-topic", "target_humidity_command_topic": "test-topic", "command_topic": "test-topic" }', + "humidifier.hello_id", + "Hello World 7", + "humidifier", + ), + ( + "homeassistant/number/object/bla/config", + '{ "name": "Hello World 8", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "number.hello_id", + "Hello World 8", + "number", + ), + ( + "homeassistant/scene/object/bla/config", + '{ "name": "Hello World 9", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "scene.hello_id", + "Hello World 9", + "scene", + ), + ( + "homeassistant/select/object/bla/config", + '{ "name": "Hello World 10", "obj_id": "hello_id", "state_topic": "test-topic", "options": [ "opt1", "opt2" ], "command_topic": "test-topic" }', + "select.hello_id", + "Hello World 10", + "select", + ), + ( + "homeassistant/sensor/object/bla/config", + '{ "name": "Hello World 11", "obj_id": "hello_id", "state_topic": "test-topic" }', + "sensor.hello_id", + "Hello World 11", + "sensor", + ), + ( + "homeassistant/switch/object/bla/config", + '{ "name": "Hello World 12", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "switch.hello_id", + "Hello World 12", + "switch", + ), + ( + "homeassistant/light/object/bla/config", + '{ "name": "Hello World 13", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "light.hello_id", + "Hello World 13", + "light", + ), + ( + "homeassistant/light/object/bla/config", + '{ "name": "Hello World 14", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic", "schema": "json" }', + "light.hello_id", + "Hello World 14", + "light", + ), + ( + "homeassistant/light/object/bla/config", + '{ "name": "Hello World 15", "obj_id": "hello_id", "state_topic": "test-topic", "command_off_template": "template", "command_on_template": "template", "command_topic": "test-topic", "schema": "template" }', + "light.hello_id", + "Hello World 15", + "light", + ), + ( + "homeassistant/vacuum/object/bla/config", + '{ "name": "Hello World 16", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "state" }', + "vacuum.hello_id", + "Hello World 16", + "vacuum", + ), + ( + "homeassistant/vacuum/object/bla/config", + '{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "legacy" }', + "vacuum.hello_id", + "Hello World 17", + "vacuum", + ), + ( + "homeassistant/lock/object/bla/config", + '{ "name": "Hello World 18", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }', + "lock.hello_id", + "Hello World 18", + "lock", + ), + ( + "homeassistant/device_tracker/object/bla/config", + '{ "name": "Hello World 19", "obj_id": "hello_id", "state_topic": "test-topic" }', + "device_tracker.hello_id", + "Hello World 19", + "device_tracker", + ), + ], +) +async def test_discovery_with_object_id( + hass, mqtt_mock, caplog, topic, config, entity_id, name, domain +): + """Test discovering an MQTT entity with object_id.""" + async_fire_mqtt_message(hass, topic, config) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state is not None + assert state.name == name + assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED] + + async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" async_fire_mqtt_message( From 4224cb043b1f2f01ceed478f38713d5943f2a521 Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Mon, 8 Nov 2021 15:49:10 +0100 Subject: [PATCH 0309/1452] Allow overriding ensure_ascii in the "to_json" template filter (#54527) * FIX: "ensureascii" to to_json * fixup: parameter name --- homeassistant/helpers/template.py | 4 ++-- tests/helpers/test_template.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f090b28210f..7f3937d41c1 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1623,9 +1623,9 @@ def from_json(value): return json.loads(value) -def to_json(value): +def to_json(value, ensure_ascii=True): """Convert an object to a JSON string.""" - return json.dumps(value) + return json.dumps(value, ensure_ascii=ensure_ascii) @pass_context diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 557ec81c2dc..dd0c4c96a11 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -746,6 +746,21 @@ def test_to_json(hass): assert actual_result == expected_result +def test_to_json_string(hass): + """Test the object to JSON string filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + actual_value_ascii = template.Template( + "{{ 'Bar ҝ éèà' | to_json }}", hass + ).async_render() + assert actual_value_ascii == '"Bar \\u049d \\u00e9\\u00e8\\u00e0"' + actual_value = template.Template( + "{{ 'Bar ҝ éèà' | to_json(ensure_ascii=False) }}", hass + ).async_render() + assert actual_value == '"Bar ҝ éèà"' + + def test_from_json(hass): """Test the JSON string to object filter.""" From 0edb0c9bc9c8e24ed7311d9ef619e5c67235c0e4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 8 Nov 2021 16:08:07 +0100 Subject: [PATCH 0310/1452] Correct name of end apc sensor (#59200) --- homeassistant/components/apcupsd/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index f7a350925ec..6f3b83a4867 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -120,7 +120,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:timer-outline", ), SensorEntityDescription( - key="endapc", + key="end apc", name="Date and Time", icon="mdi:calendar-clock", ), From 089353e949cf2fc921c8dbcb04093ca67cdfce3a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Nov 2021 17:26:00 +0100 Subject: [PATCH 0311/1452] Use DeviceInfo in velbus (#58622) --- homeassistant/components/velbus/__init__.py | 44 ++++++++++------ tests/components/velbus/conftest.py | 31 ++++++++++++ tests/components/velbus/const.py | 3 ++ tests/components/velbus/test_config_flow.py | 44 ++++++++-------- tests/components/velbus/test_init.py | 56 +++++++++++++++++++++ 5 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 tests/components/velbus/conftest.py create mode 100644 tests/components/velbus/const.py create mode 100644 tests/components/velbus/test_init.py diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index acc90116269..d907557bb01 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -9,8 +9,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_INTERFACE, @@ -58,6 +60,22 @@ async def velbus_connect_task( await controller.connect() +def _migrate_device_identifiers(hass: HomeAssistant, entry_id: str) -> None: + """Migrate old device indentifiers.""" + dev_reg = device_registry.async_get(hass) + devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry( + dev_reg, entry_id + ) + for device in devices: + old_identifier = list(next(iter(device.identifiers))) + if len(old_identifier) > 2: + new_identifier = {(old_identifier.pop(0), old_identifier.pop(0))} + _LOGGER.debug( + "migrate identifier '%s' to '%s'", device.identifiers, new_identifier + ) + dev_reg.async_update_device(device.id, new_identifiers=new_identifier) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with velbus.""" hass.data.setdefault(DOMAIN, {}) @@ -72,6 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: velbus_connect_task(controller, hass, entry.entry_id) ) + _migrate_device_identifiers(hass, entry.entry_id) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SCAN): @@ -176,18 +196,14 @@ class VelbusEntity(Entity): self.async_write_ha_state() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": { - ( - DOMAIN, - self._channel.get_module_address(), - self._channel.get_module_serial(), - ) + return DeviceInfo( + identifiers={ + (DOMAIN, self._channel.get_module_address()), }, - "name": self._channel.get_full_name(), - "manufacturer": "Velleman", - "model": self._channel.get_module_type_name(), - "sw_version": self._channel.get_module_sw_version(), - } + manufacturer="Velleman", + model=self._channel.get_module_type_name(), + name=self._channel.get_full_name(), + sw_version=self._channel.get_module_sw_version(), + ) diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py new file mode 100644 index 00000000000..c13ce3127fa --- /dev/null +++ b/tests/components/velbus/conftest.py @@ -0,0 +1,31 @@ +"""Fixtures for the Velbus tests.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.velbus.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.velbus.const import PORT_TCP + + +@pytest.fixture(name="controller") +def mock_controller(): + """Mock a successful velbus controller.""" + controller = AsyncMock() + with patch("velbusaio.controller.Velbus", return_value=controller): + yield controller + + +@pytest.fixture(name="config_entry") +def mock_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus home"}, + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/velbus/const.py b/tests/components/velbus/const.py new file mode 100644 index 00000000000..374dbce2529 --- /dev/null +++ b/tests/components/velbus/const.py @@ -0,0 +1,3 @@ +"""Constants for the Velbus tests.""" +PORT_SERIAL = "/dev/ttyACME100" +PORT_TCP = "127.0.1.0.1:3788" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 723b6664fd7..6c10b3c84f4 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,36 +7,36 @@ from velbusaio.exceptions import VelbusConnectionFailed from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry - -PORT_SERIAL = "/dev/ttyACME100" -PORT_TCP = "127.0.1.0.1:3788" +from .const import PORT_SERIAL, PORT_TCP -@pytest.fixture(name="controller_assert") -def mock_controller_assert(): +@pytest.fixture(autouse=True) +def override_async_setup_entry() -> AsyncMock: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.velbus.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="controller_connection_failed") +def mock_controller_connection_failed(): """Mock the velbus controller with an assert.""" with patch("velbusaio.controller.Velbus", side_effect=VelbusConnectionFailed()): yield -@pytest.fixture(name="controller") -def mock_controller(): - """Mock a successful velbus controller.""" - controller = AsyncMock() - with patch("velbusaio.controller.Velbus", return_value=controller): - yield controller - - -def init_config_flow(hass): +def init_config_flow(hass: HomeAssistant): """Init a configuration flow.""" flow = config_flow.VelbusConfigFlow() flow.hass = hass return flow -async def test_user(hass, controller): +@pytest.mark.usefixtures("controller") +async def test_user(hass: HomeAssistant): """Test user config.""" flow = init_config_flow(hass) @@ -59,7 +59,8 @@ async def test_user(hass, controller): assert result["data"][CONF_PORT] == PORT_TCP -async def test_user_fail(hass, controller_assert): +@pytest.mark.usefixtures("controller_connection_failed") +async def test_user_fail(hass: HomeAssistant): """Test user config.""" flow = init_config_flow(hass) @@ -76,7 +77,8 @@ async def test_user_fail(hass, controller_assert): assert result["errors"] == {CONF_PORT: "cannot_connect"} -async def test_import(hass, controller): +@pytest.mark.usefixtures("controller") +async def test_import(hass: HomeAssistant): """Test import step.""" flow = init_config_flow(hass) @@ -85,12 +87,10 @@ async def test_import(hass, controller): assert result["title"] == "velbus_import" -async def test_abort_if_already_setup(hass): +@pytest.mark.usefixtures("config_entry") +async def test_abort_if_already_setup(hass: HomeAssistant): """Test we abort if Daikin is already setup.""" flow = init_config_flow(hass) - MockConfigEntry( - domain="velbus", data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus home"} - ).add_to_hass(hass) result = await flow.async_step_import( {CONF_PORT: PORT_TCP, CONF_NAME: "velbus import test"} diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py new file mode 100644 index 00000000000..dee00cce16b --- /dev/null +++ b/tests/components/velbus/test_init.py @@ -0,0 +1,56 @@ +"""Tests for the Velbus component initialisation.""" +import pytest + +from homeassistant.components.velbus.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import mock_device_registry + + +@pytest.mark.usefixtures("controller") +async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Test being able to unload an 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.LOADED + + assert 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 not hass.data.get(DOMAIN) + + +@pytest.mark.usefixtures("controller") +async def test_device_identifier_migration( + hass: HomeAssistant, config_entry: ConfigEntry +): + """Test being able to unload an entry.""" + original_identifiers = {(DOMAIN, "module_address", "module_serial")} + target_identifiers = {(DOMAIN, "module_address")} + + device_registry = mock_device_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers=original_identifiers, + name="channel_name", + manufacturer="Velleman", + model="module_type_name", + sw_version="module_sw_version", + ) + assert device_registry.async_get_device(original_identifiers) + assert not device_registry.async_get_device(target_identifiers) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert not device_registry.async_get_device(original_identifiers) + device_entry = device_registry.async_get_device(target_identifiers) + assert device_entry + assert device_entry.name == "channel_name" + assert device_entry.manufacturer == "Velleman" + assert device_entry.model == "module_type_name" + assert device_entry.sw_version == "module_sw_version" From 296f678d529f899eee9918ddfb528015f9e316fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Nov 2021 08:56:27 -0800 Subject: [PATCH 0312/1452] Add Evil Genius Labs integration (#58720) Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 1 + .../components/evil_genius_labs/__init__.py | 99 ++++++ .../evil_genius_labs/config_flow.py | 84 +++++ .../components/evil_genius_labs/const.py | 3 + .../components/evil_genius_labs/light.py | 120 +++++++ .../components/evil_genius_labs/manifest.json | 9 + .../components/evil_genius_labs/strings.json | 15 + .../evil_genius_labs/translations/en.json | 15 + .../components/evil_genius_labs/util.py | 21 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/evil_genius_labs/__init__.py | 1 + tests/components/evil_genius_labs/conftest.py | 49 +++ .../evil_genius_labs/fixtures/data.json | 331 ++++++++++++++++++ .../evil_genius_labs/fixtures/info.json | 30 ++ .../evil_genius_labs/test_config_flow.py | 85 +++++ .../components/evil_genius_labs/test_init.py | 13 + .../components/evil_genius_labs/test_light.py | 76 ++++ 21 files changed, 971 insertions(+) create mode 100644 homeassistant/components/evil_genius_labs/__init__.py create mode 100644 homeassistant/components/evil_genius_labs/config_flow.py create mode 100644 homeassistant/components/evil_genius_labs/const.py create mode 100644 homeassistant/components/evil_genius_labs/light.py create mode 100644 homeassistant/components/evil_genius_labs/manifest.json create mode 100644 homeassistant/components/evil_genius_labs/strings.json create mode 100644 homeassistant/components/evil_genius_labs/translations/en.json create mode 100644 homeassistant/components/evil_genius_labs/util.py create mode 100644 tests/components/evil_genius_labs/__init__.py create mode 100644 tests/components/evil_genius_labs/conftest.py create mode 100644 tests/components/evil_genius_labs/fixtures/data.json create mode 100644 tests/components/evil_genius_labs/fixtures/info.json create mode 100644 tests/components/evil_genius_labs/test_config_flow.py create mode 100644 tests/components/evil_genius_labs/test_init.py create mode 100644 tests/components/evil_genius_labs/test_light.py diff --git a/.strict-typing b/.strict-typing index ac5d2b6a8ac..4c9e626afa6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -42,6 +42,7 @@ homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.esphome.* homeassistant.components.energy.* +homeassistant.components.evil_genius_labs.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* diff --git a/CODEOWNERS b/CODEOWNERS index 84ab3a80c5e..ad69391ad29 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -160,6 +160,7 @@ homeassistant/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter @jesserockz +homeassistant/components/evil_genius_labs/* @balloob homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py new file mode 100644 index 00000000000..78445a42e7d --- /dev/null +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -0,0 +1,99 @@ +"""The Evil Genius Labs integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import cast + +from async_timeout import timeout +import pyevilgenius + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + update_coordinator, +) +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN + +PLATFORMS = ["light"] + +UPDATE_INTERVAL = 10 + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Evil Genius Labs from a config entry.""" + coordinator = EvilGeniusUpdateCoordinator( + hass, + entry.title, + pyevilgenius.EvilGeniusDevice( + entry.data["host"], aiohttp_client.async_get_clientsession(hass) + ), + ) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(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 EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict]): + """Update coordinator for Evil Genius data.""" + + info: dict + + def __init__( + self, hass: HomeAssistant, name: str, client: pyevilgenius.EvilGeniusDevice + ) -> None: + """Initialize the data update coordinator.""" + self.client = client + super().__init__( + hass, + logging.getLogger(__name__), + name=name, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + @property + def device_name(self) -> str: + """Return the device name.""" + return cast(str, self.data["name"]["value"]) + + async def _async_update_data(self) -> dict: + """Update Evil Genius data.""" + if not hasattr(self, "info"): + async with timeout(5): + self.info = await self.client.get_info() + + async with timeout(5): + return cast(dict, await self.client.get_data()) + + +class EvilGeniusEntity(update_coordinator.CoordinatorEntity): + """Base entity for Evil Genius.""" + + coordinator: EvilGeniusUpdateCoordinator + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + info = self.coordinator.info + return DeviceInfo( + identifiers={(DOMAIN, info["wiFiChipId"])}, + connections={(dr.CONNECTION_NETWORK_MAC, info["macAddress"])}, + name=self.coordinator.device_name, + manufacturer="Evil Genius Labs", + sw_version=info["coreVersion"].replace("_", "."), + configuration_url=self.coordinator.client.url, + ) diff --git a/homeassistant/components/evil_genius_labs/config_flow.py b/homeassistant/components/evil_genius_labs/config_flow.py new file mode 100644 index 00000000000..f4f7b464904 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for Evil Genius Labs integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import aiohttp +import pyevilgenius +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = pyevilgenius.EvilGeniusDevice( + data["host"], aiohttp_client.async_get_clientsession(hass) + ) + + try: + data = await hub.get_data() + info = await hub.get_info() + except aiohttp.ClientError as err: + raise CannotConnect from err + + return {"title": data["name"]["value"], "unique_id": info["wiFiChipId"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Evil Genius Labs.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("host"): str, + } + ), + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info["unique_id"]) + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("host", default=user_input["host"]): str, + } + ), + errors=errors, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/evil_genius_labs/const.py b/homeassistant/components/evil_genius_labs/const.py new file mode 100644 index 00000000000..c335e5eaee2 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/const.py @@ -0,0 +1,3 @@ +"""Constants for the Evil Genius Labs integration.""" + +DOMAIN = "evil_genius_labs" diff --git a/homeassistant/components/evil_genius_labs/light.py b/homeassistant/components/evil_genius_labs/light.py new file mode 100644 index 00000000000..cb837668a4c --- /dev/null +++ b/homeassistant/components/evil_genius_labs/light.py @@ -0,0 +1,120 @@ +"""Light platform for Evil Genius Light.""" +from __future__ import annotations + +from typing import Any, cast + +from async_timeout import timeout + +from homeassistant.components import light +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EvilGeniusEntity, EvilGeniusUpdateCoordinator +from .const import DOMAIN +from .util import update_when_done + +HA_NO_EFFECT = "None" +FIB_NO_EFFECT = "Solid Color" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Evil Genius light platform.""" + coordinator: EvilGeniusUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([EvilGeniusLight(coordinator)]) + + +class EvilGeniusLight(EvilGeniusEntity, light.LightEntity): + """Evil Genius Labs light.""" + + _attr_supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_EFFECT | light.SUPPORT_COLOR + ) + _attr_supported_color_modes = {light.COLOR_MODE_RGB} + _attr_color_mode = light.COLOR_MODE_RGB + + def __init__(self, coordinator: EvilGeniusUpdateCoordinator) -> None: + """Initialize the Evil Genius light.""" + super().__init__(coordinator) + self._attr_unique_id = self.coordinator.info["wiFiChipId"] + self._attr_effect_list = [ + pattern + for pattern in self.coordinator.data["pattern"]["options"] + if pattern != FIB_NO_EFFECT + ] + self._attr_effect_list.insert(0, HA_NO_EFFECT) + + @property + def name(self) -> str: + """Return name.""" + return cast(str, self.coordinator.data["name"]["value"]) + + @property + def is_on(self) -> bool: + """Return if light is on.""" + return cast(int, self.coordinator.data["power"]["value"]) == 1 + + @property + def brightness(self) -> int: + """Return brightness.""" + return cast(int, self.coordinator.data["brightness"]["value"]) + + @property + def rgb_color(self) -> tuple[int, int, int]: + """Return the rgb color value [int, int, int].""" + return cast( + "tuple[int, int, int]", + tuple( + int(val) + for val in self.coordinator.data["solidColor"]["value"].split(",") + ), + ) + + @property + def effect(self) -> str: + """Return current effect.""" + value = cast( + str, + self.coordinator.data["pattern"]["options"][ + self.coordinator.data["pattern"]["value"] + ], + ) + if value == FIB_NO_EFFECT: + return HA_NO_EFFECT + return value + + @update_when_done + async def async_turn_on( + self, + **kwargs: Any, + ) -> None: + """Turn light on.""" + if (brightness := kwargs.get(light.ATTR_BRIGHTNESS)) is not None: + async with timeout(5): + await self.coordinator.client.set_path_value("brightness", brightness) + + # Setting a color will change the effect to "Solid Color" so skip setting effect + if (rgb_color := kwargs.get(light.ATTR_RGB_COLOR)) is not None: + async with timeout(5): + await self.coordinator.client.set_rgb_color(*rgb_color) + + elif (effect := kwargs.get(light.ATTR_EFFECT)) is not None: + if effect == HA_NO_EFFECT: + effect = FIB_NO_EFFECT + async with timeout(5): + await self.coordinator.client.set_path_value( + "pattern", self.coordinator.data["pattern"]["options"].index(effect) + ) + + async with timeout(5): + await self.coordinator.client.set_path_value("power", 1) + + @update_when_done + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn light off.""" + async with timeout(5): + await self.coordinator.client.set_path_value("power", 0) diff --git a/homeassistant/components/evil_genius_labs/manifest.json b/homeassistant/components/evil_genius_labs/manifest.json new file mode 100644 index 00000000000..698c13b43e6 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "evil_genius_labs", + "name": "Evil Genius Labs", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/evil_genius_labs", + "requirements": ["pyevilgenius==1.0.0"], + "codeowners": ["@balloob"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/evil_genius_labs/strings.json b/homeassistant/components/evil_genius_labs/strings.json new file mode 100644 index 00000000000..16c5de158a9 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/evil_genius_labs/translations/en.json b/homeassistant/components/evil_genius_labs/translations/en.json new file mode 100644 index 00000000000..b059e11aa28 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/util.py b/homeassistant/components/evil_genius_labs/util.py new file mode 100644 index 00000000000..42088f69797 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/util.py @@ -0,0 +1,21 @@ +"""Utilities for Evil Genius Labs.""" +from collections.abc import Callable +from functools import wraps +from typing import Any, TypeVar, cast + +from . import EvilGeniusEntity + +CallableT = TypeVar("CallableT", bound=Callable) + + +def update_when_done(func: CallableT) -> CallableT: + """Decorate function to trigger update when function is done.""" + + @wraps(func) + async def wrapper(self: EvilGeniusEntity, *args: Any, **kwargs: Any) -> Any: + """Wrap function.""" + result = await func(self, *args, **kwargs) + await self.coordinator.async_request_refresh() + return result + + return cast(CallableT, wrapper) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f65cf964ef3..4af6b656745 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -82,6 +82,7 @@ FLOWS = [ "environment_canada", "epson", "esphome", + "evil_genius_labs", "ezviz", "faa_delays", "fireservicerota", diff --git a/mypy.ini b/mypy.ini index ccacbca2da2..6859b8b2d48 100644 --- a/mypy.ini +++ b/mypy.ini @@ -473,6 +473,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.evil_genius_labs.*] +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.fastdotcom.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index f3082ca55cf..977264c1d73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,6 +1470,9 @@ pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 +# homeassistant.components.evil_genius_labs +pyevilgenius==1.0.0 + # homeassistant.components.ezviz pyezviz==0.1.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d7adcf59f0..e4b6fe6f708 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -867,6 +867,9 @@ pyefergy==0.1.4 # homeassistant.components.everlights pyeverlights==0.1.0 +# homeassistant.components.evil_genius_labs +pyevilgenius==1.0.0 + # homeassistant.components.ezviz pyezviz==0.1.9.4 diff --git a/tests/components/evil_genius_labs/__init__.py b/tests/components/evil_genius_labs/__init__.py new file mode 100644 index 00000000000..70b122eb460 --- /dev/null +++ b/tests/components/evil_genius_labs/__init__.py @@ -0,0 +1 @@ +"""Tests for the Evil Genius Labs integration.""" diff --git a/tests/components/evil_genius_labs/conftest.py b/tests/components/evil_genius_labs/conftest.py new file mode 100644 index 00000000000..063e31704a5 --- /dev/null +++ b/tests/components/evil_genius_labs/conftest.py @@ -0,0 +1,49 @@ +"""Test helpers for Evil Genius Labs.""" +import json +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(scope="session") +def data_fixture(): + """Fixture data.""" + data = json.loads(load_fixture("data.json", "evil_genius_labs")) + return {item["name"]: item for item in data} + + +@pytest.fixture(scope="session") +def info_fixture(): + """Fixture info.""" + return json.loads(load_fixture("info.json", "evil_genius_labs")) + + +@pytest.fixture +def config_entry(hass): + """Evil genius labs config entry.""" + entry = MockConfigEntry(domain="evil_genius_labs", data={"host": "192.168.1.113"}) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def setup_evil_genius_labs( + hass, config_entry, data_fixture, info_fixture, platforms +): + """Test up Evil Genius Labs instance.""" + with patch( + "pyevilgenius.EvilGeniusDevice.get_data", + return_value=data_fixture, + ), patch( + "pyevilgenius.EvilGeniusDevice.get_info", + return_value=info_fixture, + ), patch( + "homeassistant.components.evil_genius_labs.PLATFORMS", platforms + ): + assert await async_setup_component(hass, "evil_genius_labs", {}) + await hass.async_block_till_done() + yield diff --git a/tests/components/evil_genius_labs/fixtures/data.json b/tests/components/evil_genius_labs/fixtures/data.json new file mode 100644 index 00000000000..555fe38da7e --- /dev/null +++ b/tests/components/evil_genius_labs/fixtures/data.json @@ -0,0 +1,331 @@ +[ + { + "name": "name", + "label": "Name", + "type": "Label", + "value": "Fibonacci256-23D4" + }, + { "name": "power", "label": "Power", "type": "Boolean", "value": 1 }, + { + "name": "brightness", + "label": "Brightness", + "type": "Number", + "value": 128, + "min": 1, + "max": 255 + }, + { + "name": "pattern", + "label": "Pattern", + "type": "Select", + "value": 70, + "options": [ + "Pride", + "Pride Fibonacci", + "Color Waves", + "Color Waves Fibonacci", + "Pride Playground", + "Pride Playground Fibonacci", + "Color Waves Playground", + "Color Waves Playground Fibonacci", + "Wheel", + "Swirl Fibonacci", + "Fire Fibonacci", + "Water Fibonacci", + "Emitter Fibonacci", + "Pacifica", + "Pacifica Fibonacci", + "Angle Palette", + "Radius Palette", + "X Axis Palette", + "Y Axis Palette", + "XY Axis Palette", + "Angle Gradient Palette", + "Radius Gradient Palette", + "X Axis Gradient Palette", + "Y Axis Gradient Palette", + "XY Axis Gradient Palette", + "Fire Noise", + "Fire Noise 2", + "Lava Noise", + "Rainbow Noise", + "Rainbow Stripe Noise", + "Party Noise", + "Forest Noise", + "Cloud Noise", + "Ocean Noise", + "Black & White Noise", + "Black & Blue Noise", + "Analog Clock", + "Spiral Analog Clock 13", + "Spiral Analog Clock 21", + "Spiral Analog Clock 34", + "Spiral Analog Clock 55", + "Spiral Analog Clock 89", + "Spiral Analog Clock 21 & 34", + "Spiral Analog Clock 13, 21 & 34", + "Spiral Analog Clock 34, 21 & 13", + "Pride Playground", + "Color Waves Playground", + "Rainbow Twinkles", + "Snow Twinkles", + "Cloud Twinkles", + "Incandescent Twinkles", + "Retro C9 Twinkles", + "Red & White Twinkles", + "Blue & White Twinkles", + "Red, Green & White Twinkles", + "Fairy Light Twinkles", + "Snow 2 Twinkles", + "Holly Twinkles", + "Ice Twinkles", + "Party Twinkles", + "Forest Twinkles", + "Lava Twinkles", + "Fire Twinkles", + "Cloud 2 Twinkles", + "Ocean Twinkles", + "Rainbow", + "Rainbow With Glitter", + "Solid Rainbow", + "Confetti", + "Sinelon", + "Beat", + "Juggle", + "Fire", + "Water", + "Strand Test", + "Solid Color" + ] + }, + { + "name": "palette", + "label": "Palette", + "type": "Select", + "value": 0, + "options": [ + "Rainbow", + "Rainbow Stripe", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Party", + "Heat" + ] + }, + { + "name": "speed", + "label": "Speed", + "type": "Number", + "value": 30, + "min": 1, + "max": 255 + }, + { "name": "autoplaySection", "label": "Autoplay", "type": "Section" }, + { "name": "autoplay", "label": "Autoplay", "type": "Boolean", "value": 0 }, + { + "name": "autoplayDuration", + "label": "Autoplay Duration", + "type": "Number", + "value": 10, + "min": 0, + "max": 255 + }, + { "name": "clock", "label": "Clock", "type": "Section" }, + { "name": "showClock", "label": "Show Clock", "type": "Boolean", "value": 0 }, + { + "name": "clockBackgroundFade", + "label": "Background Fade", + "type": "Number", + "value": 240, + "min": 0, + "max": 255 + }, + { "name": "solidColorSection", "label": "Solid Color", "type": "Section" }, + { + "name": "solidColor", + "label": "Color", + "type": "Color", + "value": "0,0,255" + }, + { "name": "prideSection", "label": "Pride & ColorWaves", "type": "Section" }, + { + "name": "saturationBpm", + "label": "Saturation BPM", + "type": "Number", + "value": 87, + "min": 0, + "max": 255 + }, + { + "name": "saturationMin", + "label": "Saturation Min", + "type": "Number", + "value": 220, + "min": 0, + "max": 255 + }, + { + "name": "saturationMax", + "label": "Saturation Max", + "type": "Number", + "value": 250, + "min": 0, + "max": 255 + }, + { + "name": "brightDepthBpm", + "label": "Brightness Depth BPM", + "type": "Number", + "value": 1, + "min": 0, + "max": 255 + }, + { + "name": "brightDepthMin", + "label": "Brightness Depth Min", + "type": "Number", + "value": 96, + "min": 0, + "max": 255 + }, + { + "name": "brightDepthMax", + "label": "Brightness Depth Max", + "type": "Number", + "value": 224, + "min": 0, + "max": 255 + }, + { + "name": "brightThetaIncBpm", + "label": "Bright Theta Inc BPM", + "type": "Number", + "value": 203, + "min": 0, + "max": 255 + }, + { + "name": "brightThetaIncMin", + "label": "Bright Theta Inc Min", + "type": "Number", + "value": 25, + "min": 0, + "max": 255 + }, + { + "name": "brightThetaIncMax", + "label": "Bright Theta Inc Max", + "type": "Number", + "value": 40, + "min": 0, + "max": 255 + }, + { + "name": "msMultiplierBpm", + "label": "Time Multiplier BPM", + "type": "Number", + "value": 147, + "min": 0, + "max": 255 + }, + { + "name": "msMultiplierMin", + "label": "Time Multiplier Min", + "type": "Number", + "value": 23, + "min": 0, + "max": 255 + }, + { + "name": "msMultiplierMax", + "label": "Time Multiplier Max", + "type": "Number", + "value": 60, + "min": 0, + "max": 255 + }, + { + "name": "hueIncBpm", + "label": "Hue Inc BPM", + "type": "Number", + "value": 113, + "min": 0, + "max": 255 + }, + { + "name": "hueIncMin", + "label": "Hue Inc Min", + "type": "Number", + "value": 1, + "min": 0, + "max": 255 + }, + { + "name": "hueIncMax", + "label": "Hue Inc Max", + "type": "Number", + "value": 12, + "min": 0, + "max": 255 + }, + { + "name": "sHueBpm", + "label": "S Hue BPM", + "type": "Number", + "value": 2, + "min": 0, + "max": 255 + }, + { + "name": "sHueMin", + "label": "S Hue Min", + "type": "Number", + "value": 5, + "min": 0, + "max": 255 + }, + { + "name": "sHueMax", + "label": "S Hue Max", + "type": "Number", + "value": 9, + "min": 0, + "max": 255 + }, + { "name": "fireSection", "label": "Fire & Water", "type": "Section" }, + { + "name": "cooling", + "label": "Cooling", + "type": "Number", + "value": 49, + "min": 0, + "max": 255 + }, + { + "name": "sparking", + "label": "Sparking", + "type": "Number", + "value": 60, + "min": 0, + "max": 255 + }, + { "name": "twinklesSection", "label": "Twinkles", "type": "Section" }, + { + "name": "twinkleSpeed", + "label": "Twinkle Speed", + "type": "Number", + "value": 4, + "min": 0, + "max": 8 + }, + { + "name": "twinkleDensity", + "label": "Twinkle Density", + "type": "Number", + "value": 5, + "min": 0, + "max": 8 + } +] diff --git a/tests/components/evil_genius_labs/fixtures/info.json b/tests/components/evil_genius_labs/fixtures/info.json new file mode 100644 index 00000000000..c11ab369316 --- /dev/null +++ b/tests/components/evil_genius_labs/fixtures/info.json @@ -0,0 +1,30 @@ +{ + "millis": 62099724, + "vcc": 3005, + "wiFiChipId": "1923d4", + "flashChipId": "1640d8", + "flashChipSize": 4194304, + "flashChipRealSize": 4194304, + "sdkVersion": "2.2.2-dev(38a443e)", + "coreVersion": "2_7_4", + "bootVersion": 6, + "cpuFreqMHz": 160, + "freeHeap": 21936, + "sketchSize": 476352, + "freeSketchSpace": 1617920, + "resetReason": "External System", + "isConnected": true, + "wiFiSsidDefault": "My Wi-Fi", + "wiFiSSID": "My Wi-Fi", + "localIP": "192.168.1.113", + "gatewayIP": "192.168.1.1", + "subnetMask": "255.255.255.0", + "dnsIP": "192.168.1.1", + "hostname": "ESP-1923D4", + "macAddress": "BC:FF:4D:19:23:D4", + "autoConnect": true, + "softAPSSID": "FaryLink_1923D4", + "softAPIP": "(IP unset)", + "BSSID": "FC:EC:DA:77:1A:CE", + "softAPmacAddress": "BE:FF:4D:19:23:D4" +} diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py new file mode 100644 index 00000000000..55e207ba7e0 --- /dev/null +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -0,0 +1,85 @@ +"""Test the Evil Genius Labs config flow.""" +from unittest.mock import patch + +import aiohttp + +from homeassistant import config_entries +from homeassistant.components.evil_genius_labs.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant, data_fixture, info_fixture) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "pyevilgenius.EvilGeniusDevice.get_data", + return_value=data_fixture, + ), patch( + "pyevilgenius.EvilGeniusDevice.get_info", + return_value=info_fixture, + ), patch( + "homeassistant.components.evil_genius_labs.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Fibonacci256-23D4" + assert result2["data"] == { + "host": "1.1.1.1", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyevilgenius.EvilGeniusDevice.get_data", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyevilgenius.EvilGeniusDevice.get_data", + side_effect=ValueError("BOOM"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/evil_genius_labs/test_init.py b/tests/components/evil_genius_labs/test_init.py new file mode 100644 index 00000000000..c0c7d00e1e5 --- /dev/null +++ b/tests/components/evil_genius_labs/test_init.py @@ -0,0 +1,13 @@ +"""Test evil genius labs init.""" +import pytest + +from homeassistant import config_entries +from homeassistant.components.evil_genius_labs import PLATFORMS + + +@pytest.mark.parametrize("platforms", [PLATFORMS]) +async def test_setup_unload_entry(hass, setup_evil_genius_labs, config_entry): + """Test setting up and unloading a config entry.""" + assert len(hass.states.async_entity_ids()) == 1 + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state == config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/evil_genius_labs/test_light.py b/tests/components/evil_genius_labs/test_light.py new file mode 100644 index 00000000000..de053c58037 --- /dev/null +++ b/tests/components/evil_genius_labs/test_light.py @@ -0,0 +1,76 @@ +"""Test Evil Genius Labs light.""" +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize("platforms", [("light",)]) +async def test_works(hass, setup_evil_genius_labs): + """Test it works.""" + state = hass.states.get("light.fibonacci256_23d4") + assert state is not None + assert state.state == "on" + assert state.attributes["brightness"] == 128 + + +@pytest.mark.parametrize("platforms", [("light",)]) +async def test_turn_on_color(hass, setup_evil_genius_labs): + """Test turning on with a color.""" + with patch( + "pyevilgenius.EvilGeniusDevice.set_path_value" + ) as mock_set_path_value, patch( + "pyevilgenius.EvilGeniusDevice.set_rgb_color" + ) as mock_set_rgb_color: + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.fibonacci256_23d4", + "brightness": 100, + "rgb_color": (10, 20, 30), + }, + blocking=True, + ) + + assert len(mock_set_path_value.mock_calls) == 2 + mock_set_path_value.mock_calls[0][1] == ("brightness", 100) + mock_set_path_value.mock_calls[1][1] == ("power", 1) + + assert len(mock_set_rgb_color.mock_calls) == 1 + mock_set_rgb_color.mock_calls[0][1] == (10, 20, 30) + + +@pytest.mark.parametrize("platforms", [("light",)]) +async def test_turn_on_effect(hass, setup_evil_genius_labs): + """Test turning on with an effect.""" + with patch("pyevilgenius.EvilGeniusDevice.set_path_value") as mock_set_path_value: + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.fibonacci256_23d4", + "effect": "Pride Playground", + }, + blocking=True, + ) + + assert len(mock_set_path_value.mock_calls) == 2 + mock_set_path_value.mock_calls[0][1] == ("pattern", 4) + mock_set_path_value.mock_calls[1][1] == ("power", 1) + + +@pytest.mark.parametrize("platforms", [("light",)]) +async def test_turn_off(hass, setup_evil_genius_labs): + """Test turning off.""" + with patch("pyevilgenius.EvilGeniusDevice.set_path_value") as mock_set_path_value: + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": "light.fibonacci256_23d4", + }, + blocking=True, + ) + + assert len(mock_set_path_value.mock_calls) == 1 + mock_set_path_value.mock_calls[0][1] == ("power", 0) From 875aecd4e20b08dca5d6bf6838b16843516442e5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 8 Nov 2021 17:09:08 +0000 Subject: [PATCH 0313/1452] System Bridge - Add configuration URL (#59320) --- homeassistant/components/system_bridge/__init__.py | 2 ++ homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index faea8b8418c..b3f481141b8 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -239,6 +239,7 @@ class SystemBridgeEntity(CoordinatorEntity): bridge: Bridge = coordinator.data self._key = f"{bridge.information.host}_{key}" self._name = f"{bridge.information.host} {name}" + self._configuration_url = bridge.get_configuration_url() self._hostname = bridge.information.host self._mac = bridge.information.mac self._manufacturer = bridge.system.system.manufacturer @@ -263,6 +264,7 @@ class SystemBridgeDeviceEntity(SystemBridgeEntity): def device_info(self) -> DeviceInfo: """Return device information about this System Bridge instance.""" return DeviceInfo( + configuration_url=self._configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, manufacturer=self._manufacturer, model=self._model, diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 0a6bca604e7..cd4ee5a51a1 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==2.2.1"], + "requirements": ["systembridge==2.2.3"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 977264c1d73..a775f7887de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==2.2.1 +systembridge==2.2.3 # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4b6fe6f708..9b6941bc279 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridge==2.2.1 +systembridge==2.2.3 # homeassistant.components.tellduslive tellduslive==0.10.11 From 7b9715bec343ae3de98858a5ba4047471ab24cd1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Nov 2021 18:16:44 +0100 Subject: [PATCH 0314/1452] Bump paho-mqtt to 1.6.1 (#59339) --- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index c5d9ad21ed6..6fb81deeb4d 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -3,7 +3,7 @@ "name": "MQTT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mqtt", - "requirements": ["paho-mqtt==1.5.1"], + "requirements": ["paho-mqtt==1.6.1"], "dependencies": ["http"], "codeowners": ["@emontnemery"], "iot_class": "local_push" diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index f7f04eb5a86..fc475c2f48e 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -2,7 +2,7 @@ "domain": "shiftr", "name": "shiftr.io", "documentation": "https://www.home-assistant.io/integrations/shiftr", - "requirements": ["paho-mqtt==1.5.1"], + "requirements": ["paho-mqtt==1.6.1"], "codeowners": ["@fabaff"], "iot_class": "cloud_push" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d3e9475e442..b63567b35f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ home-assistant-frontend==20211103.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 pillow==8.2.0 pip>=8.0.3,<20.3 pyserial==3.5 diff --git a/requirements_all.txt b/requirements_all.txt index a775f7887de..9a04c047b7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1169,7 +1169,7 @@ p1monitor==1.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 # homeassistant.components.panasonic_bluray panacotta==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b6941bc279..329d27d76bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -695,7 +695,7 @@ p1monitor==1.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.6 From 2b68b9292c9cd2360a1b9e818cb8ffab558d66dd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 8 Nov 2021 18:41:25 +0100 Subject: [PATCH 0315/1452] Set tradfri entities to non-available when hub is not available (#59278) * Set available when needed. Co-authored-by: Martin Hjelmare --- homeassistant/components/tradfri/__init__.py | 6 ++++++ .../components/tradfri/base_class.py | 21 +++++++++++++++++-- homeassistant/components/tradfri/const.py | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 3988775ad2b..206f4e07007 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import Event, async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -34,6 +35,7 @@ from .const import ( GROUPS, KEY_API, PLATFORMS, + SIGNAL_GW, ) _LOGGER = logging.getLogger(__name__) @@ -137,10 +139,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.is_stopping: return + gw_status = True try: await api(gateway.get_gateway_info()) except RequestError: _LOGGER.error("Keep-alive failed") + gw_status = False + + async_dispatcher_send(hass, SIGNAL_GW, gw_status) listeners.append( async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60)) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 8a7cc6a2f4a..34ad7b792b9 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -20,9 +20,10 @@ from pytradfri.device.socket_control import SocketControl from pytradfri.error import PytradfriError from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_GW _LOGGER = logging.getLogger(__name__) @@ -122,8 +123,24 @@ class TradfriBaseDevice(TradfriBaseClass): ) -> None: """Initialize a device.""" self._attr_available = device.reachable + self._hub_available = True super().__init__(device, api, gateway_id) + async def async_added_to_hass(self) -> None: + """Start thread when added to hass.""" + # Only devices shall receive SIGNAL_GW + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_GW, self.set_hub_available) + ) + await super().async_added_to_hass() + + @callback + def set_hub_available(self, available: bool) -> None: + """Set status of hub.""" + if available != self._hub_available: + self._hub_available = available + self._refresh(self._device) + @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -142,5 +159,5 @@ class TradfriBaseDevice(TradfriBaseClass): # The base class _refresh cannot be used, because # there are devices (group) that do not have .reachable # so set _attr_available here and let the base class do the rest. - self._attr_available = device.reachable + self._attr_available = device.reachable and self._hub_available super()._refresh(device, write_ha) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 8efb1837ae4..5bcd1f376e1 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -21,6 +21,7 @@ DOMAIN = "tradfri" KEY_API = "tradfri_api" DEVICES = "tradfri_devices" GROUPS = "tradfri_groups" +SIGNAL_GW = "tradfri.gw_status" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION From 80f1e8770aac986e2f231e39447f1da7312ed00f Mon Sep 17 00:00:00 2001 From: Piotr Majkrzak Date: Mon, 8 Nov 2021 19:13:35 +0100 Subject: [PATCH 0316/1452] Add Water Content Measurement clusters (#59300) * Add Water Content Measurement clusters * Fix typo * Add device test Co-authored-by: Alexei Chetroi --- .../zha/core/channels/measurement.py | 24 ++++++++++++ homeassistant/components/zha/core/const.py | 2 + .../components/zha/core/registries.py | 2 + homeassistant/components/zha/sensor.py | 24 ++++++++++++ tests/components/zha/zha_devices_list.py | 38 +++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 19ecc8a6335..093c04245c4 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -62,6 +62,30 @@ class RelativeHumidity(ZigbeeChannel): ] +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.SoilMoisture.cluster_id) +class SoilMoisture(ZigbeeChannel): + """Soil Moisture measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), + } + ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.LeafWetness.cluster_id) +class LeafWetness(ZigbeeChannel): + """Leaf Wetness measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), + } + ] + + @registries.ZIGBEE_CHANNEL_REGISTRY.register( measurement.TemperatureMeasurement.cluster_id ) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index dd6832e0d6b..de29ac0f9f6 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -84,6 +84,8 @@ CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" CHANNEL_HUMIDITY = "humidity" +CHANNEL_SOIL_MOISTURE = "soil_moisture" +CHANNEL_LEAF_WETNESS = "leaf_wetness" CHANNEL_IAS_ACE = "ias_ace" CHANNEL_IAS_WD = "ias_wd" CHANNEL_IDENTIFY = "identify" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 8b2c4d11fbf..eeee0c5c629 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -82,6 +82,8 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.measurement.OccupancySensing.cluster_id: BINARY_SENSOR, zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR, zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR, + zcl.clusters.measurement.SoilMoisture.cluster_id: SENSOR, + zcl.clusters.measurement.LeafWetness.cluster_id: SENSOR, zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR, zcl.clusters.security.IasZone.cluster_id: BINARY_SENSOR, } diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2281d5295bc..365a7d8084f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -61,9 +61,11 @@ from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, + CHANNEL_LEAF_WETNESS, CHANNEL_POWER_CONFIGURATION, CHANNEL_PRESSURE, CHANNEL_SMARTENERGY_METERING, + CHANNEL_SOIL_MOISTURE, CHANNEL_TEMPERATURE, CHANNEL_THERMOSTAT, DATA_ZHA, @@ -353,6 +355,28 @@ class Humidity(Sensor): _unit = PERCENTAGE +@STRICT_MATCH(channel_names=CHANNEL_SOIL_MOISTURE) +class SoilMoisture(Sensor): + """Soil Moisture sensor.""" + + SENSOR_ATTR = "measured_value" + _device_class = DEVICE_CLASS_HUMIDITY + _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT + _unit = PERCENTAGE + + +@STRICT_MATCH(channel_names=CHANNEL_LEAF_WETNESS) +class LeafWetness(Sensor): + """Leaf Wetness sensor.""" + + SENSOR_ATTR = "measured_value" + _device_class = DEVICE_CLASS_HUMIDITY + _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT + _unit = PERCENTAGE + + @STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) class Illuminance(Sensor): """Illuminance Sensor.""" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index e85f4c270d5..2ef2e578dce 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3985,4 +3985,42 @@ DEVICES = [ SIG_MODEL: "XBee3", SIG_NODE_DESC: b"\x01@\x8e\x1e\x10R\xff\x00\x00,\xff\x00\x00", }, + { + DEV_SIG_DEV_NO: 99, + SIG_ENDPOINTS: { + 1: { + SIG_EP_TYPE: 0x000C, + DEV_SIG_EP_ID: 1, + SIG_EP_INPUT: [0x0000, 0x0001, 0x0402, 0x0408], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 260, + } + }, + DEV_SIG_ENTITIES: [ + "sensor.efektalab_ru_efekta_pws_77665544_power", + "sensor.efektalab_ru_efekta_pws_77665544_temperature", + "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + ], + DEV_SIG_ENT_MAP: { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { + DEV_SIG_CHANNELS: ["soil_moisture"], + DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + }, + }, + DEV_SIG_EVT_CHANNELS: [], + SIG_MANUFACTURER: "efektalab.ru", + SIG_MODEL: "EFEKTA_PWS", + SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", + }, ] From ac354ecff53eff5d83638cb49d149db0df2f2ed7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 8 Nov 2021 20:20:19 +0100 Subject: [PATCH 0317/1452] Bump pytradfri to v7.2.0. (#59342) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 3579d9a9bd8..5b23ec4e418 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.1.1"], + "requirements": ["pytradfri[async]==7.2.0"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 9a04c047b7d..5fc3fabbe7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pytouchline==0.7 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.1.1 +pytradfri[async]==7.2.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 329d27d76bd..fbc58d5ef52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1163,7 +1163,7 @@ pytile==5.2.4 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.1.1 +pytradfri[async]==7.2.0 # homeassistant.components.usb pyudev==0.22.0 From 4ac7dfc983ac0d6a05efbad6d18ee0ff4ff53545 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Nov 2021 21:46:23 +0200 Subject: [PATCH 0318/1452] Cleanup Shelly light - use separate sets for dual mode and effects (#59363) --- homeassistant/components/shelly/__init__.py | 4 +++- homeassistant/components/shelly/const.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6a981ab2b86..ca690e33361 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -45,6 +45,7 @@ from .const import ( ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, + MODELS_SUPPORTING_LIGHT_EFFECTS, POLLING_TIMEOUT_SEC, REST, REST_SENSORS_UPDATE_INTERVAL, @@ -313,11 +314,12 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): # For dual mode bulbs ignore change if it is due to mode/effect change if self.model in DUAL_MODE_LIGHT_MODELS: - if "mode" in block.sensor_ids and self.model != "SHRGBW2": + if "mode" in block.sensor_ids: if self._last_mode != block.mode: self._last_cfg_changed = None self._last_mode = block.mode + if self.model in MODELS_SUPPORTING_LIGHT_EFFECTS: if "effect" in block.sensor_ids: if self._last_effect != block.effect: self._last_cfg_changed = None diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 5fceb64e65c..d241998a138 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -35,14 +35,18 @@ MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( "SHVIN-1", ) -# Bulbs that support white & color modes -DUAL_MODE_LIGHT_MODELS: Final = ( - "SHBDUO-1", +MODELS_SUPPORTING_LIGHT_EFFECTS: Final = ( "SHBLB-1", "SHCB-1", "SHRGBW2", ) +# Bulbs that support white & color modes +DUAL_MODE_LIGHT_MODELS: Final = ( + "SHBLB-1", + "SHCB-1", +) + # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC: Final = 18 From adfbcfa704bfc2da44e77a1d8705a51852c43b12 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Nov 2021 21:47:01 +0200 Subject: [PATCH 0319/1452] Add Shelly Gen1 beta_version attribute to firmware update sensor (#59359) --- homeassistant/components/shelly/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 69f5f0e4440..4a918506b00 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -126,6 +126,7 @@ REST_SENSORS: Final = { extra_state_attributes=lambda status: { "latest_stable_version": status["update"]["new_version"], "installed_version": status["update"]["old_version"], + "beta_version": status["update"].get("beta_version", ""), }, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), From 69da2b29e61fa053c603ad39b08e916389025ba7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 8 Nov 2021 21:45:40 +0100 Subject: [PATCH 0320/1452] Update frontend to 20211108.0 (#59364) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c913de3367f..43e0e7e86bc 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211103.0" + "home-assistant-frontend==20211108.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b63567b35f5..bc26dba5342 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5fc3fabbe7a..a12767f2a46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbc58d5ef52..5bad1e5712e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -506,7 +506,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bb1203c61d956e2fe3dd03641dcfe38a0bc9b89c Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 8 Nov 2021 21:56:17 +0100 Subject: [PATCH 0321/1452] Bump velbusaio to 2021.11.6 (#59353) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 454b9d24d07..5fb3c58c3c7 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.11.0"], + "requirements": ["velbus-aio==2021.11.6"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a12767f2a46..cd076d64a88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ vallox-websocket-api==2.8.1 vehicle==0.1.0 # homeassistant.components.velbus -velbus-aio==2021.11.0 +velbus-aio==2021.11.6 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bad1e5712e..57d5a9a8944 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ uvcclient==0.11.0 vehicle==0.1.0 # homeassistant.components.velbus -velbus-aio==2021.11.0 +velbus-aio==2021.11.6 # homeassistant.components.venstar venstarcolortouch==0.15 From 20b93132dd8b935995209bd8ba51b86b8baaa69e Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 8 Nov 2021 23:13:00 +0100 Subject: [PATCH 0322/1452] Support generic xiaomi_miio vacuums (#59317) * Support generic xiaomi_miio vacuums Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Fix lint Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Remove warning log Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> --- homeassistant/components/xiaomi_miio/__init__.py | 12 ++++++++++-- homeassistant/components/xiaomi_miio/const.py | 4 +++- homeassistant/components/xiaomi_miio/sensor.py | 10 ++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 5513485f51e..9d983d32252 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -66,6 +66,8 @@ from .const import ( MODELS_PURIFIER_MIOT, MODELS_SWITCH, MODELS_VACUUM, + ROBOROCK_GENERIC, + ROCKROBO_GENERIC, AuthException, SetupException, ) @@ -267,7 +269,7 @@ async def async_create_miio_device_and_coordinator( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up a data coordinator and one miio device to service multiple entities.""" - model = entry.data[CONF_MODEL] + model: str = entry.data[CONF_MODEL] host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title @@ -280,6 +282,8 @@ async def async_create_miio_device_and_coordinator( model not in MODELS_HUMIDIFIER and model not in MODELS_FAN and model not in MODELS_VACUUM + and not model.startswith(ROBOROCK_GENERIC) + and not model.startswith(ROCKROBO_GENERIC) ): return @@ -304,7 +308,11 @@ async def async_create_miio_device_and_coordinator( device = AirPurifier(host, token) elif model.startswith("zhimi.airfresh."): device = AirFresh(host, token) - elif model in MODELS_VACUUM: + elif ( + model in MODELS_VACUUM + or model.startswith(ROBOROCK_GENERIC) + or model.startswith(ROCKROBO_GENERIC) + ): device = Vacuum(host, token) update_method = _async_update_data_vacuum coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData] diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 60c16a0717a..8c83c8015b2 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -202,7 +202,8 @@ ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" -ROCKROBO_GENERIC = "roborock.vacuum" +ROBOROCK_GENERIC = "roborock.vacuum" +ROCKROBO_GENERIC = "rockrobo.vacuum" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, @@ -214,6 +215,7 @@ MODELS_VACUUM = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROBOROCK_GENERIC, ROCKROBO_GENERIC, ] MODELS_VACUUM_WITH_MOP = [ diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ac26bc97fce..0d67014ced9 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -81,6 +81,8 @@ from .const import ( MODELS_PURIFIER_MIIO, MODELS_PURIFIER_MIOT, MODELS_VACUUM, + ROBOROCK_GENERIC, + ROCKROBO_GENERIC, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice @@ -592,7 +594,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] - model = config_entry.data[CONF_MODEL] + model: str = config_entry.data[CONF_MODEL] if model in (MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5): return @@ -624,7 +626,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = PURIFIER_MIIO_SENSORS elif model in MODELS_PURIFIER_MIOT: sensors = PURIFIER_MIOT_SENSORS - elif model in MODELS_VACUUM: + elif ( + model in MODELS_VACUUM + or model.startswith(ROBOROCK_GENERIC) + or model.startswith(ROCKROBO_GENERIC) + ): return _setup_vacuum_sensors(hass, config_entry, async_add_entities) for sensor, description in SENSOR_TYPES.items(): From 2924f4605bc62f5b14c68d3cc5ec1ea7b6ab80d8 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Mon, 8 Nov 2021 23:25:19 +0100 Subject: [PATCH 0323/1452] Fix statistics startup error for None value states (#59199) * Catch statistics startup error for None value states, fix #49254 * Add test for statistics None handling * Update tests/components/statistics/test_sensor.py Co-authored-by: Erik Montnemery * Switch test case logic to remove sensor last Co-authored-by: Erik Montnemery --- homeassistant/components/statistics/sensor.py | 2 +- tests/components/statistics/test_sensor.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 6bf882c861d..3d5857e2b03 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -175,7 +175,7 @@ class StatisticsSensor(SensorEntity): def _add_state_to_queue(self, new_state): """Add the state to the queue.""" - if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE, None): return try: diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 41ec88deaec..a2eaa9add03 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -135,6 +135,13 @@ class TestStatisticsSensor(unittest.TestCase): new_state = self.hass.states.get("sensor.test") assert state == new_state + # Source sensor is removed, unit and state should not change + # This is equal to a None value being published + self.hass.states.remove("sensor.test_monitored") + self.hass.block_till_done() + new_state = self.hass.states.get("sensor.test") + assert state == new_state + def test_sampling_size(self): """Test rotation.""" assert setup_component( From 9eaf8bd21bb0e1f29f88511e0d9174a9a806c833 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Mon, 8 Nov 2021 23:26:00 +0100 Subject: [PATCH 0324/1452] Fix statistics precision handling (#59202) * Fix statistics precision error when configured 0, fix #42547 * Add tests for statistics precision * Apply precision=0 logic to float numbers only * Implement contextlib way of exception handling --- homeassistant/components/statistics/sensor.py | 8 +- tests/components/statistics/test_sensor.py | 76 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 3d5857e2b03..7b2951a1ab6 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,5 +1,6 @@ """Support for statistics for sensor values.""" from collections import deque +import contextlib import logging import statistics @@ -203,7 +204,12 @@ class StatisticsSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self.mean if not self.is_binary else self.count + if self.is_binary: + return self.count + if self._precision == 0: + with contextlib.suppress(TypeError, ValueError): + return int(self.mean) + return self.mean @property def native_unit_of_measurement(self): diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index a2eaa9add03..c3b14ba360d 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -103,7 +103,9 @@ class TestStatisticsSensor(unittest.TestCase): for value in self.values: self.hass.states.set( - "sensor.test_monitored", value, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() @@ -163,7 +165,9 @@ class TestStatisticsSensor(unittest.TestCase): for value in self.values: self.hass.states.set( - "sensor.test_monitored", value, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() @@ -193,7 +197,9 @@ class TestStatisticsSensor(unittest.TestCase): for value in self.values[-3:]: # just the last 3 will do self.hass.states.set( - "sensor.test_monitored", value, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() @@ -363,6 +369,66 @@ class TestStatisticsSensor(unittest.TestCase): ) == state.attributes.get("max_age") assert self.change_rate == state.attributes.get("change_rate") + def test_precision_0(self): + """Test correct result with precision=0 as integer.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "precision": 0, + } + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == str(int(state.attributes.get("mean"))) + + def test_precision_1(self): + """Test correct result with precision=1 rounded to one decimal.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "precision": 1, + } + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == str(round(sum(self.values) / len(self.values), 1)) + def test_initialize_from_database(self): """Test initializing the statistics from the database.""" # enable the recorder @@ -372,7 +438,9 @@ class TestStatisticsSensor(unittest.TestCase): # store some values for value in self.values: self.hass.states.set( - "sensor.test_monitored", value, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() # wait for the recorder to really store the data From cf22bd880748f90da10251accfc8e949e8267cfe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Nov 2021 00:27:36 +0100 Subject: [PATCH 0325/1452] Use zeroconf attributes in freebox (#58967) * Use zeroconf attributes in freebox * Use zeroconf.HaServiceInfo in tests --- homeassistant/components/freebox/config_flow.py | 11 ++++++++--- tests/components/freebox/test_config_flow.py | 17 +++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 0e10a8328c5..58c106ab467 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -5,7 +5,10 @@ from freebox_api.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN from .router import get_api @@ -105,8 +108,10 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_zeroconf(self, discovery_info: dict): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Initialize flow from zeroconf.""" - host = discovery_info["properties"]["api_domain"] - port = discovery_info["properties"]["https_port"] + host = discovery_info[zeroconf.ATTR_PROPERTIES]["api_domain"] + port = discovery_info[zeroconf.ATTR_PROPERTIES]["https_port"] return await self.async_step_user({CONF_HOST: host, CONF_PORT: port}) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 2d86ee1de20..73711c47c8a 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -8,6 +8,7 @@ from freebox_api.exceptions import ( ) from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT @@ -17,13 +18,13 @@ from .const import MOCK_HOST, MOCK_PORT from tests.common import MockConfigEntry -MOCK_ZEROCONF_DATA = { - "host": "192.168.0.254", - "port": 80, - "hostname": "Freebox-Server.local.", - "type": "_fbx-api._tcp.local.", - "name": "Freebox Server._fbx-api._tcp.local.", - "properties": { +MOCK_ZEROCONF_DATA = zeroconf.HaServiceInfo( + host="192.168.0.254", + port=80, + hostname="Freebox-Server.local.", + type="_fbx-api._tcp.local.", + name="Freebox Server._fbx-api._tcp.local.", + properties={ "api_version": "8.0", "device_type": "FreeboxServer1,2", "api_base_url": "/api/", @@ -34,7 +35,7 @@ MOCK_ZEROCONF_DATA = { "box_model_name": "Freebox Server (r2)", "api_domain": MOCK_HOST, }, -} +) async def test_user(hass: HomeAssistant): From a989fd2e663472c3c69f1fed3a8446f21d523bd0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 9 Nov 2021 00:15:20 +0000 Subject: [PATCH 0326/1452] [ci skip] Translation update --- .../components/airly/translations/bg.json | 3 +++ .../components/brother/translations/bg.json | 3 +++ .../components/button/translations/nl.json | 11 ++++++++++ .../components/button/translations/no.json | 11 ++++++++++ .../components/elgato/translations/bg.json | 3 +++ .../evil_genius_labs/translations/de.json | 15 +++++++++++++ .../evil_genius_labs/translations/et.json | 15 +++++++++++++ .../evil_genius_labs/translations/hu.json | 15 +++++++++++++ .../translations/zh-Hant.json | 15 +++++++++++++ .../components/gdacs/translations/bg.json | 7 ++++++ .../components/gios/translations/bg.json | 6 +++++ .../components/icloud/translations/bg.json | 4 ++++ .../components/mikrotik/translations/bg.json | 7 ++++++ .../minecraft_server/translations/bg.json | 3 +++ .../components/nest/translations/nl.json | 7 ++++++ .../components/nest/translations/no.json | 7 ++++++ .../components/rdw/translations/bg.json | 7 ++++++ .../components/rdw/translations/nl.json | 15 +++++++++++++ .../components/rdw/translations/no.json | 15 +++++++++++++ .../components/ridwell/translations/no.json | 2 +- .../ridwell/translations/zh-Hant.json | 2 +- .../components/ring/translations/bg.json | 22 +++++++++++++++++++ .../components/samsungtv/translations/bg.json | 14 +++++++++++- .../components/sentry/translations/bg.json | 3 +++ .../shelly/translations/es-419.json | 7 ++++++ .../components/soma/translations/bg.json | 1 + .../components/withings/translations/bg.json | 3 +++ 27 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/button/translations/nl.json create mode 100644 homeassistant/components/button/translations/no.json create mode 100644 homeassistant/components/evil_genius_labs/translations/de.json create mode 100644 homeassistant/components/evil_genius_labs/translations/et.json create mode 100644 homeassistant/components/evil_genius_labs/translations/hu.json create mode 100644 homeassistant/components/evil_genius_labs/translations/zh-Hant.json create mode 100644 homeassistant/components/gdacs/translations/bg.json create mode 100644 homeassistant/components/rdw/translations/bg.json create mode 100644 homeassistant/components/rdw/translations/nl.json create mode 100644 homeassistant/components/rdw/translations/no.json create mode 100644 homeassistant/components/ring/translations/bg.json create mode 100644 homeassistant/components/shelly/translations/es-419.json diff --git a/homeassistant/components/airly/translations/bg.json b/homeassistant/components/airly/translations/bg.json index f0836c8e5ed..45b4cb225aa 100644 --- a/homeassistant/components/airly/translations/bg.json +++ b/homeassistant/components/airly/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly." }, diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json index afb3272e575..a2b013df69e 100644 --- a/homeassistant/components/brother/translations/bg.json +++ b/homeassistant/components/brother/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/button/translations/nl.json b/homeassistant/components/button/translations/nl.json new file mode 100644 index 00000000000..31fe69d11f5 --- /dev/null +++ b/homeassistant/components/button/translations/nl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Druk op de knop {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} is ingedrukt" + } + }, + "title": "Knop" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/no.json b/homeassistant/components/button/translations/no.json new file mode 100644 index 00000000000..94daca72747 --- /dev/null +++ b/homeassistant/components/button/translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Trykk p\u00e5 {entity_name} -knappen" + }, + "trigger_type": { + "pressed": "{entity_name} har blitt trykket" + } + }, + "title": "Knapp" +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/bg.json b/homeassistant/components/elgato/translations/bg.json index d00cbb30191..0e3a6d80ab1 100644 --- a/homeassistant/components/elgato/translations/bg.json +++ b/homeassistant/components/elgato/translations/bg.json @@ -1,14 +1,17 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{serial_number}", "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/evil_genius_labs/translations/de.json b/homeassistant/components/evil_genius_labs/translations/de.json new file mode 100644 index 00000000000..e45f7c2b063 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/et.json b/homeassistant/components/evil_genius_labs/translations/et.json new file mode 100644 index 00000000000..dedad93b36c --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/hu.json b/homeassistant/components/evil_genius_labs/translations/hu.json new file mode 100644 index 00000000000..6ed148ceabb --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/zh-Hant.json b/homeassistant/components/evil_genius_labs/translations/zh-Hant.json new file mode 100644 index 00000000000..a91b061b492 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/bg.json b/homeassistant/components/gdacs/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/gdacs/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/bg.json b/homeassistant/components/gios/translations/bg.json index 35cfa0ad1d7..85c36ea1383 100644 --- a/homeassistant/components/gios/translations/bg.json +++ b/homeassistant/components/gios/translations/bg.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index d074f0ff9b9..f7f1faf74f5 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "error": { "send_verification_code": "\u041a\u043e\u0434\u044a\u0442 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0435 \u0431\u0435 \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d", "validate_verification_code": "\u041f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043a\u043e\u0434\u0430 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" @@ -7,6 +10,7 @@ "step": { "user": { "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "Email" } } diff --git a/homeassistant/components/mikrotik/translations/bg.json b/homeassistant/components/mikrotik/translations/bg.json index 9d19b786d10..d81e97f2d68 100644 --- a/homeassistant/components/mikrotik/translations/bg.json +++ b/homeassistant/components/mikrotik/translations/bg.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/minecraft_server/translations/bg.json b/homeassistant/components/minecraft_server/translations/bg.json index a051d6ca487..43e9a8a20d0 100644 --- a/homeassistant/components/minecraft_server/translations/bg.json +++ b/homeassistant/components/minecraft_server/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index a849b76f1c9..7709ab400f2 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -18,6 +18,13 @@ "unknown": "Onverwachte fout" }, "step": { + "auth": { + "data": { + "code": "Toegangstoken" + }, + "description": "Om uw Google account te koppelen, [authoriseer uw account]({url}).\n\nNa autorisatie, copy-paste u de gegeven toegangstoken hieronder.", + "title": "Link Google Account" + }, "init": { "data": { "flow_impl": "Leverancier" diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 914545d5a54..276fb862298 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -18,6 +18,13 @@ "unknown": "Uventet feil" }, "step": { + "auth": { + "data": { + "code": "Tilgangstoken" + }, + "description": "For \u00e5 koble til Google-kontoen din, [autoriser kontoen din]( {url} ). \n\n Etter autorisasjon, kopier og lim inn den oppgitte Auth Token-koden nedenfor.", + "title": "Koble til Google-kontoen" + }, "init": { "data": { "flow_impl": "Tilbyder" diff --git a/homeassistant/components/rdw/translations/bg.json b/homeassistant/components/rdw/translations/bg.json new file mode 100644 index 00000000000..e9a9c468402 --- /dev/null +++ b/homeassistant/components/rdw/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/nl.json b/homeassistant/components/rdw/translations/nl.json new file mode 100644 index 00000000000..4adb309d0e2 --- /dev/null +++ b/homeassistant/components/rdw/translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown_license_plate": "Onbekend kentekenplaat" + }, + "step": { + "user": { + "data": { + "license_plate": "Kentekenplaat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/no.json b/homeassistant/components/rdw/translations/no.json new file mode 100644 index 00000000000..f6d742f1bac --- /dev/null +++ b/homeassistant/components/rdw/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown_license_plate": "Ukjent nummerskilt" + }, + "step": { + "user": { + "data": { + "license_plate": "Bilskilt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/no.json b/homeassistant/components/ridwell/translations/no.json index bfc6dbd5bb3..0e161b5d614 100644 --- a/homeassistant/components/ridwell/translations/no.json +++ b/homeassistant/components/ridwell/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", + "already_configured": "Kontoen er allerede konfigurert", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { diff --git a/homeassistant/components/ridwell/translations/zh-Hant.json b/homeassistant/components/ridwell/translations/zh-Hant.json index f0ce6bd4327..358e0f26518 100644 --- a/homeassistant/components/ridwell/translations/zh-Hant.json +++ b/homeassistant/components/ridwell/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/ring/translations/bg.json b/homeassistant/components/ring/translations/bg.json new file mode 100644 index 00000000000..90fdd44ba71 --- /dev/null +++ b/homeassistant/components/ring/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "2fa": { + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index 1432000c6b9..332b6b62236 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -1,10 +1,22 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, - "flow_title": "{device}" + "flow_title": "{device}", + "step": { + "confirm": { + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/bg.json b/homeassistant/components/sentry/translations/bg.json index fcb2c982fdb..70cbc98dd7d 100644 --- a/homeassistant/components/sentry/translations/bg.json +++ b/homeassistant/components/sentry/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/shelly/translations/es-419.json b/homeassistant/components/shelly/translations/es-419.json new file mode 100644 index 00000000000..6f31b87105f --- /dev/null +++ b/homeassistant/components/shelly/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "long_push": "Pulsaci\u00f3n larga {subtype}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/bg.json b/homeassistant/components/soma/translations/bg.json index bfdebd385a6..a8361597baa 100644 --- a/homeassistant/components/soma/translations/bg.json +++ b/homeassistant/components/soma/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Soma \u0430\u043a\u0430\u0443\u043d\u0442.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Soma \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." }, "create_entry": { diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index b258b9d548a..9d5313ee391 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." }, From a22a966fac262b7131643dca83716fdac78356a1 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 9 Nov 2021 05:42:20 +0100 Subject: [PATCH 0327/1452] Increase timeout for fetching camera data on Synology DSM (#59237) --- homeassistant/components/synology_dsm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index bbc78ba42a3..1351f403306 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: surveillance_station = api.surveillance_station try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(30): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err From cc872b4618f38d4da5b5f148009f2881d4a9c6f7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 9 Nov 2021 00:29:25 -0500 Subject: [PATCH 0328/1452] Add tests for goalzero (#57008) * Add tests for goalzero * clean up --- .coveragerc | 4 - homeassistant/components/goalzero/__init__.py | 2 + .../components/goalzero/config_flow.py | 4 +- homeassistant/components/goalzero/const.py | 2 +- homeassistant/components/goalzero/sensor.py | 2 + homeassistant/components/goalzero/switch.py | 2 + tests/components/goalzero/__init__.py | 81 ++++++++++-- .../goalzero/fixtures/info_data.json | 7 ++ .../goalzero/fixtures/state_data.json | 38 ++++++ .../components/goalzero/test_binary_sensor.py | 36 ++++++ tests/components/goalzero/test_config_flow.py | 115 +++++++----------- tests/components/goalzero/test_init.py | 91 ++++++++++++++ tests/components/goalzero/test_sensor.py | 112 +++++++++++++++++ tests/components/goalzero/test_switch.py | 49 ++++++++ 14 files changed, 452 insertions(+), 93 deletions(-) create mode 100644 tests/components/goalzero/fixtures/info_data.json create mode 100644 tests/components/goalzero/fixtures/state_data.json create mode 100644 tests/components/goalzero/test_binary_sensor.py create mode 100644 tests/components/goalzero/test_init.py create mode 100644 tests/components/goalzero/test_sensor.py create mode 100644 tests/components/goalzero/test_switch.py diff --git a/.coveragerc b/.coveragerc index 6987e985467..1893b9988ae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,10 +386,6 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* - homeassistant/components/goalzero/__init__.py - homeassistant/components/goalzero/binary_sensor.py - homeassistant/components/goalzero/sensor.py - homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 774f1fd0e21..37b9aac7216 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -101,6 +102,7 @@ class YetiEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.api.sysdata["macAddress"])}, identifiers={(DOMAIN, self._server_unique_id)}, manufacturer="Goal Zero", model=self.api.sysdata[ATTR_MODEL], diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index f192c71cbf8..73dbc66794a 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -15,7 +15,7 @@ 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 +from .const import DEFAULT_NAME, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Allow the user to confirm adding the device.""" if user_input is not None: return self.async_create_entry( - title="Goal Zero", + title=MANUFACTURER, data={ CONF_HOST: self.ip_address, CONF_NAME: DEFAULT_NAME, diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index d99cacb253e..fef1636005d 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -8,5 +8,5 @@ DATA_KEY_COORDINATOR = "coordinator" DOMAIN = "goalzero" DEFAULT_NAME = "Yeti" DATA_KEY_API = "api" - +MANUFACTURER = "Goal Zero" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 6677936b35a..ed22a9dc26d 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -38,6 +38,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +PARALLEL_UPDATES = 0 + SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 6c80a773a74..8f12251d2d6 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -13,6 +13,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +PARALLEL_UPDATES = 0 + SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="v12PortStatus", diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 1b5302dbc1b..36bf707fdc8 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,33 +1,50 @@ """Tests for the Goal Zero Yeti integration.""" - from unittest.mock import AsyncMock, patch from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.goalzero import DOMAIN +from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker HOST = "1.2.3.4" -NAME = "Yeti" +MAC = "aa:bb:cc:dd:ee:ff" CONF_DATA = { CONF_HOST: HOST, - CONF_NAME: NAME, -} - -CONF_CONFIG_FLOW = { - CONF_HOST: HOST, - CONF_NAME: NAME, + CONF_NAME: DEFAULT_NAME, } CONF_DHCP_FLOW = { - IP_ADDRESS: "1.1.1.1", - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "any", + IP_ADDRESS: HOST, + MAC_ADDRESS: format_mac("AA:BB:CC:DD:EE:FF"), + HOSTNAME: "yeti", } -async def _create_mocked_yeti(raise_exception=False): +def create_entry(hass: HomeAssistant): + """Add config entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + unique_id=MAC, + ) + entry.add_to_hass(hass) + return entry + + +async def _create_mocked_yeti(): mocked_yeti = AsyncMock() - mocked_yeti.get_state = AsyncMock() + mocked_yeti.data = {} + mocked_yeti.data["firmwareVersion"] = "1.0.0" + mocked_yeti.sysdata = {} + mocked_yeti.sysdata["model"] = "test_model" + mocked_yeti.sysdata["macAddress"] = MAC return mocked_yeti @@ -40,3 +57,41 @@ def _patch_config_flow_yeti(mocked_yeti): "homeassistant.components.goalzero.config_flow.Yeti", return_value=mocked_yeti, ) + + +async def async_init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Goal Zero integration in Home Assistant.""" + entry = create_entry(hass) + base_url = f"http://{HOST}/" + aioclient_mock.get( + f"{base_url}state", + text=load_fixture("goalzero/state_data.json"), + ) + aioclient_mock.get( + f"{base_url}sysinfo", + text=load_fixture("goalzero/info_data.json"), + ) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def async_setup_platform( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + platform: str, +): + """Set up the platform.""" + entry = await async_init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.goalzero.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + + return entry diff --git a/tests/components/goalzero/fixtures/info_data.json b/tests/components/goalzero/fixtures/info_data.json new file mode 100644 index 00000000000..6be95e6c482 --- /dev/null +++ b/tests/components/goalzero/fixtures/info_data.json @@ -0,0 +1,7 @@ +{ + "name":"yeti123456789012", + "model":"Yeti 1400", + "firmwareVersion":"1.5.7", + "macAddress":"123456789012", + "platform":"esp32" +} \ No newline at end of file diff --git a/tests/components/goalzero/fixtures/state_data.json b/tests/components/goalzero/fixtures/state_data.json new file mode 100644 index 00000000000..455524584f7 --- /dev/null +++ b/tests/components/goalzero/fixtures/state_data.json @@ -0,0 +1,38 @@ +{ + "thingName":"yeti123456789012", + "v12PortStatus":0, + "usbPortStatus":0, + "acPortStatus":1, + "backlight":1, + "app_online":0, + "wattsIn":0.0, + "ampsIn":0.0, + "wattsOut":50.5, + "ampsOut":2.1, + "whOut":5.23, + "whStored":1330, + "volts":12.0, + "socPercent":95, + "isCharging":0, + "inputDetected":0, + "timeToEmptyFull":-1, + "temperature":25, + "wifiStrength":-62, + "ssid":"wifi", + "ipAddr":"1.2.3.4", + "timestamp":1720984, + "firmwareVersion":"1.5.7", + "version":3, + "ota":{ + "delay":0, + "status":"000-000-100_001-000-100_002-000-100_003-000-100" + }, + "notify":{ + "enabled":1048575, + "trigger":0 + }, + "foreignAcsry":{ + "model":"Yeti MPPT", + "firmwareVersion":"1.1.2" + } +} \ No newline at end of file diff --git a/tests/components/goalzero/test_binary_sensor.py b/tests/components/goalzero/test_binary_sensor.py new file mode 100644 index 00000000000..1c130013283 --- /dev/null +++ b/tests/components/goalzero/test_binary_sensor.py @@ -0,0 +1,36 @@ +"""Binary sensor tests for the Goalzero integration.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CONNECTIVITY, + DOMAIN, +) +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEVICE_CLASS_POWER, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from . import async_setup_platform + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test we get sensor data.""" + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_backlight") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_app_online") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CONNECTIVITY + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_charging") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY_CHARGING + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_input_detected") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 838a0f8a124..320731486ce 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -3,44 +3,26 @@ from unittest.mock import patch from goalzero import exceptions -from homeassistant.components.goalzero.const import DOMAIN +from homeassistant import data_entry_flow +from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.core import HomeAssistant from . import ( - CONF_CONFIG_FLOW, CONF_DATA, CONF_DHCP_FLOW, - CONF_HOST, - CONF_NAME, - NAME, + MAC, _create_mocked_yeti, _patch_config_flow_yeti, + create_entry, ) -from tests.common import MockConfigEntry - - -def _flow_next(hass, flow_id): - return next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == flow_id - ) - def _patch_setup(): - return patch( - "homeassistant.components.goalzero.async_setup_entry", - return_value=True, - ) + return patch("homeassistant.components.goalzero.async_setup_entry") -async def test_flow_user(hass): +async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" mocked_yeti = await _create_mocked_yeti() with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): @@ -50,74 +32,62 @@ async def test_flow_user(hass): ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW, + user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA + assert result["result"].unique_id == MAC -async def test_flow_user_already_configured(hass): +async def test_flow_user_already_configured(hass: HomeAssistant): """Test user initialized flow with duplicate server.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.2.3.4", CONF_NAME: "Yeti"}, - ) - - entry.add_to_hass(hass) - - service_info = { - "host": "1.2.3.4", - "name": "Yeti", - } + create_entry(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=service_info + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_flow_user_cannot_connect(hass): +async def test_flow_user_cannot_connect(hass: HomeAssistant): """Test user initialized flow with unreachable server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"]["base"] == "cannot_connect" -async def test_flow_user_invalid_host(hass): +async def test_flow_user_invalid_host(hass: HomeAssistant): """Test user initialized flow with invalid server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: yetimock.side_effect = exceptions.InvalidHost result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_host"} + assert result["errors"]["base"] == "invalid_host" -async def test_flow_user_unknown_error(hass): +async def test_flow_user_unknown_error(hass: HomeAssistant): """Test user initialized flow with unreachable server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: yetimock.side_effect = Exception result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "unknown"} + assert result["errors"]["base"] == "unknown" -async def test_dhcp_discovery(hass): +async def test_dhcp_discovery(hass: HomeAssistant): """Test we can process the discovery from dhcp.""" mocked_yeti = await _create_mocked_yeti() @@ -127,31 +97,30 @@ async def test_dhcp_discovery(hass): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_NAME: "Yeti", - } + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MANUFACTURER + assert result["data"] == CONF_DATA + assert result["result"].unique_id == MAC result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_dhcp_discovery_failed(hass): +async def test_dhcp_discovery_failed(hass: HomeAssistant): """Test failed setup from dhcp.""" - mocked_yeti = await _create_mocked_yeti(True) + mocked_yeti = await _create_mocked_yeti() with _patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( @@ -159,7 +128,7 @@ async def test_dhcp_discovery_failed(hass): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" with _patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -169,7 +138,7 @@ async def test_dhcp_discovery_failed(hass): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "invalid_host" with _patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -179,5 +148,5 @@ async def test_dhcp_discovery_failed(hass): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unknown" diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py new file mode 100644 index 00000000000..5a649debee2 --- /dev/null +++ b/tests/components/goalzero/test_init.py @@ -0,0 +1,91 @@ +"""Test Goal Zero integration.""" +from datetime import timedelta +from unittest.mock import patch + +from goalzero import exceptions + +from homeassistant.components.goalzero.const import ( + DATA_KEY_COORDINATOR, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from . import ( + CONF_DATA, + _create_mocked_yeti, + _patch_init_yeti, + async_init_integration, + create_entry, +) + +from tests.common import async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup_config_and_unload(hass: HomeAssistant): + """Test Goal Zero setup and unload.""" + entry = create_entry(hass) + with _patch_init_yeti(await _create_mocked_yeti()): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.data == CONF_DATA + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = create_entry(hass) + with patch( + "homeassistant.components.goalzero.Yeti.init_connect", + side_effect=exceptions.ConnectError, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_update_failed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test data update failure.""" + entry = await async_init_integration(hass, aioclient_mock) + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_KEY_COORDINATOR + ] + with patch( + "homeassistant.components.goalzero.Yeti.get_state", + side_effect=exceptions.ConnectError, + ) as updater: + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + updater.assert_called_once() + assert not coordinator.last_update_success + + +async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test device info.""" + entry = await async_init_integration(hass, aioclient_mock) + device_registry = await dr.async_get_registry(hass) + + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.connections == {("mac", "12:34:56:78:90:12")} + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == MANUFACTURER + assert device.model == "Yeti 1400" + assert device.name == DEFAULT_NAME + assert device.sw_version == "1.5.7" diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py new file mode 100644 index 00000000000..592c43b5d43 --- /dev/null +++ b/tests/components/goalzero/test_sensor.py @@ -0,0 +1,112 @@ +"""Sensor tests for the Goalzero integration.""" +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.components.goalzero.sensor import SENSOR_TYPES +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + 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 . import async_setup_platform + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test we get sensor data.""" + for description in SENSOR_TYPES: + description.entity_registry_enabled_default = True + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") + assert state.state == "0.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") + assert state.state == "0.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") + assert state.state == "50.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") + assert state.state == "2.1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") + assert state.state == "5.23" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") + assert state.state == "1330" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") + assert state.state == "12.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") + assert state.state == "95" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") + assert state.state == "-1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") + assert state.state == "25" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") + assert state.state == "-62" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") + assert state.state == "1720984" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid") + assert state.state == "wifi" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_ip_address") + assert state.state == "1.2.3.4" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None diff --git a/tests/components/goalzero/test_switch.py b/tests/components/goalzero/test_switch.py new file mode 100644 index 00000000000..d2fd0216f45 --- /dev/null +++ b/tests/components/goalzero/test_switch.py @@ -0,0 +1,49 @@ +"""Switch tests for the Goalzero integration.""" +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.components.switch import DOMAIN as DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from . import async_setup_platform + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switches_states( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test we get sensor data.""" + aioclient_mock.post( + "http://1.2.3.4/state", + text=load_fixture("goalzero/state_data.json"), + ) + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + entity_id = f"switch.{DEFAULT_NAME}_12v_port_status" + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + entity_id = f"switch.{DEFAULT_NAME}_usb_port_status" + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + entity_id = f"switch.{DEFAULT_NAME}_ac_port_status" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) From 4e59e6c9a70537afd783ec3ef7b6882eae1c3194 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 9 Nov 2021 05:46:33 +0000 Subject: [PATCH 0329/1452] System Bridge - Handle OSError for switched off devices (#59312) --- homeassistant/components/system_bridge/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index 5560d79a769..f2e83ceb186 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -16,4 +16,5 @@ BRIDGE_CONNECTION_ERRORS = ( ClientConnectionError, ClientConnectorError, ClientResponseError, + OSError, ) From 012e005e0dd5a735f9bc540cb5c492b31109affc Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Tue, 9 Nov 2021 08:00:51 +0200 Subject: [PATCH 0330/1452] Store SB data Class. (#59266) --- homeassistant/components/switchbot/coordinator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 4976af18809..e901cc539ea 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -30,6 +30,7 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): ) -> None: """Initialize global switchbot data updater.""" self.switchbot_api = api + self.switchbot_data = self.switchbot_api.GetSwitchbotDevices() self.retry_count = retry_count self.scan_timeout = scan_timeout self.update_interval = timedelta(seconds=update_interval) @@ -43,7 +44,7 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): def _update_data(self) -> dict | None: """Fetch device states from switchbot api.""" - return self.switchbot_api.GetSwitchbotDevices().discover( + return self.switchbot_data.discover( retry=self.retry_count, scan_timeout=self.scan_timeout ) From 121a0915bcb236bea5bb8c4b071c9c5f09b8182f Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Tue, 9 Nov 2021 08:01:05 +0200 Subject: [PATCH 0331/1452] Switchbot bump api version (#59398) * Bump Switchbot api for reliability improvements. * Bump api version, update mock test import to reflect api data changes. --- .../components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/conftest.py | 32 ++++++++----------- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 6dd23e2eec5..69f9eddc6cd 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.12.0"], + "requirements": ["PySwitchbot==0.13.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index cd076d64a88..1dcd70030a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.12.0 +# PySwitchbot==0.13.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57d5a9a8944..928a9d6d568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -# PySwitchbot==0.12.0 +# PySwitchbot==0.13.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 52e5fd4fa15..2fdea69de16 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -14,45 +14,42 @@ class MocGetSwitchbotDevices: self._all_services_data = { "e78943999999": { "mac_address": "e7:89:43:99:99:99", - "Flags": "06", - "Manufacturer": "5900e78943d9fe7c", - "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "isEncrypted": False, + "model": "H", "data": { "switchMode": "true", "isOn": "true", "battery": 91, "rssi": -71, }, - "model": "H", "modelName": "WoHand", }, "e78943909090": { "mac_address": "e7:89:43:90:90:90", - "Flags": "06", - "Manufacturer": "5900e78943d9fe7c", - "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "isEncrypted": False, + "model": "c", "data": { "calibration": True, "battery": 74, + "inMotion": False, "position": 100, "lightLevel": 2, + "deviceChain": 1, "rssi": -73, }, - "model": "c", "modelName": "WoCurtain", }, "ffffff19ffff": { "mac_address": "ff:ff:ff:19:ff:ff", - "Flags": "06", - "Manufacturer": "5900ffffff19ffff", - "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "isEncrypted": False, + "model": "m", + "rawAdvData": "000d6d00", }, } self._curtain_all_services_data = { "mac_address": "e7:89:43:90:90:90", - "Flags": "06", - "Manufacturer": "5900e78943d9fe7c", - "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "isEncrypted": False, + "model": "c", "data": { "calibration": True, "battery": 74, @@ -60,21 +57,18 @@ class MocGetSwitchbotDevices: "lightLevel": 2, "rssi": -73, }, - "model": "c", "modelName": "WoCurtain", } self._unsupported_device = { "mac_address": "test", - "Flags": "06", - "Manufacturer": "5900e78943d9fe7c", - "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "isEncrypted": False, + "model": "HoN", "data": { "switchMode": "true", "isOn": "true", "battery": 91, "rssi": -71, }, - "model": "HoN", "modelName": "WoOther", } From f46ba2b38b9c2445e54755d1a1223093c01c28d7 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 9 Nov 2021 06:28:39 +0000 Subject: [PATCH 0332/1452] Add state class to Coinbase sensors (#59109) --- homeassistant/components/coinbase/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index b4ef4bb8e35..88e61b59fff 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,7 +1,7 @@ """Support for Coinbase sensors.""" import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import DeviceInfo @@ -105,6 +105,7 @@ class AccountSensor(SensorEntity): API_ACCOUNT_CURRENCY ] break + self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", entry_type="service", @@ -177,6 +178,7 @@ class ExchangeRateSensor(SensorEntity): 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = exchange_base + self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", entry_type="service", From a102c425a997ec5a617ba6eb2869af68693272d2 Mon Sep 17 00:00:00 2001 From: rianadon Date: Mon, 8 Nov 2021 23:12:28 -0800 Subject: [PATCH 0333/1452] Add speed conversion function & add speed to units system (#53846) * Add speed conversion function * Add test for speed utility functions * Update unit system tests * Fix incorrect unit conversions in tests * Fix some test errors * Calculate speed units from smaller set of constants * Fix typo in speed test * Use pytest.approx for checking floating point values * Change other instance of speeds needing to be pytest.approx * Revert changes to unit system * Fix oopsie in defining in/day and in/hr * Parametrize test * Add comments describing calculations & remove duplicate test --- homeassistant/components/climacell/weather.py | 18 ++++- homeassistant/components/met/weather.py | 9 ++- .../components/met_eireann/weather.py | 10 +-- homeassistant/components/nws/sensor.py | 7 +- homeassistant/components/nws/weather.py | 11 ++- homeassistant/const.py | 2 +- homeassistant/util/speed.py | 56 +++++++++++++++ tests/components/nws/const.py | 15 ++-- tests/util/test_speed.py | 70 +++++++++++++++++++ 9 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 homeassistant/util/speed.py create mode 100644 tests/util/test_speed.py diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index cb0783c6bee..6a1cab5e1bf 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -37,6 +37,8 @@ from homeassistant.const import ( LENGTH_MILES, PRESSURE_HPA, PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant @@ -45,6 +47,7 @@ from homeassistant.helpers.sun import is_up from homeassistant.util import dt as dt_util from homeassistant.util.distance import convert as distance_convert from homeassistant.util.pressure import convert as pressure_convert +from homeassistant.util.speed import convert as speed_convert from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity from .const import ( @@ -166,7 +169,10 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): ) if wind_speed: wind_speed = round( - distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + speed_convert( + wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + ), + 4, ) data = { @@ -188,7 +194,10 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): wind_gust = self.wind_gust if wind_gust and self.hass.config.units.is_metric: wind_gust = round( - distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4 + speed_convert( + self.wind_gust, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + ), + 4, ) cloud_cover = self.cloud_cover return { @@ -236,7 +245,10 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Return the wind speed.""" if self.hass.config.units.is_metric and self._wind_speed: return round( - distance_convert(self._wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + speed_convert( + self._wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + ), + 4, ) return self._wind_speed diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index d938ede5cb2..04ce37a388e 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -27,11 +27,11 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_NAME, LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -46,6 +46,7 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed from .const import ( ATTR_FORECAST_PRECIPITATION, @@ -226,7 +227,9 @@ class MetWeather(CoordinatorEntity, WeatherEntity): if self._is_metric or speed_km_h is None: return speed_km_h - speed_mi_h = convert_distance(speed_km_h, LENGTH_KILOMETERS, LENGTH_MILES) + speed_mi_h = convert_speed( + speed_km_h, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR + ) return int(round(speed_mi_h)) @property diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 3a8eed0d7be..ddd39d82c3b 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -13,11 +13,11 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_NAME, LENGTH_INCHES, - LENGTH_METERS, - LENGTH_MILES, LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.helpers.entity import DeviceInfo @@ -25,6 +25,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP @@ -130,8 +131,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): if self._is_metric or speed_m_s is None: return speed_m_s - speed_mi_s = convert_distance(speed_m_s, LENGTH_METERS, LENGTH_MILES) - speed_mi_h = speed_mi_s / 3600.0 + speed_mi_h = convert_speed( + speed_m_s, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR + ) return int(round(speed_mi_h)) @property diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 49387896962..35bbcef838d 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -4,12 +4,12 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, PERCENTAGE, PRESSURE_INHG, PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) @@ -19,6 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed from . import base_unique_id, device_info from .const import ( @@ -86,7 +87,9 @@ class NWSSensor(CoordinatorEntity, SensorEntity): # Set alias to unit property -> prevent unnecessary hasattr calls 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)) + return round( + convert_speed(value, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR) + ) if unit_of_measurement == LENGTH_MILES: return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) if unit_of_measurement == PRESSURE_INHG: diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index dc76ebc25e5..3a795095c5d 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -19,6 +19,8 @@ from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -28,6 +30,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature from . import base_unique_id, device_info @@ -196,7 +199,9 @@ class NWSWeather(WeatherEntity): if self.is_metric: wind = wind_km_hr else: - wind = convert_distance(wind_km_hr, LENGTH_KILOMETERS, LENGTH_MILES) + wind = convert_speed( + wind_km_hr, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR + ) return round(wind) @property @@ -271,7 +276,9 @@ class NWSWeather(WeatherEntity): if wind_speed is not None: if self.is_metric: data[ATTR_FORECAST_WIND_SPEED] = round( - convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + convert_speed( + wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + ) ) else: data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed) diff --git a/homeassistant/const.py b/homeassistant/const.py index 229fe704348..3ce1a15cf0d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -695,7 +695,7 @@ MASS: Final = "mass" PRESSURE: Final = "pressure" VOLUME: Final = "volume" TEMPERATURE: Final = "temperature" -SPEED_MS: Final = "speed_ms" +SPEED: Final = "speed" ILLUMINANCE: Final = "illuminance" WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py new file mode 100644 index 00000000000..f3fc652e90f --- /dev/null +++ b/homeassistant/util/speed.py @@ -0,0 +1,56 @@ +"""Distance util functions.""" +from __future__ import annotations + +from numbers import Number + +from homeassistant.const import ( + SPEED, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, + UNIT_NOT_RECOGNIZED_TEMPLATE, +) + +VALID_UNITS: tuple[str, ...] = ( + SPEED_METERS_PER_SECOND, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, +) + +HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds +KM_TO_M = 1000 # 1 km = 1000 m +KM_TO_MILE = 0.62137119 # 1 km = 0.62137119 mi +M_TO_IN = 39.3700787 # 1 m = 39.3700787 in + +# Units in terms of m/s +UNIT_CONVERSION: dict[str, float] = { + SPEED_METERS_PER_SECOND: 1, + SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, + SPEED_MILES_PER_HOUR: HRS_TO_SECS * KM_TO_MILE / KM_TO_M, + SPEED_MILLIMETERS_PER_DAY: (24 * HRS_TO_SECS) * 1000, + SPEED_INCHES_PER_DAY: (24 * HRS_TO_SECS) * M_TO_IN, + SPEED_INCHES_PER_HOUR: HRS_TO_SECS * M_TO_IN, +} + + +def convert(value: float, unit_1: str, unit_2: str) -> float: + """Convert one unit of measurement to another.""" + if unit_1 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, SPEED)) + if unit_2 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, SPEED)) + + if not isinstance(value, Number): + raise TypeError(f"{value} is not of numeric type") + + if unit_1 == unit_2: + return value + + meters_per_second = value / UNIT_CONVERSION[unit_1] + return meters_per_second * UNIT_CONVERSION[unit_2] diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 4f4b140dbf9..c7387d4bf8c 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -25,11 +25,14 @@ from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature NWS_CONFIG = { @@ -80,8 +83,12 @@ SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { "windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), "heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))), "relativeHumidity": "10", - "windSpeed": str(round(convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES))), - "windGust": str(round(convert_distance(20, LENGTH_KILOMETERS, LENGTH_MILES))), + "windSpeed": str( + round(convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + ), + "windGust": str( + round(convert_speed(20, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + ), "windDirection": "180", "barometricPressure": str( round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) @@ -98,7 +105,7 @@ WEATHER_EXPECTED_OBSERVATION_IMPERIAL = { ), ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: round( - convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES) + convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR) ), ATTR_WEATHER_PRESSURE: round( convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 @@ -152,7 +159,7 @@ EXPECTED_FORECAST_METRIC = { ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", ATTR_FORECAST_TEMP: round(convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS)), ATTR_FORECAST_WIND_SPEED: round( - convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) + convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR) ), ATTR_FORECAST_WIND_BEARING: 180, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, diff --git a/tests/util/test_speed.py b/tests/util/test_speed.py new file mode 100644 index 00000000000..7f52c67ed50 --- /dev/null +++ b/tests/util/test_speed.py @@ -0,0 +1,70 @@ +"""Test Home Assistant speed utility functions.""" +import pytest + +from homeassistant.const import ( + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, +) +import homeassistant.util.speed as speed_util + +INVALID_SYMBOL = "bob" +VALID_SYMBOL = SPEED_KILOMETERS_PER_HOUR + + +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert speed_util.convert(2, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_DAY) == 2 + assert speed_util.convert(3, SPEED_INCHES_PER_HOUR, SPEED_INCHES_PER_HOUR) == 3 + assert ( + speed_util.convert(4, SPEED_KILOMETERS_PER_HOUR, SPEED_KILOMETERS_PER_HOUR) == 4 + ) + assert speed_util.convert(5, SPEED_METERS_PER_SECOND, SPEED_METERS_PER_SECOND) == 5 + assert speed_util.convert(6, SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR) == 6 + assert ( + speed_util.convert(7, SPEED_MILLIMETERS_PER_DAY, SPEED_MILLIMETERS_PER_DAY) == 7 + ) + + +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + speed_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with pytest.raises(ValueError): + speed_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + speed_util.convert("a", SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR) + + +@pytest.mark.parametrize( + "from_value, from_unit, expected, to_unit", + [ + # 5 km/h / 1.609 km/mi = 3.10686 mi/h + (5, SPEED_KILOMETERS_PER_HOUR, 3.10686, SPEED_MILES_PER_HOUR), + # 5 mi/h * 1.609 km/mi = 8.04672 km/h + (5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR), + # 5 in/day * 25.4 mm/in = 127 mm/day + (5, SPEED_INCHES_PER_DAY, 127, SPEED_MILLIMETERS_PER_DAY), + # 5 mm/day / 25.4 mm/in = 0.19685 in/day + (5, SPEED_MILLIMETERS_PER_DAY, 0.19685, SPEED_INCHES_PER_DAY), + # 5 in/hr * 24 hr/day = 3048 mm/day + (5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY), + # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661 + (5, SPEED_METERS_PER_SECOND, 708661, SPEED_INCHES_PER_HOUR), + # 5000 in/hr / 39.3701 in/m / 3600 s/hr = 0.03528 m/s + (5000, SPEED_INCHES_PER_HOUR, 0.03528, SPEED_METERS_PER_SECOND), + ], +) +def test_convert_different_units(from_value, from_unit, expected, to_unit): + """Test conversion between units.""" + assert speed_util.convert(from_value, from_unit, to_unit) == pytest.approx( + expected, rel=1e-4 + ) From d226df25110c5f4ca63c7da07890bc1910fa52b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Nov 2021 10:38:51 +0100 Subject: [PATCH 0334/1452] Dyson removal (#59401) Co-authored-by: epenet --- homeassistant/components/dyson/__init__.py | 153 ------ homeassistant/components/dyson/air_quality.py | 93 ---- homeassistant/components/dyson/climate.py | 316 ------------ homeassistant/components/dyson/fan.py | 469 ------------------ homeassistant/components/dyson/manifest.json | 9 - homeassistant/components/dyson/sensor.py | 248 --------- homeassistant/components/dyson/services.yaml | 108 ---- homeassistant/components/dyson/vacuum.py | 171 ------- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/dyson/__init__.py | 1 - tests/components/dyson/common.py | 103 ---- tests/components/dyson/conftest.py | 38 -- tests/components/dyson/test_air_quality.py | 67 --- tests/components/dyson/test_climate.py | 348 ------------- tests/components/dyson/test_fan.py | 441 ---------------- tests/components/dyson/test_init.py | 100 ---- tests/components/dyson/test_sensor.py | 183 ------- tests/components/dyson/test_vacuum.py | 115 ----- 19 files changed, 2969 deletions(-) delete mode 100644 homeassistant/components/dyson/__init__.py delete mode 100644 homeassistant/components/dyson/air_quality.py delete mode 100644 homeassistant/components/dyson/climate.py delete mode 100644 homeassistant/components/dyson/fan.py delete mode 100644 homeassistant/components/dyson/manifest.json delete mode 100644 homeassistant/components/dyson/sensor.py delete mode 100644 homeassistant/components/dyson/services.yaml delete mode 100644 homeassistant/components/dyson/vacuum.py delete mode 100644 tests/components/dyson/__init__.py delete mode 100644 tests/components/dyson/common.py delete mode 100644 tests/components/dyson/conftest.py delete mode 100644 tests/components/dyson/test_air_quality.py delete mode 100644 tests/components/dyson/test_climate.py delete mode 100644 tests/components/dyson/test_fan.py delete mode 100644 tests/components/dyson/test_init.py delete mode 100644 tests/components/dyson/test_sensor.py delete mode 100644 tests/components/dyson/test_vacuum.py diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py deleted file mode 100644 index d8023b42973..00000000000 --- a/homeassistant/components/dyson/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Support for Dyson Pure Cool Link devices.""" -import logging - -from libpurecool.dyson import DysonAccount -import voluptuous as vol - -from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -CONF_LANGUAGE = "language" -CONF_RETRY = "retry" - -DEFAULT_TIMEOUT = 5 -DEFAULT_RETRY = 10 -DYSON_DEVICES = "dyson_devices" -PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"] - -DOMAIN = "dyson" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_LANGUAGE): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, - vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [dict]), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass, config): - """Set up the Dyson parent component.""" - _LOGGER.info("Creating new Dyson component") - - if DYSON_DEVICES not in hass.data: - hass.data[DYSON_DEVICES] = [] - - dyson_account = DysonAccount( - config[DOMAIN].get(CONF_USERNAME), - config[DOMAIN].get(CONF_PASSWORD), - config[DOMAIN].get(CONF_LANGUAGE), - ) - - logged = dyson_account.login() - - timeout = config[DOMAIN].get(CONF_TIMEOUT) - retry = config[DOMAIN].get(CONF_RETRY) - - if not logged: - _LOGGER.error("Not connected to Dyson account. Unable to add devices") - return False - - _LOGGER.info("Connected to Dyson account") - dyson_devices = dyson_account.devices() - if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): - configured_devices = config[DOMAIN].get(CONF_DEVICES) - for device in configured_devices: - dyson_device = next( - (d for d in dyson_devices if d.serial == device["device_id"]), None - ) - if dyson_device: - try: - connected = dyson_device.connect(device["device_ip"]) - if connected: - _LOGGER.info("Connected to device %s", dyson_device) - hass.data[DYSON_DEVICES].append(dyson_device) - else: - _LOGGER.warning("Unable to connect to device %s", dyson_device) - except OSError as ose: - _LOGGER.error( - "Unable to connect to device %s: %s", - str(dyson_device.network_device), - str(ose), - ) - else: - _LOGGER.warning( - "Unable to find device %s in Dyson account", device["device_id"] - ) - else: - # Not yet reliable - for device in dyson_devices: - _LOGGER.info( - "Trying to connect to device %s with timeout=%i and retry=%i", - device, - timeout, - retry, - ) - connected = device.auto_connect(timeout, retry) - if connected: - _LOGGER.info("Connected to device %s", device) - hass.data[DYSON_DEVICES].append(device) - else: - _LOGGER.warning("Unable to connect to device %s", device) - - # Start fan/sensors components - if hass.data[DYSON_DEVICES]: - _LOGGER.debug("Starting sensor/fan components") - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -class DysonEntity(Entity): - """Representation of a Dyson entity.""" - - def __init__(self, device, state_type): - """Initialize the entity.""" - self._device = device - self._state_type = state_type - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message_filter) - - def on_message_filter(self, message): - """Filter new messages received.""" - if self._state_type is None or isinstance(message, self._state_type): - _LOGGER.debug( - "Message received for device %s : %s", - self.name, - message, - ) - self.on_message(message) - - def on_message(self, message): - """Handle new messages received.""" - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the Dyson sensor.""" - return self._device.name - - @property - def unique_id(self): - """Return the sensor's unique id.""" - return self._device.serial diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py deleted file mode 100644 index 48b66fe7683..00000000000 --- a/homeassistant/components/dyson/air_quality.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Support for Dyson Pure Cool Air Quality Sensors.""" -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - -from homeassistant.components.air_quality import AirQualityEntity - -from . import DYSON_DEVICES, DysonEntity - -ATTRIBUTION = "Dyson purifier air quality sensor" - -DYSON_AIQ_DEVICES = "dyson_aiq_devices" - -ATTR_VOC = "volatile_organic_compounds" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Dyson Sensors.""" - - if discovery_info is None: - return - - hass.data.setdefault(DYSON_AIQ_DEVICES, []) - - # Get Dyson Devices from parent component - device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]] - new_entities = [] - for device in hass.data[DYSON_DEVICES]: - if isinstance(device, DysonPureCool) and device.serial not in device_ids: - new_entities.append(DysonAirSensor(device)) - - if not new_entities: - return - - hass.data[DYSON_AIQ_DEVICES].extend(new_entities) - add_entities(hass.data[DYSON_AIQ_DEVICES]) - - -class DysonAirSensor(DysonEntity, AirQualityEntity): - """Representation of a generic Dyson air quality sensor.""" - - def __init__(self, device): - """Create a new generic air quality Dyson sensor.""" - super().__init__(device, DysonEnvironmentalSensorV2State) - self._old_value = None - - def on_message(self, message): - """Handle new messages which are received from the fan.""" - if ( - self._old_value is None - or self._old_value != self._device.environmental_state - ): - self._old_value = self._device.environmental_state - self.schedule_update_ha_state() - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def air_quality_index(self): - """Return the Air Quality Index (AQI).""" - return max( - self.particulate_matter_2_5, - self.particulate_matter_10, - self.nitrogen_dioxide, - self.volatile_organic_compounds, - ) - - @property - def particulate_matter_2_5(self): - """Return the particulate matter 2.5 level.""" - return int(self._device.environmental_state.particulate_matter_25) - - @property - def particulate_matter_10(self): - """Return the particulate matter 10 level.""" - return int(self._device.environmental_state.particulate_matter_10) - - @property - def nitrogen_dioxide(self): - """Return the NO2 (nitrogen dioxide) level.""" - return int(self._device.environmental_state.nitrogen_dioxide) - - @property - def volatile_organic_compounds(self): - """Return the VOC (Volatile Organic Compounds) level.""" - return int(self._device.environmental_state.volatile_organic_compounds) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return {ATTR_VOC: self.volatile_organic_compounds} diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py deleted file mode 100644 index 19993a6498c..00000000000 --- a/homeassistant/components/dyson/climate.py +++ /dev/null @@ -1,316 +0,0 @@ -"""Support for Dyson Pure Hot+Cool link fan.""" -import logging - -from libpurecool.const import ( - AutoMode, - FanPower, - FanSpeed, - FanState, - FocusMode, - HeatMode, - HeatState, - HeatTarget, -) -from libpurecool.dyson_pure_hotcool import DysonPureHotCool -from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink -from libpurecool.dyson_pure_state import DysonPureHotCoolState -from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State - -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - FAN_AUTO, - FAN_DIFFUSE, - FAN_FOCUS, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, - FAN_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, - SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE, -) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS - -from . import DYSON_DEVICES, DysonEntity - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_FAN = [FAN_FOCUS, FAN_DIFFUSE] -SUPPORT_FAN_PCOOL = [FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] -SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT] -SUPPORT_HVAC_PCOOL = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - -DYSON_KNOWN_CLIMATE_DEVICES = "dyson_known_climate_devices" - -SPEED_MAP = { - FanSpeed.FAN_SPEED_1.value: FAN_LOW, - FanSpeed.FAN_SPEED_2.value: FAN_LOW, - FanSpeed.FAN_SPEED_3.value: FAN_LOW, - FanSpeed.FAN_SPEED_4.value: FAN_LOW, - FanSpeed.FAN_SPEED_AUTO.value: FAN_AUTO, - FanSpeed.FAN_SPEED_5.value: FAN_MEDIUM, - FanSpeed.FAN_SPEED_6.value: FAN_MEDIUM, - FanSpeed.FAN_SPEED_7.value: FAN_MEDIUM, - FanSpeed.FAN_SPEED_8.value: FAN_HIGH, - FanSpeed.FAN_SPEED_9.value: FAN_HIGH, - FanSpeed.FAN_SPEED_10.value: FAN_HIGH, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Dyson fan components.""" - if discovery_info is None: - return - - known_devices = hass.data.setdefault(DYSON_KNOWN_CLIMATE_DEVICES, set()) - - # Get Dyson Devices from parent component - new_entities = [] - - for device in hass.data[DYSON_DEVICES]: - if device.serial not in known_devices: - if isinstance(device, DysonPureHotCool): - dyson_entity = DysonPureHotCoolEntity(device) - new_entities.append(dyson_entity) - known_devices.add(device.serial) - elif isinstance(device, DysonPureHotCoolLink): - dyson_entity = DysonPureHotCoolLinkEntity(device) - new_entities.append(dyson_entity) - known_devices.add(device.serial) - - add_entities(new_entities) - - -class DysonClimateEntity(DysonEntity, ClimateEntity): - """Representation of a Dyson climate fan.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if ( - self._device.environmental_state - and self._device.environmental_state.temperature - ): - temperature_kelvin = self._device.environmental_state.temperature - return float(f"{temperature_kelvin - 273:.1f}") - return None - - @property - def target_temperature(self): - """Return the target temperature.""" - heat_target = int(self._device.state.heat_target) / 10 - return int(heat_target - 273) - - @property - def current_humidity(self): - """Return the current humidity.""" - # Humidity equaling to 0 means invalid value so we don't check for None here - # https://github.com/home-assistant/core/pull/45172#discussion_r559069756 - if ( - self._device.environmental_state - and self._device.environmental_state.humidity - ): - return self._device.environmental_state.humidity - return None - - @property - def min_temp(self): - """Return the minimum temperature.""" - return 1 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 37 - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is None: - _LOGGER.error("Missing target temperature %s", kwargs) - return - target_temp = int(target_temp) - _LOGGER.debug("Set %s temperature %s", self.name, target_temp) - # Limit the target temperature into acceptable range. - target_temp = min(self.max_temp, target_temp) - target_temp = max(self.min_temp, target_temp) - self.set_heat_target(HeatTarget.celsius(target_temp)) - - def set_heat_target(self, heat_target): - """Set heating target temperature.""" - - -class DysonPureHotCoolLinkEntity(DysonClimateEntity): - """Representation of a Dyson climate fan.""" - - def __init__(self, device): - """Initialize the fan.""" - super().__init__(device, DysonPureHotCoolState) - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._device.state.heat_mode == HeatMode.HEAT_ON.value: - return HVAC_MODE_HEAT - return HVAC_MODE_COOL - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - return SUPPORT_HVAC - - @property - def hvac_action(self): - """Return the current running hvac operation if supported. - - Need to be one of CURRENT_HVAC_*. - """ - if self._device.state.heat_mode == HeatMode.HEAT_ON.value: - if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - return CURRENT_HVAC_COOL - - @property - def fan_mode(self): - """Return the fan setting.""" - if self._device.state.focus_mode == FocusMode.FOCUS_ON.value: - return FAN_FOCUS - return FAN_DIFFUSE - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return SUPPORT_FAN - - def set_heat_target(self, heat_target): - """Set heating target temperature.""" - self._device.set_configuration( - heat_target=heat_target, heat_mode=HeatMode.HEAT_ON - ) - - def set_fan_mode(self, fan_mode): - """Set new fan mode.""" - _LOGGER.debug("Set %s focus mode %s", self.name, fan_mode) - if fan_mode == FAN_FOCUS: - self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON) - elif fan_mode == FAN_DIFFUSE: - self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF) - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - _LOGGER.debug("Set %s heat mode %s", self.name, hvac_mode) - if hvac_mode == HVAC_MODE_HEAT: - self._device.set_configuration(heat_mode=HeatMode.HEAT_ON) - elif hvac_mode == HVAC_MODE_COOL: - self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF) - - -class DysonPureHotCoolEntity(DysonClimateEntity): - """Representation of a Dyson climate hot+cool fan.""" - - def __init__(self, device): - """Initialize the fan.""" - super().__init__(device, DysonPureHotCoolV2State) - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._device.state.fan_power == FanPower.POWER_OFF.value: - return HVAC_MODE_OFF - if self._device.state.heat_mode == HeatMode.HEAT_ON.value: - return HVAC_MODE_HEAT - return HVAC_MODE_COOL - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - return SUPPORT_HVAC_PCOOL - - @property - def hvac_action(self): - """Return the current running hvac operation if supported. - - Need to be one of CURRENT_HVAC_*. - """ - if self._device.state.fan_power == FanPower.POWER_OFF.value: - return CURRENT_HVAC_OFF - if self._device.state.heat_mode == HeatMode.HEAT_ON.value: - if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - return CURRENT_HVAC_COOL - - @property - def fan_mode(self): - """Return the fan setting.""" - if ( - self._device.state.auto_mode != AutoMode.AUTO_ON.value - and self._device.state.fan_state == FanState.FAN_OFF.value - ): - return FAN_OFF - - return SPEED_MAP[self._device.state.speed] - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return SUPPORT_FAN_PCOOL - - def set_heat_target(self, heat_target): - """Set heating target temperature.""" - self._device.set_heat_target(heat_target) - - def set_fan_mode(self, fan_mode): - """Set new fan mode.""" - _LOGGER.debug("Set %s focus mode %s", self.name, fan_mode) - if fan_mode == FAN_OFF: - self._device.turn_off() - elif fan_mode == FAN_LOW: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) - elif fan_mode == FAN_MEDIUM: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_7) - elif fan_mode == FAN_HIGH: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) - elif fan_mode == FAN_AUTO: - self._device.enable_auto_mode() - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - _LOGGER.debug("Set %s heat mode %s", self.name, hvac_mode) - if hvac_mode == HVAC_MODE_OFF: - self._device.turn_off() - elif self._device.state.fan_power == FanPower.POWER_OFF.value: - self._device.turn_on() - if hvac_mode == HVAC_MODE_HEAT: - self._device.enable_heat_mode() - elif hvac_mode == HVAC_MODE_COOL: - self._device.disable_heat_mode() diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py deleted file mode 100644 index 38b4b511df4..00000000000 --- a/homeassistant/components/dyson/fan.py +++ /dev/null @@ -1,469 +0,0 @@ -"""Support for Dyson Pure Cool link fan.""" -from __future__ import annotations - -import logging -import math - -from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_cool_link import DysonPureCoolLink -from libpurecool.dyson_pure_state import DysonPureCoolState -from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State -import voluptuous as vol - -from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity -from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.util.percentage import ( - int_states_in_range, - percentage_to_ranged_value, - ranged_value_to_percentage, -) - -from . import DYSON_DEVICES, DysonEntity - -_LOGGER = logging.getLogger(__name__) - -ATTR_NIGHT_MODE = "night_mode" -ATTR_AUTO_MODE = "auto_mode" -ATTR_ANGLE_LOW = "angle_low" -ATTR_ANGLE_HIGH = "angle_high" -ATTR_FLOW_DIRECTION_FRONT = "flow_direction_front" -ATTR_TIMER = "timer" -ATTR_HEPA_FILTER = "hepa_filter" -ATTR_CARBON_FILTER = "carbon_filter" -ATTR_DYSON_SPEED = "dyson_speed" -ATTR_DYSON_SPEED_LIST = "dyson_speed_list" - -DYSON_DOMAIN = "dyson" -DYSON_FAN_DEVICES = "dyson_fan_devices" - -SERVICE_SET_NIGHT_MODE = "set_night_mode" -SERVICE_SET_AUTO_MODE = "set_auto_mode" -SERVICE_SET_ANGLE = "set_angle" -SERVICE_SET_FLOW_DIRECTION_FRONT = "set_flow_direction_front" -SERVICE_SET_TIMER = "set_timer" -SERVICE_SET_DYSON_SPEED = "set_speed" - -SET_NIGHT_MODE_SCHEMA = { - vol.Required(ATTR_NIGHT_MODE): cv.boolean, -} - -SET_AUTO_MODE_SCHEMA = { - vol.Required(ATTR_AUTO_MODE): cv.boolean, -} - -SET_ANGLE_SCHEMA = { - vol.Required(ATTR_ANGLE_LOW): cv.positive_int, - vol.Required(ATTR_ANGLE_HIGH): cv.positive_int, -} - -SET_FLOW_DIRECTION_FRONT_SCHEMA = { - vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean, -} - -SET_TIMER_SCHEMA = { - vol.Required(ATTR_TIMER): cv.positive_int, -} - -SET_DYSON_SPEED_SCHEMA = { - vol.Required(ATTR_DYSON_SPEED): cv.positive_int, -} - - -PRESET_MODE_AUTO = "auto" -PRESET_MODES = [PRESET_MODE_AUTO] - -ORDERED_DYSON_SPEEDS = [ - FanSpeed.FAN_SPEED_1, - FanSpeed.FAN_SPEED_2, - FanSpeed.FAN_SPEED_3, - FanSpeed.FAN_SPEED_4, - FanSpeed.FAN_SPEED_5, - FanSpeed.FAN_SPEED_6, - FanSpeed.FAN_SPEED_7, - FanSpeed.FAN_SPEED_8, - FanSpeed.FAN_SPEED_9, - FanSpeed.FAN_SPEED_10, -] -DYSON_SPEED_TO_INT_VALUE = {k: int(k.value) for k in ORDERED_DYSON_SPEEDS} -INT_VALUE_TO_DYSON_SPEED = {v: k for k, v in DYSON_SPEED_TO_INT_VALUE.items()} - -SPEED_LIST_DYSON = list(DYSON_SPEED_TO_INT_VALUE.values()) - -SPEED_RANGE = ( - SPEED_LIST_DYSON[0], - SPEED_LIST_DYSON[-1], -) # off is not included - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Dyson fan components.""" - - if discovery_info is None: - return - - _LOGGER.debug("Creating new Dyson fans") - if DYSON_FAN_DEVICES not in hass.data: - hass.data[DYSON_FAN_DEVICES] = [] - - # Get Dyson Devices from parent component - has_purecool_devices = False - device_serials = [device.serial for device in hass.data[DYSON_FAN_DEVICES]] - for device in hass.data[DYSON_DEVICES]: - if device.serial not in device_serials: - if isinstance(device, DysonPureCool): - has_purecool_devices = True - dyson_entity = DysonPureCoolEntity(device) - hass.data[DYSON_FAN_DEVICES].append(dyson_entity) - elif isinstance(device, DysonPureCoolLink): - dyson_entity = DysonPureCoolLinkEntity(device) - hass.data[DYSON_FAN_DEVICES].append(dyson_entity) - - async_add_entities(hass.data[DYSON_FAN_DEVICES]) - - # Register custom services - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_SET_NIGHT_MODE, SET_NIGHT_MODE_SCHEMA, "set_night_mode" - ) - platform.async_register_entity_service( - SERVICE_SET_AUTO_MODE, SET_AUTO_MODE_SCHEMA, "set_auto_mode" - ) - platform.async_register_entity_service( - SERVICE_SET_DYSON_SPEED, SET_DYSON_SPEED_SCHEMA, "service_set_dyson_speed" - ) - if has_purecool_devices: - platform.async_register_entity_service( - SERVICE_SET_ANGLE, SET_ANGLE_SCHEMA, "set_angle" - ) - platform.async_register_entity_service( - SERVICE_SET_FLOW_DIRECTION_FRONT, - SET_FLOW_DIRECTION_FRONT_SCHEMA, - "set_flow_direction_front", - ) - platform.async_register_entity_service( - SERVICE_SET_TIMER, SET_TIMER_SCHEMA, "set_timer" - ) - - -class DysonFanEntity(DysonEntity, FanEntity): - """Representation of a Dyson fan.""" - - @property - def percentage(self): - """Return the current speed percentage.""" - if self.auto_mode: - return None - return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed)) - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def preset_modes(self): - """Return the available preset modes.""" - return PRESET_MODES - - @property - def preset_mode(self): - """Return the current preset mode.""" - if self.auto_mode: - return PRESET_MODE_AUTO - return None - - @property - def dyson_speed(self): - """Return the current speed.""" - if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: - return self._device.state.speed - return int(self._device.state.speed) - - @property - def dyson_speed_list(self) -> list: - """Get the list of available dyson speeds.""" - return SPEED_LIST_DYSON - - @property - def night_mode(self): - """Return Night mode.""" - return self._device.state.night_mode == "ON" - - @property - def auto_mode(self): - """Return auto mode.""" - raise NotImplementedError - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - - @property - def extra_state_attributes(self) -> dict: - """Return optional state attributes.""" - return { - ATTR_NIGHT_MODE: self.night_mode, - ATTR_AUTO_MODE: self.auto_mode, - ATTR_DYSON_SPEED: self.dyson_speed, - ATTR_DYSON_SPEED_LIST: self.dyson_speed_list, - } - - def set_auto_mode(self, auto_mode: bool) -> None: - """Set auto mode.""" - raise NotImplementedError - - def set_percentage(self, percentage: int) -> None: - """Set the speed percentage of the fan.""" - if percentage == 0: - self.turn_off() - return - dyson_speed = INT_VALUE_TO_DYSON_SPEED[ - math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - ] - self.set_dyson_speed(dyson_speed) - - def set_preset_mode(self, preset_mode: str) -> None: - """Set a preset mode on the fan.""" - self._valid_preset_mode_or_raise(preset_mode) - # There currently is only one - self.set_auto_mode(True) - - def set_dyson_speed(self, speed: FanSpeed) -> None: - """Set the exact speed of the fan.""" - raise NotImplementedError - - def service_set_dyson_speed(self, dyson_speed: int) -> None: - """Handle the service to set dyson speed.""" - if dyson_speed not in SPEED_LIST_DYSON: - raise ValueError(f'"{dyson_speed}" is not a valid Dyson speed') - _LOGGER.debug("Set exact speed to %s", dyson_speed) - speed = FanSpeed(f"{int(dyson_speed):04d}") - self.set_dyson_speed(speed) - - def turn_on( - self, - speed: str | None = None, - percentage: int | None = None, - preset_mode: str | None = None, - **kwargs, - ) -> None: - """Turn on the fan.""" - _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage) - if preset_mode: - self.set_preset_mode(preset_mode) - elif percentage is None: - # percentage not set, just turn on - self._device.set_configuration(fan_mode=FanMode.FAN) - else: - self.set_percentage(percentage) - - -class DysonPureCoolLinkEntity(DysonFanEntity): - """Representation of a Dyson fan.""" - - def __init__(self, device): - """Initialize the fan.""" - super().__init__(device, DysonPureCoolState) - - def turn_off(self, **kwargs) -> None: - """Turn off the fan.""" - _LOGGER.debug("Turn off fan %s", self.name) - self._device.set_configuration(fan_mode=FanMode.OFF) - - def set_dyson_speed(self, speed: FanSpeed) -> None: - """Set the exact speed of the fan.""" - self._device.set_configuration(fan_mode=FanMode.FAN, fan_speed=speed) - - def oscillate(self, oscillating: bool) -> None: - """Turn on/off oscillating.""" - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) - - if oscillating: - self._device.set_configuration(oscillation=Oscillation.OSCILLATION_ON) - else: - self._device.set_configuration(oscillation=Oscillation.OSCILLATION_OFF) - - @property - def oscillating(self): - """Return the oscillation state.""" - return self._device.state.oscillation == "ON" - - @property - def is_on(self): - """Return true if the entity is on.""" - return self._device.state.fan_mode in ["FAN", "AUTO"] - - def set_night_mode(self, night_mode: bool) -> None: - """Turn fan in night mode.""" - _LOGGER.debug("Set %s night mode %s", self.name, night_mode) - if night_mode: - self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) - else: - self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF) - - @property - def auto_mode(self): - """Return auto mode.""" - return self._device.state.fan_mode == "AUTO" - - def set_auto_mode(self, auto_mode: bool) -> None: - """Turn fan in auto mode.""" - _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) - if auto_mode: - self._device.set_configuration(fan_mode=FanMode.AUTO) - else: - self._device.set_configuration(fan_mode=FanMode.FAN) - - -class DysonPureCoolEntity(DysonFanEntity): - """Representation of a Dyson Purecool (TP04/DP04) fan.""" - - def __init__(self, device): - """Initialize the fan.""" - super().__init__(device, DysonPureCoolV2State) - - def turn_on( - self, - speed: str | None = None, - percentage: int | None = None, - preset_mode: str | None = None, - **kwargs, - ) -> None: - """Turn on the fan.""" - _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage) - if preset_mode: - self.set_preset_mode(preset_mode) - elif percentage is None: - # percentage not set, just turn on - self._device.turn_on() - else: - self.set_percentage(percentage) - - def turn_off(self, **kwargs): - """Turn off the fan.""" - _LOGGER.debug("Turn off fan %s", self.name) - self._device.turn_off() - - def set_dyson_speed(self, speed: FanSpeed) -> None: - """Set the exact speed of the purecool fan.""" - self._device.set_fan_speed(speed) - - def oscillate(self, oscillating: bool) -> None: - """Turn on/off oscillating.""" - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) - - if oscillating: - self._device.enable_oscillation() - else: - self._device.disable_oscillation() - - def set_night_mode(self, night_mode: bool) -> None: - """Turn on/off night mode.""" - _LOGGER.debug("Turn night mode %s for device %s", night_mode, self.name) - - if night_mode: - self._device.enable_night_mode() - else: - self._device.disable_night_mode() - - def set_auto_mode(self, auto_mode: bool) -> None: - """Turn auto mode on/off.""" - _LOGGER.debug("Turn auto mode %s for device %s", auto_mode, self.name) - if auto_mode: - self._device.enable_auto_mode() - else: - self._device.disable_auto_mode() - - def set_angle(self, angle_low: int, angle_high: int) -> None: - """Set device angle.""" - _LOGGER.debug( - "set low %s and high angle %s for device %s", - angle_low, - angle_high, - self.name, - ) - self._device.enable_oscillation(angle_low, angle_high) - - def set_flow_direction_front(self, flow_direction_front: bool) -> None: - """Set frontal airflow direction.""" - _LOGGER.debug( - "Set frontal flow direction to %s for device %s", - flow_direction_front, - self.name, - ) - - if flow_direction_front: - self._device.enable_frontal_direction() - else: - self._device.disable_frontal_direction() - - def set_timer(self, timer) -> None: - """Set timer.""" - _LOGGER.debug("Set timer to %s for device %s", timer, self.name) - - if timer == 0: - self._device.disable_sleep_timer() - else: - self._device.enable_sleep_timer(timer) - - @property - def oscillating(self): - """Return the oscillation state.""" - return self._device.state and self._device.state.oscillation == "OION" - - @property - def is_on(self): - """Return true if the entity is on.""" - return self._device.state.fan_power == "ON" - - @property - def auto_mode(self): - """Return Auto mode.""" - return self._device.state.auto_mode == "ON" - - @property - def angle_low(self): - """Return angle high.""" - return int(self._device.state.oscillation_angle_low) - - @property - def angle_high(self): - """Return angle low.""" - return int(self._device.state.oscillation_angle_high) - - @property - def flow_direction_front(self): - """Return frontal flow direction.""" - return self._device.state.front_direction == "ON" - - @property - def timer(self): - """Return timer.""" - return self._device.state.sleep_timer - - @property - def hepa_filter(self): - """Return the HEPA filter state.""" - return int(self._device.state.hepa_filter_state) - - @property - def carbon_filter(self): - """Return the carbon filter state.""" - if self._device.state.carbon_filter_state == "INV": - return self._device.state.carbon_filter_state - return int(self._device.state.carbon_filter_state) - - @property - def extra_state_attributes(self) -> dict: - """Return optional state attributes.""" - return { - **super().extra_state_attributes, - ATTR_ANGLE_LOW: self.angle_low, - ATTR_ANGLE_HIGH: self.angle_high, - ATTR_FLOW_DIRECTION_FRONT: self.flow_direction_front, - ATTR_TIMER: self.timer, - ATTR_HEPA_FILTER: self.hepa_filter, - ATTR_CARBON_FILTER: self.carbon_filter, - } diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json deleted file mode 100644 index 0f5da0691c4..00000000000 --- a/homeassistant/components/dyson/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "dyson", - "name": "Dyson", - "documentation": "https://www.home-assistant.io/integrations/dyson", - "requirements": ["libpurecool==0.6.4"], - "after_dependencies": ["zeroconf"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py deleted file mode 100644 index be83a7e4373..00000000000 --- a/homeassistant/components/dyson/sensor.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Support for Dyson Pure Cool Link Sensors.""" -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - STATE_OFF, - TEMP_CELSIUS, - TIME_HOURS, -) - -from . import DYSON_DEVICES, DysonEntity - -SENSOR_ATTRIBUTES = { - "air_quality": {ATTR_ICON: "mdi:fan"}, - "dust": {ATTR_ICON: "mdi:cloud"}, - "humidity": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - "temperature": {ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, - "filter_life": { - ATTR_ICON: "mdi:filter-outline", - ATTR_UNIT_OF_MEASUREMENT: TIME_HOURS, - }, - "carbon_filter_state": { - ATTR_ICON: "mdi:filter-outline", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - "combi_filter_state": { - ATTR_ICON: "mdi:filter-outline", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - "hepa_filter_state": { - ATTR_ICON: "mdi:filter-outline", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, -} - -SENSOR_NAMES = { - "air_quality": "AQI", - "dust": "Dust", - "humidity": "Humidity", - "temperature": "Temperature", - "filter_life": "Filter Life", - "carbon_filter_state": "Carbon Filter Remaining Life", - "combi_filter_state": "Combi Filter Remaining Life", - "hepa_filter_state": "HEPA Filter Remaining Life", -} - -DYSON_SENSOR_DEVICES = "dyson_sensor_devices" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Dyson Sensors.""" - - if discovery_info is None: - return - - hass.data.setdefault(DYSON_SENSOR_DEVICES, []) - unit = hass.config.units.temperature_unit - devices = hass.data[DYSON_SENSOR_DEVICES] - - # Get Dyson Devices from parent component - device_ids = [device.unique_id for device in hass.data[DYSON_SENSOR_DEVICES]] - new_entities = [] - for device in hass.data[DYSON_DEVICES]: - if isinstance(device, DysonPureCool): - if f"{device.serial}-temperature" not in device_ids: - new_entities.append(DysonTemperatureSensor(device, unit)) - if f"{device.serial}-humidity" not in device_ids: - new_entities.append(DysonHumiditySensor(device)) - - # For PureCool+Humidify devices, a single filter exists, called "Combi Filter". - # It's reported with the HEPA state, while the Carbon state is set to INValid. - if device.state and device.state.carbon_filter_state == "INV": - if f"{device.serial}-hepa_filter_state" not in device_ids: - new_entities.append(DysonHepaFilterLifeSensor(device, "combi")) - else: - if f"{device.serial}-hepa_filter_state" not in device_ids: - new_entities.append(DysonHepaFilterLifeSensor(device)) - if f"{device.serial}-carbon_filter_state" not in device_ids: - new_entities.append(DysonCarbonFilterLifeSensor(device)) - elif isinstance(device, DysonPureCoolLink): - new_entities.append(DysonFilterLifeSensor(device)) - new_entities.append(DysonDustSensor(device)) - new_entities.append(DysonHumiditySensor(device)) - new_entities.append(DysonTemperatureSensor(device, unit)) - new_entities.append(DysonAirQualitySensor(device)) - - if not new_entities: - return - - devices.extend(new_entities) - add_entities(devices) - - -class DysonSensor(DysonEntity, SensorEntity): - """Representation of a generic Dyson sensor.""" - - def __init__(self, device, sensor_type): - """Create a new generic Dyson sensor.""" - super().__init__(device, None) - self._old_value = None - self._sensor_type = sensor_type - self._attributes = SENSOR_ATTRIBUTES[sensor_type] - - def on_message(self, message): - """Handle new messages which are received from the fan.""" - # Prevent refreshing if not needed - if self._old_value is None or self._old_value != self.state: - self._old_value = self.state - self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the Dyson sensor name.""" - return f"{super().name} {SENSOR_NAMES[self._sensor_type]}" - - @property - def unique_id(self): - """Return the sensor's unique id.""" - return f"{self._device.serial}-{self._sensor_type}" - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - @property - def icon(self): - """Return the icon for this sensor.""" - return self._attributes.get(ATTR_ICON) - - @property - def device_class(self): - """Return the device class of this sensor.""" - return self._attributes.get(ATTR_DEVICE_CLASS) - - -class DysonFilterLifeSensor(DysonSensor): - """Representation of Dyson Filter Life sensor (in hours).""" - - def __init__(self, device): - """Create a new Dyson Filter Life sensor.""" - super().__init__(device, "filter_life") - - @property - def native_value(self): - """Return filter life in hours.""" - return int(self._device.state.filter_life) - - -class DysonCarbonFilterLifeSensor(DysonSensor): - """Representation of Dyson Carbon Filter Life sensor (in percent).""" - - def __init__(self, device): - """Create a new Dyson Carbon Filter Life sensor.""" - super().__init__(device, "carbon_filter_state") - - @property - def native_value(self): - """Return filter life remaining in percent.""" - return int(self._device.state.carbon_filter_state) - - -class DysonHepaFilterLifeSensor(DysonSensor): - """Representation of Dyson HEPA (or Combi) Filter Life sensor (in percent).""" - - def __init__(self, device, filter_type="hepa"): - """Create a new Dyson Filter Life sensor.""" - super().__init__(device, f"{filter_type}_filter_state") - - @property - def native_value(self): - """Return filter life remaining in percent.""" - return int(self._device.state.hepa_filter_state) - - -class DysonDustSensor(DysonSensor): - """Representation of Dyson Dust sensor (lower is better).""" - - def __init__(self, device): - """Create a new Dyson Dust sensor.""" - super().__init__(device, "dust") - - @property - def native_value(self): - """Return Dust value.""" - return self._device.environmental_state.dust - - -class DysonHumiditySensor(DysonSensor): - """Representation of Dyson Humidity sensor.""" - - def __init__(self, device): - """Create a new Dyson Humidity sensor.""" - super().__init__(device, "humidity") - - @property - def native_value(self): - """Return Humidity value.""" - if self._device.environmental_state.humidity == 0: - return STATE_OFF - return self._device.environmental_state.humidity - - -class DysonTemperatureSensor(DysonSensor): - """Representation of Dyson Temperature sensor.""" - - def __init__(self, device, unit): - """Create a new Dyson Temperature sensor.""" - super().__init__(device, "temperature") - self._unit = unit - - @property - def native_value(self): - """Return Temperature value.""" - temperature_kelvin = self._device.environmental_state.temperature - if temperature_kelvin == 0: - return STATE_OFF - if self._unit == TEMP_CELSIUS: - return float(f"{(temperature_kelvin - 273.15):.1f}") - return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - -class DysonAirQualitySensor(DysonSensor): - """Representation of Dyson Air Quality sensor (lower is better).""" - - def __init__(self, device): - """Create a new Dyson Air Quality sensor.""" - super().__init__(device, "air_quality") - - @property - def native_value(self): - """Return Air Quality value.""" - return int(self._device.environmental_state.volatil_organic_compounds) diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml deleted file mode 100644 index 10b27c1c5e6..00000000000 --- a/homeassistant/components/dyson/services.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# Describes the format for available fan services - -set_night_mode: - name: Set night mode - description: Set the fan in night mode. - target: - entity: - integration: dyson - domain: fan - fields: - night_mode: - name: Night mode - description: Night mode status - required: true - selector: - boolean: - -set_auto_mode: - name: Set auto mode - description: Set the fan in auto mode. - target: - entity: - integration: dyson - domain: fan - fields: - auto_mode: - name: Auto Mode - description: Auto mode status - required: true - selector: - boolean: - -set_angle: - name: Set angle - description: Set the oscillation angle of the selected fan(s). - target: - entity: - integration: dyson - domain: fan - fields: - angle_low: - name: Angle low - description: The angle at which the oscillation should start - required: true - selector: - number: - min: 5 - max: 355 - unit_of_measurement: '°' - angle_high: - name: Angle high - description: The angle at which the oscillation should end - required: true - selector: - number: - min: 5 - max: 355 - unit_of_measurement: '°' - -set_flow_direction_front: - name: Set flow direction front - description: Set the fan flow direction. - target: - entity: - integration: dyson - domain: fan - fields: - flow_direction_front: - name: Flow direction front - description: Frontal flow direction - required: true - selector: - boolean: - -set_timer: - name: Set timer - description: Set the sleep timer. - target: - entity: - integration: dyson - domain: fan - fields: - timer: - name: Timer - description: The value in minutes to set the timer to, 0 to disable it - required: true - selector: - number: - min: 0 - max: 720 - unit_of_measurement: minutes - -set_speed: - name: Set speed - description: Set the exact speed of the fan. - target: - entity: - integration: dyson - domain: fan - fields: - dyson_speed: - name: Speed - description: Speed - required: true - selector: - number: - min: 1 - max: 10 diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py deleted file mode 100644 index f4035d33cf3..00000000000 --- a/homeassistant/components/dyson/vacuum.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Support for the Dyson 360 eye vacuum cleaner robot.""" -import logging - -from libpurecool.const import Dyson360EyeMode, PowerMode -from libpurecool.dyson_360_eye import Dyson360Eye - -from homeassistant.components.vacuum import ( - SUPPORT_BATTERY, - SUPPORT_FAN_SPEED, - SUPPORT_PAUSE, - SUPPORT_RETURN_HOME, - SUPPORT_STATUS, - SUPPORT_STOP, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - VacuumEntity, -) -from homeassistant.helpers.icon import icon_for_battery_level - -from . import DYSON_DEVICES, DysonEntity - -_LOGGER = logging.getLogger(__name__) - -ATTR_CLEAN_ID = "clean_id" -ATTR_FULL_CLEAN_TYPE = "full_clean_type" -ATTR_POSITION = "position" - -DYSON_360_EYE_DEVICES = "dyson_360_eye_devices" - -SUPPORT_DYSON = ( - SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_PAUSE - | SUPPORT_RETURN_HOME - | SUPPORT_FAN_SPEED - | SUPPORT_STATUS - | SUPPORT_BATTERY - | SUPPORT_STOP -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Dyson 360 Eye robot vacuum platform.""" - _LOGGER.debug("Creating new Dyson 360 Eye robot vacuum") - if DYSON_360_EYE_DEVICES not in hass.data: - hass.data[DYSON_360_EYE_DEVICES] = [] - - # Get Dyson Devices from parent component - for device in [d for d in hass.data[DYSON_DEVICES] if isinstance(d, Dyson360Eye)]: - dyson_entity = Dyson360EyeDevice(device) - hass.data[DYSON_360_EYE_DEVICES].append(dyson_entity) - - add_entities(hass.data[DYSON_360_EYE_DEVICES]) - return True - - -class Dyson360EyeDevice(DysonEntity, VacuumEntity): - """Dyson 360 Eye robot vacuum device.""" - - def __init__(self, device): - """Dyson 360 Eye robot vacuum device.""" - super().__init__(device, None) - - @property - def status(self): - """Return the status of the vacuum cleaner.""" - dyson_labels = { - Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", - Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", - Dyson360EyeMode.FULL_CLEAN_PAUSED: "Paused", - Dyson360EyeMode.FULL_CLEAN_RUNNING: "Cleaning", - Dyson360EyeMode.FULL_CLEAN_ABORTED: "Returning home", - Dyson360EyeMode.FULL_CLEAN_INITIATED: "Start cleaning", - Dyson360EyeMode.FAULT_USER_RECOVERABLE: "Error - device blocked", - Dyson360EyeMode.FAULT_REPLACE_ON_DOCK: "Error - Replace device on dock", - Dyson360EyeMode.FULL_CLEAN_FINISHED: "Finished", - Dyson360EyeMode.FULL_CLEAN_NEEDS_CHARGE: "Need charging", - } - return dyson_labels.get(self._device.state.state, self._device.state.state) - - @property - def battery_level(self): - """Return the battery level of the vacuum cleaner.""" - return self._device.state.battery_level - - @property - def fan_speed(self): - """Return the fan speed of the vacuum cleaner.""" - speed_labels = {PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet"} - return speed_labels[self._device.state.power_mode] - - @property - def fan_speed_list(self): - """Get the list of available fan speed steps of the vacuum cleaner.""" - return ["Quiet", "Max"] - - @property - def extra_state_attributes(self): - """Return the specific state attributes of this vacuum cleaner.""" - return {ATTR_POSITION: str(self._device.state.position)} - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._device.state.state in [ - Dyson360EyeMode.FULL_CLEAN_INITIATED, - Dyson360EyeMode.FULL_CLEAN_ABORTED, - Dyson360EyeMode.FULL_CLEAN_RUNNING, - ] - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return True - - @property - def supported_features(self): - """Flag vacuum cleaner robot features that are supported.""" - return SUPPORT_DYSON - - @property - def battery_icon(self): - """Return the battery icon for the vacuum cleaner.""" - charging = self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGING] - return icon_for_battery_level( - battery_level=self.battery_level, charging=charging - ) - - def turn_on(self, **kwargs): - """Turn the vacuum on.""" - _LOGGER.debug("Turn on device %s", self.name) - if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: - self._device.resume() - else: - self._device.start() - - def turn_off(self, **kwargs): - """Turn the vacuum off and return to home.""" - _LOGGER.debug("Turn off device %s", self.name) - self._device.pause() - - def stop(self, **kwargs): - """Stop the vacuum cleaner.""" - _LOGGER.debug("Stop device %s", self.name) - self._device.pause() - - def set_fan_speed(self, fan_speed, **kwargs): - """Set fan speed.""" - _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) - power_modes = {"Quiet": PowerMode.QUIET, "Max": PowerMode.MAX} - self._device.set_power_mode(power_modes[fan_speed]) - - def start_pause(self, **kwargs): - """Start, pause or resume the cleaning task.""" - if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: - _LOGGER.debug("Resume device %s", self.name) - self._device.resume() - elif self._device.state.state in [ - Dyson360EyeMode.INACTIVE_CHARGED, - Dyson360EyeMode.INACTIVE_CHARGING, - ]: - _LOGGER.debug("Start device %s", self.name) - self._device.start() - else: - _LOGGER.debug("Pause device %s", self.name) - self._device.pause() - - def return_to_base(self, **kwargs): - """Set the vacuum cleaner to return to the dock.""" - _LOGGER.debug("Return to base device %s", self.name) - self._device.abort() diff --git a/requirements_all.txt b/requirements_all.txt index 1dcd70030a3..f6007b5ee73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -923,9 +923,6 @@ krakenex==2.1.0 # homeassistant.components.eufy lakeside==0.12 -# homeassistant.components.dyson -libpurecool==0.6.4 - # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 928a9d6d568..2b4fcb1f964 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -566,9 +566,6 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 -# homeassistant.components.dyson -libpurecool==0.6.4 - # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/tests/components/dyson/__init__.py b/tests/components/dyson/__init__.py deleted file mode 100644 index d4c814a37db..00000000000 --- a/tests/components/dyson/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the dyson component.""" diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py deleted file mode 100644 index 4fde47183d2..00000000000 --- a/tests/components/dyson/common.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Common utils for Dyson tests.""" -from __future__ import annotations - -from unittest import mock -from unittest.mock import MagicMock - -from libpurecool.const import SLEEP_TIMER_OFF, Dyson360EyeMode, FanMode, PowerMode -from libpurecool.dyson_360_eye import Dyson360Eye -from libpurecool.dyson_device import DysonDevice -from libpurecool.dyson_pure_cool import DysonPureCool, FanSpeed -from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - -from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback - -SERIAL = "XX-XXXXX-XX" -NAME = "Temp Name" -ENTITY_NAME = "temp_name" -IP_ADDRESS = "0.0.0.0" - -BASE_PATH = "homeassistant.components.dyson" - -CONFIG = { - DOMAIN: { - CONF_USERNAME: "user@example.com", - CONF_PASSWORD: "password", - CONF_LANGUAGE: "US", - CONF_DEVICES: [ - { - "device_id": SERIAL, - "device_ip": IP_ADDRESS, - } - ], - } -} - - -@callback -def async_get_basic_device(spec: type[DysonDevice]) -> DysonDevice: - """Return a basic device with common fields filled out.""" - device = MagicMock(spec=spec) - device.serial = SERIAL - device.name = NAME - device.connect = mock.Mock(return_value=True) - device.auto_connect = mock.Mock(return_value=True) - return device - - -@callback -def async_get_360eye_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye: - """Return a Dyson 360 Eye device.""" - device = async_get_basic_device(Dyson360Eye) - device.state.state = state - device.state.battery_level = 85 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device - - -@callback -def async_get_purecoollink_device() -> DysonPureCoolLink: - """Return a Dyson Pure Cool Link device.""" - device = async_get_basic_device(DysonPureCoolLink) - device.state.fan_mode = FanMode.FAN.value - device.state.speed = FanSpeed.FAN_SPEED_1.value - device.state.night_mode = "ON" - device.state.oscillation = "ON" - return device - - -@callback -def async_get_purecool_device() -> DysonPureCool: - """Return a Dyson Pure Cool device.""" - device = async_get_basic_device(DysonPureCool) - device.state.fan_power = "ON" - device.state.speed = FanSpeed.FAN_SPEED_1.value - device.state.night_mode = "ON" - device.state.oscillation = "OION" - device.state.oscillation_angle_low = "0024" - device.state.oscillation_angle_high = "0254" - device.state.auto_mode = "OFF" - device.state.front_direction = "ON" - device.state.sleep_timer = SLEEP_TIMER_OFF - device.state.hepa_filter_state = "0100" - device.state.carbon_filter_state = "0100" - return device - - -async def async_update_device( - hass: HomeAssistant, device: DysonDevice, state_type: type | None = None -) -> None: - """Update the device using callback function.""" - callbacks = [args[0][0] for args in device.add_message_listener.call_args_list] - message = MagicMock(spec=state_type) - - # Combining sync calls to avoid multiple executors - def _run_callbacks(): - for callback_fn in callbacks: - callback_fn(message) - - await hass.async_add_executor_job(_run_callbacks) - await hass.async_block_till_done() diff --git a/tests/components/dyson/conftest.py b/tests/components/dyson/conftest.py deleted file mode 100644 index 300c80f3a73..00000000000 --- a/tests/components/dyson/conftest.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Configure pytest for Dyson tests.""" -from unittest.mock import patch - -from libpurecool.dyson_device import DysonDevice -import pytest - -from homeassistant.components.dyson import DOMAIN -from homeassistant.core import HomeAssistant - -from .common import BASE_PATH, CONFIG - -from tests.common import async_setup_component - - -@pytest.fixture() -async def device(hass: HomeAssistant, request) -> DysonDevice: - """Fixture to provide Dyson 360 Eye device.""" - platform = request.module.PLATFORM_DOMAIN - get_device = request.module.async_get_device - if hasattr(request, "param"): - if isinstance(request.param, list): - device = get_device(*request.param) - else: - device = get_device(request.param) - else: - device = get_device() - with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( - f"{BASE_PATH}.DysonAccount.devices", return_value=[device] - ), patch(f"{BASE_PATH}.PLATFORMS", [platform]): - # PLATFORMS is patched so that only the platform being tested is set up - await async_setup_component( - hass, - DOMAIN, - CONFIG, - ) - await hass.async_block_till_done() - - return device diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py deleted file mode 100644 index 51b38303a58..00000000000 --- a/tests/components/dyson/test_air_quality.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test the Dyson air quality component.""" - -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - -from homeassistant.components.air_quality import ( - ATTR_AQI, - ATTR_NO2, - ATTR_PM_2_5, - ATTR_PM_10, - DOMAIN as PLATFORM_DOMAIN, -) -from homeassistant.components.dyson.air_quality import ATTR_VOC -from homeassistant.core import HomeAssistant, callback - -from .common import ENTITY_NAME, async_get_purecool_device, async_update_device - -ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" - -MOCKED_VALUES = { - ATTR_PM_2_5: 10, - ATTR_PM_10: 20, - ATTR_NO2: 30, - ATTR_VOC: 40, -} - -MOCKED_UPDATED_VALUES = { - ATTR_PM_2_5: 60, - ATTR_PM_10: 50, - ATTR_NO2: 40, - ATTR_VOC: 30, -} - - -def _async_assign_values(device: DysonPureCool, values=MOCKED_VALUES) -> None: - """Assign mocked environmental states to the device.""" - device.environmental_state.particulate_matter_25 = values[ATTR_PM_2_5] - device.environmental_state.particulate_matter_10 = values[ATTR_PM_10] - device.environmental_state.nitrogen_dioxide = values[ATTR_NO2] - device.environmental_state.volatile_organic_compounds = values[ATTR_VOC] - - -@callback -def async_get_device() -> DysonPureCool: - """Return a device of the given type.""" - device = async_get_purecool_device() - _async_assign_values(device) - return device - - -async def test_air_quality(hass: HomeAssistant, device: DysonPureCool) -> None: - """Test the state and attributes of the air quality entity.""" - state = hass.states.get(ENTITY_ID) - assert state.state == str(MOCKED_VALUES[ATTR_PM_2_5]) - attributes = state.attributes - for attr, value in MOCKED_VALUES.items(): - assert attributes[attr] == value - assert attributes[ATTR_AQI] == 40 - - _async_assign_values(device, MOCKED_UPDATED_VALUES) - await async_update_device(hass, device, DysonEnvironmentalSensorV2State) - state = hass.states.get(ENTITY_ID) - assert state.state == str(MOCKED_UPDATED_VALUES[ATTR_PM_2_5]) - attributes = state.attributes - for attr, value in MOCKED_UPDATED_VALUES.items(): - assert attributes[attr] == value - assert attributes[ATTR_AQI] == 60 diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py deleted file mode 100644 index 2591b90f596..00000000000 --- a/tests/components/dyson/test_climate.py +++ /dev/null @@ -1,348 +0,0 @@ -"""Test the Dyson fan component.""" -from __future__ import annotations - -from libpurecool.const import ( - AutoMode, - FanPower, - FanSpeed, - FanState, - FocusMode, - HeatMode, - HeatState, -) -from libpurecool.dyson_device import DysonDevice -from libpurecool.dyson_pure_hotcool import DysonPureHotCool -from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink -from libpurecool.dyson_pure_state import DysonPureHotCoolState -from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State -import pytest - -from homeassistant.components.climate import DOMAIN as PLATFORM_DOMAIN -from homeassistant.components.climate.const import ( - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, - ATTR_FAN_MODE, - ATTR_FAN_MODES, - ATTR_HVAC_ACTION, - ATTR_HVAC_MODE, - ATTR_HVAC_MODES, - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - FAN_AUTO, - FAN_DIFFUSE, - FAN_FOCUS, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, - FAN_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, - SERVICE_SET_FAN_MODE, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_TEMPERATURE, -) -from homeassistant.components.dyson.climate import ( - SUPPORT_FAN, - SUPPORT_FAN_PCOOL, - SUPPORT_FLAGS, - SUPPORT_HVAC, - SUPPORT_HVAC_PCOOL, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er - -from .common import ( - ENTITY_NAME, - NAME, - SERIAL, - async_get_basic_device, - async_update_device, -) - -ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" - - -@callback -def async_get_device(spec: type[DysonDevice]) -> DysonDevice: - """Return a Dyson climate device.""" - device = async_get_basic_device(spec) - device.state.heat_target = 2900 - device.environmental_state.temperature = 275 - device.environmental_state.humidity = 50 - if spec == DysonPureHotCoolLink: - device.state.heat_mode = HeatMode.HEAT_ON.value - device.state.heat_state = HeatState.HEAT_STATE_ON.value - device.state.focus_mode = FocusMode.FOCUS_ON.value - else: - device.state.fan_power = FanPower.POWER_ON.value - device.state.heat_mode = HeatMode.HEAT_ON.value - device.state.heat_state = HeatState.HEAT_STATE_ON.value - device.state.auto_mode = AutoMode.AUTO_ON.value - device.state.fan_state = FanState.FAN_OFF.value - device.state.speed = FanSpeed.FAN_SPEED_AUTO.value - return device - - -@pytest.mark.parametrize( - "device", [DysonPureHotCoolLink, DysonPureHotCool], indirect=True -) -async def test_state_common(hass: HomeAssistant, device: DysonDevice) -> None: - """Test common state and attributes of two types of climate entities.""" - entity_registry = er.async_get(hass) - assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL - - state = hass.states.get(ENTITY_ID) - assert state.name == NAME - attributes = state.attributes - assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_FLAGS - assert attributes[ATTR_CURRENT_TEMPERATURE] == 2 - assert attributes[ATTR_CURRENT_HUMIDITY] == 50 - assert attributes[ATTR_TEMPERATURE] == 17 - assert attributes[ATTR_MIN_TEMP] == 1 - assert attributes[ATTR_MAX_TEMP] == 37 - - device.state.heat_target = 2800 - device.environmental_state.temperature = 0 - device.environmental_state.humidity = 0 - await async_update_device( - hass, - device, - DysonPureHotCoolState - if isinstance(device, DysonPureHotCoolLink) - else DysonPureHotCoolV2State, - ) - attributes = hass.states.get(ENTITY_ID).attributes - assert attributes[ATTR_CURRENT_TEMPERATURE] is None - assert ATTR_CURRENT_HUMIDITY not in attributes - assert attributes[ATTR_TEMPERATURE] == 7 - - -@pytest.mark.parametrize("device", [DysonPureHotCoolLink], indirect=True) -async def test_state_purehotcoollink( - hass: HomeAssistant, device: DysonPureHotCoolLink -) -> None: - """Test common state and attributes of a PureHotCoolLink entity.""" - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT - attributes = state.attributes - assert attributes[ATTR_HVAC_MODES] == SUPPORT_HVAC - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - assert attributes[ATTR_FAN_MODE] == FAN_FOCUS - assert attributes[ATTR_FAN_MODES] == SUPPORT_FAN - - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - device.state.focus_mode = FocusMode.FOCUS_OFF - await async_update_device(hass, device, DysonPureHotCoolState) - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT - attributes = state.attributes - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert attributes[ATTR_FAN_MODE] == FAN_DIFFUSE - - device.state.heat_mode = HeatMode.HEAT_OFF.value - await async_update_device(hass, device, DysonPureHotCoolState) - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_COOL - attributes = state.attributes - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL - - -@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) -async def test_state_purehotcool(hass: HomeAssistant, device: DysonPureHotCool) -> None: - """Test common state and attributes of a PureHotCool entity.""" - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT - attributes = state.attributes - assert attributes[ATTR_HVAC_MODES] == SUPPORT_HVAC_PCOOL - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - assert attributes[ATTR_FAN_MODE] == FAN_AUTO - assert attributes[ATTR_FAN_MODES] == SUPPORT_FAN_PCOOL - - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - device.state.auto_mode = AutoMode.AUTO_OFF.value - await async_update_device(hass, device, DysonPureHotCoolV2State) - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT - attributes = state.attributes - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert attributes[ATTR_FAN_MODE] == FAN_OFF - - device.state.heat_mode = HeatMode.HEAT_OFF.value - device.state.fan_state = FanState.FAN_ON.value - device.state.speed = FanSpeed.FAN_SPEED_1.value - await async_update_device(hass, device, DysonPureHotCoolV2State) - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_COOL - attributes = state.attributes - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL - assert attributes[ATTR_FAN_MODE] == FAN_LOW - - device.state.fan_power = FanPower.POWER_OFF.value - await async_update_device(hass, device, DysonPureHotCoolV2State) - state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_OFF - attributes = state.attributes - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF - - -@pytest.mark.parametrize( - "service,service_data,configuration_data", - [ - ( - SERVICE_SET_TEMPERATURE, - {ATTR_TEMPERATURE: -5}, - {"heat_target": "2740", "heat_mode": HeatMode.HEAT_ON}, - ), - ( - SERVICE_SET_TEMPERATURE, - {ATTR_TEMPERATURE: 40}, - {"heat_target": "3100", "heat_mode": HeatMode.HEAT_ON}, - ), - ( - SERVICE_SET_TEMPERATURE, - {ATTR_TEMPERATURE: 20}, - {"heat_target": "2930", "heat_mode": HeatMode.HEAT_ON}, - ), - ( - SERVICE_SET_FAN_MODE, - {ATTR_FAN_MODE: FAN_FOCUS}, - {"focus_mode": FocusMode.FOCUS_ON}, - ), - ( - SERVICE_SET_FAN_MODE, - {ATTR_FAN_MODE: FAN_DIFFUSE}, - {"focus_mode": FocusMode.FOCUS_OFF}, - ), - ( - SERVICE_SET_HVAC_MODE, - {ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - {"heat_mode": HeatMode.HEAT_ON}, - ), - ( - SERVICE_SET_HVAC_MODE, - {ATTR_HVAC_MODE: HVAC_MODE_COOL}, - {"heat_mode": HeatMode.HEAT_OFF}, - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureHotCoolLink], indirect=True) -async def test_commands_purehotcoollink( - hass: HomeAssistant, - device: DysonPureHotCoolLink, - service: str, - service_data: dict, - configuration_data: dict, -) -> None: - """Test sending commands to a PureHotCoolLink entity.""" - await hass.services.async_call( - PLATFORM_DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - device.set_configuration.assert_called_once_with(**configuration_data) - - -@pytest.mark.parametrize( - "service,service_data,command,command_args", - [ - (SERVICE_SET_TEMPERATURE, {ATTR_TEMPERATURE: 20}, "set_heat_target", ["2930"]), - (SERVICE_SET_FAN_MODE, {ATTR_FAN_MODE: FAN_OFF}, "turn_off", []), - ( - SERVICE_SET_FAN_MODE, - {ATTR_FAN_MODE: FAN_LOW}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_4], - ), - ( - SERVICE_SET_FAN_MODE, - {ATTR_FAN_MODE: FAN_MEDIUM}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_7], - ), - ( - SERVICE_SET_FAN_MODE, - {ATTR_FAN_MODE: FAN_HIGH}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_10], - ), - (SERVICE_SET_FAN_MODE, {ATTR_FAN_MODE: FAN_AUTO}, "enable_auto_mode", []), - (SERVICE_SET_HVAC_MODE, {ATTR_HVAC_MODE: HVAC_MODE_OFF}, "turn_off", []), - ( - SERVICE_SET_HVAC_MODE, - {ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - "enable_heat_mode", - [], - ), - ( - SERVICE_SET_HVAC_MODE, - {ATTR_HVAC_MODE: HVAC_MODE_COOL}, - "disable_heat_mode", - [], - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) -async def test_commands_purehotcool( - hass: HomeAssistant, - device: DysonPureHotCoolLink, - service: str, - service_data: dict, - command: str, - command_args: list, -) -> None: - """Test sending commands to a PureHotCool entity.""" - await hass.services.async_call( - PLATFORM_DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - getattr(device, command).assert_called_once_with(*command_args) - - -@pytest.mark.parametrize("hvac_mode", [HVAC_MODE_HEAT, HVAC_MODE_COOL]) -@pytest.mark.parametrize( - "fan_power,turn_on_call_count", - [ - (FanPower.POWER_ON.value, 0), - (FanPower.POWER_OFF.value, 1), - ], -) -@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) -async def test_set_hvac_mode_purehotcool( - hass: HomeAssistant, - device: DysonPureHotCoolLink, - hvac_mode: str, - fan_power: str, - turn_on_call_count: int, -) -> None: - """Test setting HVAC mode of a PureHotCool entity turns on the device when it's off.""" - device.state.fan_power = fan_power - await async_update_device(hass, device) - await hass.services.async_call( - PLATFORM_DOMAIN, - SERVICE_SET_HVAC_MODE, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_HVAC_MODE: hvac_mode, - }, - blocking=True, - ) - assert device.turn_on.call_count == turn_on_call_count diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py deleted file mode 100644 index 67149ff7f2e..00000000000 --- a/tests/components/dyson/test_fan.py +++ /dev/null @@ -1,441 +0,0 @@ -"""Test the Dyson fan component.""" -from __future__ import annotations - -from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation -from libpurecool.dyson_pure_cool import DysonPureCool, DysonPureCoolLink -from libpurecool.dyson_pure_state import DysonPureCoolState -from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State -import pytest - -from homeassistant.components.dyson import DOMAIN -from homeassistant.components.dyson.fan import ( - ATTR_ANGLE_HIGH, - ATTR_ANGLE_LOW, - ATTR_AUTO_MODE, - ATTR_CARBON_FILTER, - ATTR_DYSON_SPEED, - ATTR_DYSON_SPEED_LIST, - ATTR_FLOW_DIRECTION_FRONT, - ATTR_HEPA_FILTER, - ATTR_NIGHT_MODE, - ATTR_TIMER, - PRESET_MODE_AUTO, - SERVICE_SET_ANGLE, - SERVICE_SET_AUTO_MODE, - SERVICE_SET_DYSON_SPEED, - SERVICE_SET_FLOW_DIRECTION_FRONT, - SERVICE_SET_NIGHT_MODE, - SERVICE_SET_TIMER, -) -from homeassistant.components.fan import ( - ATTR_OSCILLATING, - ATTR_PERCENTAGE, - ATTR_PRESET_MODE, - ATTR_SPEED, - ATTR_SPEED_LIST, - DOMAIN as PLATFORM_DOMAIN, - SERVICE_OSCILLATE, - SERVICE_SET_SPEED, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er - -from .common import ( - ENTITY_NAME, - NAME, - SERIAL, - async_get_purecool_device, - async_get_purecoollink_device, - async_update_device, -) - -ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" - - -@callback -def async_get_device(spec: type[DysonPureCoolLink]) -> DysonPureCoolLink: - """Return a Dyson fan device.""" - if spec == DysonPureCoolLink: - return async_get_purecoollink_device() - return async_get_purecool_device() - - -@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) -async def test_state_purecoollink( - hass: HomeAssistant, device: DysonPureCoolLink -) -> None: - """Test the state of a PureCoolLink fan.""" - entity_registry = er.async_get(hass) - assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL - - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_ON - assert state.name == NAME - attributes = state.attributes - assert attributes[ATTR_NIGHT_MODE] is True - assert attributes[ATTR_OSCILLATING] is True - assert attributes[ATTR_PERCENTAGE] == 10 - assert attributes[ATTR_PRESET_MODE] is None - assert attributes[ATTR_SPEED] == SPEED_LOW - assert attributes[ATTR_SPEED_LIST] == [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - PRESET_MODE_AUTO, - ] - assert attributes[ATTR_DYSON_SPEED] == 1 - assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) - assert attributes[ATTR_AUTO_MODE] is False - assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - - device.state.fan_mode = FanMode.OFF.value - await async_update_device(hass, device, DysonPureCoolState) - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF - - device.state.fan_mode = FanMode.AUTO.value - device.state.speed = FanSpeed.FAN_SPEED_AUTO.value - device.state.night_mode = "OFF" - device.state.oscillation = "OFF" - await async_update_device(hass, device, DysonPureCoolState) - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_ON - attributes = state.attributes - assert attributes[ATTR_NIGHT_MODE] is False - assert attributes[ATTR_OSCILLATING] is False - assert attributes[ATTR_PERCENTAGE] is None - assert attributes[ATTR_PRESET_MODE] == "auto" - assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO - assert attributes[ATTR_DYSON_SPEED] == "AUTO" - assert attributes[ATTR_AUTO_MODE] is True - - -@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) -async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> None: - """Test the state of a PureCool fan.""" - entity_registry = er.async_get(hass) - assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL - - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_ON - assert state.name == NAME - attributes = state.attributes - assert attributes[ATTR_NIGHT_MODE] is True - assert attributes[ATTR_OSCILLATING] is True - assert attributes[ATTR_ANGLE_LOW] == 24 - assert attributes[ATTR_ANGLE_HIGH] == 254 - assert attributes[ATTR_PERCENTAGE] == 10 - assert attributes[ATTR_PRESET_MODE] is None - assert attributes[ATTR_SPEED] == SPEED_LOW - assert attributes[ATTR_SPEED_LIST] == [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - PRESET_MODE_AUTO, - ] - assert attributes[ATTR_DYSON_SPEED] == 1 - assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) - assert attributes[ATTR_AUTO_MODE] is False - assert attributes[ATTR_FLOW_DIRECTION_FRONT] is True - assert attributes[ATTR_TIMER] == "OFF" - assert attributes[ATTR_HEPA_FILTER] == 100 - assert attributes[ATTR_CARBON_FILTER] == 100 - assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - - device.state.auto_mode = "ON" - device.state.night_mode = "OFF" - device.state.oscillation = "OIOF" - device.state.speed = "AUTO" - device.state.front_direction = "OFF" - device.state.sleep_timer = "0120" - device.state.carbon_filter_state = "INV" - await async_update_device(hass, device, DysonPureCoolV2State) - state = hass.states.get(ENTITY_ID) - attributes = state.attributes - assert attributes[ATTR_NIGHT_MODE] is False - assert attributes[ATTR_OSCILLATING] is False - assert attributes[ATTR_PERCENTAGE] is None - assert attributes[ATTR_PRESET_MODE] == "auto" - assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO - assert attributes[ATTR_DYSON_SPEED] == "AUTO" - assert attributes[ATTR_AUTO_MODE] is True - assert attributes[ATTR_FLOW_DIRECTION_FRONT] is False - assert attributes[ATTR_TIMER] == "0120" - assert attributes[ATTR_CARBON_FILTER] == "INV" - - device.state.fan_power = "OFF" - await async_update_device(hass, device, DysonPureCoolV2State) - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF - - -@pytest.mark.parametrize( - "service,service_data,configuration_args", - [ - (SERVICE_TURN_ON, {}, {"fan_mode": FanMode.FAN}), - ( - SERVICE_TURN_ON, - {ATTR_SPEED: SPEED_LOW}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, - ), - ( - SERVICE_TURN_ON, - {ATTR_PERCENTAGE: 40}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, - ), - (SERVICE_TURN_OFF, {}, {"fan_mode": FanMode.OFF}), - ( - SERVICE_OSCILLATE, - {ATTR_OSCILLATING: True}, - {"oscillation": Oscillation.OSCILLATION_ON}, - ), - ( - SERVICE_OSCILLATE, - {ATTR_OSCILLATING: False}, - {"oscillation": Oscillation.OSCILLATION_OFF}, - ), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_LOW}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, - ), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_MEDIUM}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_7}, - ), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_HIGH}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_10}, - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) -async def test_commands_purecoollink( - hass: HomeAssistant, - device: DysonPureCoolLink, - service: str, - service_data: dict, - configuration_args: dict, -) -> None: - """Test sending commands to a PureCoolLink fan.""" - await hass.services.async_call( - PLATFORM_DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - device.set_configuration.assert_called_once_with(**configuration_args) - - -@pytest.mark.parametrize( - "service,service_data,command,command_args", - [ - (SERVICE_TURN_ON, {}, "turn_on", []), - ( - SERVICE_TURN_ON, - {ATTR_SPEED: SPEED_LOW}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_4], - ), - ( - SERVICE_TURN_ON, - {ATTR_PERCENTAGE: 40}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_4], - ), - ( - SERVICE_TURN_ON, - {ATTR_PRESET_MODE: "auto"}, - "enable_auto_mode", - [], - ), - (SERVICE_TURN_OFF, {}, "turn_off", []), - (SERVICE_OSCILLATE, {ATTR_OSCILLATING: True}, "enable_oscillation", []), - (SERVICE_OSCILLATE, {ATTR_OSCILLATING: False}, "disable_oscillation", []), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_LOW}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_4], - ), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_MEDIUM}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_7], - ), - ( - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_HIGH}, - "set_fan_speed", - [FanSpeed.FAN_SPEED_10], - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) -async def test_commands_purecool( - hass: HomeAssistant, - device: DysonPureCool, - service: str, - service_data: dict, - command: str, - command_args: list, -) -> None: - """Test sending commands to a PureCool fan.""" - await hass.services.async_call( - PLATFORM_DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - getattr(device, command).assert_called_once_with(*command_args) - - -@pytest.mark.parametrize( - "service,service_data,configuration_args", - [ - ( - SERVICE_SET_NIGHT_MODE, - {ATTR_NIGHT_MODE: True}, - {"night_mode": NightMode.NIGHT_MODE_ON}, - ), - ( - SERVICE_SET_NIGHT_MODE, - {ATTR_NIGHT_MODE: False}, - {"night_mode": NightMode.NIGHT_MODE_OFF}, - ), - (SERVICE_SET_AUTO_MODE, {"auto_mode": True}, {"fan_mode": FanMode.AUTO}), - (SERVICE_SET_AUTO_MODE, {"auto_mode": False}, {"fan_mode": FanMode.FAN}), - ( - SERVICE_SET_DYSON_SPEED, - {ATTR_DYSON_SPEED: "4"}, - {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) -async def test_custom_services_purecoollink( - hass: HomeAssistant, - device: DysonPureCoolLink, - service: str, - service_data: dict, - configuration_args: dict, -) -> None: - """Test custom services of a PureCoolLink fan.""" - await hass.services.async_call( - DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - device.set_configuration.assert_called_once_with(**configuration_args) - - -@pytest.mark.parametrize( - "service,service_data,command,command_args", - [ - (SERVICE_SET_NIGHT_MODE, {ATTR_NIGHT_MODE: True}, "enable_night_mode", []), - (SERVICE_SET_NIGHT_MODE, {ATTR_NIGHT_MODE: False}, "disable_night_mode", []), - (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: True}, "enable_auto_mode", []), - (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: False}, "disable_auto_mode", []), - (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: False}, "disable_auto_mode", []), - ( - SERVICE_SET_ANGLE, - {ATTR_ANGLE_LOW: 10, ATTR_ANGLE_HIGH: 200}, - "enable_oscillation", - [10, 200], - ), - ( - SERVICE_SET_FLOW_DIRECTION_FRONT, - {ATTR_FLOW_DIRECTION_FRONT: True}, - "enable_frontal_direction", - [], - ), - ( - SERVICE_SET_FLOW_DIRECTION_FRONT, - {ATTR_FLOW_DIRECTION_FRONT: False}, - "disable_frontal_direction", - [], - ), - (SERVICE_SET_TIMER, {ATTR_TIMER: 0}, "disable_sleep_timer", []), - (SERVICE_SET_TIMER, {ATTR_TIMER: 10}, "enable_sleep_timer", [10]), - ( - SERVICE_SET_DYSON_SPEED, - {ATTR_DYSON_SPEED: "4"}, - "set_fan_speed", - [FanSpeed("0004")], - ), - ], -) -@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) -async def test_custom_services_purecool( - hass: HomeAssistant, - device: DysonPureCool, - service: str, - service_data: dict, - command: str, - command_args: list, -) -> None: - """Test custom services of a PureCool fan.""" - await hass.services.async_call( - DOMAIN, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **service_data, - }, - blocking=True, - ) - getattr(device, command).assert_called_once_with(*command_args) - - -@pytest.mark.parametrize( - "domain,service,data", - [ - (PLATFORM_DOMAIN, SERVICE_TURN_ON, {ATTR_SPEED: "AUTO"}), - (PLATFORM_DOMAIN, SERVICE_SET_SPEED, {ATTR_SPEED: "AUTO"}), - (DOMAIN, SERVICE_SET_DYSON_SPEED, {ATTR_DYSON_SPEED: "11"}), - ], -) -@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) -async def test_custom_services_invalid_data( - hass: HomeAssistant, device: DysonPureCool, domain: str, service: str, data: dict -) -> None: - """Test custom services calling with invalid data.""" - with pytest.raises(ValueError): - await hass.services.async_call( - domain, - service, - { - ATTR_ENTITY_ID: ENTITY_ID, - **data, - }, - blocking=True, - ) diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py deleted file mode 100644 index 714ac919c19..00000000000 --- a/tests/components/dyson/test_init.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Test the parent Dyson component.""" -import copy -from unittest.mock import MagicMock, patch - -from homeassistant.components.dyson import DOMAIN -from homeassistant.const import CONF_DEVICES -from homeassistant.core import HomeAssistant - -from .common import ( - BASE_PATH, - CONFIG, - ENTITY_NAME, - IP_ADDRESS, - async_get_360eye_device, - async_get_purecool_device, - async_get_purecoollink_device, -) - -from tests.common import async_setup_component - - -async def test_setup_manual(hass: HomeAssistant): - """Test set up the component with manually configured device IPs.""" - SERIAL_TEMPLATE = "XX-XXXXX-X{}" - - # device1 works - device1 = async_get_purecoollink_device() - device1.serial = SERIAL_TEMPLATE.format(1) - - # device2 failed to connect - device2 = async_get_purecool_device() - device2.serial = SERIAL_TEMPLATE.format(2) - device2.connect = MagicMock(return_value=False) - - # device3 throws exception during connection - device3 = async_get_360eye_device() - device3.serial = SERIAL_TEMPLATE.format(3) - device3.connect = MagicMock(side_effect=OSError) - - # device4 not configured in configuration - device4 = async_get_360eye_device() - device4.serial = SERIAL_TEMPLATE.format(4) - - devices = [device1, device2, device3, device4] - config = copy.deepcopy(CONFIG) - config[DOMAIN][CONF_DEVICES] = [ - { - "device_id": SERIAL_TEMPLATE.format(i), - "device_ip": IP_ADDRESS, - } - for i in [1, 2, 3, 5] # 1 device missing and 1 device not existed - ] - - with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True) as login, patch( - f"{BASE_PATH}.DysonAccount.devices", return_value=devices - ) as devices_method, patch( - f"{BASE_PATH}.PLATFORMS", ["fan", "vacuum"] - ): # Patch platforms to get rid of sensors - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - login.assert_called_once_with() - devices_method.assert_called_once_with() - - # Only one fan and zero vacuum is set up successfully - assert hass.states.async_entity_ids() == [f"fan.{ENTITY_NAME}"] - device1.connect.assert_called_once_with(IP_ADDRESS) - device2.connect.assert_called_once_with(IP_ADDRESS) - device3.connect.assert_called_once_with(IP_ADDRESS) - device4.connect.assert_not_called() - - -async def test_setup_autoconnect(hass: HomeAssistant): - """Test set up the component with auto connect.""" - # device1 works - device1 = async_get_purecoollink_device() - - # device2 failed to auto connect - device2 = async_get_purecool_device() - device2.auto_connect = MagicMock(return_value=False) - - devices = [device1, device2] - config = copy.deepcopy(CONFIG) - config[DOMAIN].pop(CONF_DEVICES) - - with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( - f"{BASE_PATH}.DysonAccount.devices", return_value=devices - ), patch( - f"{BASE_PATH}.PLATFORMS", ["fan"] - ): # Patch platforms to get rid of sensors - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.async_entity_ids_count() == 1 - - -async def test_login_failed(hass: HomeAssistant): - """Test login failure during setup.""" - with patch(f"{BASE_PATH}.DysonAccount.login", return_value=False): - assert not await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py deleted file mode 100644 index 5bd6fd85c3a..00000000000 --- a/tests/components/dyson/test_sensor.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Test the Dyson sensor(s) component.""" -from __future__ import annotations - -from unittest.mock import patch - -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_cool_link import DysonPureCoolLink -import pytest - -from homeassistant.components.dyson import DOMAIN -from homeassistant.components.dyson.sensor import SENSOR_ATTRIBUTES, SENSOR_NAMES -from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem - -from .common import ( - BASE_PATH, - CONFIG, - ENTITY_NAME, - NAME, - SERIAL, - async_get_basic_device, - async_update_device, -) - -from tests.common import async_setup_component - -ENTITY_ID_PREFIX = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" - -MOCKED_VALUES = { - "filter_life": 100, - "dust": 5, - "humidity": 45, - "temperature_kelvin": 295, - "temperature": 21.9, - "air_quality": 5, - "hepa_filter_state": 50, - "combi_filter_state": 50, - "carbon_filter_state": 10, -} - -MOCKED_UPDATED_VALUES = { - "filter_life": 30, - "dust": 2, - "humidity": 80, - "temperature_kelvin": 240, - "temperature": -33.1, - "air_quality": 3, - "hepa_filter_state": 30, - "combi_filter_state": 30, - "carbon_filter_state": 20, -} - - -@callback -def _async_assign_values( - device: DysonPureCoolLink, values=MOCKED_VALUES, combi=False -) -> None: - """Assign mocked values to the device.""" - if isinstance(device, DysonPureCool): - device.state.hepa_filter_state = values["hepa_filter_state"] - device.state.carbon_filter_state = ( - "INV" if combi else values["carbon_filter_state"] - ) - device.environmental_state.humidity = values["humidity"] - device.environmental_state.temperature = values["temperature_kelvin"] - else: # DysonPureCoolLink - device.state.filter_life = values["filter_life"] - device.environmental_state.dust = values["dust"] - device.environmental_state.humidity = values["humidity"] - device.environmental_state.temperature = values["temperature_kelvin"] - device.environmental_state.volatil_organic_compounds = values["air_quality"] - - -@callback -def async_get_device(spec: type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink: - """Return a device of the given type.""" - device = async_get_basic_device(spec) - _async_assign_values(device, combi=combi) - return device - - -@callback -def _async_get_entity_id(sensor_type: str) -> str: - """Get the expected entity id from the type of the sensor.""" - sensor_name = SENSOR_NAMES[sensor_type] - entity_id_suffix = sensor_name.lower().replace(" ", "_") - return f"{ENTITY_ID_PREFIX}_{entity_id_suffix}" - - -@pytest.mark.parametrize( - "device,sensors", - [ - ( - DysonPureCoolLink, - ["filter_life", "dust", "humidity", "temperature", "air_quality"], - ), - ( - DysonPureCool, - ["hepa_filter_state", "carbon_filter_state", "humidity", "temperature"], - ), - ( - [DysonPureCool, True], - ["combi_filter_state", "humidity", "temperature"], - ), - ], - indirect=["device"], -) -async def test_sensors( - hass: HomeAssistant, device: DysonPureCoolLink, sensors: list[str] -) -> None: - """Test the sensors.""" - # Temperature is given by the device in kelvin - # Make sure no other sensors are set up - assert len(hass.states.async_all()) == len(sensors) - - entity_registry = er.async_get(hass) - for sensor in sensors: - entity_id = _async_get_entity_id(sensor) - - # Test unique id - assert entity_registry.async_get(entity_id).unique_id == f"{SERIAL}-{sensor}" - - # Test state - state = hass.states.get(entity_id) - assert state.state == str(MOCKED_VALUES[sensor]) - assert state.name == f"{NAME} {SENSOR_NAMES[sensor]}" - - # Test attributes - attributes = state.attributes - for attr, value in SENSOR_ATTRIBUTES[sensor].items(): - assert attributes[attr] == value - - # Test data update - _async_assign_values(device, MOCKED_UPDATED_VALUES) - await async_update_device(hass, device) - for sensor in sensors: - state = hass.states.get(_async_get_entity_id(sensor)) - assert state.state == str(MOCKED_UPDATED_VALUES[sensor]) - - -@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) -async def test_sensors_off(hass: HomeAssistant, device: DysonPureCoolLink) -> None: - """Test the case where temperature and humidity are not available.""" - device.environmental_state.temperature = 0 - device.environmental_state.humidity = 0 - await async_update_device(hass, device) - assert hass.states.get(f"{ENTITY_ID_PREFIX}_temperature").state == STATE_OFF - assert hass.states.get(f"{ENTITY_ID_PREFIX}_humidity").state == STATE_OFF - - -@pytest.mark.parametrize( - "unit_system,temp_unit,temperature", - [(METRIC_SYSTEM, TEMP_CELSIUS, 21.9), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, 71.3)], -) -async def test_temperature( - hass: HomeAssistant, unit_system: UnitSystem, temp_unit: str, temperature: float -) -> None: - """Test the temperature sensor in different units.""" - hass.config.units = unit_system - - device = async_get_device(DysonPureCoolLink) - with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( - f"{BASE_PATH}.DysonAccount.devices", return_value=[device] - ), patch(f"{BASE_PATH}.PLATFORMS", [PLATFORM_DOMAIN]): - # PLATFORMS is patched so that only the platform being tested is set up - await async_setup_component( - hass, - DOMAIN, - CONFIG, - ) - await hass.async_block_till_done() - - state = hass.states.get(f"{ENTITY_ID_PREFIX}_temperature") - assert state.state == str(temperature) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == temp_unit diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py deleted file mode 100644 index b77dee3270f..00000000000 --- a/tests/components/dyson/test_vacuum.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Test the Dyson 360 eye robot vacuum component.""" -from libpurecool.const import Dyson360EyeMode, PowerMode -from libpurecool.dyson_360_eye import Dyson360Eye -import pytest - -from homeassistant.components.dyson.vacuum import ATTR_POSITION, SUPPORT_DYSON -from homeassistant.components.vacuum import ( - ATTR_FAN_SPEED, - ATTR_FAN_SPEED_LIST, - ATTR_STATUS, - DOMAIN as PLATFORM_DOMAIN, - SERVICE_RETURN_TO_BASE, - SERVICE_SET_FAN_SPEED, - SERVICE_START_PAUSE, - SERVICE_STOP, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er - -from .common import ( - ENTITY_NAME, - NAME, - SERIAL, - async_get_360eye_device, - async_update_device, -) - -ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" - - -@callback -def async_get_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye: - """Return a Dyson 360 Eye device.""" - return async_get_360eye_device(state) - - -async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None: - """Test the state of the vacuum.""" - entity_registry = er.async_get(hass) - assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL - - state = hass.states.get(ENTITY_ID) - assert state.name == NAME - assert state.state == STATE_ON - attributes = state.attributes - assert attributes[ATTR_STATUS] == "Cleaning" - assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_DYSON - assert attributes[ATTR_BATTERY_LEVEL] == 85 - assert attributes[ATTR_POSITION] == "(0, 0)" - assert attributes[ATTR_FAN_SPEED] == "Quiet" - assert attributes[ATTR_FAN_SPEED_LIST] == ["Quiet", "Max"] - - device.state.state = Dyson360EyeMode.INACTIVE_CHARGING - device.state.power_mode = PowerMode.MAX - await async_update_device(hass, device) - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF - assert state.attributes[ATTR_STATUS] == "Stopped - Charging" - assert state.attributes[ATTR_FAN_SPEED] == "Max" - - device.state.state = Dyson360EyeMode.FULL_CLEAN_PAUSED - await async_update_device(hass, device) - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF - assert state.attributes[ATTR_STATUS] == "Paused" - - -@pytest.mark.parametrize( - "service,command,device", - [ - (SERVICE_TURN_ON, "start", Dyson360EyeMode.INACTIVE_CHARGED), - (SERVICE_TURN_ON, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), - (SERVICE_TURN_OFF, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), - (SERVICE_STOP, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), - (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), - (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), - (SERVICE_START_PAUSE, "start", Dyson360EyeMode.INACTIVE_CHARGED), - (SERVICE_START_PAUSE, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), - (SERVICE_RETURN_TO_BASE, "abort", Dyson360EyeMode.FULL_CLEAN_PAUSED), - ], - indirect=["device"], -) -async def test_commands( - hass: HomeAssistant, device: Dyson360Eye, service: str, command: str -) -> None: - """Test sending commands to the vacuum.""" - await hass.services.async_call( - PLATFORM_DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True - ) - getattr(device, command).assert_called_once_with() - - -async def test_set_fan_speed(hass: HomeAssistant, device: Dyson360Eye): - """Test setting fan speed of the vacuum.""" - fan_speed_map = { - "Max": PowerMode.MAX, - "Quiet": PowerMode.QUIET, - } - for service_speed, command_speed in fan_speed_map.items(): - await hass.services.async_call( - PLATFORM_DOMAIN, - SERVICE_SET_FAN_SPEED, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: service_speed}, - blocking=True, - ) - device.set_power_mode.assert_called_with(command_speed) From 5418e76c841e1a1ad4e8afa5f3639f084d44d7e3 Mon Sep 17 00:00:00 2001 From: lambtho Date: Tue, 9 Nov 2021 10:40:28 +0100 Subject: [PATCH 0335/1452] IOTA removal (#59380) --- .coveragerc | 1 - homeassistant/components/iota/__init__.py | 78 ---------------- homeassistant/components/iota/manifest.json | 8 -- homeassistant/components/iota/sensor.py | 99 --------------------- requirements_all.txt | 3 - 5 files changed, 189 deletions(-) delete mode 100644 homeassistant/components/iota/__init__.py delete mode 100644 homeassistant/components/iota/manifest.json delete mode 100644 homeassistant/components/iota/sensor.py diff --git a/.coveragerc b/.coveragerc index 1893b9988ae..b6270668913 100644 --- a/.coveragerc +++ b/.coveragerc @@ -498,7 +498,6 @@ omit = homeassistant/components/incomfort/* homeassistant/components/intesishome/* homeassistant/components/ios/* - homeassistant/components/iota/* homeassistant/components/iperf3/* homeassistant/components/iqvia/* homeassistant/components/irish_rail_transport/sensor.py diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py deleted file mode 100644 index 04db9140122..00000000000 --- a/homeassistant/components/iota/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Support for IOTA wallets.""" -from datetime import timedelta - -from iota import Iota -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.entity import Entity - -CONF_IRI = "iri" -CONF_TESTNET = "testnet" -CONF_WALLET_NAME = "name" -CONF_WALLET_SEED = "seed" -CONF_WALLETS = "wallets" - -DOMAIN = "iota" - -IOTA_PLATFORMS = ["sensor"] - -SCAN_INTERVAL = timedelta(minutes=10) - -WALLET_CONFIG = vol.Schema( - { - vol.Required(CONF_WALLET_NAME): cv.string, - vol.Required(CONF_WALLET_SEED): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_IRI): cv.string, - vol.Optional(CONF_TESTNET, default=False): cv.boolean, - vol.Required(CONF_WALLETS): vol.All(cv.ensure_list, [WALLET_CONFIG]), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass, config): - """Set up the IOTA component.""" - iota_config = config[DOMAIN] - - for platform in IOTA_PLATFORMS: - load_platform(hass, platform, DOMAIN, iota_config, config) - - return True - - -class IotaDevice(Entity): - """Representation of a IOTA device.""" - - def __init__(self, name, seed, iri, is_testnet=False): - """Initialise the IOTA device.""" - self._name = name - self._seed = seed - self.iri = iri - self.is_testnet = is_testnet - - @property - def name(self): - """Return the default name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {CONF_WALLET_NAME: self._name} - - @property - def api(self): - """Construct API object for interaction with the IRI node.""" - - return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json deleted file mode 100644 index 36e9a79d8d4..00000000000 --- a/homeassistant/components/iota/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "iota", - "name": "IOTA", - "documentation": "https://www.home-assistant.io/integrations/iota", - "requirements": ["pyota==2.0.5"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py deleted file mode 100644 index 687a4ca35d6..00000000000 --- a/homeassistant/components/iota/sensor.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Support for IOTA wallet sensors.""" -from datetime import timedelta - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_NAME - -from . import CONF_WALLETS, IotaDevice - -ATTR_TESTNET = "testnet" -ATTR_URL = "url" - -CONF_IRI = "iri" -CONF_SEED = "seed" -CONF_TESTNET = "testnet" - -SCAN_INTERVAL = timedelta(minutes=3) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the IOTA sensor.""" - iota_config = discovery_info - sensors = [ - IotaBalanceSensor(wallet, iota_config) for wallet in iota_config[CONF_WALLETS] - ] - - sensors.append(IotaNodeSensor(iota_config=iota_config)) - - add_entities(sensors) - - -class IotaBalanceSensor(IotaDevice, SensorEntity): - """Implement an IOTA sensor for displaying wallets balance.""" - - def __init__(self, wallet_config, iota_config): - """Initialize the sensor.""" - super().__init__( - name=wallet_config[CONF_NAME], - seed=wallet_config[CONF_SEED], - iri=iota_config[CONF_IRI], - is_testnet=iota_config[CONF_TESTNET], - ) - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} Balance" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return "IOTA" - - def update(self): - """Fetch new balance from IRI.""" - self._state = self.api.get_inputs()["totalBalance"] - - -class IotaNodeSensor(IotaDevice, SensorEntity): - """Implement an IOTA sensor for displaying attributes of node.""" - - def __init__(self, iota_config): - """Initialize the sensor.""" - super().__init__( - name="Node Info", - seed=None, - iri=iota_config[CONF_IRI], - is_testnet=iota_config[CONF_TESTNET], - ) - self._state = None - self._attr = {ATTR_URL: self.iri, ATTR_TESTNET: self.is_testnet} - - @property - def name(self): - """Return the name of the sensor.""" - return "IOTA Node" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._attr - - def update(self): - """Fetch new attributes IRI node.""" - node_info = self.api.get_node_info() - self._state = node_info.get("appVersion") - - # convert values to raw string formats - self._attr.update({k: str(v) for k, v in node_info.items()}) diff --git a/requirements_all.txt b/requirements_all.txt index f6007b5ee73..aa08cceb33a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,9 +1696,6 @@ pyopnsense==0.2.0 # homeassistant.components.opple pyoppleio==1.0.5 -# homeassistant.components.iota -pyota==2.0.5 - # homeassistant.components.opentherm_gw pyotgw==1.1b1 From 7e9863f9c8571c65ba25966003806239d2e60bf1 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 9 Nov 2021 01:51:54 -0800 Subject: [PATCH 0336/1452] Removing trailing `\` in tts entity description (#59313) --- homeassistant/components/tts/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index f93a81cf2b2..7dcbe1287cb 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -6,7 +6,7 @@ say: fields: entity_id: name: Entity - description: Name(s) of media player entities.\ + description: Name(s) of media player entities. required: true selector: entity: From 2fd640095203dc0c256b0da6a339887c73f0f03f Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:17:48 +0200 Subject: [PATCH 0337/1452] Bump aioasuswrt to 1.4.0 (#59357) --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index b66c3bb5db9..1470c075b04 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -3,7 +3,7 @@ "name": "ASUSWRT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.3.4"], + "requirements": ["aioasuswrt==1.4.0"], "codeowners": ["@kennedyshead", "@ollo69"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index aa08cceb33a..13028f90ccb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -136,7 +136,7 @@ aio_georss_gdacs==0.5 aioambient==2021.10.1 # homeassistant.components.asuswrt -aioasuswrt==1.3.4 +aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b4fcb1f964..6844e6b2570 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aio_georss_gdacs==0.5 aioambient==2021.10.1 # homeassistant.components.asuswrt -aioasuswrt==1.3.4 +aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 From ff837c736e26a3c7f2fc75c822623fd89e225053 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Nov 2021 11:34:18 +0100 Subject: [PATCH 0338/1452] Return False from state conditions on missing attributes (#59405) --- homeassistant/helpers/condition.py | 21 +++-- tests/helpers/test_condition.py | 130 ++++++++++++++++++++--------- 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a7064640307..5608e61a07f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -335,10 +335,11 @@ def async_numeric_state( # noqa: C901 entity_id = entity.entity_id if attribute is not None and attribute not in entity.attributes: - raise ConditionErrorMessage( - "numeric_state", - f"attribute '{attribute}' (of entity {entity_id}) does not exist", + condition_trace_set_result( + False, + message=f"attribute '{attribute}' of entity {entity_id} does not exist", ) + return False value: Any = None if value_template is None: @@ -356,8 +357,12 @@ def async_numeric_state( # noqa: C901 "numeric_state", f"template error: {ex}" ) from ex - # Known states that never match the numeric condition - if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): + # Known states or attribute values that never match the numeric condition + if value in (None, STATE_UNAVAILABLE, STATE_UNKNOWN): + condition_trace_set_result( + False, + message=f"value '{value}' is non-numeric and treated as False", + ) return False try: @@ -501,9 +506,11 @@ def state( entity_id = entity.entity_id if attribute is not None and attribute not in entity.attributes: - raise ConditionErrorMessage( - "state", f"attribute '{attribute}' (of entity {entity_id}) does not exist" + condition_trace_set_result( + False, + message=f"attribute '{attribute}' of entity {entity_id} does not exist", ) + return False assert isinstance(entity, State) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index b1cbff83e33..cd355129770 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -938,21 +938,6 @@ async def test_state_raises(hass): with pytest.raises(ConditionError, match="unknown entity.*window"): test(hass) - # Unknown attribute - with pytest.raises(ConditionError, match=r"attribute .* does not exist"): - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": "sensor.door", - "attribute": "model", - "state": "acme", - }, - ) - - hass.states.async_set("sensor.door", "open") - test(hass) - # Unknown state entity with pytest.raises(ConditionError, match="input_text.missing"): test = await condition.async_from_config( @@ -968,6 +953,36 @@ async def test_state_raises(hass): test(hass) +async def test_state_unknown_attribute(hass): + """Test that state returns False on unknown attribute.""" + # Unknown attribute + test = await condition.async_from_config( + hass, + { + "condition": "state", + "entity_id": "sensor.door", + "attribute": "model", + "state": "acme", + }, + ) + + hass.states.async_set("sensor.door", "open") + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "entity_id/0": [ + { + "result": { + "result": False, + "message": "attribute 'model' of entity sensor.door does not exist", + } + } + ], + } + ) + + async def test_state_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( @@ -1042,8 +1057,7 @@ async def test_state_attribute(hass): ) hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 200}) - with pytest.raises(ConditionError): - test(hass) + assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": 200}) assert test(hass) @@ -1077,8 +1091,7 @@ async def test_state_attribute_boolean(hass): assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"no_happening": 201}) - with pytest.raises(ConditionError): - test(hass) + assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"happening": False}) assert test(hass) @@ -1180,10 +1193,38 @@ async def test_numeric_state_known_non_matching(hass): # Unavailable state assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "entity_id/0": [ + { + "result": { + "result": False, + "message": "value 'unavailable' is non-numeric and treated as False", + } + } + ], + } + ) + # Unknown state hass.states.async_set("sensor.temperature", "unknown") assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "entity_id/0": [ + { + "result": { + "result": False, + "message": "value 'unknown' is non-numeric and treated as False", + } + } + ], + } + ) + async def test_numeric_state_raises(hass): """Test that numeric_state raises ConditionError on errors.""" @@ -1201,21 +1242,6 @@ async def test_numeric_state_raises(hass): with pytest.raises(ConditionError, match="unknown entity.*humidity"): test(hass) - # Unknown attribute - with pytest.raises(ConditionError, match=r"attribute .* does not exist"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "attribute": "temperature", - "above": 0, - }, - ) - - hass.states.async_set("sensor.temperature", 50) - test(hass) - # Template error with pytest.raises(ConditionError, match="ZeroDivisionError"): test = await condition.async_from_config( @@ -1290,6 +1316,36 @@ async def test_numeric_state_raises(hass): test(hass) +async def test_numeric_state_unknown_attribute(hass): + """Test that numeric_state returns False on unknown attribute.""" + # Unknown attribute + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "attribute": "temperature", + "above": 0, + }, + ) + + hass.states.async_set("sensor.temperature", 50) + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "entity_id/0": [ + { + "result": { + "result": False, + "message": "attribute 'temperature' of entity sensor.temperature does not exist", + } + } + ], + } + ) + + async def test_numeric_state_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( @@ -1338,8 +1394,7 @@ async def test_numeric_state_attribute(hass): ) hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 10}) - with pytest.raises(ConditionError): - assert test(hass) + assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": 49}) assert test(hass) @@ -1351,8 +1406,7 @@ async def test_numeric_state_attribute(hass): assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": None}) - with pytest.raises(ConditionError): - assert test(hass) + assert not test(hass) async def test_numeric_state_using_input_number(hass): From 4c2bf428d63ebaef05b39b14e967a88f3a3979c9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 9 Nov 2021 06:01:12 -0500 Subject: [PATCH 0339/1452] Revert "Add tests for goalzero" (#59407) --- .coveragerc | 4 + homeassistant/components/goalzero/__init__.py | 2 - .../components/goalzero/config_flow.py | 4 +- homeassistant/components/goalzero/const.py | 2 +- homeassistant/components/goalzero/sensor.py | 2 - homeassistant/components/goalzero/switch.py | 2 - tests/components/goalzero/__init__.py | 81 ++---------- .../goalzero/fixtures/info_data.json | 7 -- .../goalzero/fixtures/state_data.json | 38 ------ .../components/goalzero/test_binary_sensor.py | 36 ------ tests/components/goalzero/test_config_flow.py | 117 +++++++++++------- tests/components/goalzero/test_init.py | 91 -------------- tests/components/goalzero/test_sensor.py | 112 ----------------- tests/components/goalzero/test_switch.py | 49 -------- 14 files changed, 94 insertions(+), 453 deletions(-) delete mode 100644 tests/components/goalzero/fixtures/info_data.json delete mode 100644 tests/components/goalzero/fixtures/state_data.json delete mode 100644 tests/components/goalzero/test_binary_sensor.py delete mode 100644 tests/components/goalzero/test_init.py delete mode 100644 tests/components/goalzero/test_sensor.py delete mode 100644 tests/components/goalzero/test_switch.py diff --git a/.coveragerc b/.coveragerc index b6270668913..28a6eff3073 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,6 +386,10 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* + homeassistant/components/goalzero/__init__.py + homeassistant/components/goalzero/binary_sensor.py + homeassistant/components/goalzero/sensor.py + homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 37b9aac7216..774f1fd0e21 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -12,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -102,7 +101,6 @@ class YetiEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self.api.sysdata["macAddress"])}, identifiers={(DOMAIN, self._server_unique_id)}, manufacturer="Goal Zero", model=self.api.sysdata[ATTR_MODEL], diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 73dbc66794a..f192c71cbf8 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -15,7 +15,7 @@ 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, MANUFACTURER +from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Allow the user to confirm adding the device.""" if user_input is not None: return self.async_create_entry( - title=MANUFACTURER, + title="Goal Zero", data={ CONF_HOST: self.ip_address, CONF_NAME: DEFAULT_NAME, diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index fef1636005d..d99cacb253e 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -8,5 +8,5 @@ DATA_KEY_COORDINATOR = "coordinator" DOMAIN = "goalzero" DEFAULT_NAME = "Yeti" DATA_KEY_API = "api" -MANUFACTURER = "Goal Zero" + MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index ed22a9dc26d..6677936b35a 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -38,8 +38,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN -PARALLEL_UPDATES = 0 - SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 8f12251d2d6..6c80a773a74 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -13,8 +13,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN -PARALLEL_UPDATES = 0 - SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="v12PortStatus", diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 36bf707fdc8..1b5302dbc1b 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,50 +1,33 @@ """Tests for the Goal Zero Yeti integration.""" + from unittest.mock import AsyncMock, patch from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS -from homeassistant.components.goalzero import DOMAIN -from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import format_mac -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry, load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker HOST = "1.2.3.4" -MAC = "aa:bb:cc:dd:ee:ff" +NAME = "Yeti" CONF_DATA = { CONF_HOST: HOST, - CONF_NAME: DEFAULT_NAME, + CONF_NAME: NAME, +} + +CONF_CONFIG_FLOW = { + CONF_HOST: HOST, + CONF_NAME: NAME, } CONF_DHCP_FLOW = { - IP_ADDRESS: HOST, - MAC_ADDRESS: format_mac("AA:BB:CC:DD:EE:FF"), - HOSTNAME: "yeti", + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", } -def create_entry(hass: HomeAssistant): - """Add config entry in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=CONF_DATA, - unique_id=MAC, - ) - entry.add_to_hass(hass) - return entry - - -async def _create_mocked_yeti(): +async def _create_mocked_yeti(raise_exception=False): mocked_yeti = AsyncMock() - mocked_yeti.data = {} - mocked_yeti.data["firmwareVersion"] = "1.0.0" - mocked_yeti.sysdata = {} - mocked_yeti.sysdata["model"] = "test_model" - mocked_yeti.sysdata["macAddress"] = MAC + mocked_yeti.get_state = AsyncMock() return mocked_yeti @@ -57,41 +40,3 @@ def _patch_config_flow_yeti(mocked_yeti): "homeassistant.components.goalzero.config_flow.Yeti", return_value=mocked_yeti, ) - - -async def async_init_integration( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - skip_setup: bool = False, -) -> MockConfigEntry: - """Set up the Goal Zero integration in Home Assistant.""" - entry = create_entry(hass) - base_url = f"http://{HOST}/" - aioclient_mock.get( - f"{base_url}state", - text=load_fixture("goalzero/state_data.json"), - ) - aioclient_mock.get( - f"{base_url}sysinfo", - text=load_fixture("goalzero/info_data.json"), - ) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry - - -async def async_setup_platform( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - platform: str, -): - """Set up the platform.""" - entry = await async_init_integration(hass, aioclient_mock) - - with patch("homeassistant.components.goalzero.PLATFORMS", [platform]): - assert await async_setup_component(hass, DOMAIN, {}) - - return entry diff --git a/tests/components/goalzero/fixtures/info_data.json b/tests/components/goalzero/fixtures/info_data.json deleted file mode 100644 index 6be95e6c482..00000000000 --- a/tests/components/goalzero/fixtures/info_data.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name":"yeti123456789012", - "model":"Yeti 1400", - "firmwareVersion":"1.5.7", - "macAddress":"123456789012", - "platform":"esp32" -} \ No newline at end of file diff --git a/tests/components/goalzero/fixtures/state_data.json b/tests/components/goalzero/fixtures/state_data.json deleted file mode 100644 index 455524584f7..00000000000 --- a/tests/components/goalzero/fixtures/state_data.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "thingName":"yeti123456789012", - "v12PortStatus":0, - "usbPortStatus":0, - "acPortStatus":1, - "backlight":1, - "app_online":0, - "wattsIn":0.0, - "ampsIn":0.0, - "wattsOut":50.5, - "ampsOut":2.1, - "whOut":5.23, - "whStored":1330, - "volts":12.0, - "socPercent":95, - "isCharging":0, - "inputDetected":0, - "timeToEmptyFull":-1, - "temperature":25, - "wifiStrength":-62, - "ssid":"wifi", - "ipAddr":"1.2.3.4", - "timestamp":1720984, - "firmwareVersion":"1.5.7", - "version":3, - "ota":{ - "delay":0, - "status":"000-000-100_001-000-100_002-000-100_003-000-100" - }, - "notify":{ - "enabled":1048575, - "trigger":0 - }, - "foreignAcsry":{ - "model":"Yeti MPPT", - "firmwareVersion":"1.1.2" - } -} \ No newline at end of file diff --git a/tests/components/goalzero/test_binary_sensor.py b/tests/components/goalzero/test_binary_sensor.py deleted file mode 100644 index 1c130013283..00000000000 --- a/tests/components/goalzero/test_binary_sensor.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Binary sensor tests for the Goalzero integration.""" -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, - DOMAIN, -) -from homeassistant.components.goalzero.const import DEFAULT_NAME -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - DEVICE_CLASS_POWER, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant - -from . import async_setup_platform - -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): - """Test we get sensor data.""" - await async_setup_platform(hass, aioclient_mock, DOMAIN) - - state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_backlight") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_app_online") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CONNECTIVITY - state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_charging") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY_CHARGING - state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_input_detected") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 320731486ce..838a0f8a124 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -3,26 +3,44 @@ from unittest.mock import patch from goalzero import exceptions -from homeassistant import data_entry_flow -from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER +from homeassistant.components.goalzero.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER -from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from . import ( + CONF_CONFIG_FLOW, CONF_DATA, CONF_DHCP_FLOW, - MAC, + CONF_HOST, + CONF_NAME, + NAME, _create_mocked_yeti, _patch_config_flow_yeti, - create_entry, ) +from tests.common import MockConfigEntry + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) + def _patch_setup(): - return patch("homeassistant.components.goalzero.async_setup_entry") + return patch( + "homeassistant.components.goalzero.async_setup_entry", + return_value=True, + ) -async def test_flow_user(hass: HomeAssistant): +async def test_flow_user(hass): """Test user initialized flow.""" mocked_yeti = await _create_mocked_yeti() with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): @@ -32,62 +50,74 @@ async def test_flow_user(hass: HomeAssistant): ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_DATA, + user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME assert result["data"] == CONF_DATA - assert result["result"].unique_id == MAC -async def test_flow_user_already_configured(hass: HomeAssistant): +async def test_flow_user_already_configured(hass): """Test user initialized flow with duplicate server.""" - create_entry(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4", CONF_NAME: "Yeti"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + entry.add_to_hass(hass) + + service_info = { + "host": "1.2.3.4", + "name": "Yeti", + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_flow_user_cannot_connect(hass: HomeAssistant): +async def test_flow_user_cannot_connect(hass): """Test user initialized flow with unreachable server.""" - with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"]["base"] == "cannot_connect" + assert result["errors"] == {"base": "cannot_connect"} -async def test_flow_user_invalid_host(hass: HomeAssistant): +async def test_flow_user_invalid_host(hass): """Test user initialized flow with invalid server.""" - with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.InvalidHost result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"]["base"] == "invalid_host" + assert result["errors"] == {"base": "invalid_host"} -async def test_flow_user_unknown_error(hass: HomeAssistant): +async def test_flow_user_unknown_error(hass): """Test user initialized flow with unreachable server.""" - with _patch_config_flow_yeti(await _create_mocked_yeti()) as yetimock: + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = Exception result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"]["base"] == "unknown" + assert result["errors"] == {"base": "unknown"} -async def test_dhcp_discovery(hass: HomeAssistant): +async def test_dhcp_discovery(hass): """Test we can process the discovery from dhcp.""" mocked_yeti = await _create_mocked_yeti() @@ -97,30 +127,31 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == "form" result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MANUFACTURER - assert result["data"] == CONF_DATA - assert result["result"].unique_id == MAC + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_NAME: "Yeti", + } result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_dhcp_discovery_failed(hass: HomeAssistant): +async def test_dhcp_discovery_failed(hass): """Test failed setup from dhcp.""" - mocked_yeti = await _create_mocked_yeti() + mocked_yeti = await _create_mocked_yeti(True) with _patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( @@ -128,7 +159,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" with _patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -138,7 +169,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "invalid_host" with _patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -148,5 +179,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "unknown" diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py deleted file mode 100644 index 5a649debee2..00000000000 --- a/tests/components/goalzero/test_init.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Test Goal Zero integration.""" -from datetime import timedelta -from unittest.mock import patch - -from goalzero import exceptions - -from homeassistant.components.goalzero.const import ( - DATA_KEY_COORDINATOR, - DEFAULT_NAME, - DOMAIN, - MANUFACTURER, -) -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -import homeassistant.util.dt as dt_util - -from . import ( - CONF_DATA, - _create_mocked_yeti, - _patch_init_yeti, - async_init_integration, - create_entry, -) - -from tests.common import async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_setup_config_and_unload(hass: HomeAssistant): - """Test Goal Zero setup and unload.""" - entry = create_entry(hass) - with _patch_init_yeti(await _create_mocked_yeti()): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.data == CONF_DATA - - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state is ConfigEntryState.NOT_LOADED - assert not hass.data.get(DOMAIN) - - -async def test_async_setup_entry_not_ready(hass: HomeAssistant): - """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" - entry = create_entry(hass) - with patch( - "homeassistant.components.goalzero.Yeti.init_connect", - side_effect=exceptions.ConnectError, - ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_update_failed( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test data update failure.""" - entry = await async_init_integration(hass, aioclient_mock) - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_KEY_COORDINATOR - ] - with patch( - "homeassistant.components.goalzero.Yeti.get_state", - side_effect=exceptions.ConnectError, - ) as updater: - next_update = dt_util.utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - updater.assert_called_once() - assert not coordinator.last_update_success - - -async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): - """Test device info.""" - entry = await async_init_integration(hass, aioclient_mock) - device_registry = await dr.async_get_registry(hass) - - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) - - assert device.connections == {("mac", "12:34:56:78:90:12")} - assert device.identifiers == {(DOMAIN, entry.entry_id)} - assert device.manufacturer == MANUFACTURER - assert device.model == "Yeti 1400" - assert device.name == DEFAULT_NAME - assert device.sw_version == "1.5.7" diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py deleted file mode 100644 index 592c43b5d43..00000000000 --- a/tests/components/goalzero/test_sensor.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Sensor tests for the Goalzero integration.""" -from homeassistant.components.goalzero.const import DEFAULT_NAME -from homeassistant.components.goalzero.sensor import SENSOR_TYPES -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - 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 . import async_setup_platform - -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): - """Test we get sensor data.""" - for description in SENSOR_TYPES: - description.entity_registry_enabled_default = True - await async_setup_platform(hass, aioclient_mock, DOMAIN) - - state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") - assert state.state == "0.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") - assert state.state == "0.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") - assert state.state == "50.5" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") - assert state.state == "2.1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") - assert state.state == "5.23" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") - assert state.state == "1330" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") - assert state.state == "12.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") - assert state.state == "95" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") - assert state.state == "-1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == TIME_MINUTES - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") - assert state.state == "25" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") - assert state.state == "-62" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") - assert state.state == "1720984" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid") - assert state.state == "wifi" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_ip_address") - assert state.state == "1.2.3.4" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None diff --git a/tests/components/goalzero/test_switch.py b/tests/components/goalzero/test_switch.py deleted file mode 100644 index d2fd0216f45..00000000000 --- a/tests/components/goalzero/test_switch.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Switch tests for the Goalzero integration.""" -from homeassistant.components.goalzero.const import DEFAULT_NAME -from homeassistant.components.switch import DOMAIN as DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant - -from . import async_setup_platform - -from tests.common import load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_switches_states( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -): - """Test we get sensor data.""" - aioclient_mock.post( - "http://1.2.3.4/state", - text=load_fixture("goalzero/state_data.json"), - ) - await async_setup_platform(hass, aioclient_mock, DOMAIN) - - entity_id = f"switch.{DEFAULT_NAME}_12v_port_status" - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: [entity_id]}, - blocking=True, - ) - entity_id = f"switch.{DEFAULT_NAME}_usb_port_status" - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - entity_id = f"switch.{DEFAULT_NAME}_ac_port_status" - state = hass.states.get(entity_id) - assert state.state == STATE_ON - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: [entity_id]}, - blocking=True, - ) From b5ce84cd89868d18cceddf6eeff434c707a50d46 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Nov 2021 12:04:14 +0100 Subject: [PATCH 0340/1452] Add MQTT button (#59348) --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/button.py | 88 ++++++ homeassistant/components/mqtt/discovery.py | 1 + tests/components/mqtt/test_button.py | 266 ++++++++++++++++++ tests/components/mqtt/test_discovery.py | 7 + 5 files changed, 363 insertions(+) create mode 100644 homeassistant/components/mqtt/button.py create mode 100644 tests/components/mqtt/test_button.py diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 71a8d109c3f..1daa6f837c7 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -135,6 +135,7 @@ ABBREVIATIONS = { "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_prs": "payload_press", "pl_rst": "payload_reset", "pl_rst_hum": "payload_reset_humidity", "pl_rst_mode": "payload_reset_mode", diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py new file mode 100644 index 00000000000..4006b8bfab9 --- /dev/null +++ b/homeassistant/components/mqtt/button.py @@ -0,0 +1,88 @@ +"""Support for MQTT buttons.""" +from __future__ import annotations + +import functools + +import voluptuous as vol + +from homeassistant.components import button +from homeassistant.components.button import ButtonEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType + +from . import PLATFORMS +from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper + +CONF_PAYLOAD_PRESS = "payload_press" +DEFAULT_NAME = "MQTT Button" +DEFAULT_PAYLOAD_PRESS = "PRESS" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT button through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(hass, async_add_entities, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT button dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, button.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass, async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT button.""" + async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) + + +class MqttButton(MqttEntity, ButtonEntity): + """Representation of a switch that can be toggled using MQTT.""" + + _entity_id_format = button.ENTITY_ID_FORMAT + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the MQTT button.""" + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return DISCOVERY_SCHEMA + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + async def async_press(self, **kwargs): + """Turn the device on. + + This method is a coroutine. + """ + await mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + self._config[CONF_PAYLOAD_PRESS], + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d8ab9193cc4..d490374ed53 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -38,6 +38,7 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ "alarm_control_panel", "binary_sensor", + "button", "camera", "climate", "cover", diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py new file mode 100644 index 00000000000..0a1d8af2a0c --- /dev/null +++ b/tests/components/mqtt/test_button.py @@ -0,0 +1,266 @@ +"""The tests for the MQTT button platform.""" +import copy +from unittest.mock import patch + +import pytest + +from homeassistant.components import button +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +DEFAULT_CONFIG = { + button.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} +} + + +@pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") +async def test_sending_mqtt_commands(hass, mqtt_mock): + """Test the sending MQTT commands.""" + assert await async_setup_component( + hass, + button.DOMAIN, + { + button.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "object_id": "test_button", + "payload_press": "beer press", + "platform": "mqtt", + "qos": "2", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.test_button") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_button"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "beer press", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("button.test_button") + assert state.state == "2021-11-08T13:31:44+00:00" + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + config = { + button.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } + } + + await help_test_default_availability_payload( + hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = { + button.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } + } + + await help_test_custom_availability_payload( + hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG, None + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one button per unique_id.""" + config = { + button.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, button.DOMAIN, config) + + +async def test_discovery_removal_button(hass, mqtt_mock, caplog): + """Test removal of discovered button.""" + data = '{ "name": "test", "command_topic": "test_topic" }' + await help_test_discovery_removal(hass, mqtt_mock, caplog, button.DOMAIN, data) + + +async def test_discovery_update_button(hass, mqtt_mock, caplog): + """Test update of discovered button.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + button.DOMAIN, + config1, + config2, + ) + + +async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): + """Test update of discovered button.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.button.MqttButton.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, button.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' + await help_test_discovery_broken( + hass, mqtt_mock, caplog, button.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT button device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT button device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 08eb4b882c5..a97047195fb 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -208,6 +208,13 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): "Hello World 2", "binary_sensor", ), + ( + "homeassistant/button/object/bla/config", + '{ "name": "Hello World button", "obj_id": "hello_id", "command_topic": "test-topic" }', + "button.hello_id", + "Hello World button", + "button", + ), ( "homeassistant/camera/object/bla/config", '{ "name": "Hello World 3", "obj_id": "hello_id", "state_topic": "test-topic", "topic": "test-topic" }', From 23fad6076968610a2c1559dac27990347b2826d3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Nov 2021 12:43:21 +0100 Subject: [PATCH 0341/1452] Don't use template in cover device condition (#59408) --- .../components/cover/device_condition.py | 26 ++-- .../components/cover/test_device_condition.py | 125 +++++++++++++----- 2 files changed, 98 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index c163bd097ae..28a057cb17f 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -18,12 +18,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import ( - condition, - config_validation as cv, - entity_registry, - template, -) +from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -148,22 +143,19 @@ def async_condition_from_config( return test_is_state if config[CONF_TYPE] == "is_position": - position = "current_position" + position_attr = "current_position" if config[CONF_TYPE] == "is_tilt_position": - position = "current_tilt_position" + position_attr = "current_tilt_position" min_pos = config.get(CONF_ABOVE) max_pos = config.get(CONF_BELOW) - value_template = template.Template( # type: ignore - f"{{{{ state.attributes.{position} }}}}" - ) @callback - def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: - """Validate template based if-condition.""" - value_template.hass = hass - + def check_numeric_state( + hass: HomeAssistant, variables: TemplateVarsType = None + ) -> bool: + """Return whether the criteria are met.""" return condition.async_numeric_state( - hass, config[ATTR_ENTITY_ID], max_pos, min_pos, value_template + hass, config[ATTR_ENTITY_ID], max_pos, min_pos, attribute=position_attr ) - return template_if + return check_numeric_state diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index cf05a112a0a..0812a7fefa0 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -15,6 +15,7 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -353,7 +354,7 @@ async def test_if_state(hass, calls): assert calls[3].data["some"] == "is_closing - event - test_event4" -async def test_if_position(hass, calls, enable_custom_integrations): +async def test_if_position(hass, calls, caplog, enable_custom_integrations): """Test for position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -368,20 +369,28 @@ async def test_if_position(hass, calls, enable_custom_integrations): automation.DOMAIN: [ { "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent.entity_id, - "type": "is_position", - "above": 45, - } - ], "action": { - "service": "test.automation", - "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "choose": { + "conditions": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "above": 45, + }, + "sequence": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + "default": { + "service": "test.automation", + "data_template": { + "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, }, }, }, @@ -427,8 +436,13 @@ async def test_if_position(hass, calls, enable_custom_integrations): ] }, ) + + caplog.clear() + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() hass.bus.async_fire("test_event2") + await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() assert len(calls) == 3 @@ -440,11 +454,14 @@ async def test_if_position(hass, calls, enable_custom_integrations): ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} ) hass.bus.async_fire("test_event1") + await hass.async_block_till_done() hass.bus.async_fire("test_event2") + await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + assert len(calls) == 5 + assert calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} @@ -453,11 +470,20 @@ async def test_if_position(hass, calls, enable_custom_integrations): hass.bus.async_fire("test_event2") hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" + assert len(calls) == 6 + assert calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" + + hass.states.async_set(ent.entity_id, STATE_UNAVAILABLE, attributes={}) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 7 + assert calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" + + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") -async def test_if_tilt_position(hass, calls, enable_custom_integrations): +async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations): """Test for tilt position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -472,20 +498,28 @@ async def test_if_tilt_position(hass, calls, enable_custom_integrations): automation.DOMAIN: [ { "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent.entity_id, - "type": "is_tilt_position", - "above": 45, - } - ], "action": { - "service": "test.automation", - "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "choose": { + "conditions": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "above": 45, + }, + "sequence": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + "default": { + "service": "test.automation", + "data_template": { + "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, }, }, }, @@ -531,8 +565,13 @@ async def test_if_tilt_position(hass, calls, enable_custom_integrations): ] }, ) + + caplog.clear() + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() hass.bus.async_fire("test_event2") + await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() assert len(calls) == 3 @@ -544,18 +583,32 @@ async def test_if_tilt_position(hass, calls, enable_custom_integrations): ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} ) hass.bus.async_fire("test_event1") + await hass.async_block_till_done() hass.bus.async_fire("test_event2") + await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + assert len(calls) == 5 + assert calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} ) hass.bus.async_fire("test_event1") + await hass.async_block_till_done() hass.bus.async_fire("test_event2") + await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" + assert len(calls) == 6 + assert calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" + + hass.states.async_set(ent.entity_id, STATE_UNAVAILABLE, attributes={}) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 7 + assert calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" + + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") From 62e7b0b887090bde086cdeb534281a45d055591e Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:59:50 +0100 Subject: [PATCH 0342/1452] Add category diagnostic to Switchbot 'calibrated' binary sensor (#59409) --- homeassistant/components/switchbot/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index d58e244d57c..899066ff502 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_MAC, CONF_NAME, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,6 +19,7 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( key="calibration", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } From fc58df6df95a5866e021e0d41b0592d7253a411a Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 9 Nov 2021 12:00:12 +0000 Subject: [PATCH 0343/1452] Change Coinbase account state class to total (#59404) --- homeassistant/components/coinbase/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 88e61b59fff..817a99d9eb1 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,7 +1,11 @@ """Support for Coinbase sensors.""" import logging -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + SensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import DeviceInfo @@ -105,7 +109,7 @@ class AccountSensor(SensorEntity): API_ACCOUNT_CURRENCY ] break - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", entry_type="service", From 06d29040b922890ec2a33faf8cf39c25ad5f71a0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 9 Nov 2021 13:00:50 +0100 Subject: [PATCH 0344/1452] Catch connection errors that makes tradfri hang in startup (#59368) --- homeassistant/components/tradfri/__init__.py | 17 +++++++++-------- homeassistant/components/tradfri/const.py | 1 + tests/components/tradfri/conftest.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 206f4e07007..2acd79a3e49 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta import logging from typing import Any -from pytradfri import Gateway, RequestError +from pytradfri import Gateway, PytradfriError, RequestError from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol @@ -36,6 +36,7 @@ from .const import ( KEY_API, PLATFORMS, SIGNAL_GW, + TIMEOUT_API, ) _LOGGER = logging.getLogger(__name__) @@ -107,14 +108,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = Gateway() try: - gateway_info = await api(gateway.get_gateway_info()) - devices_commands = await api(gateway.get_devices()) - devices = await api(devices_commands) - groups_commands = await api(gateway.get_groups()) - groups = await api(groups_commands) - except RequestError as err: + gateway_info = await api(gateway.get_gateway_info(), timeout=TIMEOUT_API) + devices_commands = await api(gateway.get_devices(), timeout=TIMEOUT_API) + devices = await api(devices_commands, timeout=TIMEOUT_API) + groups_commands = await api(gateway.get_groups(), timeout=TIMEOUT_API) + groups = await api(groups_commands, timeout=TIMEOUT_API) + except PytradfriError as exc: await factory.shutdown() - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady from exc tradfri_data[KEY_API] = api tradfri_data[FACTORY] = factory diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 5bcd1f376e1..abd3e8aff4a 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -26,3 +26,4 @@ KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION PLATFORMS = ["cover", "fan", "light", "sensor", "switch"] +TIMEOUT_API = 30 diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index c5310d52b9c..f4e871d79e1 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -59,7 +59,7 @@ def mock_gateway_fixture(): def mock_api_fixture(mock_gateway): """Mock api.""" - async def api(command): + async def api(command, timeout=None): """Mock api function.""" # Store the data for "real" command objects. if hasattr(command, "_data") and not isinstance(command, Mock): From 51510c542a09c75587507369a1a496dc119d85ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 9 Nov 2021 14:01:46 +0200 Subject: [PATCH 0345/1452] Remove const.HTTP_* status constants (#58380) --- homeassistant/const.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3ce1a15cf0d..302cf3b00ab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -660,21 +660,6 @@ URL_API_ERROR_LOG: Final = "/api/error_log" URL_API_LOG_OUT: Final = "/api/log_out" URL_API_TEMPLATE: Final = "/api/template" -HTTP_OK: Final = 200 -HTTP_CREATED: Final = 201 -HTTP_ACCEPTED: Final = 202 -HTTP_MOVED_PERMANENTLY: Final = 301 -HTTP_BAD_REQUEST: Final = 400 -HTTP_UNAUTHORIZED: Final = 401 -HTTP_FORBIDDEN: Final = 403 -HTTP_NOT_FOUND: Final = 404 -HTTP_METHOD_NOT_ALLOWED: Final = 405 -HTTP_UNPROCESSABLE_ENTITY: Final = 422 -HTTP_TOO_MANY_REQUESTS: Final = 429 -HTTP_INTERNAL_SERVER_ERROR: Final = 500 -HTTP_BAD_GATEWAY: Final = 502 -HTTP_SERVICE_UNAVAILABLE: Final = 503 - HTTP_BASIC_AUTHENTICATION: Final = "basic" HTTP_BEARER_AUTHENTICATION: Final = "bearer_token" HTTP_DIGEST_AUTHENTICATION: Final = "digest" From 56b1f26e7dcefb671e4de1dfc26885236c12cfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 9 Nov 2021 14:49:45 +0100 Subject: [PATCH 0346/1452] Bump pycfdns from 1.2.1 to 1.2.2 (#59416) --- homeassistant/components/cloudflare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index c831dbeb34d..ebb9e4b5f62 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -2,7 +2,7 @@ "domain": "cloudflare", "name": "Cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare", - "requirements": ["pycfdns==1.2.1"], + "requirements": ["pycfdns==1.2.2"], "codeowners": ["@ludeeus", "@ctalkington"], "config_flow": true, "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 13028f90ccb..0d1d3ac10aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1384,7 +1384,7 @@ pybotvac==0.0.22 pycarwings2==2.12 # homeassistant.components.cloudflare -pycfdns==1.2.1 +pycfdns==1.2.2 # homeassistant.components.channels pychannels==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6844e6b2570..1bcba9650f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -829,7 +829,7 @@ pyblackbird==0.5 pybotvac==0.0.22 # homeassistant.components.cloudflare -pycfdns==1.2.1 +pycfdns==1.2.2 # homeassistant.components.cast pychromecast==9.3.1 From 5177fabee0ed4ac56e5cf0269013ac5521127621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 9 Nov 2021 14:50:35 +0100 Subject: [PATCH 0347/1452] Bump pyuptimerobot from 21.9.0 to 21.11.0 (#59418) --- homeassistant/components/uptimerobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 8f9a9d74103..d19ada33158 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -3,7 +3,7 @@ "name": "UptimeRobot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ - "pyuptimerobot==21.9.0" + "pyuptimerobot==21.11.0" ], "codeowners": [ "@ludeeus" diff --git a/requirements_all.txt b/requirements_all.txt index 0d1d3ac10aa..657ad0838da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1977,7 +1977,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.uptimerobot -pyuptimerobot==21.9.0 +pyuptimerobot==21.11.0 # homeassistant.components.keyboard # pyuserinput==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bcba9650f7..27540d11806 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ pytradfri[async]==7.2.0 pyudev==0.22.0 # homeassistant.components.uptimerobot -pyuptimerobot==21.9.0 +pyuptimerobot==21.11.0 # homeassistant.components.vera pyvera==0.3.13 From ad91e4b417889d6da27c9934c47bb7d95af8742b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 9 Nov 2021 15:18:13 +0100 Subject: [PATCH 0348/1452] Simplify setup of tradfri entities (#59343) * Simplify detection of devices. --- homeassistant/components/tradfri/cover.py | 6 +++--- homeassistant/components/tradfri/fan.py | 10 +++++----- homeassistant/components/tradfri/light.py | 10 +++++----- homeassistant/components/tradfri/sensor.py | 18 +++++++++--------- homeassistant/components/tradfri/switch.py | 8 +++----- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 074b6ddd726..554650f9005 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -26,9 +26,9 @@ async def async_setup_entry( api = tradfri_data[KEY_API] devices = tradfri_data[DEVICES] - covers = [dev for dev in devices if dev.has_blind_control] - if covers: - async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) + async_add_entities( + TradfriCover(dev, api, gateway_id) for dev in devices if dev.has_blind_control + ) class TradfriCover(TradfriBaseDevice, CoverEntity): diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index d1b5de8b7cc..845d5e6d9c3 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -31,11 +31,11 @@ async def async_setup_entry( api = tradfri_data[KEY_API] devices = tradfri_data[DEVICES] - purifiers = [dev for dev in devices if dev.has_air_purifier_control] - if purifiers: - async_add_entities( - TradfriAirPurifierFan(purifier, api, gateway_id) for purifier in purifiers - ) + async_add_entities( + TradfriAirPurifierFan(dev, api, gateway_id) + for dev in devices + if dev.has_air_purifier_control + ) def _from_percentage(percentage: int) -> int: diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 5a2da96152f..ca078a37e81 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -49,12 +49,12 @@ async def async_setup_entry( api = tradfri_data[KEY_API] devices = tradfri_data[DEVICES] - lights = [dev for dev in devices if dev.has_light_control] - if lights: - async_add_entities(TradfriLight(light, api, gateway_id) for light in lights) - + entities: list[TradfriBaseClass] = [ + TradfriLight(dev, api, gateway_id) for dev in devices if dev.has_light_control + ] if config_entry.data[CONF_IMPORT_GROUPS] and (groups := tradfri_data[GROUPS]): - async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) + entities.extend([TradfriGroup(group, api, gateway_id) for group in groups]) + async_add_entities(entities) class TradfriGroup(TradfriBaseClass, LightEntity): diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index c6214773b97..27d2690ce34 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -27,17 +27,17 @@ async def async_setup_entry( api = tradfri_data[KEY_API] devices = tradfri_data[DEVICES] - sensors = ( - dev + async_add_entities( + TradfriSensor(dev, api, gateway_id) for dev in devices - if not dev.has_light_control - and not dev.has_socket_control - and not dev.has_blind_control - and not dev.has_signal_repeater_control - and not dev.has_air_purifier_control + if ( + not dev.has_light_control + and not dev.has_socket_control + and not dev.has_blind_control + and not dev.has_signal_repeater_control + and not dev.has_air_purifier_control + ) ) - if sensors: - async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors) class TradfriSensor(TradfriBaseDevice, SensorEntity): diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index d308e78c3d1..72d311fe555 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -26,11 +26,9 @@ async def async_setup_entry( api = tradfri_data[KEY_API] devices = tradfri_data[DEVICES] - switches = [dev for dev in devices if dev.has_socket_control] - if switches: - async_add_entities( - TradfriSwitch(switch, api, gateway_id) for switch in switches - ) + async_add_entities( + TradfriSwitch(dev, api, gateway_id) for dev in devices if dev.has_socket_control + ) class TradfriSwitch(TradfriBaseDevice, SwitchEntity): From d05c80c8e41717acb5b84928856fdecbe53b4485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 9 Nov 2021 16:22:11 +0100 Subject: [PATCH 0349/1452] Bump pylaunches from 1.0.0 to 1.2.0 (#59420) --- homeassistant/components/launch_library/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index f7820a1d408..fee5aeea0fe 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -2,7 +2,7 @@ "domain": "launch_library", "name": "Launch Library", "documentation": "https://www.home-assistant.io/integrations/launch_library", - "requirements": ["pylaunches==1.0.0"], + "requirements": ["pylaunches==1.2.0"], "codeowners": ["@ludeeus"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 657ad0838da..0e0330ccff8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1592,7 +1592,7 @@ pylacrosse==0.4 pylast==4.2.1 # homeassistant.components.launch_library -pylaunches==1.0.0 +pylaunches==1.2.0 # homeassistant.components.lg_netcast pylgnetcast==0.3.5 From 3d909b00d50af11eec707599475e29f6f6736c90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Nov 2021 16:41:45 +0100 Subject: [PATCH 0350/1452] Remove unneeded dynamic lookup of domain (#59423) --- homeassistant/components/lg_netcast/const.py | 2 ++ .../components/lg_netcast/media_player.py | 5 +++-- .../components/template/alarm_control_panel.py | 10 +++++----- homeassistant/components/template/cover.py | 13 ++++++------- homeassistant/components/template/fan.py | 18 ++++++++---------- homeassistant/components/template/light.py | 17 ++++++++--------- homeassistant/components/template/lock.py | 7 +++---- homeassistant/components/template/number.py | 8 +++----- homeassistant/components/template/select.py | 8 +++----- homeassistant/components/template/switch.py | 7 +++---- homeassistant/components/template/vacuum.py | 18 ++++++++---------- .../components/wake_on_lan/__init__.py | 4 ++-- homeassistant/components/wake_on_lan/const.py | 2 ++ homeassistant/components/wake_on_lan/switch.py | 5 +++-- 14 files changed, 59 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/lg_netcast/const.py create mode 100644 homeassistant/components/wake_on_lan/const.py diff --git a/homeassistant/components/lg_netcast/const.py b/homeassistant/components/lg_netcast/const.py new file mode 100644 index 00000000000..6cb44a5a3f8 --- /dev/null +++ b/homeassistant/components/lg_netcast/const.py @@ -0,0 +1,2 @@ +"""Constants for the lg_netcast component.""" +DOMAIN = "lg_netcast" diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 5b5ce313689..5938fc8d616 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -31,6 +31,8 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script +from .const import DOMAIN + DEFAULT_NAME = "LG TV Remote" CONF_ON_ACTION = "turn_on_action" @@ -69,8 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): on_action = config.get(CONF_ON_ACTION) client = LgNetCastClient(host, access_token) - domain = __name__.split(".")[-2] - on_action_script = Script(hass, on_action, name, domain) if on_action else None + on_action_script = Script(hass, on_action, name, DOMAIN) if on_action else None add_entities([LgTVDevice(client, name, on_action_script)], True) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 2cb830e54c2..05006049b02 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -36,6 +36,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import DOMAIN from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -158,18 +159,17 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._disarm_script = None self._code_arm_required = code_arm_required self._code_format = code_format - domain = __name__.split(".")[-2] if disarm_action is not None: - self._disarm_script = Script(hass, disarm_action, name, domain) + self._disarm_script = Script(hass, disarm_action, name, DOMAIN) self._arm_away_script = None if arm_away_action is not None: - self._arm_away_script = Script(hass, arm_away_action, name, domain) + self._arm_away_script = Script(hass, arm_away_action, name, DOMAIN) self._arm_home_script = None if arm_home_action is not None: - self._arm_home_script = Script(hass, arm_home_action, name, domain) + self._arm_home_script = Script(hass, arm_home_action, name, DOMAIN) self._arm_night_script = None if arm_night_action is not None: - self._arm_night_script = Script(hass, arm_night_action, name, domain) + self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) self._state = None self._unique_id = unique_id diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index a9f28d56669..d5cd04e94f2 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -40,7 +40,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -195,21 +195,20 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._tilt_template = tilt_template self._device_class = device_class self._open_script = None - domain = __name__.split(".")[-2] if open_action is not None: - self._open_script = Script(hass, open_action, friendly_name, domain) + self._open_script = Script(hass, open_action, friendly_name, DOMAIN) self._close_script = None if close_action is not None: - self._close_script = Script(hass, close_action, friendly_name, domain) + self._close_script = Script(hass, close_action, friendly_name, DOMAIN) self._stop_script = None if stop_action is not None: - self._stop_script = Script(hass, stop_action, friendly_name, domain) + self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) self._position_script = None if position_action is not None: - self._position_script = Script(hass, position_action, friendly_name, domain) + self._position_script = Script(hass, position_action, friendly_name, DOMAIN) self._tilt_script = None if tilt_action is not None: - self._tilt_script = Script(hass, tilt_action, friendly_name, domain) + self._tilt_script = Script(hass, tilt_action, friendly_name, DOMAIN) self._optimistic = optimistic or (not state_template and not position_template) self._tilt_optimistic = tilt_optimistic or not tilt_template self._position = None diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index f39d49fa9fd..4a872d51dbc 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -39,7 +39,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -209,39 +209,37 @@ class TemplateFan(TemplateEntity, FanEntity): self._direction_template = direction_template self._supported_features = 0 - domain = __name__.split(".")[-2] - - self._on_script = Script(hass, on_action, friendly_name, domain) - self._off_script = Script(hass, off_action, friendly_name, domain) + self._on_script = Script(hass, on_action, friendly_name, DOMAIN) + self._off_script = Script(hass, off_action, friendly_name, DOMAIN) self._set_speed_script = None if set_speed_action: self._set_speed_script = Script( - hass, set_speed_action, friendly_name, domain + hass, set_speed_action, friendly_name, DOMAIN ) self._set_percentage_script = None if set_percentage_action: self._set_percentage_script = Script( - hass, set_percentage_action, friendly_name, domain + hass, set_percentage_action, friendly_name, DOMAIN ) self._set_preset_mode_script = None if set_preset_mode_action: self._set_preset_mode_script = Script( - hass, set_preset_mode_action, friendly_name, domain + hass, set_preset_mode_action, friendly_name, DOMAIN ) self._set_oscillating_script = None if set_oscillating_action: self._set_oscillating_script = Script( - hass, set_oscillating_action, friendly_name, domain + hass, set_oscillating_action, friendly_name, DOMAIN ) self._set_direction_script = None if set_direction_action: self._set_direction_script = Script( - hass, set_direction_action, friendly_name, domain + hass, set_direction_action, friendly_name, DOMAIN ) self._state = STATE_OFF diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b8ebe03ceba..fce35f312f8 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -37,7 +37,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -211,32 +211,31 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._name = friendly_name self._template = state_template - domain = __name__.split(".")[-2] - self._on_script = Script(hass, on_action, friendly_name, domain) - self._off_script = Script(hass, off_action, friendly_name, domain) + self._on_script = Script(hass, on_action, friendly_name, DOMAIN) + self._off_script = Script(hass, off_action, friendly_name, DOMAIN) self._level_script = None if level_action is not None: - self._level_script = Script(hass, level_action, friendly_name, domain) + self._level_script = Script(hass, level_action, friendly_name, DOMAIN) self._level_template = level_template self._temperature_script = None if temperature_action is not None: self._temperature_script = Script( - hass, temperature_action, friendly_name, domain + hass, temperature_action, friendly_name, DOMAIN ) self._temperature_template = temperature_template self._color_script = None if color_action is not None: - self._color_script = Script(hass, color_action, friendly_name, domain) + self._color_script = Script(hass, color_action, friendly_name, DOMAIN) self._color_template = color_template self._white_value_script = None if white_value_action is not None: self._white_value_script = Script( - hass, white_value_action, friendly_name, domain + hass, white_value_action, friendly_name, DOMAIN ) self._white_value_template = white_value_template self._effect_script = None if effect_action is not None: - self._effect_script = Script(hass, effect_action, friendly_name, domain) + self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) self._effect_list_template = effect_list_template self._effect_template = effect_template self._max_mireds_template = max_mireds_template diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 51431d133f7..a078ce778b6 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity CONF_LOCK = "lock" @@ -88,9 +88,8 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = None self._name = name self._state_template = value_template - domain = __name__.split(".")[-2] - self._command_lock = Script(hass, command_lock, name, domain) - self._command_unlock = Script(hass, command_unlock, name, domain) + self._command_lock = Script(hass, command_lock, name, DOMAIN) + self._command_unlock = Script(hass, command_unlock, name, DOMAIN) self._optimistic = optimistic self._unique_id = unique_id diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 86cc4886430..0f5e8e4bee8 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -31,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.helpers.template import Template, TemplateError -from .const import CONF_AVAILABILITY +from .const import CONF_AVAILABILITY, DOMAIN from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity @@ -139,9 +139,8 @@ class TemplateNumber(TemplateEntity, NumberEntity): with contextlib.suppress(TemplateError): self._attr_name = name_template.async_render(parse_result=False) self._value_template = value_template - domain = __name__.split(".")[-2] self._command_set_value = Script( - hass, command_set_value, self._attr_name, domain + hass, command_set_value, self._attr_name, DOMAIN ) self._step_template = step_template self._min_value_template = minimum_template @@ -212,12 +211,11 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): ) -> None: """Initialize the entity.""" super().__init__(hass, coordinator, config) - domain = __name__.split(".")[-2] self._command_set_value = Script( hass, config[CONF_SET_VALUE], self._rendered.get(CONF_NAME, DEFAULT_NAME), - domain, + DOMAIN, ) @property diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 03136c0193d..ee43ba906c0 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -27,7 +27,7 @@ from homeassistant.helpers.script import Script from homeassistant.helpers.template import Template, TemplateError from . import TriggerUpdateCoordinator -from .const import CONF_AVAILABILITY +from .const import CONF_AVAILABILITY, DOMAIN from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity @@ -129,9 +129,8 @@ class TemplateSelect(TemplateEntity, SelectEntity): self._attr_name = name_template.async_render(parse_result=False) self._name_template = name_template self._value_template = value_template - domain = __name__.split(".")[-2] self._command_select_option = Script( - hass, command_select_option, self._attr_name, domain + hass, command_select_option, self._attr_name, DOMAIN ) self._options_template = options_template self._attr_assumed_state = self._optimistic = optimistic @@ -182,12 +181,11 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity): ) -> None: """Initialize the entity.""" super().__init__(hass, coordinator, config) - domain = __name__.split(".")[-2] self._command_select_option = Script( hass, config[CONF_SELECT_OPTION], self._rendered.get(CONF_NAME, DEFAULT_NAME), - domain, + DOMAIN, ) @property diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index a654e59eaa2..a5b85e4b408 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -119,9 +119,8 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): ) self._name = friendly_name self._template = state_template - domain = __name__.split(".")[-2] - self._on_script = Script(hass, on_action, friendly_name, domain) - self._off_script = Script(hass, off_action, friendly_name, domain) + self._on_script = Script(hass, on_action, friendly_name, DOMAIN) + self._off_script = Script(hass, off_action, friendly_name, DOMAIN) self._state = False self._unique_id = unique_id diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 78c51d2009c..a6b8bf0f379 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,7 +43,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -187,43 +187,41 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._fan_speed_template = fan_speed_template self._supported_features = SUPPORT_START - domain = __name__.split(".")[-2] - - self._start_script = Script(hass, start_action, friendly_name, domain) + self._start_script = Script(hass, start_action, friendly_name, DOMAIN) self._pause_script = None if pause_action: - self._pause_script = Script(hass, pause_action, friendly_name, domain) + self._pause_script = Script(hass, pause_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_PAUSE self._stop_script = None if stop_action: - self._stop_script = Script(hass, stop_action, friendly_name, domain) + self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_STOP self._return_to_base_script = None if return_to_base_action: self._return_to_base_script = Script( - hass, return_to_base_action, friendly_name, domain + hass, return_to_base_action, friendly_name, DOMAIN ) self._supported_features |= SUPPORT_RETURN_HOME self._clean_spot_script = None if clean_spot_action: self._clean_spot_script = Script( - hass, clean_spot_action, friendly_name, domain + hass, clean_spot_action, friendly_name, DOMAIN ) self._supported_features |= SUPPORT_CLEAN_SPOT self._locate_script = None if locate_action: - self._locate_script = Script(hass, locate_action, friendly_name, domain) + self._locate_script = Script(hass, locate_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_LOCATE self._set_fan_speed_script = None if set_fan_speed_action: self._set_fan_speed_script = Script( - hass, set_fan_speed_action, friendly_name, domain + hass, set_fan_speed_action, friendly_name, DOMAIN ) self._supported_features |= SUPPORT_FAN_SPEED diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index d6a254ebf52..dba25b44d75 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -8,9 +8,9 @@ import wakeonlan from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN -DOMAIN = "wake_on_lan" +_LOGGER = logging.getLogger(__name__) SERVICE_SEND_MAGIC_PACKET = "send_magic_packet" diff --git a/homeassistant/components/wake_on_lan/const.py b/homeassistant/components/wake_on_lan/const.py new file mode 100644 index 00000000000..14f2bd0263f --- /dev/null +++ b/homeassistant/components/wake_on_lan/const.py @@ -0,0 +1,2 @@ +"""Constants for the Wake-On-LAN component.""" +DOMAIN = "wake_on_lan" diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 4bbd1522c91..f7e5426c73f 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -18,6 +18,8 @@ from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) CONF_OFF_ACTION = "turn_off" @@ -82,9 +84,8 @@ class WolSwitch(SwitchEntity): self._mac_address = mac_address self._broadcast_address = broadcast_address self._broadcast_port = broadcast_port - domain = __name__.split(".")[-2] self._off_script = ( - Script(hass, off_action, name, domain) if off_action else None + Script(hass, off_action, name, DOMAIN) if off_action else None ) self._state = False self._assumed_state = host is None From 6e7712da3c8325f5ba2c65af9b5d109f9b8b932a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 9 Nov 2021 17:29:39 +0100 Subject: [PATCH 0351/1452] Add periods to statistics_during_period ws (#59425) --- homeassistant/components/history/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 960b9749220..72bcba104a4 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -119,7 +119,7 @@ class LazyState(history_models.LazyState): vol.Required("start_time"): str, vol.Optional("end_time"): str, vol.Optional("statistic_ids"): [str], - vol.Required("period"): vol.Any("hour", "5minute"), + vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), } ) @websocket_api.async_response From 36ebbef243cec4226529893998875295349b26aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 9 Nov 2021 18:02:50 +0100 Subject: [PATCH 0352/1452] Bump pytautulli from 21.10.0 to 21.11.0 (#59426) --- homeassistant/components/tautulli/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index cad31683c73..68edea99838 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==21.10.0"], + "requirements": ["pytautulli==21.11.0"], "codeowners": ["@ludeeus"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0e0330ccff8..39637e26570 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1841,7 +1841,7 @@ pysyncthru==0.7.10 pytankerkoenig==0.0.6 # homeassistant.components.tautulli -pytautulli==21.10.0 +pytautulli==21.11.0 # homeassistant.components.tfiac pytfiac==0.4 From 355b3c2c3d2777e6cb8b626438c64db734e0c6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 9 Nov 2021 18:20:34 +0100 Subject: [PATCH 0353/1452] Bump pytraccar from 0.9.0 to 0.10.0 (#59429) --- homeassistant/components/traccar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index fd8908a3264..77a8511a671 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar", - "requirements": ["pytraccar==0.9.0", "stringcase==1.2.0"], + "requirements": ["pytraccar==0.10.0", "stringcase==1.2.0"], "dependencies": ["webhook"], "codeowners": ["@ludeeus"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 39637e26570..c87275fbac8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1964,7 +1964,7 @@ pytile==5.2.4 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.9.0 +pytraccar==0.10.0 # homeassistant.components.tradfri pytradfri[async]==7.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27540d11806..7d985d8dc3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1157,7 +1157,7 @@ python_awair==0.2.1 pytile==5.2.4 # homeassistant.components.traccar -pytraccar==0.9.0 +pytraccar==0.10.0 # homeassistant.components.tradfri pytradfri[async]==7.2.0 From 59a0e5a773ad18742e5701ac5c4211d83a108042 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Tue, 9 Nov 2021 11:23:03 -0600 Subject: [PATCH 0354/1452] Update python-ecobee-api to 0.2.14 (#59381) --- homeassistant/components/ecobee/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index c1d11a8ee7b..bf6d0b922dd 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,7 +3,11 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.11"], - "codeowners": ["@marthoc"], + "requirements": [ + "python-ecobee-api==0.2.14" + ], + "codeowners": [ + "@marthoc" + ], "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index c87275fbac8..a37974f6566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1859,7 +1859,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.11 +python-ecobee-api==0.2.14 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d985d8dc3b..54ff3fa51fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1112,7 +1112,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.10 # homeassistant.components.ecobee -python-ecobee-api==0.2.11 +python-ecobee-api==0.2.14 # homeassistant.components.darksky python-forecastio==1.4.0 From 032786fcd86c981cb03962f57549c308143a5802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kiss?= <70820303+g-kiss@users.noreply.github.com> Date: Tue, 9 Nov 2021 18:24:40 +0100 Subject: [PATCH 0355/1452] Improve Shelly color mode switch for dual mode bulbs (#58971) * Shelly color mode switch for SHCB-1 * Update light.py * Update light.py * Update homeassistant/components/shelly/light.py Co-authored-by: Shay Levy * Update light.py Co-authored-by: Shay Levy --- homeassistant/components/shelly/light.py | 36 ++++-------------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 3e0fce43681..60810c9df4e 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,12 +1,10 @@ """Light for Shelly.""" from __future__ import annotations -import asyncio import logging from typing import Any, Final, cast from aioshelly.block_device import Block -import async_timeout from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -35,10 +33,10 @@ from homeassistant.util.color import ( from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import ( - AIOSHELLY_DEVICE_TIMEOUT_SEC, BLOCK, DATA_CONFIG_ENTRY, DOMAIN, + DUAL_MODE_LIGHT_MODELS, FIRMWARE_PATTERN, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, @@ -136,7 +134,6 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): """Initialize light.""" super().__init__(wrapper, block) self.control_result: dict[str, Any] | None = None - self.mode_result: dict[str, Any] | None = None self._supported_color_modes: set[str] = set() self._supported_features: int = 0 self._min_kelvin: int = KELVIN_MIN_VALUE_WHITE @@ -185,8 +182,8 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): @property def mode(self) -> str: """Return the color mode of the light.""" - if self.mode_result: - return cast(str, self.mode_result["mode"]) + if self.control_result and self.control_result.get("mode"): + return cast(str, self.control_result["mode"]) if hasattr(self.block, "mode"): return cast(str, self.block.mode) @@ -369,9 +366,10 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): self.wrapper.model, ) - if await self.set_light_mode(set_mode): - self.control_result = await self.set_state(**params) + if set_mode and self.wrapper.model in DUAL_MODE_LIGHT_MODELS: + params["mode"] = set_mode + self.control_result = await self.set_state(**params) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -387,32 +385,10 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): self.async_write_ha_state() - async def set_light_mode(self, set_mode: str | None) -> bool: - """Change device mode color/white if mode has changed.""" - if set_mode is None or self.mode == set_mode: - return True - - _LOGGER.debug("Setting light mode for entity %s, mode: %s", self.name, set_mode) - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) - except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( - "Setting light mode for entity %s failed, state: %s, error: %s", - self.name, - set_mode, - repr(err), - ) - self.wrapper.last_update_success = False - return False - - return True - @callback def _update_callback(self) -> None: """When device updates, clear control & mode result that overrides state.""" self.control_result = None - self.mode_result = None super()._update_callback() From 4b228e3add544d6b0b345f2f6b4982089abe8c6a Mon Sep 17 00:00:00 2001 From: Thanasis Date: Tue, 9 Nov 2021 18:25:19 +0100 Subject: [PATCH 0356/1452] Add entity categories to most NUT entities (#58798) * Add entity cateogories to most NUT entites * changes to categories --- homeassistant/components/nut/const.py | 66 +++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 59d5cd484af..51258471b0a 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -16,8 +16,8 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_SYSTEM, FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, @@ -81,58 +81,62 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Overload Setting", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.id": SensorEntityDescription( key="ups.id", name="System identifier", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.delay.start": SensorEntityDescription( key="ups.delay.start", name="Load Restart Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.delay.reboot": SensorEntityDescription( key="ups.delay.reboot", name="UPS Reboot Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.delay.shutdown": SensorEntityDescription( key="ups.delay.shutdown", name="UPS Shutdown Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.timer.start": SensorEntityDescription( key="ups.timer.start", name="Load Start Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.timer.reboot": SensorEntityDescription( key="ups.timer.reboot", name="Load Reboot Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.timer.shutdown": SensorEntityDescription( key="ups.timer.shutdown", name="Load Shutdown Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.test.interval": SensorEntityDescription( key="ups.test.interval", name="Self-Test Interval", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.test.result": SensorEntityDescription( key="ups.test.result", @@ -150,11 +154,13 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.display.language", name="Language", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.contacts": SensorEntityDescription( key="ups.contacts", name="External Contacts", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.efficiency": SensorEntityDescription( key="ups.efficiency", @@ -162,6 +168,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.power": SensorEntityDescription( key="ups.power", @@ -169,12 +176,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.power.nominal": SensorEntityDescription( key="ups.power.nominal", name="Nominal Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.realpower": SensorEntityDescription( key="ups.realpower", @@ -182,22 +191,26 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.realpower.nominal": SensorEntityDescription( key="ups.realpower.nominal", name="Nominal Real Power", native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.beeper.status": SensorEntityDescription( key="ups.beeper.status", name="Beeper Status", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.type": SensorEntityDescription( key="ups.type", name="UPS Type", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_SYSTEM, ), "ups.watchdog.status": SensorEntityDescription( key="ups.watchdog.status", @@ -209,21 +222,25 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.start.auto", name="Start on AC", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.start.battery": SensorEntityDescription( key="ups.start.battery", name="Start on Battery", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.start.reboot": SensorEntityDescription( key="ups.start.reboot", name="Reboot on Battery", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.shutdown": SensorEntityDescription( key="ups.shutdown", name="Shutdown Ability", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charge": SensorEntityDescription( key="battery.charge", @@ -231,27 +248,27 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charge.low": SensorEntityDescription( key="battery.charge.low", name="Low Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charge.restart": SensorEntityDescription( key="battery.charge.restart", name="Minimum Battery to Start", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charge.warning": SensorEntityDescription( key="battery.charge.warning", name="Warning Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charger.status": SensorEntityDescription( key="battery.charger.status", @@ -264,30 +281,35 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.voltage.nominal": SensorEntityDescription( key="battery.voltage.nominal", name="Nominal Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.voltage.low": SensorEntityDescription( key="battery.voltage.low", name="Low Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.voltage.high": SensorEntityDescription( key="battery.voltage.high", name="High Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.capacity": SensorEntityDescription( key="battery.capacity", name="Battery Capacity", native_unit_of_measurement="Ah", icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.current": SensorEntityDescription( key="battery.current", @@ -295,12 +317,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.current.total": SensorEntityDescription( key="battery.current.total", name="Total Battery Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.temperature": SensorEntityDescription( key="battery.temperature", @@ -322,65 +346,76 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.runtime.restart": SensorEntityDescription( key="battery.runtime.restart", name="Minimum Battery Runtime to Start", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.alarm.threshold": SensorEntityDescription( key="battery.alarm.threshold", name="Battery Alarm Threshold", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.date": SensorEntityDescription( key="battery.date", name="Battery Date", icon="mdi:calendar", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.mfr.date": SensorEntityDescription( key="battery.mfr.date", name="Battery Manuf. Date", icon="mdi:calendar", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.packs": SensorEntityDescription( key="battery.packs", name="Number of Batteries", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.packs.bad": SensorEntityDescription( key="battery.packs.bad", name="Number of Bad Batteries", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.type": SensorEntityDescription( key="battery.type", name="Battery Chemistry", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.sensitivity": SensorEntityDescription( key="input.sensitivity", name="Input Power Sensitivity", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.transfer.low": SensorEntityDescription( key="input.transfer.low", name="Low Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.transfer.high": SensorEntityDescription( key="input.transfer.high", name="High Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.transfer.reason": SensorEntityDescription( key="input.transfer.reason", name="Voltage Transfer Reason", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.voltage": SensorEntityDescription( key="input.voltage", @@ -394,6 +429,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.frequency": SensorEntityDescription( key="input.frequency", @@ -401,17 +437,20 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.frequency.nominal": SensorEntityDescription( key="input.frequency.nominal", name="Nominal Input Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "input.frequency.status": SensorEntityDescription( key="input.frequency.status", name="Input Frequency Status", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "output.current": SensorEntityDescription( key="output.current", @@ -419,12 +458,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "output.current.nominal": SensorEntityDescription( key="output.current.nominal", name="Nominal Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "output.voltage": SensorEntityDescription( key="output.voltage", @@ -438,6 +479,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "output.frequency": SensorEntityDescription( key="output.frequency", @@ -445,12 +487,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "output.frequency.nominal": SensorEntityDescription( key="output.frequency.nominal", name="Nominal Output Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ambient.humidity": SensorEntityDescription( key="ambient.humidity", @@ -458,6 +502,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ambient.temperature": SensorEntityDescription( key="ambient.temperature", @@ -465,6 +510,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "watts": SensorEntityDescription( key="watts", From d5fcf0b6223ddd583950d87911d10b77fe1c978b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 9 Nov 2021 18:26:34 +0100 Subject: [PATCH 0357/1452] Add zwave_js binary sensor entity category (#58703) * Add zwave_js binary sensor entity category * Handle non idle notification state * Fix door state * Fix duplicate door state description * Add tests --- .../components/zwave_js/binary_sensor.py | 84 +++++++++++++------ tests/components/zwave_js/common.py | 3 + .../components/zwave_js/test_binary_sensor.py | 63 +++++++++++++- 3 files changed, 122 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index a5883e9bcbf..af094407359 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -19,15 +19,18 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LOCK, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -63,6 +66,7 @@ NOTIFICATION_GAS = "18" class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription): """Represent a Z-Wave JS binary sensor entity description.""" + off_state: str = "0" states: tuple[str, ...] | None = None @@ -145,17 +149,12 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = device_class=DEVICE_CLASS_LOCK, ), NotificationZWaveJSEntityDescription( - # NotificationType 6: Access Control - State Id 16 (door/window open) + # NotificationType 6: Access Control - State Id 22 (door/window open) key=NOTIFICATION_ACCESS_CONTROL, - states=("22",), + off_state="23", + states=("22", "23"), device_class=DEVICE_CLASS_DOOR, ), - NotificationZWaveJSEntityDescription( - # NotificationType 6: Access Control - State Id 17 (door/window closed) - key=NOTIFICATION_ACCESS_CONTROL, - states=("23",), - entity_registry_enabled_default=False, - ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) key=NOTIFICATION_HOME_SECURITY, @@ -166,7 +165,8 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) key=NOTIFICATION_HOME_SECURITY, states=("3", "4", "9"), - device_class=DEVICE_CLASS_SAFETY, + device_class=DEVICE_CLASS_TAMPER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) @@ -180,6 +180,23 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = states=("7", "8"), device_class=DEVICE_CLASS_MOTION, ), + NotificationZWaveJSEntityDescription( + # NotificationType 8: Power Management - + # State Id's 2, 3 (Mains status) + key=NOTIFICATION_POWER_MANAGEMENT, + off_state="2", + states=("2", "3"), + device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + NotificationZWaveJSEntityDescription( + # NotificationType 8: Power Management - + # State Id's 10, 11, 17 (Battery maintenance status) + key=NOTIFICATION_POWER_MANAGEMENT, + states=("10", "11", "17"), + device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), NotificationZWaveJSEntityDescription( # NotificationType 9: System - State Id's 1, 2, 6, 7 key=NOTIFICATION_SYSTEM, @@ -228,6 +245,7 @@ BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { CommandClass.BATTERY: BinarySensorEntityDescription( key=str(CommandClass.BATTERY), device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } @@ -251,16 +269,41 @@ async def async_setup_entry( # ignore idle key (0) if state_key == "0": continue + + notification_description: NotificationZWaveJSEntityDescription | None = ( + None + ) + + for description in NOTIFICATION_SENSOR_MAPPINGS: + if ( + int(description.key) + == info.primary_value.metadata.cc_specific[ + CC_SPECIFIC_NOTIFICATION_TYPE + ] + ) and (not description.states or state_key in description.states): + notification_description = description + break + + if ( + notification_description + and notification_description.off_state == state_key + ): + continue + entities.append( - ZWaveNotificationBinarySensor(config_entry, client, info, state_key) + ZWaveNotificationBinarySensor( + config_entry, client, info, state_key, notification_description + ) ) elif info.platform_hint == "property" and ( - description := PROPERTY_SENSOR_MAPPINGS.get( + property_description := PROPERTY_SENSOR_MAPPINGS.get( info.primary_value.property_name ) ): entities.append( - ZWavePropertyBinarySensor(config_entry, client, info, description) + ZWavePropertyBinarySensor( + config_entry, client, info, property_description + ) ) else: # boolean sensor @@ -313,12 +356,12 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): client: ZwaveClient, info: ZwaveDiscoveryInfo, state_key: str, + description: NotificationZWaveJSEntityDescription | None = None, ) -> None: """Initialize a ZWaveNotificationBinarySensor entity.""" super().__init__(config_entry, client, info) self.state_key = state_key - # check if we have a custom mapping for this value - if description := self._get_sensor_description(): + if description: self.entity_description = description # Entity class attributes @@ -336,19 +379,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return None return int(self.info.primary_value.value) == int(self.state_key) - @callback - def _get_sensor_description(self) -> NotificationZWaveJSEntityDescription | None: - """Try to get a device specific mapping for this sensor.""" - for description in NOTIFICATION_SENSOR_MAPPINGS: - if ( - int(description.key) - == self.info.primary_value.metadata.cc_specific[ - CC_SPECIFIC_NOTIFICATION_TYPE - ] - ) and (not description.states or self.state_key in description.states): - return description - return None - class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Representation of a Z-Wave binary_sensor from a property.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c16ab00b2eb..d73daacdc75 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,6 +1,9 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" BATTERY_SENSOR = "sensor.multisensor_6_battery_level" +TAMPER_SENSOR = ( + "binary_sensor.multisensor_6_home_security_tampering_product_cover_removed" +) HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 1cb91547a0a..d5aab6cd0f9 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,11 +1,19 @@ """Test the Z-Wave JS binary sensor platform.""" from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, + DEVICE_CLASS_TAMPER, ) -from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .common import ( @@ -14,8 +22,11 @@ from .common import ( LOW_BATTERY_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR, PROPERTY_DOOR_STATUS_BINARY_SENSOR, + TAMPER_SENSOR, ) +from tests.common import MockConfigEntry + async def test_low_battery_sensor(hass, multisensor_6, integration): """Test boolean binary sensor of type low battery.""" @@ -25,6 +36,12 @@ async def test_low_battery_sensor(hass, multisensor_6, integration): assert state.state == STATE_OFF assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY + registry = er.async_get(hass) + entity_entry = registry.async_get(LOW_BATTERY_BINARY_SENSOR) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration): """Test enabled legacy boolean binary sensor.""" @@ -90,6 +107,50 @@ async def test_notification_sensor(hass, multisensor_6, integration): assert state.state == STATE_ON assert state.attributes["device_class"] == DEVICE_CLASS_MOTION + state = hass.states.get(TAMPER_SENSOR) + + assert state + assert state.state == STATE_OFF + assert state.attributes["device_class"] == DEVICE_CLASS_TAMPER + + registry = er.async_get(hass) + entity_entry = registry.async_get(TAMPER_SENSOR) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + +async def test_notification_off_state( + hass: HomeAssistant, + lock_popp_electric_strike_lock_control: Node, +): + """Test the description off_state attribute of certain notification sensors.""" + node = lock_popp_electric_strike_lock_control + # Remove all other values except the door state value. + node.values = { + value_id: value + for value_id, value in node.values.items() + if value_id == "62-113-0-Access Control-Door state" + } + + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + door_states = [ + state + for state in hass.states.async_all("binary_sensor") + if state.attributes.get("device_class") == DEVICE_CLASS_DOOR + ] + + # Only one entity should be created for the Door state notification states. + assert len(door_states) == 1 + + state = door_states[0] + assert state + assert state.entity_id == "binary_sensor.node_62_access_control_window_door_is_open" + async def test_property_sensor_door_status(hass, lock_august_pro, integration): """Test property binary sensor with sensor mapping (doorStatus).""" From 7e81c6a5910e453a6e0fb87bfdcccb6ffb093fc8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Nov 2021 18:30:05 +0100 Subject: [PATCH 0358/1452] Move onewire device compatibility checks (#59338) * Move device compatibility checks to onewirehub * Add test for dtoverlay warning * Add tests for unknown device warning * Move dtoverlay error * Empty commit to retrigger tests * Update description * Patch asyncio.sleep to speed up the tests Co-authored-by: epenet --- homeassistant/components/onewire/const.py | 19 ++++++++++ .../components/onewire/onewirehub.py | 38 ++++++++++++++++++- homeassistant/components/onewire/sensor.py | 30 ++------------- tests/components/onewire/const.py | 28 ++------------ .../components/onewire/test_binary_sensor.py | 18 +++++++-- tests/components/onewire/test_init.py | 14 +++++++ tests/components/onewire/test_sensor.py | 37 +++++++++++++++--- tests/components/onewire/test_switch.py | 18 +++++++-- 8 files changed, 136 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 7bdba0d73d4..4904d1568ef 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -20,6 +20,25 @@ DOMAIN = "onewire" DEVICE_KEYS_0_7 = range(8) DEVICE_KEYS_A_B = ("A", "B") +DEVICE_SUPPORT_OWSERVER = { + "05": (), + "10": (), + "12": (), + "1D": (), + "1F": (), + "22": (), + "26": (), + "28": (), + "29": (), + "3A": (), + "3B": (), + "42": (), + "7E": ("EDS0066", "EDS0068"), + "EF": ("HB_MOISTURE_METER", "HobbyBoards_EF"), +} +DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] + + MANUFACTURER_MAXIM = "Maxim Integrated" MANUFACTURER_HOBBYBOARDS = "Hobby Boards" MANUFACTURER_EDS = "Embedded Data Systems" diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 0d25a546941..65f49261f56 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -29,6 +29,8 @@ from .const import ( CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, + DEVICE_SUPPORT_OWSERVER, + DEVICE_SUPPORT_SYSBUS, DOMAIN, MANUFACTURER_EDS, MANUFACTURER_HOBBYBOARDS, @@ -53,6 +55,13 @@ DEVICE_MANUFACTURER = { _LOGGER = logging.getLogger(__name__) +def _is_known_owserver_device(device_family: str, device_type: str) -> bool: + """Check if device family/type is known to the library.""" + if device_family in ("7E", "EF"): # EDS or HobbyBoard + return device_type in DEVICE_SUPPORT_OWSERVER[device_family] + return device_family in DEVICE_SUPPORT_OWSERVER + + class OneWireHub: """Hub to communicate with SysBus or OWServer.""" @@ -83,10 +92,13 @@ class OneWireHub: """Initialize a config entry.""" self.type = config_entry.data[CONF_TYPE] if self.type == CONF_TYPE_SYSBUS: - await self.check_mount_dir(config_entry.data[CONF_MOUNT_DIR]) + mount_dir = config_entry.data[CONF_MOUNT_DIR] + _LOGGER.debug("Initializing using SysBus %s", mount_dir) + await self.check_mount_dir(mount_dir) elif self.type == CONF_TYPE_OWSERVER: host = config_entry.data[CONF_HOST] port = config_entry.data[CONF_PORT] + _LOGGER.debug("Initializing using OWServer %s:%s", host, port) await self.connect(host, port) await self.discover_devices() if TYPE_CHECKING: @@ -120,9 +132,23 @@ class OneWireHub: """Discover all sysbus devices.""" devices: list[OWDeviceDescription] = [] assert self.pi1proxy - for interface in self.pi1proxy.find_all_sensors(): + all_sensors = self.pi1proxy.find_all_sensors() + if not all_sensors: + _LOGGER.error( + "No onewire sensor found. Check if dtoverlay=w1-gpio " + "is in your /boot/config.txt. " + "Check the mount_dir parameter if it's defined" + ) + for interface in all_sensors: device_family = interface.mac_address[:2] device_id = f"{device_family}-{interface.mac_address[2:]}" + if device_family not in DEVICE_SUPPORT_SYSBUS: + _LOGGER.warning( + "Ignoring unknown device family (%s) found for device %s", + device_family, + device_id, + ) + continue device_info: DeviceInfo = { ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, ATTR_MANUFACTURER: DEVICE_MANUFACTURER.get( @@ -149,6 +175,14 @@ class OneWireHub: device_family = self.owproxy.read(f"{device_path}family").decode() _LOGGER.debug("read `%sfamily`: %s", device_path, device_family) device_type = self._get_device_type_owserver(device_path) + if not _is_known_owserver_device(device_family, device_type): + _LOGGER.warning( + "Ignoring unknown device family/type (%s/%s) found for device %s", + device_family, + device_type, + device_id, + ) + continue device_info: DeviceInfo = { ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, ATTR_MANUFACTURER: DEVICE_MANUFACTURER.get( diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 7efa082d482..fa477ff27e0 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -43,7 +43,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import ( - CONF_MOUNT_DIR, CONF_NAMES, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, @@ -212,12 +211,8 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=STATE_CLASS_TOTAL_INCREASING, ), ), - "EF": (), # "HobbyBoard": special - "7E": (), # "EDS": special } -DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] - # EF sensors are usually hobbyboards specialized sensors. # These can only be read by OWFS. Currently this driver only supports them # via owserver (network protocol) @@ -343,7 +338,9 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { } -def get_sensor_types(device_sub_type: str) -> dict[str, Any]: +def get_sensor_types( + device_sub_type: str, +) -> dict[str, tuple[OneWireSensorEntityDescription, ...]]: """Return the proper info array for the device type.""" if "HobbyBoard" in device_sub_type: return HOBBYBOARD_EF @@ -398,11 +395,6 @@ def get_entities( family = device_type if family not in get_sensor_types(device_sub_type): - _LOGGER.warning( - "Ignoring unknown family (%s) of sensor found for device: %s", - family, - device_id, - ) continue for description in get_sensor_types(device_sub_type)[family]: if description.key.startswith("moisture/"): @@ -434,8 +426,6 @@ def get_entities( # We have a raw GPIO ow sensor on a Pi elif conf_type == CONF_TYPE_SYSBUS: - base_dir = config[CONF_MOUNT_DIR] - _LOGGER.debug("Initializing using SysBus %s", base_dir) for device in onewirehub.devices: if TYPE_CHECKING: assert isinstance(device, OWDirectDeviceDescription) @@ -443,14 +433,6 @@ def get_entities( family = p1sensor.mac_address[:2] device_id = f"{family}-{p1sensor.mac_address[2:]}" device_info = device.device_info - if family not in DEVICE_SUPPORT_SYSBUS: - _LOGGER.warning( - "Ignoring unknown family (%s) of sensor found for device: %s", - family, - device_id, - ) - continue - description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave" name = f"{device_names.get(device_id, device_id)} {description.name}" @@ -464,12 +446,6 @@ def get_entities( owsensor=p1sensor, ) ) - if not entities: - _LOGGER.error( - "No onewire sensor found. Check if dtoverlay=w1-gpio " - "is in your /boot/config.txt. " - "Check the mount_dir parameter if it's defined" - ) return entities diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 777cb1f3d25..1d0bf0f8e84 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -49,6 +49,7 @@ ATTR_DEVICE_FILE = "device_file" ATTR_DEVICE_INFO = "device_info" ATTR_INJECT_READS = "inject_reads" ATTR_UNIQUE_ID = "unique_id" +ATTR_UNKNOWN_DEVICE = "unknown_device" FIXED_ATTRIBUTES = ( ATTR_DEVICE_CLASS, @@ -62,7 +63,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: [ b"", # read device type ], - SENSOR_DOMAIN: [], + ATTR_UNKNOWN_DEVICE: True, }, "05.111111111111": { ATTR_INJECT_READS: [ @@ -879,7 +880,7 @@ MOCK_OWPROXY_DEVICES = { MOCK_SYSBUS_DEVICES = { "00-111111111111": { - SENSOR_DOMAIN: [], + ATTR_UNKNOWN_DEVICE: True, }, "10-111111111111": { ATTR_DEVICE_INFO: { @@ -900,12 +901,6 @@ MOCK_SYSBUS_DEVICES = { }, ], }, - "12-111111111111": { - SENSOR_DOMAIN: [], - }, - "1D-111111111111": { - SENSOR_DOMAIN: [], - }, "22-111111111111": { ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "22-111111111111")}, @@ -913,7 +908,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "22", ATTR_NAME: "22-111111111111", }, - "sensor": [ + SENSOR_DOMAIN: [ { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", @@ -925,9 +920,6 @@ MOCK_SYSBUS_DEVICES = { }, ], }, - "26-111111111111": { - SENSOR_DOMAIN: [], - }, "28-111111111111": { ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "28-111111111111")}, @@ -947,12 +939,6 @@ MOCK_SYSBUS_DEVICES = { }, ], }, - "29-111111111111": { - SENSOR_DOMAIN: [], - }, - "3A-111111111111": { - SENSOR_DOMAIN: [], - }, "3B-111111111111": { ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "3B-111111111111")}, @@ -1029,10 +1015,4 @@ MOCK_SYSBUS_DEVICES = { }, ], }, - "EF-111111111111": { - SENSOR_DOMAIN: [], - }, - "EF-111111111112": { - SENSOR_DOMAIN: [], - }, } diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 90e53924cab..32a2c018028 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire devices connected on OWServer.""" +import logging from unittest.mock import MagicMock, patch import pytest @@ -14,7 +15,7 @@ from . import ( check_entities, setup_owproxy_mock_devices, ) -from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES +from .const import ATTR_DEVICE_INFO, ATTR_UNKNOWN_DEVICE, MOCK_OWPROXY_DEVICES from tests.common import mock_device_registry, mock_registry @@ -27,7 +28,11 @@ def override_platforms(): async def test_owserver_binary_sensor( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, + device_id: str, + caplog: pytest.LogCaptureFixture, ): """Test for 1-Wire binary sensor. @@ -41,8 +46,13 @@ async def test_owserver_binary_sensor( expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + if mock_device.get(ATTR_UNKNOWN_DEVICE): + assert "Ignoring unknown device family/type" in caplog.text + else: + assert "Ignoring unknown device family/type" not in caplog.text check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 1bf95ee5c0c..5425802c6d7 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,4 +1,5 @@ """Tests for 1-Wire config flow.""" +import logging from unittest.mock import MagicMock, patch import pytest @@ -41,6 +42,19 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): assert not hass.data.get(DOMAIN) +@pytest.mark.usefixtures("sysbus") +async def test_warning_no_devices( + hass: HomeAssistant, + sysbus_config_entry: ConfigEntry, + caplog: pytest.LogCaptureFixture, +): + """Test warning is generated when no sysbus devices found.""" + with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): + await hass.config_entries.async_setup(sysbus_config_entry.entry_id) + await hass.async_block_till_done() + assert "No onewire sensor found. Check if dtoverlay=w1-gpio" in caplog.text + + @pytest.mark.usefixtures("sysbus") async def test_unload_sysbus_entry( hass: HomeAssistant, sysbus_config_entry: ConfigEntry diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index ffa9d0b5319..81143943057 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire sensor platform.""" +import logging from unittest.mock import MagicMock, patch import pytest @@ -15,7 +16,12 @@ from . import ( setup_owproxy_mock_devices, setup_sysbus_mock_devices, ) -from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES +from .const import ( + ATTR_DEVICE_INFO, + ATTR_UNKNOWN_DEVICE, + MOCK_OWPROXY_DEVICES, + MOCK_SYSBUS_DEVICES, +) from tests.common import mock_device_registry, mock_registry @@ -28,7 +34,11 @@ def override_platforms(): async def test_owserver_sensor( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, + device_id: str, + caplog: pytest.LogCaptureFixture, ): """Test for 1-Wire device. @@ -46,8 +56,13 @@ async def test_owserver_sensor( expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + if mock_device.get(ATTR_UNKNOWN_DEVICE): + assert "Ignoring unknown device family/type" in caplog.text + else: + assert "Ignoring unknown device family/type" not in caplog.text check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) @@ -63,7 +78,10 @@ async def test_owserver_sensor( @pytest.mark.usefixtures("sysbus") @pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys(), indirect=True) async def test_onewiredirect_setup_valid_device( - hass: HomeAssistant, sysbus_config_entry: ConfigEntry, device_id: str + hass: HomeAssistant, + sysbus_config_entry: ConfigEntry, + device_id: str, + caplog: pytest.LogCaptureFixture, ): """Test that sysbus config entry works correctly.""" device_registry = mock_device_registry(hass) @@ -80,9 +98,18 @@ async def test_onewiredirect_setup_valid_device( with patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( "pi1wire.OneWire.get_temperature", side_effect=read_side_effect, + ), caplog.at_level( + logging.WARNING, logger="homeassistant.components.onewire" + ), patch( + "homeassistant.components.onewire.sensor.asyncio.sleep" ): await hass.config_entries.async_setup(sysbus_config_entry.entry_id) await hass.async_block_till_done() + assert "No onewire sensor found. Check if dtoverlay=w1-gpio" not in caplog.text + if mock_device.get(ATTR_UNKNOWN_DEVICE): + assert "Ignoring unknown device family" in caplog.text + else: + assert "Ignoring unknown device family" not in caplog.text check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index ffe5042b514..f170d8a85d6 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,4 +1,5 @@ """Tests for 1-Wire devices connected on OWServer.""" +import logging from unittest.mock import MagicMock, patch import pytest @@ -21,7 +22,7 @@ from . import ( check_entities, setup_owproxy_mock_devices, ) -from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES +from .const import ATTR_DEVICE_INFO, ATTR_UNKNOWN_DEVICE, MOCK_OWPROXY_DEVICES from tests.common import mock_device_registry, mock_registry @@ -34,7 +35,11 @@ def override_platforms(): async def test_owserver_switch( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, + device_id: str, + caplog: pytest.LogCaptureFixture, ): """Test for 1-Wire switch. @@ -48,8 +53,13 @@ async def test_owserver_switch( expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + if mock_device.get(ATTR_UNKNOWN_DEVICE): + assert "Ignoring unknown device family/type" in caplog.text + else: + assert "Ignoring unknown device family/type" not in caplog.text check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) From 28c07f5c437d0de20c7b9328299c9b501d400ebf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 9 Nov 2021 18:30:51 +0100 Subject: [PATCH 0359/1452] Fix CORS (#59360) * Fix CORS * rename * Update view.py --- homeassistant/components/http/__init__.py | 24 ++++++++++++-------- homeassistant/components/http/cors.py | 15 ++++-------- homeassistant/components/http/view.py | 8 +++---- tests/components/http/test_cors.py | 2 +- tests/components/http/test_data_validator.py | 1 + 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 19e3437b79c..15766acdd4c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -298,21 +298,24 @@ class HomeAssistantHTTP: # Should be instance of aiohttp.web_exceptions._HTTPMove. raise redirect_exc(redirect_to) # type: ignore[arg-type,misc] - self.app.router.add_route("GET", url, redirect) + self.app["allow_configured_cors"]( + self.app.router.add_route("GET", url, redirect) + ) def register_static_path( self, url_path: str, path: str, cache_headers: bool = True - ) -> web.FileResponse | None: + ) -> None: """Register a folder or file to serve as a static path.""" if os.path.isdir(path): if cache_headers: - resource: type[ - CachingStaticResource | web.StaticResource - ] = CachingStaticResource + resource: CachingStaticResource | web.StaticResource = ( + CachingStaticResource(url_path, path) + ) else: - resource = web.StaticResource - self.app.router.register_resource(resource(url_path, path)) - return None + resource = web.StaticResource(url_path, path) + self.app.router.register_resource(resource) + self.app["allow_configured_cors"](resource) + return async def serve_file(request: web.Request) -> web.FileResponse: """Serve file from disk.""" @@ -320,8 +323,9 @@ class HomeAssistantHTTP: return web.FileResponse(path, headers=CACHE_HEADERS) return web.FileResponse(path) - self.app.router.add_route("GET", url_path, serve_file) - return None + self.app["allow_configured_cors"]( + self.app.router.add_route("GET", url_path, serve_file) + ) async def start(self) -> None: """Start the aiohttp server.""" diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index d9310c8937f..97a0530b703 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -70,7 +70,7 @@ def setup_cors(app: Application, origins: list[str]) -> None: cors.add(route, config) cors_added.add(path_str) - app["allow_cors"] = lambda route: _allow_cors( + app["allow_all_cors"] = lambda route: _allow_cors( route, { "*": aiohttp_cors.ResourceOptions( @@ -79,12 +79,7 @@ def setup_cors(app: Application, origins: list[str]) -> None: }, ) - if not origins: - return - - async def cors_startup(app: Application) -> None: - """Initialize CORS when app starts up.""" - for resource in list(app.router.resources()): - _allow_cors(resource) - - app.on_startup.append(cors_startup) + if origins: + app["allow_configured_cors"] = _allow_cors + else: + app["allow_configured_cors"] = lambda _: None diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 6123f83563c..aeb610d265e 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -94,11 +94,11 @@ class HomeAssistantView: for url in urls: routes.append(router.add_route(method, url, handler)) - if not self.cors_allowed: - return - + allow_cors = ( + app["allow_all_cors"] if self.cors_allowed else app["allow_configured_cors"] + ) for route in routes: - app["allow_cors"](route) + allow_cors(route) def request_handler_factory( diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 141627c7763..599df194195 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -52,8 +52,8 @@ async def mock_handler(request): def client(loop, aiohttp_client): """Fixture to set up a web.Application.""" app = web.Application() - app.router.add_get("/", mock_handler) setup_cors(app, [TRUSTED_ORIGIN]) + app["allow_configured_cors"](app.router.add_get("/", mock_handler)) return loop.run_until_complete(aiohttp_client(app)) diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 4ff6d3e8c2a..28c43230c43 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -13,6 +13,7 @@ async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() app["hass"] = Mock(is_stopping=False) + app["allow_configured_cors"] = lambda _: None class TestView(HomeAssistantView): url = "/" From ca2f343c519cc944d20a12856ff124aa3567e013 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Nov 2021 20:30:23 +0100 Subject: [PATCH 0360/1452] Upgrade colorlog to 6.6.0 (#59440) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 8e683bb5a1b..6182b909f74 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -21,7 +21,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==6.4.1",) +REQUIREMENTS = ("colorlog==6.6.0",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index a37974f6566..8b90dec7a67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ co2signal==0.4.2 coinbase==2.1.0 # homeassistant.scripts.check_config -colorlog==6.4.1 +colorlog==6.6.0 # homeassistant.components.color_extractor colorthief==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54ff3fa51fe..fd993db9ad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -293,7 +293,7 @@ co2signal==0.4.2 coinbase==2.1.0 # homeassistant.scripts.check_config -colorlog==6.4.1 +colorlog==6.6.0 # homeassistant.components.color_extractor colorthief==0.2.1 From 11da93245592a887e1ccbfb059870dfc6d43f571 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Nov 2021 21:12:04 +0100 Subject: [PATCH 0361/1452] Upgrade debugpy to 1.5.1 (#59436) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index c8da95a006f..041c2f9e31e 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.5.0"], + "requirements": ["debugpy==1.5.1"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 8b90dec7a67..844b5f32eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -517,7 +517,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.5.0 +debugpy==1.5.1 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd993db9ad7..ced8c74e44e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -325,7 +325,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.5.0 +debugpy==1.5.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 004d88caad460ada25b466ced56d3e691ee39148 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:12:17 -0500 Subject: [PATCH 0362/1452] Bump zigpy-znp from 0.5.4 to 0.6.1 (#59442) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e953e2ef3bc..500e1ceb02b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.41.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.5.4" + "zigpy-znp==0.6.1" ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, diff --git a/requirements_all.txt b/requirements_all.txt index 844b5f32eac..fcc703fb996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2492,7 +2492,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.4 +zigpy-znp==0.6.1 # homeassistant.components.zha zigpy==0.41.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ced8c74e44e..0663d5951f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1457,7 +1457,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.4 +zigpy-znp==0.6.1 # homeassistant.components.zha zigpy==0.41.0 From 0c83a3284cf7166c786ac75f1e795091094c757d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 9 Nov 2021 15:45:14 -0500 Subject: [PATCH 0363/1452] Bump pyefergy to 0.1.5 (#59439) --- homeassistant/components/efergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index 3c2d26f9941..966df3ed858 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -3,7 +3,7 @@ "name": "Efergy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.1.4"], + "requirements": ["pyefergy==0.1.5"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index fcc703fb996..05a4494d203 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1450,7 +1450,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.1.4 +pyefergy==0.1.5 # homeassistant.components.eight_sleep pyeight==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0663d5951f1..4ab38355e08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -859,7 +859,7 @@ pydispatcher==2.0.5 pyeconet==0.1.14 # homeassistant.components.efergy -pyefergy==0.1.4 +pyefergy==0.1.5 # homeassistant.components.everlights pyeverlights==0.1.0 From 66f49d18265aeb3f15ed9984567f99a55cb13841 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 9 Nov 2021 15:46:44 -0500 Subject: [PATCH 0364/1452] Bump goalzero to 0.2.1 (#59437) --- homeassistant/components/goalzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index b19cb884353..f46401d2a6b 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -3,7 +3,7 @@ "name": "Goal Zero Yeti", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", - "requirements": ["goalzero==0.2.0"], + "requirements": ["goalzero==0.2.1"], "dhcp": [ {"hostname": "yeti*"} ], diff --git a/requirements_all.txt b/requirements_all.txt index 05a4494d203..cfb14bbc1fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -726,7 +726,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.goalzero -goalzero==0.2.0 +goalzero==0.2.1 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ab38355e08..40ad1035f5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ gios==2.1.0 glances_api==0.2.0 # homeassistant.components.goalzero -goalzero==0.2.0 +goalzero==0.2.1 # homeassistant.components.google google-api-python-client==1.6.4 From dcafee5c9798f51f11e96efb673d692f2c7e4256 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Nov 2021 22:05:01 +0100 Subject: [PATCH 0365/1452] Upgrade pipdeptree to 2.2.0 (#59438) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 69af0476c5c..05b4b19c84c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ mock-open==1.4.0 mypy==0.910 pre-commit==2.15.0 pylint==2.11.1 -pipdeptree==2.1.0 +pipdeptree==2.2.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.12.1 From 4481fe11c42489595fc687ddff1b5ed58b447268 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 9 Nov 2021 23:40:21 +0100 Subject: [PATCH 0366/1452] Update frontend to 20211109.0 (#59451) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 43e0e7e86bc..d2db6138171 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211108.0" + "home-assistant-frontend==20211109.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bc26dba5342..5bc3fe185c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index cfb14bbc1fc..241d165218e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40ad1035f5d..78cc94a313f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -506,7 +506,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 86b12af3dcd178f874c588610fb61a5458a78af0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 10 Nov 2021 00:17:39 +0000 Subject: [PATCH 0367/1452] [ci skip] Translation update --- .../components/adguard/translations/tr.json | 2 +- .../advantage_air/translations/tr.json | 2 +- .../components/agent_dvr/translations/tr.json | 2 +- .../components/airvisual/translations/tr.json | 2 +- .../alarmdecoder/translations/tr.json | 2 +- .../components/apple_tv/translations/tr.json | 8 ++--- .../components/atag/translations/tr.json | 2 +- .../aurora_abb_powerone/translations/pl.json | 12 ++++++-- .../components/awair/translations/tr.json | 4 +-- .../components/axis/translations/tr.json | 2 +- .../azure_devops/translations/tr.json | 2 +- .../binary_sensor/translations/pl.json | 6 ++-- .../binary_sensor/translations/tr.json | 16 +++++----- .../components/blebox/translations/tr.json | 2 +- .../components/bond/translations/tr.json | 6 ++-- .../components/broadlink/translations/tr.json | 2 +- .../components/brother/translations/tr.json | 2 +- .../components/bsblan/translations/tr.json | 6 ++-- .../components/button/translations/pl.json | 3 ++ .../cert_expiry/translations/tr.json | 2 +- .../cloudflare/translations/tr.json | 2 +- .../components/control4/translations/tr.json | 2 +- .../coolmaster/translations/tr.json | 2 +- .../components/cover/translations/tr.json | 2 +- .../crownstone/translations/pl.json | 2 +- .../components/daikin/translations/tr.json | 2 +- .../components/deconz/translations/tr.json | 4 +-- .../components/denonavr/translations/tr.json | 2 +- .../devolo_home_network/translations/pl.json | 13 +++++++- .../devolo_home_network/translations/tr.json | 2 +- .../components/dexcom/translations/tr.json | 2 +- .../components/directv/translations/tr.json | 2 +- .../components/dlna_dmr/translations/pl.json | 18 ++++++++--- .../components/dlna_dmr/translations/tr.json | 2 +- .../components/doorbird/translations/tr.json | 2 +- .../components/dunehd/translations/tr.json | 2 +- .../components/econet/translations/tr.json | 4 +-- .../components/efergy/translations/pl.json | 3 +- .../components/efergy/translations/tr.json | 2 +- .../components/elgato/translations/tr.json | 2 +- .../environment_canada/translations/pl.json | 7 ++++- .../environment_canada/translations/tr.json | 2 +- .../components/epson/translations/tr.json | 4 +-- .../components/esphome/translations/tr.json | 2 +- .../evil_genius_labs/translations/no.json | 15 ++++++++++ .../evil_genius_labs/translations/pl.json | 15 ++++++++++ .../evil_genius_labs/translations/ru.json | 15 ++++++++++ .../evil_genius_labs/translations/tr.json | 15 ++++++++++ .../fireservicerota/translations/tr.json | 6 ++-- .../components/flo/translations/tr.json | 2 +- .../components/flux_led/translations/pl.json | 16 ++++++++++ .../components/flux_led/translations/tr.json | 2 +- .../forked_daapd/translations/tr.json | 2 +- .../components/foscam/translations/tr.json | 6 ++-- .../components/freebox/translations/tr.json | 2 +- .../components/fritzbox/translations/tr.json | 4 +-- .../fritzbox_callmonitor/translations/tr.json | 4 +-- .../components/glances/translations/tr.json | 2 +- .../components/goalzero/translations/tr.json | 4 +-- .../components/gogogate2/translations/tr.json | 2 +- .../components/group/translations/tr.json | 2 +- .../components/guardian/translations/tr.json | 2 +- .../components/harmony/translations/tr.json | 2 +- .../components/heos/translations/tr.json | 2 +- .../components/hlk_sw16/translations/tr.json | 2 +- .../homeassistant/translations/tr.json | 2 +- .../components/homekit/translations/tr.json | 4 +-- .../homematicip_cloud/translations/pl.json | 2 +- .../huawei_lte/translations/tr.json | 2 +- .../components/hue/translations/tr.json | 4 +-- .../huisbaasje/translations/tr.json | 4 +-- .../translations/tr.json | 2 +- .../components/hyperion/translations/pl.json | 2 +- .../components/hyperion/translations/tr.json | 2 +- .../components/ialarm/translations/tr.json | 2 +- .../components/icloud/translations/tr.json | 2 +- .../components/insteon/translations/tr.json | 6 ++-- .../components/iotawatt/translations/pl.json | 2 +- .../components/ipp/translations/tr.json | 4 +-- .../components/kodi/translations/pl.json | 2 +- .../components/kodi/translations/tr.json | 2 +- .../components/konnected/translations/tr.json | 2 +- .../components/life360/translations/tr.json | 6 ++-- .../components/lookin/translations/pl.json | 5 ++++ .../lutron_caseta/translations/pl.json | 2 +- .../lutron_caseta/translations/tr.json | 2 +- .../components/lyric/translations/tr.json | 4 +-- .../components/met/translations/tr.json | 2 +- .../components/mikrotik/translations/tr.json | 2 +- .../minecraft_server/translations/tr.json | 4 +-- .../motion_blinds/translations/pl.json | 17 +++++++++-- .../motion_blinds/translations/tr.json | 4 +-- .../components/motioneye/translations/pl.json | 1 + .../components/nanoleaf/translations/pl.json | 2 +- .../components/neato/translations/tr.json | 2 +- .../components/nest/translations/pl.json | 4 +++ .../components/netatmo/translations/pl.json | 5 ++++ .../nightscout/translations/pl.json | 2 +- .../nightscout/translations/tr.json | 2 +- .../components/notify/translations/tr.json | 2 +- .../components/notion/translations/tr.json | 2 +- .../components/nuki/translations/tr.json | 4 +-- .../components/nut/translations/tr.json | 2 +- .../components/octoprint/translations/pl.json | 7 +++++ .../components/octoprint/translations/tr.json | 2 +- .../ondilo_ico/translations/tr.json | 2 +- .../components/onewire/translations/tr.json | 2 +- .../components/onvif/translations/pl.json | 2 +- .../components/onvif/translations/tr.json | 2 +- .../opengarage/translations/tr.json | 2 +- .../ovo_energy/translations/tr.json | 4 +-- .../components/pi_hole/translations/tr.json | 4 +-- .../components/plant/translations/tr.json | 4 +-- .../components/plugwise/translations/tr.json | 2 +- .../components/powerwall/translations/tr.json | 2 +- .../progettihwsw/translations/tr.json | 2 +- .../components/rdw/translations/pl.json | 10 ++++++- .../components/renault/translations/tr.json | 2 +- .../components/ridwell/translations/pl.json | 15 ++++++++-- .../components/roomba/translations/pl.json | 2 +- .../components/roomba/translations/tr.json | 6 ++-- .../components/roon/translations/pl.json | 2 +- .../components/samsungtv/translations/tr.json | 8 ++--- .../components/sense/translations/pl.json | 3 +- .../components/shelly/translations/pl.json | 1 + .../components/shelly/translations/tr.json | 2 +- .../simplisafe/translations/pl.json | 11 ++++++- .../components/smappee/translations/tr.json | 4 +-- .../smartthings/translations/tr.json | 2 +- .../components/soma/translations/tr.json | 2 +- .../somfy_mylink/translations/pl.json | 2 +- .../somfy_mylink/translations/tr.json | 4 +-- .../stookalert/translations/pl.json | 7 +++++ .../components/switchbot/translations/tr.json | 2 +- .../tellduslive/translations/pl.json | 2 +- .../components/timer/translations/tr.json | 2 +- .../components/tplink/translations/pl.json | 2 +- .../components/tplink/translations/tr.json | 2 +- .../components/tradfri/translations/pl.json | 1 + .../components/tuya/translations/pl.json | 2 ++ .../tuya/translations/select.pl.json | 30 +++++++++++++++++-- .../tuya/translations/sensor.pl.json | 18 +++++------ .../components/tuya/translations/tr.json | 8 ++--- .../components/unifi/translations/pl.json | 2 +- .../components/unifi/translations/tr.json | 4 +-- .../components/upnp/translations/tr.json | 2 +- .../components/venstar/translations/pl.json | 11 ++++--- .../components/venstar/translations/tr.json | 6 ++-- .../components/vizio/translations/pl.json | 2 +- .../components/vizio/translations/tr.json | 4 +-- .../vlc_telnet/translations/pl.json | 3 ++ .../vlc_telnet/translations/tr.json | 12 ++++---- .../components/volumio/translations/pl.json | 2 +- .../components/volumio/translations/tr.json | 4 +-- .../components/watttime/translations/pl.json | 11 +++++++ .../components/watttime/translations/tr.json | 4 +-- .../components/wemo/translations/tr.json | 2 +- .../components/wled/translations/tr.json | 6 ++-- .../xiaomi_aqara/translations/tr.json | 6 ++-- .../xiaomi_miio/translations/pl.json | 3 +- .../yale_smart_alarm/translations/tr.json | 2 +- .../components/yeelight/translations/pl.json | 4 +-- .../components/yeelight/translations/tr.json | 2 +- .../components/zwave_js/translations/pl.json | 8 +++++ 164 files changed, 472 insertions(+), 233 deletions(-) create mode 100644 homeassistant/components/evil_genius_labs/translations/no.json create mode 100644 homeassistant/components/evil_genius_labs/translations/pl.json create mode 100644 homeassistant/components/evil_genius_labs/translations/ru.json create mode 100644 homeassistant/components/evil_genius_labs/translations/tr.json diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json index 0d78b058f8d..7d9ddf4db0f 100644 --- a/homeassistant/components/adguard/translations/tr.json +++ b/homeassistant/components/adguard/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", diff --git a/homeassistant/components/advantage_air/translations/tr.json b/homeassistant/components/advantage_air/translations/tr.json index 1501c90a66b..678f9ec8cbc 100644 --- a/homeassistant/components/advantage_air/translations/tr.json +++ b/homeassistant/components/advantage_air/translations/tr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "ip_address": "\u0130p Adresi", + "ip_address": "IP Adresi", "port": "Port" }, "description": "Advantage Air duvara monte tabletinizin API'sine ba\u011flan\u0131n.", diff --git a/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant/components/agent_dvr/translations/tr.json index 31dddab7795..3ff1b149bdd 100644 --- a/homeassistant/components/agent_dvr/translations/tr.json +++ b/homeassistant/components/agent_dvr/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Agent DVR'\u0131 kurun" diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index cbe63637bcb..6ecfc74ad76 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -32,7 +32,7 @@ }, "node_pro": { "data": { - "ip_address": "Ana Bilgisayar", + "ip_address": "Ana bilgisayar", "password": "Parola" }, "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.", diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index 231c06a3955..ec30201055b 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -14,7 +14,7 @@ "data": { "device_baudrate": "Cihaz Baud H\u0131z\u0131", "device_path": "Cihaz Yolu", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Ba\u011flant\u0131 ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index 0a21f514946..007397de8d5 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -16,21 +16,21 @@ "no_usable_service": "Bir ayg\u0131t bulundu, ancak ba\u011flant\u0131 kurman\u0131n herhangi bir yolunu tan\u0131mlayamad\u0131. Bu iletiyi g\u00f6rmeye devam ederseniz, IP adresini belirtmeye veya Apple TV'nizi yeniden ba\u015flatmaya \u00e7al\u0131\u015f\u0131n.", "unknown": "Beklenmeyen hata" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { - "description": "'{name}' adl\u0131 Apple TV'yi Ev Asistan\u0131'na eklemek \u00fczeresiniz.\n\n**\u0130\u015flemi tamamlamak i\u00e7in birden fazla PIN kodu girmeniz gerekebilir.**\n\nBu entegrasyonla Apple TV'nizi kapatamayaca\u011f\u0131n\u0131z\u0131 l\u00fctfen unutmay\u0131n. Yaln\u0131zca Ev Asistan\u0131'ndaki medya oynat\u0131c\u0131 kapan\u0131r!", + "description": "{name} ` adl\u0131 Apple TV'yi Ev Asistan\u0131na eklemek \u00fczeresiniz. \n\n **\u0130\u015flemi tamamlamak i\u00e7in birden fazla PIN kodu girmeniz gerekebilir.** \n\n Bu entegrasyonla Apple TV'nizi *kapatamayaca\u011f\u0131n\u0131z\u0131* l\u00fctfen unutmay\u0131n. Yaln\u0131zca Home Assistant'taki medya oynat\u0131c\u0131 kapanacak!", "title": "Apple TV eklemeyi onaylay\u0131n" }, "pair_no_pin": { - "description": "'{protocol}' hizmeti i\u00e7in e\u015fle\u015ftirme gerekiyor. Devam etmek i\u00e7in l\u00fctfen Apple TV'nize {pin} PIN'ini girin.", + "description": "{protocol} ` hizmeti i\u00e7in e\u015fle\u015ftirme gereklidir. Devam etmek i\u00e7in l\u00fctfen Apple TV'nizde PIN {pin}", "title": "E\u015fle\u015ftirme" }, "pair_with_pin": { "data": { "pin": "PIN Kodu" }, - "description": "'{protocol}' protokol\u00fc i\u00e7in e\u015fle\u015ftirme gerekiyor. L\u00fctfen ekranda g\u00f6r\u00fcnt\u00fclenen PIN kodunu girin. Ba\u015ftaki s\u0131f\u0131rlar atlan\u0131r, yani g\u00f6r\u00fcnt\u00fclenen kod 0123 ise 123 girin.", + "description": "{protocol} ` protokol\u00fc i\u00e7in e\u015fle\u015ftirme gereklidir. L\u00fctfen ekranda g\u00f6r\u00fcnt\u00fclenen PIN kodunu girin. Ba\u015ftaki s\u0131f\u0131rlar atlanmal\u0131d\u0131r, yani g\u00f6r\u00fcnt\u00fclenen kod 0123 ise 123 girin.", "title": "E\u015fle\u015ftirme" }, "reconfigure": { diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json index 577ed02cdca..d4c5dd8acad 100644 --- a/homeassistant/components/atag/translations/tr.json +++ b/homeassistant/components/atag/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Cihaza ba\u011flan\u0131n" diff --git a/homeassistant/components/aurora_abb_powerone/translations/pl.json b/homeassistant/components/aurora_abb_powerone/translations/pl.json index 8c8c7e9a8fc..d6131cfa195 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/pl.json +++ b/homeassistant/components/aurora_abb_powerone/translations/pl.json @@ -1,16 +1,22 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_serial_ports": "Nie znaleziono port\u00f3w COM. Do komunikacji potrzebne jest prawid\u0142owe urz\u0105dzenie RS485." }, "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a port szeregowy, adres, po\u0142\u0105czenie elektryczne i czy falownik jest w\u0142\u0105czony (w \u015bwietle dziennym)", + "cannot_open_serial_port": "Nie mo\u017cna otworzy\u0107 portu szeregowego, sprawd\u017a i spr\u00f3buj ponownie", + "invalid_serial_port": "Port szeregowy nie jest prawid\u0142owym urz\u0105dzeniem lub nie mo\u017cna go otworzy\u0107", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { "data": { - "address": "Adres falownika" - } + "address": "Adres falownika", + "port": "Port RS485 lub adaptera USB-RS485" + }, + "description": "Falownik musi by\u0107 pod\u0142\u0105czony przez adapter RS485. Wybierz port szeregowy i adres falownika zgodnie z konfiguracj\u0105 na panelu LCD." } } } diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index ac6fefae913..8463aadf009 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -12,14 +12,14 @@ "step": { "reauth": { "data": { - "access_token": "Eri\u015fim Belirteci", + "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, "user": { "data": { - "access_token": "Eri\u015fim Belirteci", + "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json index 5921a5aa499..2ec66ab688d 100644 --- a/homeassistant/components/axis/translations/tr.json +++ b/homeassistant/components/axis/translations/tr.json @@ -15,7 +15,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/azure_devops/translations/tr.json b/homeassistant/components/azure_devops/translations/tr.json index 9eed8014aa8..407d0aafdbb 100644 --- a/homeassistant/components/azure_devops/translations/tr.json +++ b/homeassistant/components/azure_devops/translations/tr.json @@ -9,7 +9,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "project_error": "Proje bilgileri al\u0131namad\u0131." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 49dcc90717f..f0267fd248b 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -104,13 +104,15 @@ "device_class": { "cold": "zimno", "gas": "gaz", - "moisture": "wilgnotno\u015b\u0107", + "heat": "gor\u0105co", + "moisture": "wilgotno\u015b\u0107", "motion": "ruch", "occupancy": "obecno\u015b\u0107", + "power": "zasilanie", "problem": "problem", "smoke": "dym", "sound": "d\u017awi\u0119k", - "vibration": "wibracje" + "vibration": "wibracja" }, "state": { "_": { diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 6c9193fa6bc..72022dd9d39 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -133,14 +133,14 @@ }, "connectivity": { "off": "Ba\u011flant\u0131 kesildi", - "on": "Ba\u011fl\u0131" + "on": "Ba\u011fland\u0131" }, "door": { - "off": "Kapal\u0131", + "off": "Kapand\u0131", "on": "A\u00e7\u0131k" }, "garage_door": { - "off": "Kapal\u0131", + "off": "Kapand\u0131", "on": "A\u00e7\u0131k" }, "gas": { @@ -156,8 +156,8 @@ "on": "I\u015f\u0131k alg\u0131land\u0131" }, "lock": { - "off": "Kilit kapal\u0131", - "on": "Kilit a\u00e7\u0131k" + "off": "Kilitli", + "on": "Kilitli de\u011fil" }, "moisture": { "off": "Kuru", @@ -176,7 +176,7 @@ "on": "Alg\u0131land\u0131" }, "opening": { - "off": "Kapal\u0131", + "off": "Kapand\u0131", "on": "A\u00e7\u0131k" }, "plug": { @@ -184,7 +184,7 @@ "on": "Tak\u0131l\u0131" }, "presence": { - "off": "D\u0131\u015farda", + "off": "D\u0131\u015far\u0131da", "on": "Evde" }, "problem": { @@ -216,7 +216,7 @@ "on": "Alg\u0131land\u0131" }, "window": { - "off": "Kapal\u0131", + "off": "Kapand\u0131", "on": "A\u00e7\u0131k" } }, diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json index 6baa79c32f4..ab4fe91a097 100644 --- a/homeassistant/components/blebox/translations/tr.json +++ b/homeassistant/components/blebox/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "port": "Port" }, "description": "BleBox'\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n.", diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json index 7848022e261..e00bdd6370c 100644 --- a/homeassistant/components/bond/translations/tr.json +++ b/homeassistant/components/bond/translations/tr.json @@ -13,14 +13,14 @@ "step": { "confirm": { "data": { - "access_token": "Eri\u015fim Belirteci" + "access_token": "Eri\u015fim Anahtar\u0131" }, "description": "{name} kurmak istiyor musunuz?" }, "user": { "data": { - "access_token": "Eri\u015fim Belirteci", - "host": "Ana Bilgisayar" + "access_token": "Eri\u015fim Anahtar\u0131", + "host": "Ana bilgisayar" } } } diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json index 3db7efe4863..b2f79ddaac0 100644 --- a/homeassistant/components/broadlink/translations/tr.json +++ b/homeassistant/components/broadlink/translations/tr.json @@ -37,7 +37,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "timeout": "Zaman a\u015f\u0131m\u0131" }, "title": "Cihaza ba\u011flan\u0131n" diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index c731685c6a9..a883e719c0d 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" }, "description": "Brother yaz\u0131c\u0131 entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/brother" diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json index 9b30ef8517f..623dab14446 100644 --- a/homeassistant/components/bsblan/translations/tr.json +++ b/homeassistant/components/bsblan/translations/tr.json @@ -10,11 +10,11 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "passkey": "Ge\u00e7i\u015f anahtar\u0131 dizesi", - "password": "\u015eifre", + "password": "Parola", "port": "Port", - "username": "Kullan\u0131c\u0131 ad\u0131" + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "BSB-Lan cihaz\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n.", "title": "BSB-Lan cihaz\u0131na ba\u011flan\u0131n" diff --git a/homeassistant/components/button/translations/pl.json b/homeassistant/components/button/translations/pl.json index 0c570aa6b46..e5af8b8c29b 100644 --- a/homeassistant/components/button/translations/pl.json +++ b/homeassistant/components/button/translations/pl.json @@ -2,6 +2,9 @@ "device_automation": { "action_type": { "press": "naci\u015bnij przycisk {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} zosta\u0142 naci\u015bni\u0119ty" } }, "title": "Przycisk" diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json index d550ccbede8..0f8b7c45a6f 100644 --- a/homeassistant/components/cert_expiry/translations/tr.json +++ b/homeassistant/components/cert_expiry/translations/tr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Sertifikan\u0131n ad\u0131", "port": "Port" }, diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json index 17415c0b2f6..773f74d7a95 100644 --- a/homeassistant/components/cloudflare/translations/tr.json +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -10,7 +10,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_zone": "Ge\u00e7ersiz b\u00f6lge" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/control4/translations/tr.json b/homeassistant/components/control4/translations/tr.json index 111b6abb975..fce14cec68f 100644 --- a/homeassistant/components/control4/translations/tr.json +++ b/homeassistant/components/control4/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json index 93a85de95da..dbca2862bfb 100644 --- a/homeassistant/components/coolmaster/translations/tr.json +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -12,7 +12,7 @@ "fan_only": "Yaln\u0131zca fan modunu destekler", "heat": "Is\u0131tma modunu destekler", "heat_cool": "Otomatik \u0131s\u0131tma/so\u011futma modunu destekler", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "off": "Kapat\u0131labilir" }, "title": "CoolMasterNet ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." diff --git a/homeassistant/components/cover/translations/tr.json b/homeassistant/components/cover/translations/tr.json index 357e868e0fa..7e32eb9e846 100644 --- a/homeassistant/components/cover/translations/tr.json +++ b/homeassistant/components/cover/translations/tr.json @@ -28,7 +28,7 @@ }, "state": { "_": { - "closed": "Kapal\u0131", + "closed": "Kapand\u0131", "closing": "Kapan\u0131yor", "open": "A\u00e7\u0131k", "opening": "A\u00e7\u0131l\u0131yor", diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json index e3299767932..12ac55d668c 100644 --- a/homeassistant/components/crownstone/translations/pl.json +++ b/homeassistant/components/crownstone/translations/pl.json @@ -81,7 +81,7 @@ "data": { "usb_sphere": "Crownstone Sphere" }, - "description": "Wybierz USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", + "description": "Wybierz USB, w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", "title": "USB Crownstone Sphere" }, "usb_sphere_config_option": { diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json index d277e887f2a..0e548c96f0f 100644 --- a/homeassistant/components/daikin/translations/tr.json +++ b/homeassistant/components/daikin/translations/tr.json @@ -14,7 +14,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola" }, "description": "IP Adresi de\u011ferini girin. \n\n API Anahtar\u0131 ve Parola \u00f6\u011felerinin s\u0131ras\u0131yla BRP072Cxx ve SKYFi cihazlar\u0131 taraf\u0131ndan kullan\u0131ld\u0131\u011f\u0131n\u0131 unutmay\u0131n.", diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index 72ac69e1645..f7688fcc101 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -23,7 +23,7 @@ }, "manual_input": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" } }, @@ -65,7 +65,7 @@ "trigger_type": { "remote_awakened": "Cihaz uyand\u0131", "remote_button_double_press": "\" {subtype} \" d\u00fc\u011fmesine \u00e7ift t\u0131kland\u0131", - "remote_button_long_press": "\"{alt t\u00fcr}\" d\u00fc\u011fmesine s\u00fcrekli bas\u0131ld\u0131", + "remote_button_long_press": "\" {subtype} \" d\u00fc\u011fmesi s\u00fcrekli bas\u0131l\u0131", "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", "remote_button_quadruple_press": "\" {subtype} \" d\u00fc\u011fmesi d\u00f6rt kez t\u0131kland\u0131", "remote_button_quintuple_press": "\" {subtype} \" d\u00fc\u011fmesi be\u015f kez t\u0131kland\u0131", diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json index f86d61b08c7..2c32de293b8 100644 --- a/homeassistant/components/denonavr/translations/tr.json +++ b/homeassistant/components/denonavr/translations/tr.json @@ -25,7 +25,7 @@ }, "user": { "data": { - "host": "\u0130p Adresi" + "host": "IP Adresi" }, "description": "Al\u0131c\u0131n\u0131za ba\u011flan\u0131n, IP adresi ayarlanmazsa otomatik bulma kullan\u0131l\u0131r", "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" diff --git a/homeassistant/components/devolo_home_network/translations/pl.json b/homeassistant/components/devolo_home_network/translations/pl.json index bcdb8956f1d..4abe2667100 100644 --- a/homeassistant/components/devolo_home_network/translations/pl.json +++ b/homeassistant/components/devolo_home_network/translations/pl.json @@ -1,13 +1,24 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "home_control": "Ta jednostka devolo Home Control Central nie wsp\u00f3\u0142pracuje z t\u0105 integracj\u0105." + }, "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{product} ({name})", "step": { "user": { "data": { "ip_address": "Adres IP" - } + }, + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 do Home Assistanta urz\u0105dzenie sieciowe devolo o nazwie \"{host_name}\"?", + "title": "Wykryto urz\u0105dzenie sieciowe devolo" } } } diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json index ec3d8520c74..841ae1773ca 100644 --- a/homeassistant/components/devolo_home_network/translations/tr.json +++ b/homeassistant/components/devolo_home_network/translations/tr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "ip_address": "IP Adresleri" + "ip_address": "IP Adresi" }, "description": "Kuruluma ba\u015flamak ister misiniz?" }, diff --git a/homeassistant/components/dexcom/translations/tr.json b/homeassistant/components/dexcom/translations/tr.json index 6a1f86ea9aa..8ff66f07ccf 100644 --- a/homeassistant/components/dexcom/translations/tr.json +++ b/homeassistant/components/dexcom/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json index 67ed9646a0c..2e1f38c50d2 100644 --- a/homeassistant/components/directv/translations/tr.json +++ b/homeassistant/components/directv/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" } } } diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json index 280a63be7fe..7f831c92f99 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -2,31 +2,41 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "alternative_integration": "Urz\u0105dzenie jest lepiej obs\u0142ugiwane przez inn\u0105 integracj\u0119", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA", "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 pasuj\u0105cego urz\u0105dzenia DLNA", "incomplete_config": "W konfiguracji brakuje wymaganej zmiennej", - "non_unique_id": "Znaleziono wiele urz\u0105dze\u0144 z tym samym unikalnym identyfikatorem" + "non_unique_id": "Znaleziono wiele urz\u0105dze\u0144 z tym samym unikalnym identyfikatorem", + "not_dmr": "Urz\u0105dzenie nie jest obs\u0142ugiwanym rendererem multimedi\u00f3w cyfrowych (DMR)" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA" + "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA", + "not_dmr": "Urz\u0105dzenie nie jest obs\u0142ugiwanym rendererem multimedi\u00f3w cyfrowych (DMR)" }, "flow_title": "{name}", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, + "import_turn_on": { + "description": "W\u0142\u0105cz urz\u0105dzenie i kliknij \"Zatwierd\u017a\", aby kontynuowa\u0107 migracj\u0119" + }, "manual": { "data": { "url": "URL" - } + }, + "description": "URL do pliku XML z opisem urz\u0105dzenia", + "title": "R\u0119czne pod\u0142\u0105czanie urz\u0105dzenia DLNA DMR" }, "user": { "data": { "host": "Nazwa hosta lub adres IP", "url": "URL" - } + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania lub pozostaw puste, aby wprowadzi\u0107 adres URL", + "title": "Wykryto urz\u0105dzenia DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 59c5221b870..3a19f9455ad 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -32,7 +32,7 @@ }, "user": { "data": { - "host": "Sunucu", + "host": "Ana bilgisayar", "url": "URL" }, "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in veya bir URL girmek i\u00e7in bo\u015f b\u0131rak\u0131n", diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index 1de21e034fe..8f36c27fc40 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Cihaz ad\u0131", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json index 61f22197bcf..d51539ba12d 100644 --- a/homeassistant/components/dunehd/translations/tr.json +++ b/homeassistant/components/dunehd/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "Dune HD entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/dunehd \n\n Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Dune HD" diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json index 237a87d0268..5261e78e7e4 100644 --- a/homeassistant/components/econet/translations/tr.json +++ b/homeassistant/components/econet/translations/tr.json @@ -12,8 +12,8 @@ "step": { "user": { "data": { - "email": "Email", - "password": "\u015eifre" + "email": "E-posta", + "password": "Parola" }, "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur" } diff --git a/homeassistant/components/efergy/translations/pl.json b/homeassistant/components/efergy/translations/pl.json index 9ef0e4a5a43..b96038d24b3 100644 --- a/homeassistant/components/efergy/translations/pl.json +++ b/homeassistant/components/efergy/translations/pl.json @@ -13,7 +13,8 @@ "user": { "data": { "api_key": "Klucz API" - } + }, + "title": "Efergy" } } } diff --git a/homeassistant/components/efergy/translations/tr.json b/homeassistant/components/efergy/translations/tr.json index cc219a26772..e13f215b5fb 100644 --- a/homeassistant/components/efergy/translations/tr.json +++ b/homeassistant/components/efergy/translations/tr.json @@ -5,7 +5,7 @@ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json index bf3ab23fae3..c49720b9525 100644 --- a/homeassistant/components/elgato/translations/tr.json +++ b/homeassistant/components/elgato/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "description": "Elgato Light'\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." diff --git a/homeassistant/components/environment_canada/translations/pl.json b/homeassistant/components/environment_canada/translations/pl.json index b840de8a10e..4f4611a80ca 100644 --- a/homeassistant/components/environment_canada/translations/pl.json +++ b/homeassistant/components/environment_canada/translations/pl.json @@ -1,7 +1,10 @@ { "config": { "error": { + "bad_station_id": "Identyfikator stacji jest nieprawid\u0142owy, brakuje go lub nie mo\u017cna go znale\u017a\u0107 w bazie danych identyfikator\u00f3w stacji", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "error_response": "B\u0142\u0119dna odpowied\u017a z Environment Canada", + "too_many_attempts": "Po\u0142\u0105czenia z Environment Canada s\u0105 ograniczone; spr\u00f3buj ponownie za 60 sekund", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -11,7 +14,9 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "station": "Identyfikator stacji pogodowej" - } + }, + "description": "Nale\u017cy poda\u0107 identyfikator stacji lub szeroko\u015b\u0107/d\u0142ugo\u015b\u0107 geograficzn\u0105. Domy\u015blna szeroko\u015b\u0107/d\u0142ugo\u015b\u0107 geograficzna to warto\u015bci skonfigurowane w instalacji Home Assistant. Zostanie u\u017cyta najbli\u017csza stacja pogodowa dla tych wsp\u00f3\u0142rz\u0119dnych. Je\u015bli u\u017cywany jest kod stacji, musi on mie\u0107 format: PP/kod, gdzie PP to dwuliterowa prowincja, a kod to identyfikator stacji. List\u0119 identyfikator\u00f3w stacji mo\u017cna znale\u017a\u0107 tutaj: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Informacje o pogodzie mo\u017cna pobra\u0107 w j\u0119zyku angielskim lub francuskim.", + "title": "Environment Canada: lokalizacja pogody i j\u0119zyk" } } } diff --git a/homeassistant/components/environment_canada/translations/tr.json b/homeassistant/components/environment_canada/translations/tr.json index 41fe7a8046c..a12e6add669 100644 --- a/homeassistant/components/environment_canada/translations/tr.json +++ b/homeassistant/components/environment_canada/translations/tr.json @@ -2,7 +2,7 @@ "config": { "error": { "bad_station_id": "\u0130stasyon Kimli\u011fi ge\u00e7ersiz, eksik veya istasyon kimli\u011fi veritaban\u0131nda bulunamad\u0131", - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "cannot_connect": "Ba\u011flanma hatas\u0131", "error_response": "Environment Canada'dan hatal\u0131 yan\u0131t", "too_many_attempts": "Environment Kanada'ya ba\u011flant\u0131lar s\u0131n\u0131rl\u0131d\u0131r; 60 saniye sonra tekrar deneyin", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index e32e705b005..a369ce95960 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -7,8 +7,8 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", - "name": "\u0130sim" + "host": "Ana bilgisayar", + "name": "Ad" } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index a1124c59b29..31f54809e95 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -37,7 +37,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." diff --git a/homeassistant/components/evil_genius_labs/translations/no.json b/homeassistant/components/evil_genius_labs/translations/no.json new file mode 100644 index 00000000000..17c1c3b70dc --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/pl.json b/homeassistant/components/evil_genius_labs/translations/pl.json new file mode 100644 index 00000000000..a8bab923c35 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/ru.json b/homeassistant/components/evil_genius_labs/translations/ru.json new file mode 100644 index 00000000000..f34931312ec --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/tr.json b/homeassistant/components/evil_genius_labs/translations/tr.json new file mode 100644 index 00000000000..b97a1354e2e --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index f54d10f6cbf..2b9c1b9cb0a 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -13,15 +13,15 @@ "step": { "reauth": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "Kimlik do\u011frulama jetonlar\u0131 ge\u00e7ersiz, yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n." }, "user": { "data": { - "password": "\u015eifre", + "password": "Parola", "url": "Web sitesi", - "username": "Kullan\u0131c\u0131 ad\u0131" + "username": "Kullan\u0131c\u0131 Ad\u0131" } } } diff --git a/homeassistant/components/flo/translations/tr.json b/homeassistant/components/flo/translations/tr.json index 40c9c39b967..3fdcebd112c 100644 --- a/homeassistant/components/flo/translations/tr.json +++ b/homeassistant/components/flo/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/flux_led/translations/pl.json b/homeassistant/components/flux_led/translations/pl.json index 2e749564860..14cf6055a74 100644 --- a/homeassistant/components/flux_led/translations/pl.json +++ b/homeassistant/components/flux_led/translations/pl.json @@ -10,9 +10,25 @@ }, "flow_title": "{model} {id} ({ipaddr})", "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {model} {id} ({ipaddr})?" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" + }, + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Efekt niestandardowy: Lista kolor\u00f3w od 1 do 16 [R,G,B]. Przyk\u0142ad: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Efekt niestandardowy: szybko\u015b\u0107 efektu zmiany kolor\u00f3w (w procentach).", + "custom_effect_transition": "Efekt niestandardowy: rodzaj przej\u015bcia mi\u0119dzy kolorami.", + "mode": "Wybrany tryb jasno\u015bci." } } } diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 58d6e514adc..5ec66bfaec5 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index fd23faf2e96..f34158852a5 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -16,7 +16,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Kolay Ad\u0131", "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", "port": "API Port" diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json index 8a31a391629..4b0f450d735 100644 --- a/homeassistant/components/foscam/translations/tr.json +++ b/homeassistant/components/foscam/translations/tr.json @@ -7,13 +7,13 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_response": "Cihazdan ge\u00e7ersiz yan\u0131t", - "unknown": "Beklenmeyen Hata" + "unknown": "Beklenmeyen hata" }, "step": { "user": { "data": { - "host": "Ana Bilgisayar", - "password": "\u015eifre", + "host": "Ana bilgisayar", + "password": "Parola", "port": "Port", "rtsp_port": "RTSP port", "stream": "Ak\u0131\u015f", diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json index 7fa8a8a485c..7ebe2634020 100644 --- a/homeassistant/components/freebox/translations/tr.json +++ b/homeassistant/components/freebox/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Freebox" diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json index fafec643d28..53c8359f546 100644 --- a/homeassistant/components/fritzbox/translations/tr.json +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -21,14 +21,14 @@ }, "reauth_confirm": { "data": { - "password": "\u015eifre", + "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Giri\u015f bilgilerinizi {name} i\u00e7in g\u00fcncelleyin." }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/fritzbox_callmonitor/translations/tr.json b/homeassistant/components/fritzbox_callmonitor/translations/tr.json index 76799f24af8..e6b02a8176d 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/tr.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/tr.json @@ -17,8 +17,8 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", - "password": "\u015eifre", + "host": "Ana bilgisayar", + "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json index 8815382da1e..3a8f4852f86 100644 --- a/homeassistant/components/glances/translations/tr.json +++ b/homeassistant/components/glances/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index dbecadaed75..1101717545b 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "unknown": "Beklenmeyen hata" }, @@ -17,7 +17,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad" }, "description": "\u00d6ncelikle Goal Zero uygulamas\u0131n\u0131 indirmeniz gerekiyor: https://www.goalzero.com/product-features/yeti-app/ \n\n Yeti'nizi Wi-fi a\u011f\u0131n\u0131za ba\u011flamak i\u00e7in talimatlar\u0131 izleyin. Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json index cc4ae70d7b2..bd16dbe72f2 100644 --- a/homeassistant/components/gogogate2/translations/tr.json +++ b/homeassistant/components/gogogate2/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "ip_address": "\u0130p Adresi", + "ip_address": "IP Adresi", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index 5a596efdf01..a491fe6b244 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -9,7 +9,7 @@ "ok": "Tamam", "on": "A\u00e7\u0131k", "open": "A\u00e7\u0131k", - "problem": "Problem", + "problem": "Sorun", "unlocked": "Kilitli de\u011fil" } }, diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index 92e0f484370..9d3e903a2d6 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -11,7 +11,7 @@ }, "user": { "data": { - "ip_address": "\u0130p Adresi", + "ip_address": "IP Adresi", "port": "Port" }, "description": "Yerel bir Elexa Guardian cihaz\u0131 yap\u0131land\u0131r\u0131n." diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index 4236ff33f66..cb3658d47fb 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Hub Ad\u0131" }, "title": "Logitech Harmony Hub'\u0131 Kur" diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json index cd4a7edb067..afe563e9a96 100644 --- a/homeassistant/components/heos/translations/tr.json +++ b/homeassistant/components/heos/translations/tr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "L\u00fctfen bir Heos cihaz\u0131n\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin (tercihen a\u011fa kabloyla ba\u011fl\u0131 olan).", "title": "Heos'a ba\u011flan\u0131n" diff --git a/homeassistant/components/hlk_sw16/translations/tr.json b/homeassistant/components/hlk_sw16/translations/tr.json index 40c9c39b967..3fdcebd112c 100644 --- a/homeassistant/components/hlk_sw16/translations/tr.json +++ b/homeassistant/components/hlk_sw16/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 438b8064138..cffbee33e74 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -3,7 +3,7 @@ "info": { "arch": "CPU Mimarisi", "dev": "Geli\u015ftirme", - "docker": "Konteyner", + "docker": "Docker", "hassio": "S\u00fcperviz\u00f6r", "installation_type": "Kurulum T\u00fcr\u00fc", "os_name": "\u0130\u015fletim Sistemi Ailesi", diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index 6a93009fee4..875d8a720a8 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.", + "description": "\u201cHomeKit E\u015fle\u015ftirme\u201d alt\u0131ndaki \u201cBildirimler\u201d b\u00f6l\u00fcm\u00fcndeki talimatlar\u0131 izleyerek e\u015fle\u015ftirmeyi tamamlamak i\u00e7in.", "title": "HomeKit'i E\u015fle\u015ftir" }, "user": { @@ -33,7 +33,7 @@ "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar" }, "description": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen t\u00fcm kameralar\u0131 kontrol edin. Kamera bir H.264 ak\u0131\u015f\u0131 vermezse, sistem videoyu HomeKit i\u00e7in H.264'e d\u00f6n\u00fc\u015ft\u00fcr\u00fcr. Kod d\u00f6n\u00fc\u015ft\u00fcrme, y\u00fcksek performansl\u0131 bir CPU gerektirir ve tek kartl\u0131 bilgisayarlarda \u00e7al\u0131\u015fma olas\u0131l\u0131\u011f\u0131 d\u00fc\u015f\u00fckt\u00fcr.", - "title": "Kamera video codec bile\u015fenini se\u00e7in." + "title": "Kamera Yap\u0131land\u0131rmas\u0131" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index 7b7cdc05cab..9446fe2687b 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -21,7 +21,7 @@ "title": "Wybierz punkt dost\u0119pu HomematicIP" }, "link": { - "description": "Naci\u015bnij niebieski przycisk na punkcie dost\u0119pu i przycisk przesy\u0142ania, aby zarejestrowa\u0107 HomematicIP w Home Assistant. \n\n![Umiejscowienie przycisku na mostku](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Naci\u015bnij niebieski przycisk na punkcie dost\u0119pu i przycisk \"Zatwierd\u017a\", aby zarejestrowa\u0107 HomematicIP w Home Assistant. \n\n![Umiejscowienie przycisku na mostku](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Po\u0142\u0105czenie z punktem dost\u0119pu" } } diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index 23c47fd9288..92d40ca810b 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi.", + "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin.", "title": "Huawei LTE'yi yap\u0131land\u0131r\u0131n" } } diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index eabbf86d1dd..8f555c52c43 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "title": "Hue k\u00f6pr\u00fcs\u00fcn\u00fc se\u00e7in" }, @@ -27,7 +27,7 @@ }, "manual": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "title": "Bir Hue k\u00f6pr\u00fcs\u00fcn\u00fc manuel olarak yap\u0131land\u0131rma" } diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json index c7d38c19457..6ed28a58c79 100644 --- a/homeassistant/components/huisbaasje/translations/tr.json +++ b/homeassistant/components/huisbaasje/translations/tr.json @@ -6,12 +6,12 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen Hata" + "unknown": "Beklenmeyen hata" }, "step": { "user": { "data": { - "password": "\u015eifre", + "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json index e63fe26ec76..c489b2906d8 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/tr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u0130p Adresi" + "host": "IP Adresi" }, "title": "PowerView Hub'a ba\u011flan\u0131n" } diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json index 67e89e817f0..22c2f9b9072 100644 --- a/homeassistant/components/hyperion/translations/pl.json +++ b/homeassistant/components/hyperion/translations/pl.json @@ -27,7 +27,7 @@ "title": "Potwierdzanie dodania us\u0142ugi Hyperion Ambilight" }, "create_token": { - "description": "Naci\u015bnij **Prze\u015blij** poni\u017cej, aby za\u017c\u0105da\u0107 nowego tokena uwierzytelniania. Nast\u0105pi przekierowanie do interfejsu u\u017cytkownika Hyperion, aby zatwierdzi\u0107 \u017c\u0105danie. Sprawd\u017a, czy wy\u015bwietlany identyfikator to \u201e {auth_id} \u201d", + "description": "Naci\u015bnij **Zatwierd\u017a** poni\u017cej, aby za\u017c\u0105da\u0107 nowego tokena uwierzytelniania. Nast\u0105pi przekierowanie do interfejsu u\u017cytkownika Hyperion, aby zatwierdzi\u0107 \u017c\u0105danie. Sprawd\u017a, czy wy\u015bwietlany identyfikator to \u201e {auth_id} \u201d", "title": "Automatyczne tworzenie nowego tokena uwierzytelniaj\u0105cego" }, "create_token_external": { diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 821d7df43e3..a3c6cd935ee 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -35,7 +35,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/tr.json b/homeassistant/components/ialarm/translations/tr.json index b93c9b04dc3..b524964cfd8 100644 --- a/homeassistant/components/ialarm/translations/tr.json +++ b/homeassistant/components/ialarm/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" } } diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 405b73571f1..0f917b132f4 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "send_verification_code": "Do\u011frulama kodu g\u00f6nderilemedi", - "validate_verification_code": "Do\u011frulama kodunuzu do\u011frulamay\u0131 ba\u015faramad\u0131n\u0131z, bir g\u00fcven ayg\u0131t\u0131 se\u00e7in ve do\u011frulamay\u0131 yeniden ba\u015flat\u0131n" + "validate_verification_code": "Do\u011frulama kodunuz do\u011frulanamad\u0131, tekrar deneyin" }, "step": { "reauth": { diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index f5d5abefbfa..cd1940cbec3 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -11,7 +11,7 @@ "step": { "hubv1": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "port": "Port" }, "description": "Insteon Hub S\u00fcr\u00fcm 1'i (2014 \u00f6ncesi) yap\u0131land\u0131r\u0131n.", @@ -19,7 +19,7 @@ }, "hubv2": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" @@ -71,7 +71,7 @@ }, "change_hub_config": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/iotawatt/translations/pl.json b/homeassistant/components/iotawatt/translations/pl.json index d6fa1ba5735..dac83695f0c 100644 --- a/homeassistant/components/iotawatt/translations/pl.json +++ b/homeassistant/components/iotawatt/translations/pl.json @@ -11,7 +11,7 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Urz\u0105dzenie IoTawatt wymaga uwierzytelnienia. Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o, a nast\u0119pnie kliknij przycisk Prze\u015blij." + "description": "Urz\u0105dzenie IoTawatt wymaga uwierzytelnienia. Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o, a nast\u0119pnie kliknij przycisk \"Zatwierd\u017a\"." }, "user": { "data": { diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index a7907cc6cda..cca4103324f 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -13,12 +13,12 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "connection_upgrade": "Yaz\u0131c\u0131ya ba\u011flan\u0131lamad\u0131. L\u00fctfen SSL / TLS se\u00e7ene\u011fi i\u015faretli olarak tekrar deneyin." }, - "flow_title": "Yaz\u0131c\u0131: {name}", + "flow_title": "{name}", "step": { "user": { "data": { "base_path": "Yaz\u0131c\u0131n\u0131n yolu", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index caac93e2b2d..f5f30a67af8 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistanta?", - "title": "Wykryte urz\u0105dzenia Kodi" + "title": "Wykryto urz\u0105dzenia Kodi" }, "user": { "data": { diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json index 652dc2b1fc4..715b3e1ac79 100644 --- a/homeassistant/components/kodi/translations/tr.json +++ b/homeassistant/components/kodi/translations/tr.json @@ -27,7 +27,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r" }, diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index af63e286172..4ccb8b9c32f 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "\u0130p Adresi", + "host": "IP Adresi", "port": "Port" }, "description": "L\u00fctfen Konnected Paneliniz i\u00e7in ana bilgisayar bilgilerini girin." diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 98c52010cb8..1304151d948 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -2,16 +2,16 @@ "config": { "abort": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmedik hata" + "unknown": "Beklenmeyen hata" }, "create_entry": { "default": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n." }, "error": { - "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda", + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", - "unknown": "Beklenmedik hata" + "unknown": "Beklenmeyen hata" }, "step": { "user": { diff --git a/homeassistant/components/lookin/translations/pl.json b/homeassistant/components/lookin/translations/pl.json index d6edb1d50da..8f1bb63725e 100644 --- a/homeassistant/components/lookin/translations/pl.json +++ b/homeassistant/components/lookin/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, @@ -10,12 +11,16 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{name} ({host})", "step": { "device_name": { "data": { "name": "Nazwa" } }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" + }, "user": { "data": { "ip_address": "Adres IP" diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index b890cf3323a..bc4f9d61f39 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -15,7 +15,7 @@ "title": "Nie uda\u0142o si\u0119 zaimportowa\u0107 konfiguracji mostka Cas\u00e9ta." }, "link": { - "description": "Aby sparowa\u0107 z {name} ({host}), po przes\u0142aniu tego formularza naci\u015bnij czarny przycisk z ty\u0142u mostka.", + "description": "Aby sparowa\u0107 z {name} ({host}), po zatwierdzeniu tego formularza naci\u015bnij czarny przycisk z ty\u0142u mostka.", "title": "Sparuj z mostkiem" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json index 06b46972264..54c5530224c 100644 --- a/homeassistant/components/lutron_caseta/translations/tr.json +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, - "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (ana bilgisayar: {host}) kurulamad\u0131.", diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json index bdee23411b8..b910327ed7e 100644 --- a/homeassistant/components/lyric/translations/tr.json +++ b/homeassistant/components/lyric/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Yetki URL'si olu\u015fturulurken zaman a\u015f\u0131m\u0131 olu\u015ftu.", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, "reauth_confirm": { "description": "Lyric entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor.", diff --git a/homeassistant/components/met/translations/tr.json b/homeassistant/components/met/translations/tr.json index 5d91ce78b82..9de6ab715fa 100644 --- a/homeassistant/components/met/translations/tr.json +++ b/homeassistant/components/met/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_home": "Ev Asistan\u0131 yap\u0131land\u0131rmas\u0131nda ev koordinatlar\u0131 ayarlanmiyor" + "no_home": "Home Assistant yap\u0131land\u0131rmas\u0131nda hi\u00e7bir ev koordinat\u0131 ayarlanmad\u0131" }, "error": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json index aa8bfcf4d0a..f1f825290b2 100644 --- a/homeassistant/components/mikrotik/translations/tr.json +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/minecraft_server/translations/tr.json b/homeassistant/components/minecraft_server/translations/tr.json index 422dab32a01..f7bc8de5a61 100644 --- a/homeassistant/components/minecraft_server/translations/tr.json +++ b/homeassistant/components/minecraft_server/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Host zaten ayarlanm\u0131\u015f." + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { "cannot_connect": "Server ile ba\u011flant\u0131 kurulamad\u0131. L\u00fctfen host ve port ayarlar\u0131n\u0131 kontrol et ve tekrar dene. Ayr\u0131ca, serverda en az Minecraft s\u00fcr\u00fcm 1.7 \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131ndan emin ol.", @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad" }, "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 61e9d22c3cf..6161b6aa3da 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -6,13 +6,15 @@ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { - "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu" + "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu", + "invalid_interface": "Nieprawid\u0142owy interfejs sieciowy" }, "flow_title": "Rolety Motion", "step": { "connect": { "data": { - "api_key": "Klucz API" + "api_key": "Klucz API", + "interface": "Interfejs sieciowy" }, "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Rolety Motion" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Poczekaj na aktualizacj\u0119 multicast push" + }, + "description": "Okre\u015bl opcjonalne ustawienia", + "title": "Rolety Motion" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index d45d8a2890e..aab257b0a13 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -21,7 +21,7 @@ }, "select": { "data": { - "select_ip": "\u0130p Adresi" + "select_ip": "IP Adresi" }, "description": "Motion Gateway'e ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n", "title": "Ba\u011flamak istedi\u011finiz Hareket A\u011f Ge\u00e7idini se\u00e7in" @@ -29,7 +29,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "IP adresi" + "host": "IP Adresi" }, "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r", "title": "Hareketli Panjurlar" diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json index a34af5491ec..2488b2dae89 100644 --- a/homeassistant/components/motioneye/translations/pl.json +++ b/homeassistant/components/motioneye/translations/pl.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Szablon adresu URL strumienia", "webhook_set": "Skonfiguruj webhook motionEye, aby zg\u0142asza\u0107 zdarzenia do Home Assistanta", "webhook_set_overwrite": "Nadpisz nierozpoznane webhooki" } diff --git a/homeassistant/components/nanoleaf/translations/pl.json b/homeassistant/components/nanoleaf/translations/pl.json index 1c772fa940c..66c7ecd23f1 100644 --- a/homeassistant/components/nanoleaf/translations/pl.json +++ b/homeassistant/components/nanoleaf/translations/pl.json @@ -15,7 +15,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk zasilania na Nanoleaf przez 5 sekund, a\u017c dioda LED przycisku zacznie miga\u0107, a nast\u0119pnie kliknij **WY\u015aLIJ** w ci\u0105gu 30 sekund.", + "description": "Naci\u015bnij i przytrzymaj przycisk zasilania na Nanoleaf przez 5 sekund, a\u017c dioda LED przycisku zacznie miga\u0107, a nast\u0119pnie kliknij **ZATWIERD\u0179** w ci\u0105gu 30 sekund.", "title": "Po\u0142\u0105czenie z Nanoleaf" }, "user": { diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 3f71eb8ee6d..a461d5b0c2f 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -12,7 +12,7 @@ }, "step": { "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, "reauth_confirm": { "title": "Kuruluma ba\u015flamak ister misiniz?" diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 5f210f01dc4..23ac1d4f28f 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -19,6 +19,10 @@ }, "step": { "auth": { + "data": { + "code": "Token dost\u0119pu" + }, + "description": "Aby po\u0142\u0105czy\u0107 swoje konto Google, [authorize your account]({url}). \n\nPo autoryzacji skopiuj i wklej podany poni\u017cej token uwierzytelniaj\u0105cy.", "title": "Po\u0142\u0105cz z kontem Google" }, "init": { diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 58dfb34f7fa..4b7ac6490cc 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Netatmo wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" } } }, diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json index a544fc887c0..bc70aab3bf8 100644 --- a/homeassistant/components/nightscout/translations/pl.json +++ b/homeassistant/components/nightscout/translations/pl.json @@ -15,7 +15,7 @@ "api_key": "Klucz API", "url": "URL" }, - "description": "- URL: adres URL Twojej instancji Nightscout. Np: https://m\u00f3jhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).", + "description": "- URL: adres URL Twojej instancji Nightscout. Np: https://m\u00f3jhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalnie): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).", "title": "Wprowad\u017a informacje o serwerze Nightscout." } } diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json index 91f5c8f6a8e..8a27b28aa6e 100644 --- a/homeassistant/components/nightscout/translations/tr.json +++ b/homeassistant/components/nightscout/translations/tr.json @@ -4,7 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flan\u0131lamad\u0131", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/notify/translations/tr.json b/homeassistant/components/notify/translations/tr.json index 3f10d1f6c19..eb6283999dd 100644 --- a/homeassistant/components/notify/translations/tr.json +++ b/homeassistant/components/notify/translations/tr.json @@ -1,3 +1,3 @@ { - "title": "Bildir" + "title": "Bildirimler" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index 4bc533a7f23..cd95e502c8d 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -12,7 +12,7 @@ "step": { "reauth_confirm": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "{username} \u015fifresini tekrar girin.", "title": "Entegrasyonu Yeniden Do\u011frula" diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json index 801d9efcbd0..67ff0fb0faf 100644 --- a/homeassistant/components/nuki/translations/tr.json +++ b/homeassistant/components/nuki/translations/tr.json @@ -18,9 +18,9 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port", - "token": "Eri\u015fim Belirteci" + "token": "Eri\u015fim Anahtar\u0131" } } } diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json index d000b142c1f..0d8b170c017 100644 --- a/homeassistant/components/nut/translations/tr.json +++ b/homeassistant/components/nut/translations/tr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/octoprint/translations/pl.json b/homeassistant/components/octoprint/translations/pl.json index 4040dea0278..eb5ad0a61a1 100644 --- a/homeassistant/components/octoprint/translations/pl.json +++ b/homeassistant/components/octoprint/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "auth_failed": "Nie uda\u0142o si\u0119 pobra\u0107 klucza API aplikacji", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -9,11 +10,17 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "Drukarka OctoPrint: {host}", + "progress": { + "get_api_key": "Otw\u00f3rz interfejs OctoPrint i kliknij \u201eZezw\u00f3l\u201d przy \u017c\u0105daniu dost\u0119pu do Home Assistanta." + }, "step": { "user": { "data": { "host": "Nazwa hosta lub adres IP", + "path": "\u015acie\u017cka aplikacji", "port": "Port", + "ssl": "U\u017cyj SSL", "username": "Nazwa u\u017cytkownika" } } diff --git a/homeassistant/components/octoprint/translations/tr.json b/homeassistant/components/octoprint/translations/tr.json index 6e76ae6d8fd..44fb5399973 100644 --- a/homeassistant/components/octoprint/translations/tr.json +++ b/homeassistant/components/octoprint/translations/tr.json @@ -21,7 +21,7 @@ "path": "Uygulama Yolu", "port": "Ba\u011flant\u0131 Noktas\u0131 Numaras\u0131", "ssl": "SSL Kullan", - "username": "Kullan\u0131c\u0131 ad\u0131" + "username": "Kullan\u0131c\u0131 Ad\u0131" } } } diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json index 693cfe01a9e..8474bf04d5b 100644 --- a/homeassistant/components/ondilo_ico/translations/tr.json +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } } diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index ed0ea6fcaf2..3d29d26fb01 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -10,7 +10,7 @@ "step": { "owserver": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index 0b72b824abf..f45ad4064b2 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -53,7 +53,7 @@ "data": { "auto": "Wyszukaj automatycznie" }, - "description": "Klikaj\u0105c przycisk Zatwierd\u017a, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", + "description": "Klikaj\u0105c przycisk \"Zatwierd\u017a\", Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", "title": "Konfiguracja urz\u0105dzenia ONVIF" } } diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index b1248f4d578..559350eb0c3 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -43,7 +43,7 @@ }, "manual_input": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad", "port": "Port" }, diff --git a/homeassistant/components/opengarage/translations/tr.json b/homeassistant/components/opengarage/translations/tr.json index fcbd46ed8f6..2a851ad2046 100644 --- a/homeassistant/components/opengarage/translations/tr.json +++ b/homeassistant/components/opengarage/translations/tr.json @@ -4,7 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index 6c832b707e1..6ff9f6a25ce 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -5,11 +5,11 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, - "flow_title": "OVO Enerji: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", "title": "Yeniden kimlik do\u011frulama" diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index c85971927a1..8484e2310f1 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -15,9 +15,9 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "location": "Konum", - "name": "\u0130sim", + "name": "Ad", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", "statistics_only": "Yaln\u0131zca \u0130statistikler", diff --git a/homeassistant/components/plant/translations/tr.json b/homeassistant/components/plant/translations/tr.json index 0fe07001b3b..47ae1695630 100644 --- a/homeassistant/components/plant/translations/tr.json +++ b/homeassistant/components/plant/translations/tr.json @@ -2,8 +2,8 @@ "state": { "_": { "ok": "Tamam", - "problem": "Problem" + "problem": "Sorun" } }, - "title": "Bitki" + "title": "Tesis Monit\u00f6r\u00fc" } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index e8d5da74484..4c752efbd4a 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -8,7 +8,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json index 0ebaca3b99c..a243e22b566 100644 --- a/homeassistant/components/powerwall/translations/tr.json +++ b/homeassistant/components/powerwall/translations/tr.json @@ -10,7 +10,7 @@ "unknown": "Beklenmeyen hata", "wrong_version": "G\u00fc\u00e7 duvar\u0131n\u0131z desteklenmeyen bir yaz\u0131l\u0131m s\u00fcr\u00fcm\u00fc kullan\u0131yor. \u00c7\u00f6z\u00fclebilmesi i\u00e7in l\u00fctfen bu sorunu y\u00fckseltmeyi veya bildirmeyi d\u00fc\u015f\u00fcn\u00fcn." }, - "flow_title": "Tesla Powerwall ( {ip_address} )", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/progettihwsw/translations/tr.json b/homeassistant/components/progettihwsw/translations/tr.json index 1d3d77584dd..6c78ef295df 100644 --- a/homeassistant/components/progettihwsw/translations/tr.json +++ b/homeassistant/components/progettihwsw/translations/tr.json @@ -31,7 +31,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "title": "Panoyu kur" diff --git a/homeassistant/components/rdw/translations/pl.json b/homeassistant/components/rdw/translations/pl.json index 93684f8dfd3..43baead5444 100644 --- a/homeassistant/components/rdw/translations/pl.json +++ b/homeassistant/components/rdw/translations/pl.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown_license_plate": "Nieznana tablica rejestracyjna" + }, + "step": { + "user": { + "data": { + "license_plate": "Tablica rejestracyjna" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/tr.json b/homeassistant/components/renault/translations/tr.json index 42de45d5cd2..e2da3f97c16 100644 --- a/homeassistant/components/renault/translations/tr.json +++ b/homeassistant/components/renault/translations/tr.json @@ -17,7 +17,7 @@ }, "reauth_confirm": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "{username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", "title": "Entegrasyonu Yeniden Do\u011frula" diff --git a/homeassistant/components/ridwell/translations/pl.json b/homeassistant/components/ridwell/translations/pl.json index e052fdaa9a4..65832ab9fd5 100644 --- a/homeassistant/components/ridwell/translations/pl.json +++ b/homeassistant/components/ridwell/translations/pl.json @@ -1,16 +1,27 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, "step": { "reauth_confirm": { "data": { "password": "Has\u0142o" - } + }, + "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}:", + "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - } + }, + "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o:" } } } diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index 5f6a3a23333..c56c00a98b0 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -19,7 +19,7 @@ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" }, "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie prze\u015blij w ci\u0105gu 30 sekund.", + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie zatwierd\u017a w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" }, "link_manual": { diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index a3a7ffc9f17..1bbbb2697bf 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, - "flow_title": "iRobot {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -32,7 +32,7 @@ "manual": { "data": { "blid": "BLID", - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "A\u011f\u0131n\u0131zda Roomba veya Braava bulunamad\u0131.", "title": "Cihaza manuel olarak ba\u011flan\u0131n" @@ -42,7 +42,7 @@ "blid": "BLID", "continuous": "S\u00fcrekli", "delay": "Gecikme", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "password": "Parola" }, "description": "\u015eu anda BLID ve parola alma manuel bir i\u015flemdir. L\u00fctfen a\u015fa\u011f\u0131daki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index 7ebfd0ad777..fab5fb9657e 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "link": { - "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Prze\u015blij\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).", + "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Zatwierd\u017a\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).", "title": "Autoryzuj Home Assistant w Roon" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 8bb79aa801c..862fe05414e 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", - "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", "cannot_connect": "Ba\u011flanma hatas\u0131", "id_missing": "Bu Samsung cihaz\u0131n\u0131n Seri Numaras\u0131 yok.", @@ -14,7 +14,7 @@ "error": { "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{device}", "step": { "confirm": { "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz.", @@ -25,7 +25,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad" }, "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir." diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json index b4d8708c38f..8bc58118a23 100644 --- a/homeassistant/components/sense/translations/pl.json +++ b/homeassistant/components/sense/translations/pl.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Adres e-mail", - "password": "Has\u0142o" + "password": "Has\u0142o", + "timeout": "Limit czasu" }, "title": "Po\u0142\u0105czenie z monitorem energii Sense" } diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 3af4583d71c..062cc73de37 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -42,6 +42,7 @@ "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", "double_push": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty", + "long_push": "przycisk {subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index 2ce8d740685..c03f0a50987 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -22,7 +22,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar uyand\u0131r\u0131lmal\u0131d\u0131r, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 uyand\u0131rabilirsiniz." } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 260d9d6b148..4ec2afba5c3 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", @@ -11,6 +12,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Kod autoryzacji" + }, + "description": "Wprowad\u017a kod autoryzacyjny z adresu URL aplikacji internetowej SimpliSafe:", + "title": "Zako\u0144cz autoryzacj\u0119" + }, "mfa": { "description": "Sprawd\u017a e-mail od SimpliSafe. Po zweryfikowaniu linka, wr\u00f3\u0107 tutaj, aby doko\u0144czy\u0107 instalacj\u0119 integracji.", "title": "Uwierzytelnianie wielosk\u0142adnikowe SimpliSafe" @@ -28,6 +36,7 @@ "password": "Has\u0142o", "username": "Adres e-mail" }, + "description": "Pocz\u0105wszy od 2021 r. SimpliSafe przesz\u0142o na nowy mechanizm uwierzytelniania za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne, na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przed rozpocz\u0119ciem przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKiedy b\u0119dziesz gotowy, kliknij [tutaj]( {url} ), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i kliknij \"Zatwierd\u017a\".", "title": "Wprowad\u017a dane" } } diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json index 79b4a82b220..e75ea381bd1 100644 --- a/homeassistant/components/smappee/translations/tr.json +++ b/homeassistant/components/smappee/translations/tr.json @@ -9,7 +9,7 @@ "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { @@ -19,7 +19,7 @@ }, "local": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "Smappee yerel entegrasyonunu ba\u015flatmak i\u00e7in ana bilgisayar\u0131 girin" }, diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json index 1f6a5117900..83293cc5a03 100644 --- a/homeassistant/components/smartthings/translations/tr.json +++ b/homeassistant/components/smartthings/translations/tr.json @@ -17,7 +17,7 @@ }, "pat": { "data": { - "access_token": "Eri\u015fim Belirteci" + "access_token": "Eri\u015fim Anahtar\u0131" }, "description": "L\u00fctfen [talimatlar]( {component_url} ) daki gibi olu\u015fturulmu\u015f bir SmartThings [Ki\u015fisel Eri\u015fim Anahtar\u0131]( {token_url} } ) girin. Bu, SmartThings hesab\u0131n\u0131zda Home Assistant entegrasyonunu olu\u015fturmak i\u00e7in kullan\u0131lacakt\u0131r.", "title": "Ki\u015fisel Eri\u015fim Anahtar\u0131 Girin" diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json index 39d6bae6b7a..d7dfbff2889 100644 --- a/homeassistant/components/soma/translations/tr.json +++ b/homeassistant/components/soma/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" }, "description": "L\u00fctfen SOMA Connect'inizin ba\u011flant\u0131 ayarlar\u0131n\u0131 girin.", diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json index 3da5c423e1a..c4c10de4f9d 100644 --- a/homeassistant/components/somfy_mylink/translations/pl.json +++ b/homeassistant/components/somfy_mylink/translations/pl.json @@ -30,7 +30,7 @@ "reverse": "Roleta/pokrywa jest odwr\u00f3cona" }, "description": "Konfiguracja opcji dla \"{entity_id}\"", - "title": "Konfigurowanie encji" + "title": "Konfiguracja encji" }, "init": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/tr.json b/homeassistant/components/somfy_mylink/translations/tr.json index 29530b65659..fb402493e3b 100644 --- a/homeassistant/components/somfy_mylink/translations/tr.json +++ b/homeassistant/components/somfy_mylink/translations/tr.json @@ -8,7 +8,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, - "flow_title": "Somfy MyLink {mac} ( {ip} )", + "flow_title": "{mac} ( {ip} )", "step": { "user": { "data": { @@ -44,7 +44,7 @@ "data": { "reverse": "Kapak ters \u00e7evrildi" }, - "description": "'{target_name}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "description": "{target_name} ` i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", "title": "MyLink Kapa\u011f\u0131n\u0131 Yap\u0131land\u0131r\u0131n" } } diff --git a/homeassistant/components/stookalert/translations/pl.json b/homeassistant/components/stookalert/translations/pl.json index ef80051717e..da1663105e3 100644 --- a/homeassistant/components/stookalert/translations/pl.json +++ b/homeassistant/components/stookalert/translations/pl.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "province": "Prowincja" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 75ec7055c6a..77a2921fa38 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "cannot_connect": "Ba\u011flanma hatas\u0131", "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", "switchbot_unsupported_type": "Desteklenmeyen Switchbot T\u00fcr\u00fc.", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index 4f42a4d3810..c0b386fe5ae 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Aby po\u0142\u0105czy\u0107 konto TelldusLive: \n 1. Kliknij poni\u017cszy link \n 2. Zaloguj si\u0119 do Telldus Live \n 3. Autoryzuj **{app_name}** (kliknij **Tak**). \n 4. Wr\u00f3\u0107 tutaj i kliknij **SUBMIT**. \n\n [Link do konta TelldusLive]({auth_url})", + "description": "Aby po\u0142\u0105czy\u0107 konto TelldusLive: \n 1. Kliknij poni\u017cszy link \n 2. Zaloguj si\u0119 do Telldus Live \n 3. Autoryzuj **{app_name}** (kliknij **Tak**). \n 4. Wr\u00f3\u0107 tutaj i kliknij **Zatwierd\u017a**. \n\n [Link do konta TelldusLive]({auth_url})", "title": "Uwierzytelnienie na TelldusLive" }, "user": { diff --git a/homeassistant/components/timer/translations/tr.json b/homeassistant/components/timer/translations/tr.json index 0711eb71f7a..8a581e3069a 100644 --- a/homeassistant/components/timer/translations/tr.json +++ b/homeassistant/components/timer/translations/tr.json @@ -1,7 +1,7 @@ { "state": { "_": { - "active": "Aktif", + "active": "Etkin", "idle": "Bo\u015fta", "paused": "Durduruldu" } diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index b7fff06d4b7..35e1e7f5354 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -25,7 +25,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Je\u015bli nie podasz IP lub nazwy hosta, wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } } } diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index 5e4dca23c37..3a1710c39d2 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Sunucu" + "host": "Ana bilgisayar" }, "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } diff --git a/homeassistant/components/tradfri/translations/pl.json b/homeassistant/components/tradfri/translations/pl.json index e2fcb45f51c..bdd88ec1143 100644 --- a/homeassistant/components/tradfri/translations/pl.json +++ b/homeassistant/components/tradfri/translations/pl.json @@ -5,6 +5,7 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku" }, "error": { + "cannot_authenticate": "Nie mo\u017cna uwierzytelni\u0107, czy bramka jest sparowana z innym serwerem, np. Homekit?", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_key": "Rejestracja si\u0119 nie powiod\u0142a z podanym kluczem. Je\u015bli tak si\u0119 stanie, spr\u00f3buj ponownie uruchomi\u0107 bramk\u0119.", "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu" diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 9f40008135a..f8ce810e94b 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -26,6 +26,8 @@ }, "user": { "data": { + "access_id": "Identyfikator dost\u0119pu do Tuya IoT", + "access_secret": "Has\u0142o dost\u0119pu do Tuya IoT", "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", "password": "Has\u0142o", "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", diff --git a/homeassistant/components/tuya/translations/select.pl.json b/homeassistant/components/tuya/translations/select.pl.json index 36bc447f766..c5d71d2eb55 100644 --- a/homeassistant/components/tuya/translations/select.pl.json +++ b/homeassistant/components/tuya/translations/select.pl.json @@ -1,25 +1,49 @@ { "state": { "tuya__basic_anti_flickr": { + "0": "Wy\u0142\u0105czone", "1": "50 Hz", "2": "60 Hz" }, "tuya__basic_nightvision": { - "1": "Wy\u0142\u0105czony", - "2": "W\u0142\u0105czony" + "0": "Automatycznie", + "1": "wy\u0142.", + "2": "w\u0142." + }, + "tuya__decibel_sensitivity": { + "0": "Niska czu\u0142o\u015b\u0107", + "1": "Wysoka czu\u0142o\u015b\u0107" + }, + "tuya__ipc_work_mode": { + "0": "Tryb niskiego poboru mocy", + "1": "Tryb pracy ci\u0105g\u0142ej" }, "tuya__led_type": { "halogen": "Halogen", + "incandescent": "Jarzeni\u00f3wka", "led": "LED" }, + "tuya__light_mode": { + "none": "wy\u0142.", + "pos": "Wska\u017c lokalizacj\u0119 prze\u0142\u0105cznika", + "relay": "Wska\u017c stan w\u0142./wy\u0142." + }, "tuya__motion_sensitivity": { "0": "Niska czu\u0142o\u015b\u0107", "1": "\u015arednia czu\u0142o\u015b\u0107", "2": "Wysoka czu\u0142o\u015b\u0107" }, + "tuya__record_mode": { + "1": "Nagrywaj tylko zdarzenia", + "2": "Nagrywanie ci\u0105g\u0142e" + }, "tuya__relay_status": { "last": "Zapami\u0119taj ostatni stan", - "memory": "Zapami\u0119taj ostatni stan" + "memory": "Zapami\u0119taj ostatni stan", + "off": "wy\u0142.", + "on": "w\u0142.", + "power_off": "wy\u0142.", + "power_on": "w\u0142." } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.pl.json b/homeassistant/components/tuya/translations/sensor.pl.json index 68556f9f757..090849227f8 100644 --- a/homeassistant/components/tuya/translations/sensor.pl.json +++ b/homeassistant/components/tuya/translations/sensor.pl.json @@ -1,15 +1,15 @@ { "state": { "tuya__status": { - "boiling_temp": "Temperatura wrzenia", - "cooling": "Ch\u0142odzenie", - "heating": "Ogrzewanie", - "heating_temp": "Temperatura ogrzewania", - "reserve_1": "Rezerwa 1", - "reserve_2": "Rezerwa 2", - "reserve_3": "Rezerwa 3", - "standby": "Tryb czuwania", - "warm": "Utrzymanie ciep\u0142a" + "boiling_temp": "temperatura wrzenia", + "cooling": "ch\u0142odzenie", + "heating": "grzanie", + "heating_temp": "temperatura ogrzewania", + "reserve_1": "rezerwa 1", + "reserve_2": "rezerwa 2", + "reserve_3": "rezerwa 3", + "standby": "tryb czuwania", + "warm": "utrzymywanie ciep\u0142a" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index fcd7cf3b2e4..1bcc2bc627e 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -28,15 +28,15 @@ "data": { "access_id": "Tuya IoT Eri\u015fim Kimli\u011fi", "access_secret": "Tuya IoT Eri\u015fim Anahtar\u0131", - "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", + "country_code": "\u00dclke", "password": "Parola", "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", "region": "B\u00f6lge", "tuya_project_type": "Tuya bulut proje t\u00fcr\u00fc", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "username": "Hesap" }, "description": "Tuya kimlik bilgilerinizi girin.", - "title": "Tuya" + "title": "Tuya Entegrasyonu" } } }, @@ -65,7 +65,7 @@ "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" }, - "description": "{device_type} ayg\u0131t\u0131 '{device_name}' i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "description": "{device_type} cihaz\u0131 ` {device_name} device_name} ` i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak \u00fczere se\u00e7enekleri yap\u0131land\u0131r\u0131n", "title": "Tuya Cihaz\u0131n\u0131 Yap\u0131land\u0131r\u0131n" }, "init": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index ebfb901871b..6e4d6364b27 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -62,7 +62,7 @@ "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)" }, - "description": "Konfigurowanie integracji z UniFi" + "description": "Konfigurowanie integracji UniFi" }, "statistics_sensors": { "data": { diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 700f70def14..b165bee4187 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -10,7 +10,7 @@ "service_unavailable": "Ba\u011flanma hatas\u0131", "unknown_client_mac": "Bu MAC adresinde kullan\u0131labilir istemci yok" }, - "flow_title": "UniFi A\u011f\u0131 {site} ( {host} )", + "flow_title": "{site} ( {host} )", "step": { "user": { "data": { @@ -18,7 +18,7 @@ "password": "Parola", "port": "Port", "site": "Site Kimli\u011fi", - "username": "Kullan\u0131c\u0131 ad\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, "title": "UniFi Controller'\u0131 kurun" diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json index ec26f90fa41..8176c0541c7 100644 --- a/homeassistant/components/upnp/translations/tr.json +++ b/homeassistant/components/upnp/translations/tr.json @@ -9,7 +9,7 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "flow_title": "UPnP / IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "Bo\u015f", diff --git a/homeassistant/components/venstar/translations/pl.json b/homeassistant/components/venstar/translations/pl.json index 34af592d520..ad0e76435dc 100644 --- a/homeassistant/components/venstar/translations/pl.json +++ b/homeassistant/components/venstar/translations/pl.json @@ -4,16 +4,19 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o", - "pin": "Kod PIN" - } + "pin": "Kod PIN", + "ssl": "Certyfikat SSL", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Po\u0142\u0105cz z termostatem Venstar" } } } diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json index a4493886577..a6cb7f6ddd8 100644 --- a/homeassistant/components/venstar/translations/tr.json +++ b/homeassistant/components/venstar/translations/tr.json @@ -10,11 +10,11 @@ "step": { "user": { "data": { - "host": "Sunucu", - "password": "\u015eifre", + "host": "Ana bilgisayar", + "password": "Parola", "pin": "PIN Kodu", "ssl": "SSL sertifikas\u0131 kullan\u0131r", - "username": "Kullan\u0131c\u0131 ad\u0131" + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "title": "Venstar Termostat'a ba\u011flan\u0131n" } diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 82339204a16..5b328734f99 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "complete_pairing_failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym przes\u0142aniem.", + "complete_pairing_failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym zatwierdzeniem.", "existing_config_entry_found": "Istnieje ju\u017c wpis konfiguracyjny VIZIO SmartCast z tym samym numerem seryjnym. W celu skonfigurowania tego wpisu nale\u017cy usun\u0105\u0107 istniej\u0105cy." }, "step": { diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index 332340ec0e0..093334fb485 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -28,9 +28,9 @@ }, "user": { "data": { - "access_token": "Eri\u015fim Belirteci", + "access_token": "Eri\u015fim Anahtar\u0131", "device_class": "Cihaz tipi", - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "name": "Ad" }, "description": "Eri\u015fim Anahtar\u0131 yaln\u0131zca TV'ler i\u00e7in gereklidir. Bir TV yap\u0131land\u0131r\u0131yorsan\u0131z ve hen\u00fcz bir Eri\u015fim Anahtar\u0131 , e\u015fle\u015ftirme i\u015fleminden ge\u00e7mek i\u00e7in bo\u015f b\u0131rak\u0131n.", diff --git a/homeassistant/components/vlc_telnet/translations/pl.json b/homeassistant/components/vlc_telnet/translations/pl.json index aa4e9ed9c5f..f9d342e2938 100644 --- a/homeassistant/components/vlc_telnet/translations/pl.json +++ b/homeassistant/components/vlc_telnet/translations/pl.json @@ -14,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Czy chcesz si\u0119 po\u0142\u0105czy\u0107 z dodatkiem {addon}?" + }, "reauth_confirm": { "data": { "password": "Has\u0142o" diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json index 93bd4a290e7..6e71fed22b2 100644 --- a/homeassistant/components/vlc_telnet/translations/tr.json +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -4,11 +4,11 @@ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "reauth_successful": "Kimlik do\u011frulama yeniden ba\u015far\u0131l\u0131 oldu", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "error": { - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, @@ -19,15 +19,15 @@ }, "reauth_confirm": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "L\u00fctfen sunucunun do\u011fru \u015fifresini giriniz: {host}" }, "user": { "data": { - "host": "Ana Bilgisayar", - "name": "\u0130sim", - "password": "\u015eifre", + "host": "Ana bilgisayar", + "name": "Ad", + "password": "Parola", "port": "Port" } } diff --git a/homeassistant/components/volumio/translations/pl.json b/homeassistant/components/volumio/translations/pl.json index 67d49c4b4be..f3b63230924 100644 --- a/homeassistant/components/volumio/translations/pl.json +++ b/homeassistant/components/volumio/translations/pl.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistanta?", - "title": "Wykryte Volumio" + "title": "Wykryto Volumio" }, "user": { "data": { diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json index c954aa35ac2..d651decbb3e 100644 --- a/homeassistant/components/volumio/translations/tr.json +++ b/homeassistant/components/volumio/translations/tr.json @@ -10,12 +10,12 @@ }, "step": { "discovery_confirm": { - "description": "Home Asistan\u0131'na Volumio ('{name}') eklemek istiyor musunuz?", + "description": "Home Assistant'a Volumio (` {name}", "title": "Bulunan Volumio" }, "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Ana bilgisayar", "port": "Port" } } diff --git a/homeassistant/components/watttime/translations/pl.json b/homeassistant/components/watttime/translations/pl.json index b135f54a6c0..6634f79356f 100644 --- a/homeassistant/components/watttime/translations/pl.json +++ b/homeassistant/components/watttime/translations/pl.json @@ -27,6 +27,7 @@ "data": { "password": "Has\u0142o" }, + "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}:", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { @@ -37,5 +38,15 @@ "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Poka\u017c monitorowan\u0105 lokalizacj\u0119 na mapie" + }, + "title": "Konfiguracja WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/tr.json b/homeassistant/components/watttime/translations/tr.json index 4c828b6b5be..a9531c6deaf 100644 --- a/homeassistant/components/watttime/translations/tr.json +++ b/homeassistant/components/watttime/translations/tr.json @@ -19,13 +19,13 @@ }, "location": { "data": { - "location_type": "Lokasyon" + "location_type": "Konum" }, "description": "\u0130zlemek i\u00e7in bir konum se\u00e7in:" }, "reauth_confirm": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:", "title": "Entegrasyonu Yeniden Do\u011frula" diff --git a/homeassistant/components/wemo/translations/tr.json b/homeassistant/components/wemo/translations/tr.json index 79059b00202..3ddfe182a86 100644 --- a/homeassistant/components/wemo/translations/tr.json +++ b/homeassistant/components/wemo/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index 859e8c91b10..938fa8d7f69 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -7,16 +7,16 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Ana bilgisayar" }, "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n." }, "zeroconf_confirm": { - "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?", + "description": "{name} ` adl\u0131 WLED'i Home Assistant'a eklemek istiyor musunuz?", "title": "Bulunan WLED cihaz\u0131" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 34e16ce7269..6281b7a397e 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -12,11 +12,11 @@ "invalid_key": "Ge\u00e7ersiz a\u011f ge\u00e7idi anahtar\u0131", "invalid_mac": "Ge\u00e7ersiz Mac Adresi" }, - "flow_title": "Xiaomi Aqara A\u011f Ge\u00e7idi: {name}", + "flow_title": "{name}", "step": { "select": { "data": { - "select_ip": "\u0130p Adresi" + "select_ip": "IP Adresi" }, "description": "Ek a\u011f ge\u00e7itlerini ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n.", "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" @@ -31,7 +31,7 @@ }, "user": { "data": { - "host": "\u0130p Adresi (iste\u011fe ba\u011fl\u0131)", + "host": "IP Adresi (iste\u011fe ba\u011fl\u0131)", "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc", "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" }, diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 879d0b8d7ba..60c82020a77 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -13,7 +13,8 @@ "cloud_login_error": "Nie mo\u017cna zalogowa\u0107 si\u0119 do chmury Xiaomi Miio, sprawd\u017a po\u015bwiadczenia.", "cloud_no_devices": "Na tym koncie Xiaomi Miio nie znaleziono \u017cadnych urz\u0105dze\u0144.", "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", - "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika." + "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika.", + "wrong_token": "B\u0142\u0105d sumy kontrolnej, niew\u0142a\u015bciwy token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/yale_smart_alarm/translations/tr.json b/homeassistant/components/yale_smart_alarm/translations/tr.json index 6d6b66ca166..a49189eb66f 100644 --- a/homeassistant/components/yale_smart_alarm/translations/tr.json +++ b/homeassistant/components/yale_smart_alarm/translations/tr.json @@ -10,7 +10,7 @@ "reauth_confirm": { "data": { "area_id": "Alan Kodu", - "name": "\u0130sim", + "name": "Ad", "password": "\u015eifre", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 2827e926a0d..818ff0946c4 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -21,7 +21,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Je\u015bli nie podasz IP lub nazwy hosta, wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } } }, @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (opcjonalnie)", + "model": "Model", "nightlight_switch": "U\u017cyj prze\u0142\u0105cznika Nocnego \u015bwiat\u0142a", "save_on_change": "Zachowaj status po zmianie", "transition": "Czas przej\u015bcia (ms)", diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index f33cf02e3bc..4eed4f477b4 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (Opsiyonel)", + "model": "Model", "nightlight_switch": "Gece I\u015f\u0131\u011f\u0131 Anahtar\u0131n\u0131 Kullan", "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet", "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)", diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 2cd8144789a..812d00b8e20 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Klucz sieci", + "s0_legacy_key": "Klucz S0 (Legacy)", + "s2_access_control_key": "Klucz kontroli dost\u0119pu S2", + "s2_authenticated_key": "Klucz uwierzytelniony S2", + "s2_unauthenticated_key": "Klucz nieuwierzytelniony S2", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" @@ -109,6 +113,10 @@ "emulate_hardware": "Emulacja sprz\u0119tu", "log_level": "Poziom loga", "network_key": "Klucz sieci", + "s0_legacy_key": "Klucz S0 (Legacy)", + "s2_access_control_key": "Klucz kontroli dost\u0119pu S2", + "s2_authenticated_key": "Klucz uwierzytelniony S2", + "s2_unauthenticated_key": "Klucz nieuwierzytelniony S2", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" From 5e2d71dc9037b9c9186bff846eaf74490209f3cf Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Tue, 9 Nov 2021 23:52:29 -0500 Subject: [PATCH 0368/1452] Use unknown state for octoprint temperature sensors with None value (#59130) * Mark octoprint temperature sensors as unavaible when value is not supplied * Check for none explictly * Do not mark the entity as unavailable * Swap to using er.get_async --- homeassistant/components/octoprint/sensor.py | 9 +++-- tests/components/octoprint/test_sensor.py | 35 +++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 70151d86022..af8ce9bc3c0 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -211,12 +211,15 @@ class OctoPrintTemperatureSensor(OctoPrintSensorBase): for temp in printer.temperatures: if temp.name == self._api_tool: - return round( + val = ( temp.actual_temp if self._temp_type == "actual" - else temp.target_temp, - 2, + else temp.target_temp ) + if val is None: + return None + + return round(val, 2) return None diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index c3a02c1bab5..136674f3465 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import patch +from homeassistant.helpers import entity_registry as er + from . import init_integration @@ -24,7 +26,7 @@ async def test_sensors(hass): ): await init_integration(hass, "sensor", printer=printer, job=job) - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) state = hass.states.get("sensor.octoprint_job_percentage") assert state is not None @@ -74,3 +76,34 @@ async def test_sensors(hass): assert state.name == "OctoPrint Estimated Finish Time" entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") assert entry.unique_id == "Estimated Finish Time-uuid" + + +async def test_sensors_no_target_temp(hass): + """Test the underlying sensors.""" + printer = { + "state": { + "flags": {}, + "text": "Operational", + }, + "temperature": {"tool1": {"actual": 18.83136, "target": None}}, + } + with patch( + "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) + ): + await init_integration(hass, "sensor", printer=printer) + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.octoprint_actual_tool1_temp") + assert state is not None + assert state.state == "18.83" + assert state.name == "OctoPrint actual tool1 temp" + entry = entity_registry.async_get("sensor.octoprint_actual_tool1_temp") + assert entry.unique_id == "actual tool1 temp-uuid" + + state = hass.states.get("sensor.octoprint_target_tool1_temp") + assert state is not None + assert state.state == "unknown" + assert state.name == "OctoPrint target tool1 temp" + entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") + assert entry.unique_id == "target tool1 temp-uuid" From 1ea092a54ff8f5122b08a3461b8664a44d5838e5 Mon Sep 17 00:00:00 2001 From: kreene1987 Date: Wed, 10 Nov 2021 00:08:43 -0600 Subject: [PATCH 0369/1452] Allow dict as input to zwave_js.set_value service (#59370) * Original service data as a dict Fixes per recommendation in #57336. * Update homeassistant/components/zwave_js/const.py agree, thanks Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/zwave_js/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index e484d01fccb..977055485da 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -133,4 +133,5 @@ VALUE_SCHEMA = vol.Any( vol.Coerce(float), BITMASK_SCHEMA, cv.string, + dict, ) From 4273e5b50785ea9922cadad4be0f703621ea8d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 10 Nov 2021 08:49:50 +0200 Subject: [PATCH 0370/1452] Huawei LTE sensor updates (#59374) * Add human readable name for eNodeB ID * Mark various config and diagnostic sensors' entity category as such * Add uptime sensor --- homeassistant/components/huawei_lte/sensor.py | 132 +++++++++++++++--- 1 file changed, 112 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 4479f383524..57963046cbc 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -21,6 +21,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, DATA_RATE_BYTES_PER_SECOND, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_MEGAHERTZ, PERCENTAGE, STATE_UNKNOWN, @@ -58,6 +60,7 @@ class SensorMeta(NamedTuple): unit: str | None = None state_class: str | None = None enabled_default: bool = False + entity_category: str | None = None include: re.Pattern[str] | None = None exclude: re.Pattern[str] | None = None formatter: Callable[[str], tuple[StateType, str | None]] | None = None @@ -65,19 +68,38 @@ class SensorMeta(NamedTuple): SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { KEY_DEVICE_INFORMATION: SensorMeta( - include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) + include=re.compile(r"^(WanIP.*Address|uptime)$", re.IGNORECASE) ), (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta( - name="WAN IP address", icon="mdi:ip", enabled_default=True + name="WAN IP address", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + enabled_default=True, ), (KEY_DEVICE_INFORMATION, "WanIPv6Address"): SensorMeta( - name="WAN IPv6 address", icon="mdi:ip" + name="WAN IPv6 address", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( + name="Uptime", + icon="mdi:timer-outline", + unit=TIME_SECONDS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "band"): SensorMeta( + name="Band", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "band"): SensorMeta(name="Band"), (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta( - name="Cell ID", icon="mdi:transmission-tower" + name="Cell ID", + icon="mdi:transmission-tower", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta( + name="Downlink MCS", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta(name="Downlink MCS"), (KEY_DEVICE_SIGNAL, "dlbandwidth"): SensorMeta( name="Downlink bandwidth", icon=lambda x: ( @@ -85,19 +107,48 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:speedometer-medium", "mdi:speedometer", )[bisect((8, 15), x if x is not None else -1000)], + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "earfcn"): SensorMeta( + name="EARFCN", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "lac"): SensorMeta( + name="LAC", + icon="mdi:map-marker", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( + name="PLMN", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( + name="RAC", + icon="mdi:map-marker", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( + name="RRC status", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( + name="TAC", + icon="mdi:map-marker", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( + name="TDD", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "earfcn"): SensorMeta(name="EARFCN"), - (KEY_DEVICE_SIGNAL, "lac"): SensorMeta(name="LAC", icon="mdi:map-marker"), - (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta(name="PLMN"), - (KEY_DEVICE_SIGNAL, "rac"): SensorMeta(name="RAC", icon="mdi:map-marker"), - (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta(name="RRC status"), - (KEY_DEVICE_SIGNAL, "tac"): SensorMeta(name="TAC", icon="mdi:map-marker"), - (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta(name="TDD"), (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( name="Transmit power", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( + name="Uplink MCS", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta(name="Uplink MCS"), (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( name="Uplink bandwidth", icon=lambda x: ( @@ -105,6 +156,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:speedometer-medium", "mdi:speedometer", )[bisect((8, 15), x if x is not None else -1000)], + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( name="Mode", @@ -114,8 +166,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { str(x), "mdi:signal" ) ), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "pci"): SensorMeta( + name="PCI", + icon="mdi:transmission-tower", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI", icon="mdi:transmission-tower"), (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -127,6 +184,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-11, -8, -5), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( @@ -140,6 +198,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-110, -95, -80), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( @@ -153,6 +212,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-80, -70, -60), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "sinr"): SensorMeta( @@ -166,6 +226,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((0, 5, 10), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( @@ -179,6 +240,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-95, -85, -75), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( name="EC/IO", @@ -191,22 +253,32 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-20, -10, -6), x if x is not None else -1000)], state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta( + name="Transmission mode", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta(name="Transmission mode"), (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( name="CQI 0", icon="mdi:speedometer", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( name="CQI 1", icon="mdi:speedometer", ), + (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( + name="eNodeB ID", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( name="Downlink frequency", formatter=lambda x: ( round(int(x) / 10) if x is not None else None, FREQUENCY_MEGAHERTZ, ), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( name="Uplink frequency", @@ -214,6 +286,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { round(int(x) / 10) if x is not None else None, FREQUENCY_MEGAHERTZ, ), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( @@ -250,23 +323,33 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { device_class=DEVICE_CLASS_BATTERY, unit=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "CurrentWifiUser"): SensorMeta( name="WiFi clients connected", icon="mdi:wifi", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "PrimaryDns"): SensorMeta( - name="Primary DNS server", icon="mdi:ip" + name="Primary DNS server", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( - name="Secondary DNS server", icon="mdi:ip" + name="Secondary DNS server", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( - name="Primary IPv6 DNS server", icon="mdi:ip" + name="Primary IPv6 DNS server", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "SecondaryIPv6Dns"): SensorMeta( - name="Secondary IPv6 DNS server", icon="mdi:ip" + name="Secondary IPv6 DNS server", + icon="mdi:ip", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), KEY_MONITORING_TRAFFIC_STATISTICS: SensorMeta( exclude=re.compile(r"^showtraffic$", re.IGNORECASE) @@ -322,12 +405,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( name="Operator search mode", formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), + entity_category=ENTITY_CATEGORY_CONFIG, ), (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta( name="Operator name", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), (KEY_NET_CURRENT_PLMN, "Numeric"): SensorMeta( name="Operator code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta( @@ -344,6 +430,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { }.get(x, "Unknown"), None, ), + entity_category=ENTITY_CATEGORY_CONFIG, ), (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta( name="SMS deleted (device)", @@ -514,3 +601,8 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): self._state, self._unit = formatter(value) self._available = value is not None + + @property + def entity_category(self) -> str | None: + """Return category of entity, if any.""" + return self.meta.entity_category From cde6e007bf5efd5dcdac2cf1724ba513142d4029 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 10 Nov 2021 07:53:05 +0100 Subject: [PATCH 0371/1452] Remove 'UnicodeDecodeError' exception in AsusWrt (#59447) --- homeassistant/components/asuswrt/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index c186e182f5c..3d2cac85569 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -430,7 +430,7 @@ async def _get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: info = {} try: info = await api.async_get_nvram(info_type) - except (OSError, UnicodeDecodeError) as exc: + except OSError as exc: _LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc) return info From e20127d9ffcf8bb8df4e60bac8e2c4c490415b07 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 10 Nov 2021 07:58:22 +0100 Subject: [PATCH 0372/1452] Minor refactor of template binary sensor (#59432) * Minor refactor of template binary sensor * pylint * Tweak --- .../components/template/binary_sensor.py | 82 ++++++------------- .../components/template/template_entity.py | 29 +++++-- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 9a4c3f93f63..cae751df170 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from functools import partial import logging +from typing import Any import voluptuous as vol @@ -45,7 +46,7 @@ from .const import ( CONF_OBJECT_ID, CONF_PICTURE, ) -from .template_entity import TemplateEntity +from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity from .trigger_entity import TriggerEntity CONF_DELAY_ON = "delay_on" @@ -65,20 +66,16 @@ LEGACY_FIELDS = { BINARY_SENSOR_SCHEMA = vol.Schema( { + vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.template, vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_PICTURE): cv.template, - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), - vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), - vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } -) +).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) LEGACY_BINARY_SENSOR_SCHEMA = vol.All( cv.deprecated(ATTR_ENTITY_ID), @@ -143,18 +140,6 @@ def _async_create_template_tracking_entities( sensors = [] for entity_conf in definitions: - # Still available on legacy - object_id = entity_conf.get(CONF_OBJECT_ID) - - value = entity_conf[CONF_STATE] - icon = entity_conf.get(CONF_ICON) - entity_picture = entity_conf.get(CONF_PICTURE) - availability = entity_conf.get(CONF_AVAILABILITY) - attributes = entity_conf.get(CONF_ATTRIBUTES, {}) - friendly_name = entity_conf.get(CONF_NAME) - device_class = entity_conf.get(CONF_DEVICE_CLASS) - delay_on_raw = entity_conf.get(CONF_DELAY_ON) - delay_off_raw = entity_conf.get(CONF_DELAY_OFF) unique_id = entity_conf.get(CONF_UNIQUE_ID) if unique_id and unique_id_prefix: @@ -163,16 +148,7 @@ def _async_create_template_tracking_entities( sensors.append( BinarySensorTemplate( hass, - object_id, - friendly_name, - device_class, - value, - icon, - entity_picture, - availability, - delay_on_raw, - delay_off_raw, - attributes, + entity_conf, unique_id, ) ) @@ -212,49 +188,37 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): def __init__( self, hass: HomeAssistant, - object_id: str | None, - friendly_name: template.Template | None, - device_class: str, - value_template: template.Template, - icon_template: template.Template | None, - entity_picture_template: template.Template | None, - availability_template: template.Template | None, - delay_on_raw, - delay_off_raw, - attribute_templates: dict[str, template.Template], + config: dict[str, Any], unique_id: str | None, - ): + ) -> None: """Initialize the Template binary sensor.""" - super().__init__( - attribute_templates=attribute_templates, - availability_template=availability_template, - icon_template=icon_template, - entity_picture_template=entity_picture_template, - ) - if object_id is not None: + super().__init__(config=config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) self._name: str | None = None - self._friendly_name_template: template.Template | None = friendly_name + self._friendly_name_template = config.get(CONF_NAME) # Try to render the name as it can influence the entity ID - if friendly_name: - friendly_name.hass = hass + if self._friendly_name_template: + self._friendly_name_template.hass = hass try: - self._name = friendly_name.async_render(parse_result=False) + self._name = self._friendly_name_template.async_render( + parse_result=False + ) except template.TemplateError: pass - self._device_class = device_class - self._template = value_template + self._device_class = config.get(CONF_DEVICE_CLASS) + self._template = config[CONF_STATE] self._state = None self._delay_cancel = None self._delay_on = None - self._delay_on_raw = delay_on_raw + self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_off = None - self._delay_off_raw = delay_off_raw + self._delay_off_raw = config.get(CONF_DELAY_OFF) self._unique_id = unique_id async def async_added_to_hass(self): diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 70d7b4af7dd..2e7799bd95b 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -20,9 +20,21 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.template import Template, result_as_boolean +from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE + _LOGGER = logging.getLogger(__name__) +TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + } +) + + class _TemplateAttribute: """Attribute value linked to template result.""" @@ -125,16 +137,23 @@ class TemplateEntity(Entity): icon_template=None, entity_picture_template=None, attribute_templates=None, + config=None, ): """Template Entity.""" self._template_attrs = {} self._async_update = None - self._attribute_templates = attribute_templates self._attr_extra_state_attributes = {} - self._availability_template = availability_template - self._icon_template = icon_template - self._entity_picture_template = entity_picture_template self._self_ref_update_count = 0 + if config is None: + self._attribute_templates = attribute_templates + self._availability_template = availability_template + self._icon_template = icon_template + self._entity_picture_template = entity_picture_template + else: + self._attribute_templates = config.get(CONF_ATTRIBUTES) + self._availability_template = config.get(CONF_AVAILABILITY) + self._icon_template = config.get(CONF_ICON) + self._entity_picture_template = config.get(CONF_PICTURE) @callback def _update_available(self, result): From 2ae77bd231a083fb70f0fea177fe12cd2cab7c8b Mon Sep 17 00:00:00 2001 From: enegaard Date: Wed, 10 Nov 2021 08:14:16 +0100 Subject: [PATCH 0373/1452] Fix rpi_camera setup hanging on initialization (#59316) --- homeassistant/components/rpi_camera/camera.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 980586d4def..89fe7fe55d8 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -119,10 +119,13 @@ class RaspberryCamera(Camera): cmd_args.append("-a") cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP])) - with subprocess.Popen( + # The raspistill process started below must run "forever" in + # the background until killed when Home Assistant is stopped. + # Therefore it must not be wrapped with "with", since that + # waits for the subprocess to exit before continuing. + subprocess.Popen( # pylint: disable=consider-using-with cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ): - pass + ) def camera_image( self, width: int | None = None, height: int | None = None From 01fe69511f0f3b9daa4787ad6b37a281bf1edc6f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 9 Nov 2021 23:29:33 -0800 Subject: [PATCH 0374/1452] Bump google-nest-sdm to 0.3.9 (#59458) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b7ac58a571a..18ced7b2e8f 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.8"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.9"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 241d165218e..decfa186fda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.8 +google-nest-sdm==0.3.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78cc94a313f..e6e1707b4fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.8 +google-nest-sdm==0.3.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 From eec84ad71edf6b115d84bf74142c7ca37ecca588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gru=C3=9F?= Date: Wed, 10 Nov 2021 09:03:20 +0100 Subject: [PATCH 0375/1452] Mqtt cover toggle add stop function (#59233) * Change existing toggle to add new function * Fixed using old property method to using actual protected variable. * Adding service tests to new cover toggle function * Working on comments from Pull Request 59233 * Adjust existing tests to fit new fake cover setup * MockCover is calling state method of MockEntity but should call it from CoverEntity * using different entity to get back test coverage --- homeassistant/components/cover/__init__.py | 37 ++++-- tests/components/cover/test_device_action.py | 4 +- .../components/cover/test_device_condition.py | 4 +- tests/components/cover/test_device_trigger.py | 2 +- tests/components/cover/test_init.py | 111 ++++++++++++++++++ .../custom_components/test/cover.py | 108 ++++++++++++----- 6 files changed, 224 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 7a3061d24c8..2f4abff749e 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -187,6 +187,8 @@ class CoverEntity(Entity): _attr_is_opening: bool | None = None _attr_state: None = None + _cover_is_last_toggle_direction_open = True + @property def current_cover_position(self) -> int | None: """Return current position of cover. @@ -208,8 +210,10 @@ class CoverEntity(Entity): def state(self) -> str | None: """Return the state of the cover.""" if self.is_opening: + self._cover_is_last_toggle_direction_open = True return STATE_OPENING if self.is_closing: + self._cover_is_last_toggle_direction_open = False return STATE_CLOSING if (closed := self.is_closed) is None: @@ -285,17 +289,23 @@ class CoverEntity(Entity): def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" - if self.is_closed: - self.open_cover(**kwargs) - else: - self.close_cover(**kwargs) + fns = { + "open": self.open_cover, + "close": self.close_cover, + "stop": self.stop_cover, + } + function = self._get_toggle_function(fns) + function(**kwargs) async def async_toggle(self, **kwargs): """Toggle the entity.""" - if self.is_closed: - await self.async_open_cover(**kwargs) - else: - await self.async_close_cover(**kwargs) + fns = { + "open": self.async_open_cover, + "close": self.async_close_cover, + "stop": self.async_stop_cover, + } + function = self._get_toggle_function(fns) + await function(**kwargs) def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -363,6 +373,17 @@ class CoverEntity(Entity): else: await self.async_close_cover_tilt(**kwargs) + def _get_toggle_function(self, fns): + if SUPPORT_STOP | self.supported_features and ( + self.is_closing or self.is_opening + ): + return fns["stop"] + if self.is_closed: + return fns["open"] + if self._cover_is_last_toggle_direction_open: + return fns["close"] + return fns["open"] + class CoverDevice(CoverEntity): """Representation of a cover (for backwards compatibility).""" diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 0dc76aa7e61..ab7b6b91699 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -110,7 +110,7 @@ async def test_get_action_capabilities( """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[0] + ent = platform.ENTITIES[2] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -126,7 +126,7 @@ async def test_get_action_capabilities( await hass.async_block_till_done() actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 3 # open, close, stop + assert len(actions) == 4 # open, close, stop, set_position for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 0812a7fefa0..efa8e8b3383 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -196,7 +196,7 @@ async def test_get_condition_capabilities_set_tilt_pos( """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -487,7 +487,7 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) """Test for tilt position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index b7f15de1e3c..0c7c99bc521 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -228,7 +228,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index df8df2c4bf1..b46c0417cd2 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -1,5 +1,116 @@ """The tests for Cover.""" import homeassistant.components.cover as cover +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_PLATFORM, + SERVICE_TOGGLE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.setup import async_setup_component + + +async def test_services(hass, enable_custom_integrations): + """Test the provided services.""" + platform = getattr(hass.components, "test.cover") + + platform.init() + assert await async_setup_component( + hass, cover.DOMAIN, {cover.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + # ent1 = cover without tilt and position + # ent2 = cover with position but no tilt + # ent3 = cover with simple tilt functions and no position + # ent4 = cover with all tilt functions but no position + # ent5 = cover with all functions + ent1, ent2, ent3, ent4, ent5 = platform.ENTITIES + + # Test init all covers should be open + assert is_open(hass, ent1) + assert is_open(hass, ent2) + assert is_open(hass, ent3) + assert is_open(hass, ent4) + assert is_open(hass, ent5) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities without stop should be closed and with stop should be closing + assert is_closed(hass, ent1) + assert is_closing(hass, ent2) + assert is_closed(hass, ent3) + assert is_closed(hass, ent4) + assert is_closing(hass, ent5) + + # call basic toggle services and set different cover position states + await call_service(hass, SERVICE_TOGGLE, ent1) + set_cover_position(ent2, 0) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + set_cover_position(ent5, 15) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities should be in correct state depending on the SUPPORT_STOP feature and cover position + assert is_open(hass, ent1) + assert is_closed(hass, ent2) + assert is_open(hass, ent3) + assert is_open(hass, ent4) + assert is_open(hass, ent5) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities should be in correct state depending on the SUPPORT_STOP feature and cover position + assert is_closed(hass, ent1) + assert is_opening(hass, ent2) + assert is_closed(hass, ent3) + assert is_closed(hass, ent4) + assert is_opening(hass, ent5) + + +def call_service(hass, service, ent): + """Call any service on entity.""" + return hass.services.async_call( + cover.DOMAIN, service, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True + ) + + +def set_cover_position(ent, position) -> None: + """Set a position value to a cover.""" + ent._values["current_cover_position"] = position + + +def is_open(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_OPEN) + + +def is_opening(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_OPENING) + + +def is_closed(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSED) + + +def is_closing(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSING) def test_deprecated_base_class(caplog): diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index 095489ce7b4..edd8965e4e9 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -14,6 +14,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from tests.common import MockEntity @@ -32,27 +33,53 @@ def init(empty=False): name="Simple cover", is_on=True, unique_id="unique_cover", - supports_tilt=False, + supported_features=SUPPORT_OPEN | SUPPORT_CLOSE, ), MockCover( name="Set position cover", is_on=True, unique_id="unique_set_pos_cover", current_cover_position=50, - supports_tilt=False, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION, + ), + MockCover( + name="Simple tilt cover", + is_on=True, + unique_id="unique_tilt_cover", + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT, ), MockCover( name="Set tilt position cover", is_on=True, unique_id="unique_set_pos_tilt_cover", current_cover_tilt_position=50, - supports_tilt=True, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION, ), MockCover( - name="Tilt cover", + name="All functions cover", is_on=True, - unique_id="unique_tilt_cover", - supports_tilt=True, + unique_id="unique_all_functions_cover", + current_cover_position=50, + current_cover_tilt_position=50, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION, ), ] ) @@ -71,8 +98,54 @@ class MockCover(MockEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed or not.""" + if self.supported_features & SUPPORT_STOP: + return self.current_cover_position == 0 + + if "state" in self._values: + return self._values["state"] == STATE_CLOSED return False + @property + def is_opening(self): + """Return if the cover is opening or not.""" + if self.supported_features & SUPPORT_STOP: + if "state" in self._values: + return self._values["state"] == STATE_OPENING + + return False + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + if self.supported_features & SUPPORT_STOP: + if "state" in self._values: + return self._values["state"] == STATE_CLOSING + + return False + + def open_cover(self, **kwargs) -> None: + """Open cover.""" + if self.supported_features & SUPPORT_STOP: + self._values["state"] = STATE_OPENING + else: + self._values["state"] = STATE_OPEN + + def close_cover(self, **kwargs) -> None: + """Close cover.""" + if self.supported_features & SUPPORT_STOP: + self._values["state"] = STATE_CLOSING + else: + self._values["state"] = STATE_CLOSED + + def stop_cover(self, **kwargs) -> None: + """Stop cover.""" + self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN + + @property + def state(self): + """Fake State.""" + return CoverEntity.state.fget(self) + @property def current_cover_position(self): """Return current position of cover.""" @@ -82,26 +155,3 @@ class MockCover(MockEntity, CoverEntity): def current_cover_tilt_position(self): """Return current position of cover tilt.""" return self._handle("current_cover_tilt_position") - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - - if self._handle("supports_tilt"): - supported_features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT - ) - - if self.current_cover_position is not None: - supported_features |= SUPPORT_SET_POSITION - - if self.current_cover_tilt_position is not None: - supported_features |= ( - SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - | SUPPORT_SET_TILT_POSITION - ) - - return supported_features From 93395f9b68def0b1d4dce76014367b2bd8fd61c8 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Wed, 10 Nov 2021 01:44:05 -0700 Subject: [PATCH 0376/1452] Add support for PRESSURE_CBAR (centibars) (#58762) * Add support for PRESSURE_CBAR (centibars). This is the standard UOM for tensiometers. While the data could be converted into something like MBAR, and displayed like that, the correct UOM for this type of sensor is CBAR. Displaying it as MBAR would be the same as displaying air pressure as feet of Hg, while technically correct, it's hard to understand when reading. Adding support for this UOM will fix errors in the UI(statistics) about the unit not matching a unit of the device_class. * Add tests for PRESSURE_CBAR --- homeassistant/components/onewire/const.py | 2 -- homeassistant/components/onewire/sensor.py | 2 +- homeassistant/const.py | 1 + homeassistant/util/pressure.py | 3 +++ tests/components/onewire/const.py | 2 +- tests/util/test_pressure.py | 11 +++++++++++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 4904d1568ef..f9f40e49ced 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -43,8 +43,6 @@ MANUFACTURER_MAXIM = "Maxim Integrated" MANUFACTURER_HOBBYBOARDS = "Hobby Boards" MANUFACTURER_EDS = "Embedded Data Systems" -PRESSURE_CBAR = "cbar" - READ_MODE_BOOL = "bool" READ_MODE_FLOAT = "float" READ_MODE_INT = "int" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index fa477ff27e0..54528cf05f3 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -34,6 +34,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, + PRESSURE_CBAR, PRESSURE_MBAR, TEMP_CELSIUS, ) @@ -47,7 +48,6 @@ from .const import ( CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DOMAIN, - PRESSURE_CBAR, READ_MODE_FLOAT, READ_MODE_INT, ) diff --git a/homeassistant/const.py b/homeassistant/const.py index 302cf3b00ab..2142556a217 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -479,6 +479,7 @@ PRESSURE_PA: Final = "Pa" PRESSURE_HPA: Final = "hPa" PRESSURE_KPA: Final = "kPa" PRESSURE_BAR: Final = "bar" +PRESSURE_CBAR: Final = "cbar" PRESSURE_MBAR: Final = "mbar" PRESSURE_INHG: Final = "inHg" PRESSURE_PSI: Final = "psi" diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 53bbbffc01e..8ff3f9f5f93 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -6,6 +6,7 @@ from numbers import Number from homeassistant.const import ( PRESSURE, PRESSURE_BAR, + PRESSURE_CBAR, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_KPA, @@ -20,6 +21,7 @@ VALID_UNITS: tuple[str, ...] = ( PRESSURE_HPA, PRESSURE_KPA, PRESSURE_BAR, + PRESSURE_CBAR, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_PSI, @@ -30,6 +32,7 @@ UNIT_CONVERSION: dict[str, float] = { PRESSURE_HPA: 1 / 100, PRESSURE_KPA: 1 / 1000, PRESSURE_BAR: 1 / 100000, + PRESSURE_CBAR: 1 / 1000, PRESSURE_MBAR: 1 / 100, PRESSURE_INHG: 1 / 3386.389, PRESSURE_PSI: 1 / 6894.757, diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 1d0bf0f8e84..55663c65d36 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -8,7 +8,6 @@ from homeassistant.components.onewire.const import ( MANUFACTURER_EDS, MANUFACTURER_HOBBYBOARDS, MANUFACTURER_MAXIM, - PRESSURE_CBAR, ) from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -37,6 +36,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, + PRESSURE_CBAR, PRESSURE_MBAR, STATE_OFF, STATE_ON, diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index d6211fa5343..0109d045a55 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -2,6 +2,7 @@ import pytest from homeassistant.const import ( + PRESSURE_CBAR, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_KPA, @@ -22,6 +23,7 @@ def test_convert_same_unit(): assert pressure_util.convert(4, PRESSURE_MBAR, PRESSURE_MBAR) == 4 assert pressure_util.convert(5, PRESSURE_INHG, PRESSURE_INHG) == 5 assert pressure_util.convert(6, PRESSURE_KPA, PRESSURE_KPA) == 6 + assert pressure_util.convert(7, PRESSURE_CBAR, PRESSURE_CBAR) == 7 def test_convert_invalid_unit(): @@ -57,6 +59,9 @@ def test_convert_from_hpascals(): assert pressure_util.convert( hpascals, PRESSURE_HPA, PRESSURE_MBAR ) == pytest.approx(1000) + assert pressure_util.convert( + hpascals, PRESSURE_HPA, PRESSURE_CBAR + ) == pytest.approx(100) def test_convert_from_kpascals(): @@ -77,6 +82,9 @@ def test_convert_from_kpascals(): assert pressure_util.convert( kpascals, PRESSURE_KPA, PRESSURE_MBAR ) == pytest.approx(1000) + assert pressure_util.convert( + kpascals, PRESSURE_KPA, PRESSURE_CBAR + ) == pytest.approx(100) def test_convert_from_inhg(): @@ -97,3 +105,6 @@ def test_convert_from_inhg(): assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_MBAR) == pytest.approx( 1015.9167 ) + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_CBAR) == pytest.approx( + 101.59167 + ) From 1910c0566c5fba7b6f1518a96a725bf9f4891d62 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Nov 2021 11:04:42 +0100 Subject: [PATCH 0377/1452] Upgrade jinja2 to 3.0.3 (#59468) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5bc3fe185c4..b2b2f26bb2a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ hass-nabucasa==0.50.0 home-assistant-frontend==20211109.0 httpx==0.19.0 ifaddr==0.1.7 -jinja2==3.0.2 +jinja2==3.0.3 paho-mqtt==1.6.1 pillow==8.2.0 pip>=8.0.3,<20.3 diff --git a/requirements.txt b/requirements.txt index d89db253e06..1d4fe15979c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.19.0 -jinja2==3.0.2 +jinja2==3.0.3 PyJWT==2.1.0 cryptography==3.4.8 pip>=8.0.3,<20.3 diff --git a/setup.py b/setup.py index 647d4c50190..113a11d9c97 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "certifi>=2021.5.30", "ciso8601==2.2.0", "httpx==0.19.0", - "jinja2==3.0.2", + "jinja2==3.0.3", "PyJWT==2.1.0", # PyJWT has loose dependency. We want the latest one. "cryptography==3.4.8", From c03fdd5da667e6a246555d1c7c285c60a8011d5c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 10 Nov 2021 10:49:04 +0000 Subject: [PATCH 0378/1452] Add Azure DevOps coordinator and entity description (#54978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen Co-authored-by: Ludeeus --- .../components/azure_devops/__init__.py | 126 ++++++++----- .../components/azure_devops/const.py | 5 - .../components/azure_devops/sensor.py | 166 +++++++----------- 3 files changed, 148 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index fe27ec8bcec..f97dfe730d0 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -1,43 +1,84 @@ """Support for Azure DevOps.""" from __future__ import annotations +from dataclasses import dataclass +from datetime import timedelta import logging +from typing import Final +from aioazuredevops.builds import DevOpsBuild from aioazuredevops.client import DevOpsClient +from aioazuredevops.core import DevOpsProject import aiohttp from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) -from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DATA_AZURE_DEVOPS_CLIENT, DOMAIN +from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] +BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" + + +@dataclass +class AzureDevOpsEntityDescription(EntityDescription): + """Class describing Azure DevOps entities.""" + + organization: str = "" + project: DevOpsProject = None + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" client = DevOpsClient() - try: - if entry.data[CONF_PAT] is not None: - await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) - if not client.authorized: - raise ConfigEntryAuthFailed( - "Could not authorize with Azure DevOps. You may need to update your token" - ) - await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) - except aiohttp.ClientError as exception: - _LOGGER.warning(exception) - raise ConfigEntryNotReady from exception + if entry.data.get(CONF_PAT) is not None: + await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) + if not client.authorized: + raise ConfigEntryAuthFailed( + "Could not authorize with Azure DevOps. You will need to update your token" + ) - instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" - hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client + project = await client.get_project( + entry.data[CONF_ORG], + entry.data[CONF_PROJECT], + ) + + async def async_update_data() -> list[DevOpsBuild]: + """Fetch data from Azure DevOps.""" + + try: + return await client.get_builds( + entry.data[CONF_ORG], + entry.data[CONF_PROJECT], + BUILDS_QUERY, + ) + except (aiohttp.ClientError, aiohttp.ClientError) as exception: + raise UpdateFailed from exception + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}_coordinator", + update_method=async_update_data, + update_interval=timedelta(seconds=300), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator, project - # Setup components hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -45,36 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Azure DevOps config entry.""" - del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok -class AzureDevOpsEntity(Entity): +class AzureDevOpsEntity(CoordinatorEntity): """Defines a base Azure DevOps entity.""" - def __init__(self, organization: str, project: str, name: str, icon: str) -> None: + coordinator: DataUpdateCoordinator[list[DevOpsBuild]] + entity_description: AzureDevOpsEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[DevOpsBuild]], + entity_description: AzureDevOpsEntityDescription, + ) -> None: """Initialize the Azure DevOps entity.""" - self._attr_name = name - self._attr_icon = icon - self.organization = organization - self.project = project - - async def async_update(self) -> None: - """Update Azure DevOps entity.""" - if await self._azure_devops_update(): - self._attr_available = True - else: - if self._attr_available: - _LOGGER.debug( - "An error occurred while updating Azure DevOps sensor", - exc_info=True, - ) - self._attr_available = False - - async def _azure_devops_update(self) -> bool: - """Update Azure DevOps entity.""" - raise NotImplementedError() + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id: str = "_".join( + [entity_description.organization, entity_description.key] + ) + self._organization: str = entity_description.organization + self._project_name: str = entity_description.project.name class AzureDevOpsDeviceEntity(AzureDevOpsEntity): @@ -85,7 +121,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): """Return device information about this Azure DevOps instance.""" return DeviceInfo( entry_type="service", - identifiers={(DOMAIN, self.organization, self.project)}, # type: ignore - manufacturer=self.organization, - name=self.project, + identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore + manufacturer=self._organization, + name=self._project_name, ) diff --git a/homeassistant/components/azure_devops/const.py b/homeassistant/components/azure_devops/const.py index 40610ba7baa..adaf5ebe767 100644 --- a/homeassistant/components/azure_devops/const.py +++ b/homeassistant/components/azure_devops/const.py @@ -1,11 +1,6 @@ """Constants for the Azure DevOps integration.""" DOMAIN = "azure_devops" -DATA_AZURE_DEVOPS_CLIENT = "azure_devops_client" -DATA_ORG = "organization" -DATA_PROJECT = "project" -DATA_PAT = "personal_access_token" - CONF_ORG = "organization" CONF_PROJECT = "project" CONF_PAT = "personal_access_token" diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 67d472abc1e..dc81cf53171 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -1,126 +1,94 @@ """Support for Azure DevOps sensors.""" from __future__ import annotations -from datetime import timedelta -import logging +from dataclasses import dataclass +from typing import Any, Callable from aioazuredevops.builds import DevOpsBuild -from aioazuredevops.client import DevOpsClient -import aiohttp -from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity -from homeassistant.components.azure_devops.const import ( - CONF_ORG, - CONF_PROJECT, - DATA_AZURE_DEVOPS_CLIENT, - DATA_ORG, - DATA_PROJECT, - DOMAIN, +from homeassistant.components.azure_devops import ( + AzureDevOpsDeviceEntity, + AzureDevOpsEntityDescription, ) -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.azure_devops.const import CONF_ORG, DOMAIN +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType -_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=300) -PARALLEL_UPDATES = 4 +@dataclass +class AzureDevOpsSensorEntityDescriptionMixin: + """Mixin class for required Azure DevOps sensor description keys.""" -BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" + build_key: int + + +@dataclass +class AzureDevOpsSensorEntityDescription( + AzureDevOpsEntityDescription, + SensorEntityDescription, + AzureDevOpsSensorEntityDescriptionMixin, +): + """Class describing Azure DevOps sensor entities.""" + + attrs: Callable[[DevOpsBuild], Any] = round + value: Callable[[DevOpsBuild], StateType] = round async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Azure DevOps sensor based on a config entry.""" - instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" - client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT] - organization = entry.data[DATA_ORG] - project = entry.data[DATA_PROJECT] - sensors = [] + coordinator, project = hass.data[DOMAIN][entry.entry_id] - try: - builds: list[DevOpsBuild] = await client.get_builds( - organization, project, BUILDS_QUERY - ) - except aiohttp.ClientError as exception: - _LOGGER.warning(exception) - raise PlatformNotReady from exception - - for build in builds: - sensors.append( - AzureDevOpsLatestBuildSensor(client, organization, project, build) + sensors = [ + AzureDevOpsSensor( + coordinator, + AzureDevOpsSensorEntityDescription( + key=f"{build.project.id}_{build.definition.id}_latest_build", + name=f"{build.project.name} {build.definition.name} Latest Build", + icon="mdi:pipe", + attrs=lambda build: { + "definition_id": build.definition.id, + "definition_name": build.definition.name, + "id": build.id, + "reason": build.reason, + "result": build.result, + "source_branch": build.source_branch, + "source_version": build.source_version, + "status": build.status, + "url": build.links.web, + "queue_time": build.queue_time, + "start_time": build.start_time, + "finish_time": build.finish_time, + }, + build_key=key, + organization=entry.data[CONF_ORG], + project=project, + value=lambda build: build.build_number, + ), ) + for key, build in enumerate(coordinator.data) + ] async_add_entities(sensors, True) class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): - """Defines a Azure DevOps sensor.""" + """Define a Azure DevOps sensor.""" - def __init__( - self, - client: DevOpsClient, - organization: str, - project: str, - key: str, - name: str, - icon: str, - measurement: str = "", - unit_of_measurement: str = "", - ) -> None: - """Initialize Azure DevOps sensor.""" - self._attr_native_unit_of_measurement = unit_of_measurement - self.client = client - self.organization = organization - self.project = project - self._attr_unique_id = "_".join([organization, key]) + entity_description: AzureDevOpsSensorEntityDescription - super().__init__(organization, project, name, icon) + @property + def native_value(self) -> StateType: + """Return the state.""" + build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key] + return self.entity_description.value(build) - -class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): - """Defines a Azure DevOps card count sensor.""" - - def __init__( - self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild - ) -> None: - """Initialize Azure DevOps sensor.""" - self.build: DevOpsBuild = build - super().__init__( - client, - organization, - project, - f"{build.project.id}_{build.definition.id}_latest_build", - f"{build.project.name} {build.definition.name} Latest Build", - "mdi:pipe", - ) - - async def _azure_devops_update(self) -> bool: - """Update Azure DevOps entity.""" - try: - build: DevOpsBuild = await self.client.get_build( - self.organization, self.project, self.build.id - ) - except aiohttp.ClientError as exception: - _LOGGER.warning(exception) - self._attr_available = False - return False - self._attr_native_value = build.build_number - self._attr_extra_state_attributes = { - "definition_id": build.definition.id, - "definition_name": build.definition.name, - "id": build.id, - "reason": build.reason, - "result": build.result, - "source_branch": build.source_branch, - "source_version": build.source_version, - "status": build.status, - "url": build.links.web, - "queue_time": build.queue_time, - "start_time": build.start_time, - "finish_time": build.finish_time, - } - self._attr_available = True - return True + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the entity.""" + build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key] + return self.entity_description.attrs(build) From 6cba03aa4a45b49420b77ae405f52138b8a499df Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:49:05 +0100 Subject: [PATCH 0379/1452] Remove resources selection from Nut config flow (#59450) * Remove resources selection from Nut config flow * Code clean-up * Requested changes * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nut/__init__.py | 14 +-- homeassistant/components/nut/config_flow.py | 58 +--------- homeassistant/components/nut/const.py | 61 ++++++++++ homeassistant/components/nut/sensor.py | 8 +- homeassistant/components/nut/strings.json | 6 - .../components/nut/translations/en.json | 17 +-- tests/components/nut/test_config_flow.py | 106 +++++------------- tests/components/nut/test_init.py | 14 +-- tests/components/nut/test_sensor.py | 68 ++++++----- tests/components/nut/util.py | 7 +- 10 files changed, 149 insertions(+), 210 deletions(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index f0f2777373a..e16e5824ff1 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -37,13 +37,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" - # strip out the stale options CONF_RESOURCES - if CONF_RESOURCES in entry.options: - new_data = {**entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES]} - new_options = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} - hass.config_entries.async_update_entry( - entry, data=new_data, options=new_options - ) + # strip out the stale setting CONF_RESOURCES from data & options + if CONF_RESOURCES in entry.data: + new_data = {k: v for k, v in entry.data.items() if k != CONF_RESOURCES} + new_opts = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} + hass.config_entries.async_update_entry(entry, data=new_data, options=new_opts) config = entry.data host = config[CONF_HOST] @@ -79,7 +77,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("NUT Sensors Available: %s", status) undo_listener = entry.add_update_listener(_async_update_listener) - unique_id = _unique_id_from_status(status) if unique_id is None: unique_id = entry.entry_id @@ -91,7 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYNUT_UNIQUE_ID: unique_id, UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index ed490eddd37..ae22526752d 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -10,23 +10,13 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, - CONF_RESOURCES, CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import PyNUTData -from .const import ( - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - KEY_STATUS, - KEY_STATUS_DISPLAY, - SENSOR_TYPES, -) +from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -48,27 +38,6 @@ def _base_schema(discovery_info): return vol.Schema(base_schema) -def _resource_schema_base(available_resources, selected_resources): - """Resource selection schema.""" - - known_available_resources = { - 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 - ].name - - return { - vol.Required(CONF_RESOURCES, default=selected_resources): cv.multi_select( - known_available_resources - ) - } - - def _ups_schema(ups_list): """UPS selection schema.""" return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)}) @@ -112,7 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the nut config flow.""" self.nut_config = {} - self.available_resources = {} self.discovery_info = {} self.ups_list = None self.title = None @@ -148,8 +116,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._host_port_alias_already_configured(self.nut_config): return self.async_abort(reason="already_configured") - self.available_resources.update(info["available_resources"]) - return await self.async_step_resources() + title = _format_host_port_alias(self.nut_config) + return self.async_create_entry(title=title, data=self.nut_config) return self.async_show_form( step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors @@ -163,10 +131,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.nut_config.update(user_input) if self._host_port_alias_already_configured(self.nut_config): return self.async_abort(reason="already_configured") - info, errors = await self._async_validate_or_error(self.nut_config) + _, errors = await self._async_validate_or_error(self.nut_config) if not errors: - self.available_resources.update(info["available_resources"]) - return await self.async_step_resources() + title = _format_host_port_alias(self.nut_config) + return self.async_create_entry(title=title, data=self.nut_config) return self.async_show_form( step_id="ups", @@ -174,20 +142,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_resources(self, user_input=None): - """Handle the picking the resources.""" - if user_input is None: - return self.async_show_form( - step_id="resources", - data_schema=vol.Schema( - _resource_schema_base(self.available_resources, []) - ), - ) - - self.nut_config.update(user_input) - title = _format_host_port_alias(self.nut_config) - return self.async_create_entry(title=title, data=self.nut_config) - def _host_port_alias_already_configured(self, user_input): """See if we already have a nut entry matching user input configured.""" existing_host_port_aliases = { diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 51258471b0a..1022dfe526d 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -68,6 +68,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.load": SensorEntityDescription( key="ups.load", @@ -82,12 +83,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.id": SensorEntityDescription( key="ups.id", name="System identifier", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.delay.start": SensorEntityDescription( key="ups.delay.start", @@ -95,6 +98,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.delay.reboot": SensorEntityDescription( key="ups.delay.reboot", @@ -102,6 +106,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.delay.shutdown": SensorEntityDescription( key="ups.delay.shutdown", @@ -109,6 +114,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.timer.start": SensorEntityDescription( key="ups.timer.start", @@ -116,6 +122,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.timer.reboot": SensorEntityDescription( key="ups.timer.reboot", @@ -123,6 +130,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.timer.shutdown": SensorEntityDescription( key="ups.timer.shutdown", @@ -130,6 +138,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.test.interval": SensorEntityDescription( key="ups.test.interval", @@ -137,30 +146,35 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.test.result": SensorEntityDescription( key="ups.test.result", name="Self-Test Result", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.test.date": SensorEntityDescription( key="ups.test.date", name="Self-Test Date", icon="mdi:calendar", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.display.language": SensorEntityDescription( key="ups.display.language", name="Language", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.contacts": SensorEntityDescription( key="ups.contacts", name="External Contacts", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.efficiency": SensorEntityDescription( key="ups.efficiency", @@ -169,6 +183,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:gauge", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.power": SensorEntityDescription( key="ups.power", @@ -177,6 +192,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.power.nominal": SensorEntityDescription( key="ups.power.nominal", @@ -184,6 +200,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.realpower": SensorEntityDescription( key="ups.realpower", @@ -192,6 +209,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.realpower.nominal": SensorEntityDescription( key="ups.realpower.nominal", @@ -199,48 +217,56 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.beeper.status": SensorEntityDescription( key="ups.beeper.status", name="Beeper Status", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.type": SensorEntityDescription( key="ups.type", name="UPS Type", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_SYSTEM, + entity_registry_enabled_default=False, ), "ups.watchdog.status": SensorEntityDescription( key="ups.watchdog.status", name="Watchdog Status", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.start.auto": SensorEntityDescription( key="ups.start.auto", name="Start on AC", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.start.battery": SensorEntityDescription( key="ups.start.battery", name="Start on Battery", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.start.reboot": SensorEntityDescription( key="ups.start.reboot", name="Reboot on Battery", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ups.shutdown": SensorEntityDescription( key="ups.shutdown", name="Shutdown Ability", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.charge": SensorEntityDescription( key="battery.charge", @@ -255,6 +281,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.charge.restart": SensorEntityDescription( key="battery.charge.restart", @@ -262,6 +289,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.charge.warning": SensorEntityDescription( key="battery.charge.warning", @@ -269,6 +297,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.charger.status": SensorEntityDescription( key="battery.charger.status", @@ -282,6 +311,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.voltage.nominal": SensorEntityDescription( key="battery.voltage.nominal", @@ -289,6 +319,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.voltage.low": SensorEntityDescription( key="battery.voltage.low", @@ -296,6 +327,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.voltage.high": SensorEntityDescription( key="battery.voltage.high", @@ -303,6 +335,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.capacity": SensorEntityDescription( key="battery.capacity", @@ -310,6 +343,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement="Ah", icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.current": SensorEntityDescription( key="battery.current", @@ -318,6 +352,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.current.total": SensorEntityDescription( key="battery.current.total", @@ -325,6 +360,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.temperature": SensorEntityDescription( key="battery.temperature", @@ -333,6 +369,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.runtime": SensorEntityDescription( key="battery.runtime", @@ -340,6 +377,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.runtime.low": SensorEntityDescription( key="battery.runtime.low", @@ -347,6 +385,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.runtime.restart": SensorEntityDescription( key="battery.runtime.restart", @@ -354,48 +393,56 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.alarm.threshold": SensorEntityDescription( key="battery.alarm.threshold", name="Battery Alarm Threshold", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.date": SensorEntityDescription( key="battery.date", name="Battery Date", icon="mdi:calendar", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.mfr.date": SensorEntityDescription( key="battery.mfr.date", name="Battery Manuf. Date", icon="mdi:calendar", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.packs": SensorEntityDescription( key="battery.packs", name="Number of Batteries", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.packs.bad": SensorEntityDescription( key="battery.packs.bad", name="Number of Bad Batteries", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "battery.type": SensorEntityDescription( key="battery.type", name="Battery Chemistry", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.sensitivity": SensorEntityDescription( key="input.sensitivity", name="Input Power Sensitivity", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.transfer.low": SensorEntityDescription( key="input.transfer.low", @@ -403,6 +450,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.transfer.high": SensorEntityDescription( key="input.transfer.high", @@ -410,12 +458,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.transfer.reason": SensorEntityDescription( key="input.transfer.reason", name="Voltage Transfer Reason", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.voltage": SensorEntityDescription( key="input.voltage", @@ -430,6 +480,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.frequency": SensorEntityDescription( key="input.frequency", @@ -438,6 +489,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.frequency.nominal": SensorEntityDescription( key="input.frequency.nominal", @@ -445,12 +497,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "input.frequency.status": SensorEntityDescription( key="input.frequency.status", name="Input Frequency Status", icon="mdi:information-outline", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "output.current": SensorEntityDescription( key="output.current", @@ -459,6 +513,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "output.current.nominal": SensorEntityDescription( key="output.current.nominal", @@ -466,6 +521,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "output.voltage": SensorEntityDescription( key="output.voltage", @@ -480,6 +536,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "output.frequency": SensorEntityDescription( key="output.frequency", @@ -488,6 +545,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { icon="mdi:flash", state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "output.frequency.nominal": SensorEntityDescription( key="output.frequency.nominal", @@ -495,6 +553,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ambient.humidity": SensorEntityDescription( key="ambient.humidity", @@ -503,6 +562,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "ambient.temperature": SensorEntityDescription( key="ambient.temperature", @@ -511,6 +571,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), "watts": SensorEntityDescription( key="watts", diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 7ec6e28401f..e937e6565df 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -5,7 +5,7 @@ import logging from homeassistant.components.nut import PyNUTData from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import CONF_RESOURCES, STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -35,9 +35,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = pynut_data[PYNUT_UNIQUE_ID] status = coordinator.data - enabled_resources = [ - resource.lower() for resource in config_entry.data[CONF_RESOURCES] - ] resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status] # Display status is a special case that falls back to the status value # of the UPS instead. @@ -50,7 +47,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SENSOR_TYPES[sensor_type], data, unique_id, - sensor_type in enabled_resources, ) for sensor_type in resources ] @@ -67,14 +63,12 @@ class NUTSensor(CoordinatorEntity, SensorEntity): sensor_description: SensorEntityDescription, data: PyNUTData, unique_id: str, - enabled_default: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = sensor_description device_name = data.name.title() - self._attr_entity_registry_enabled_default = enabled_default self._attr_name = f"{device_name} {sensor_description.name}" self._attr_unique_id = f"{unique_id}_{sensor_description.key}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 179f974b870..ad507411065 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -16,12 +16,6 @@ "alias": "Alias", "resources": "Resources" } - }, - "resources": { - "title": "Choose the Resources to Monitor", - "data": { - "resources": "Resources" - } } }, "error": { diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 3d57189f7a5..90a1176a732 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -1,19 +1,14 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "resources_not_available": "No known resources found" }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, "step": { - "resources": { - "data": { - "resources": "Resources" - }, - "title": "Choose the Resources to Monitor" - }, "ups": { "data": { "alias": "Alias", @@ -33,17 +28,11 @@ } }, "options": { - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, "step": { "init": { "data": { - "resources": "Resources", "scan_interval": "Scan Interval (seconds)" - }, - "description": "Choose Sensor Resources." + } } } } diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 68b5356a32c..8559ab50f98 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -44,18 +44,6 @@ async def test_form_zeroconf(hass): list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] ) - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, - ) - - assert result2["step_id"] == "resources" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - with patch( "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, @@ -63,22 +51,21 @@ async def test_form_zeroconf(hass): "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == "192.168.1.5:1234" - assert result3["data"] == { + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "192.168.1.5:1234" + assert result2["data"] == { CONF_HOST: "192.168.1.5", CONF_PASSWORD: "test-password", CONF_PORT: 1234, - CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], CONF_USERNAME: "test-username", } - assert result3["result"].unique_id is None + assert result2["result"].unique_id is None assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +85,10 @@ async def test_form_user_one_ups(hass): with patch( "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, - ): + ), patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -108,30 +98,14 @@ async def test_form_user_one_ups(hass): CONF_PORT: 2222, }, ) - - assert result2["step_id"] == "resources" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, - ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == "1.1.1.1:2222" - assert result3["data"] == { + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "1.1.1.1:2222" + assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", CONF_PORT: 2222, - CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 @@ -175,18 +149,6 @@ async def test_form_user_multiple_ups(hass): assert result2["step_id"] == "ups" assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_ALIAS: "ups2"}, - ) - - assert result3["step_id"] == "resources" - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - with patch( "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, @@ -194,20 +156,19 @@ async def test_form_user_multiple_ups(hass): "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], - {CONF_RESOURCES: ["battery.voltage"]}, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_ALIAS: "ups2"}, ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result4["title"] == "ups2@1.1.1.1:2222" - assert result4["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "ups2@1.1.1.1:2222" + assert result3["data"] == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", CONF_ALIAS: "ups2", CONF_PORT: 2222, - CONF_RESOURCES: ["battery.voltage"], CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 2 @@ -234,7 +195,10 @@ async def test_form_user_one_ups_with_ignored_entry(hass): with patch( "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, - ): + ), patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -244,30 +208,14 @@ async def test_form_user_one_ups_with_ignored_entry(hass): CONF_PORT: 2222, }, ) - - assert result2["step_id"] == "resources" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, - ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == "1.1.1.1:2222" - assert result3["data"] == { + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "1.1.1.1:2222" + assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", CONF_PORT: 2222, - CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py index b9b5441a86c..24b23d00f2a 100644 --- a/tests/components/nut/test_init.py +++ b/tests/components/nut/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.nut.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES, STATE_UNAVAILABLE +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE from .util import _get_mock_pynutclient @@ -14,11 +14,7 @@ async def test_async_setup_entry(hass): """Test a successful setup entry.""" entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_HOST: "mock", - CONF_PORT: "mock", - CONF_RESOURCES: ["ups.status"], - }, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, ) entry.add_to_hass(hass) @@ -52,11 +48,7 @@ async def test_config_not_ready(hass): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_HOST: "mock", - CONF_PORT: "mock", - CONF_RESOURCES: ["ups.status"], - }, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, ) entry.add_to_hass(hass) diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 5afc662251c..206bc6736f8 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry async def test_pr3000rt2u(hass): """Test creation of PR3000RT2U sensors.""" - await async_init_integration(hass, "PR3000RT2U", ["battery.charge"]) + await async_init_integration(hass, "PR3000RT2U") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -44,7 +44,7 @@ async def test_pr3000rt2u(hass): async def test_cp1350c(hass): """Test creation of CP1350C sensors.""" - config_entry = await async_init_integration(hass, "CP1350C", ["battery.charge"]) + config_entry = await async_init_integration(hass, "CP1350C") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") @@ -69,7 +69,7 @@ async def test_cp1350c(hass): async def test_5e850i(hass): """Test creation of 5E850I sensors.""" - config_entry = await async_init_integration(hass, "5E850I", ["battery.charge"]) + config_entry = await async_init_integration(hass, "5E850I") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -93,7 +93,7 @@ async def test_5e850i(hass): async def test_5e650i(hass): """Test creation of 5E650I sensors.""" - config_entry = await async_init_integration(hass, "5E650I", ["battery.charge"]) + config_entry = await async_init_integration(hass, "5E650I") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -117,7 +117,7 @@ async def test_5e650i(hass): async def test_backupsses600m1(hass): """Test creation of BACKUPSES600M1 sensors.""" - await async_init_integration(hass, "BACKUPSES600M1", ["battery.charge"]) + await async_init_integration(hass, "BACKUPSES600M1") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -144,9 +144,7 @@ async def test_backupsses600m1(hass): async def test_cp1500pfclcd(hass): """Test creation of CP1500PFCLCD sensors.""" - config_entry = await async_init_integration( - hass, "CP1500PFCLCD", ["battery.charge"] - ) + config_entry = await async_init_integration(hass, "CP1500PFCLCD") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -170,7 +168,7 @@ async def test_cp1500pfclcd(hass): async def test_dl650elcd(hass): """Test creation of DL650ELCD sensors.""" - config_entry = await async_init_integration(hass, "DL650ELCD", ["battery.charge"]) + config_entry = await async_init_integration(hass, "DL650ELCD") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -194,7 +192,7 @@ async def test_dl650elcd(hass): async def test_blazer_usb(hass): """Test creation of blazer_usb sensors.""" - config_entry = await async_init_integration(hass, "blazer_usb", ["battery.charge"]) + config_entry = await async_init_integration(hass, "blazer_usb") registry = er.async_get(hass) entry = registry.async_get("sensor.ups1_battery_charge") assert entry @@ -219,11 +217,7 @@ async def test_state_sensors(hass): """Test creation of status display sensors.""" entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_HOST: "mock", - CONF_PORT: "mock", - CONF_RESOURCES: ["ups.status", "ups.status.display"], - }, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, ) entry.add_to_hass(hass) @@ -248,11 +242,7 @@ async def test_unknown_state_sensors(hass): """Test creation of unknown status display sensors.""" entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_HOST: "mock", - CONF_PORT: "mock", - CONF_RESOURCES: ["ups.status", "ups.status.display"], - }, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, ) entry.add_to_hass(hass) @@ -275,12 +265,34 @@ async def test_unknown_state_sensors(hass): async def test_stale_options(hass): """Test creation of sensors with stale options to remove.""" - - config_entry = await async_init_integration( - hass, "blazer_usb", ["battery.charge"], True + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["battery.charge"], + }, + options={CONF_RESOURCES: ["battery.charge"]}, ) - registry = er.async_get(hass) - entry = registry.async_get("sensor.ups1_battery_charge") - assert entry - assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" - assert config_entry.options == {} + config_entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"battery.charge": "10"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = er.async_get(hass) + entry = registry.async_get("sensor.ups1_battery_charge") + assert entry + assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" + assert CONF_RESOURCES not in config_entry.data + assert config_entry.options == {} + + state = hass.states.get("sensor.ups1_battery_charge") + assert state.state == "10" diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 8ac1d110512..df8b78be7bd 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -4,7 +4,7 @@ import json from unittest.mock import MagicMock, patch from homeassistant.components.nut.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -18,7 +18,7 @@ def _get_mock_pynutclient(list_vars=None, list_ups=None): async def async_init_integration( - hass: HomeAssistant, ups_fixture: str, resources: list, add_options: bool = False + hass: HomeAssistant, ups_fixture: str ) -> MockConfigEntry: """Set up the nexia integration in Home Assistant.""" @@ -33,8 +33,7 @@ async def async_init_integration( ): entry = MockConfigEntry( domain=DOMAIN, - data={CONF_HOST: "mock", CONF_PORT: "mock", CONF_RESOURCES: resources}, - options={CONF_RESOURCES: resources} if add_options else {}, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, ) entry.add_to_hass(hass) From ab1e14204fee983530148306d6e39f28ff316d61 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Nov 2021 15:26:47 +0100 Subject: [PATCH 0380/1452] Rename zeroconf service info (#59467) Co-authored-by: epenet --- homeassistant/components/zeroconf/__init__.py | 8 ++++---- tests/components/apple_tv/test_config_flow.py | 6 +++--- tests/components/axis/test_config_flow.py | 10 +++++----- tests/components/bond/test_config_flow.py | 14 +++++++------- tests/components/bosch_shc/test_config_flow.py | 6 +++--- tests/components/brother/test_config_flow.py | 12 +++++++----- tests/components/daikin/test_config_flow.py | 2 +- tests/components/devolo_home_control/const.py | 8 +++++--- tests/components/devolo_home_network/const.py | 4 ++-- tests/components/doorbird/test_config_flow.py | 8 ++++---- tests/components/enphase_envoy/test_config_flow.py | 6 +++--- tests/components/freebox/test_config_flow.py | 2 +- tests/components/lookin/__init__.py | 4 ++-- 13 files changed, 47 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 44efc77d222..81601702656 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -61,7 +61,7 @@ MAX_PROPERTY_VALUE_LEN = 230 # Dns label max length MAX_NAME_LEN = 63 -# Attributes for HaServiceInfo +# Attributes for ZeroconfServiceInfo ATTR_HOST: Final = "host" ATTR_HOSTNAME: Final = "hostname" ATTR_NAME: Final = "name" @@ -87,7 +87,7 @@ CONFIG_SCHEMA = vol.Schema( ) -class HaServiceInfo(TypedDict): +class ZeroconfServiceInfo(TypedDict): """Prepared info from mDNS entries.""" host: str @@ -466,7 +466,7 @@ def async_get_homekit_discovery_domain( return None -def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: +def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None: """Return prepared info from mDNS entries.""" properties: dict[str, Any] = {"_raw": {}} @@ -493,7 +493,7 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: if (host := _first_non_link_local_or_v6_address(addresses)) is None: return None - return HaServiceInfo( + return ZeroconfServiceInfo( host=str(host), port=service.port, hostname=service.server, diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index ff772723cd2..a2823ac2271 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN from tests.common import MockConfigEntry -DMAP_SERVICE = zeroconf.HaServiceInfo( +DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( type="_touch-able._tcp.local.", name="dmapid.something", properties={"CtlN": "Apple TV"}, @@ -400,7 +400,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( type="_dummy._tcp.local.", properties={}, ), @@ -414,7 +414,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( type="_mediaremotetv._tcp.local.", properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 3ba1ceee906..2211936f44b 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -293,7 +293,7 @@ async def test_reauth_flow_update_configuration(hass): ), ( SOURCE_ZEROCONF, - zeroconf.HaServiceInfo( + zeroconf.ZeroconfServiceInfo( host=DEFAULT_HOST, port=80, hostname=f"axis-{MAC.lower()}.local.", @@ -363,7 +363,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ), ( SOURCE_ZEROCONF, - zeroconf.HaServiceInfo( + zeroconf.ZeroconfServiceInfo( host=DEFAULT_HOST, port=80, name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", @@ -411,7 +411,7 @@ async def test_discovered_device_already_configured( ), ( SOURCE_ZEROCONF, - zeroconf.HaServiceInfo( + zeroconf.ZeroconfServiceInfo( host="2.3.4.5", port=8080, name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", @@ -479,7 +479,7 @@ async def test_discovery_flow_updated_configuration( ), ( SOURCE_ZEROCONF, - zeroconf.HaServiceInfo( + zeroconf.ZeroconfServiceInfo( host="", port=0, name="", @@ -517,7 +517,7 @@ async def test_discovery_flow_ignore_non_axis_device( ), ( SOURCE_ZEROCONF, - zeroconf.HaServiceInfo( + zeroconf.ZeroconfServiceInfo( host="169.254.3.4", port=80, name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index f815f0432c0..286e9fff9bf 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -197,7 +197,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="test-bond-id.some-other-tail-info", host="test-host" ), ) @@ -229,7 +229,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="test-bond-id.some-other-tail-info", host="test-host" ), ) @@ -264,7 +264,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="test-bond-id.some-other-tail-info", host="test-host" ), ) @@ -302,7 +302,7 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="already-registered-bond-id.some-other-tail-info", host="updated-host", ), @@ -343,7 +343,7 @@ async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistan result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="already-registered-bond-id.some-other-tail-info", host="updated-host", ), @@ -376,7 +376,7 @@ async def test_zeroconf_already_configured_no_reload_same_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( name="already-registered-bond-id.some-other-tail-info", host="stored-host", ), @@ -393,7 +393,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): await _help_test_form_unexpected_error( hass, source=config_entries.SOURCE_ZEROCONF, - initial_input=zeroconf.HaServiceInfo( + initial_input=zeroconf.ZeroconfServiceInfo( name="test-bond-id.some-other-tail-info", host="test-host", ), diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 21892aed94e..3e52772b30f 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -20,7 +20,7 @@ MOCK_SETTINGS = { "name": "Test name", "device": {"mac": "test-mac", "hostname": "test-host"}, } -DISCOVERY_INFO = zeroconf.HaServiceInfo( +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=["169.1.1.1", "1.1.1.1"], port=0, hostname="shc012345.local.", @@ -528,7 +528,7 @@ async def test_zeroconf_cannot_connect(hass, mock_zeroconf): async def test_zeroconf_link_local(hass, mock_zeroconf): """Test we get the form.""" - DISCOVERY_INFO_LINK_LOCAL = zeroconf.HaServiceInfo( + DISCOVERY_INFO_LINK_LOCAL = zeroconf.ZeroconfServiceInfo( host=["169.1.1.1"], port=0, hostname="shc012345.local.", @@ -552,7 +552,7 @@ async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=zeroconf.HaServiceInfo(host="1.1.1.1", name="notboschshc"), + data=zeroconf.ZeroconfServiceInfo(host="1.1.1.1", name="notboschshc"), context={"source": config_entries.SOURCE_ZEROCONF}, ) assert result["type"] == "abort" diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 8a221a21c36..314748e5cde 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -144,7 +144,7 @@ async def test_zeroconf_snmp_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( hostname="example.local.", name="Brother Printer" ), ) @@ -159,7 +159,7 @@ async def test_zeroconf_unsupported_model(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( hostname="example.local.", name="Brother Printer", properties={"product": "MFC-8660DN"}, @@ -184,7 +184,7 @@ async def test_zeroconf_device_exists_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( hostname="example.local.", name="Brother Printer" ), ) @@ -201,7 +201,9 @@ async def test_zeroconf_no_probe_existing_device(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo(hostname="localhost", name="Brother Printer"), + data=zeroconf.ZeroconfServiceInfo( + hostname="localhost", name="Brother Printer" + ), ) await hass.async_block_till_done() @@ -220,7 +222,7 @@ async def test_zeroconf_confirm_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( hostname="example.local.", name="Brother Printer" ), ) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index d6ac4e12c56..56463022597 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -120,7 +120,7 @@ async def test_api_password_abort(hass): @pytest.mark.parametrize( "source, data, unique_id", [ - (SOURCE_ZEROCONF, zeroconf.HaServiceInfo(host=HOST), MAC), + (SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo(host=HOST), MAC), ], ) async def test_discovery_zeroconf( diff --git a/tests/components/devolo_home_control/const.py b/tests/components/devolo_home_control/const.py index 761a249745f..819f1c3e005 100644 --- a/tests/components/devolo_home_control/const.py +++ b/tests/components/devolo_home_control/const.py @@ -2,7 +2,7 @@ from homeassistant.components import zeroconf -DISCOVERY_INFO = zeroconf.HaServiceInfo( +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="192.168.0.1", port=14791, hostname="test.local.", @@ -19,6 +19,8 @@ DISCOVERY_INFO = zeroconf.HaServiceInfo( }, ) -DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = zeroconf.HaServiceInfo(properties={"MT": "2700"}) +DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = zeroconf.ZeroconfServiceInfo( + properties={"MT": "2700"} +) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.HaServiceInfo(properties={"Features": ""}) +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo(properties={"Features": ""}) diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index a26704c3131..44baab0e85c 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -16,7 +16,7 @@ CONNECTED_STATIONS = { ], } -DISCOVERY_INFO = zeroconf.HaServiceInfo( +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=IP, port=14791, hostname="test.local.", @@ -36,7 +36,7 @@ DISCOVERY_INFO = zeroconf.HaServiceInfo( }, ) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.HaServiceInfo(properties={"MT": "2600"}) +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo(properties={"MT": "2600"}) NEIGHBOR_ACCESS_POINTS = { "neighbor_aps": [ diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 2b5dc5e3346..9d579e4bbf5 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -82,7 +82,7 @@ async def test_form_zeroconf_wrong_oui(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"macaddress": "notdoorbirdoui"}, host="192.168.1.8", name="Doorstation - abc123._axis-video._tcp.local.", @@ -98,7 +98,7 @@ async def test_form_zeroconf_link_local_ignored(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"macaddress": "1CCAE3DOORBIRD"}, host="169.254.103.61", name="Doorstation - abc123._axis-video._tcp.local.", @@ -121,7 +121,7 @@ async def test_form_zeroconf_correct_oui(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"macaddress": "1CCAE3DOORBIRD"}, name="Doorstation - abc123._axis-video._tcp.local.", host="192.168.1.5", @@ -180,7 +180,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"macaddress": "1CCAE3DOORBIRD"}, name="Doorstation - abc123._axis-video._tcp.local.", host="192.168.1.5", diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index bbe8b014d13..812ab87e848 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -158,7 +158,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"serialnum": "1234"}, host="1.1.1.1", ), @@ -254,7 +254,7 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"serialnum": "1234"}, host="1.1.1.1", ), @@ -289,7 +289,7 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.HaServiceInfo( + data=zeroconf.ZeroconfServiceInfo( properties={"serialnum": "1234"}, host="1.1.1.1", ), diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 73711c47c8a..3bffd213b72 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -18,7 +18,7 @@ from .const import MOCK_HOST, MOCK_PORT from tests.common import MockConfigEntry -MOCK_ZEROCONF_DATA = zeroconf.HaServiceInfo( +MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host="192.168.0.254", port=80, hostname="Freebox-Server.local.", diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index c2821fafab8..c55b5228b47 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from aiolookin import Climate, Device, Remote -from homeassistant.components.zeroconf import HaServiceInfo +from homeassistant.components.zeroconf import ZeroconfServiceInfo DEVICE_ID = "98F33163" MODULE = "homeassistant.components.lookin" @@ -17,7 +17,7 @@ DEFAULT_ENTRY_TITLE = DEVICE_NAME ZC_NAME = f"LOOKin_{DEVICE_ID}" ZC_TYPE = "_lookin._tcp." -ZEROCONF_DATA: HaServiceInfo = { +ZEROCONF_DATA: ZeroconfServiceInfo = { "host": IP_ADDRESS, "hostname": f"{ZC_NAME.lower()}.local.", "port": 80, From 20f0a6730b92da952d95db74b892965bb07fba43 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Nov 2021 10:58:54 -0700 Subject: [PATCH 0381/1452] Gogogate2 Battery Sensor Should Use % Unit For Compatibility (#59434) --- homeassistant/components/gogogate2/sensor.py | 2 ++ tests/components/gogogate2/test_sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 9721f9b8722..743c8e7efc7 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -78,6 +79,7 @@ class DoorSensorBattery(DoorSensorEntity): super().__init__(config_entry, data_update_coordinator, door, unique_id) self._attr_device_class = DEVICE_CLASS_BATTERY self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_native_unit_of_measurement = PERCENTAGE @property def name(self): diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 1a59c5fd3c5..129fcac504f 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -172,6 +172,7 @@ async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: "friendly_name": "Door1 battery", "sensor_id": "ABCD", "state_class": "measurement", + "unit_of_measurement": "%", } temp_attributes = { "device_class": "temperature", @@ -248,6 +249,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: "friendly_name": "Door1 battery", "sensor_id": "ABCD", "state_class": "measurement", + "unit_of_measurement": "%", } temp_attributes = { "device_class": "temperature", From 47b6755177b47b721cc077b4b182f2a2a491a785 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Nov 2021 19:40:49 +0100 Subject: [PATCH 0382/1452] Google Cast: Use own media player app (#55524) --- homeassistant/components/cast/media_player.py | 3 +++ homeassistant/components/google_assistant/trait.py | 4 ++-- homeassistant/const.py | 8 +++++--- tests/components/google_assistant/test_smart_home.py | 2 +- tests/components/google_assistant/test_trait.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index eca7c69f5d2..d8eacfeede4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -46,6 +46,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import lookup_plex_media from homeassistant.const import ( + CAST_APP_ID_HOMEASSISTANT_MEDIA, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, @@ -244,6 +245,7 @@ class CastDevice(MediaPlayerEntity): ), ChromeCastZeroconf.get_zeroconf(), ) + chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: @@ -816,6 +818,7 @@ class DynamicCastGroup: ), ChromeCastZeroconf.get_zeroconf(), ) + chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 9f79f0f7d9b..66df9315220 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -35,7 +35,7 @@ from homeassistant.const import ( ATTR_MODE, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - CAST_APP_ID_HOMEASSISTANT, + CAST_APP_ID_HOMEASSISTANT_MEDIA, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, @@ -307,7 +307,7 @@ class CameraStreamTrait(_Trait): ) self.stream_info = { "cameraStreamAccessUrl": f"{get_url(self.hass)}{url}", - "cameraStreamReceiverAppId": CAST_APP_ID_HOMEASSISTANT, + "cameraStreamReceiverAppId": CAST_APP_ID_HOMEASSISTANT_MEDIA, } diff --git a/homeassistant/const.py b/homeassistant/const.py index 2142556a217..4c8adb3426b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -695,9 +695,6 @@ PRECISION_TENTHS: Final = 0.1 # cloud, alexa, or google_home components CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] -# The ID of the Home Assistant Cast App -CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" - ENTITY_CATEGORY_CONFIG: Final = "config" ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" ENTITY_CATEGORY_SYSTEM: Final = "system" @@ -710,3 +707,8 @@ ENTITY_CATEGORIES: Final[list[str]] = [ ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_SYSTEM, ] + +# The ID of the Home Assistant Media Player Cast App +CAST_APP_ID_HOMEASSISTANT_MEDIA: Final = "B45F4572" +# The ID of the Home Assistant Lovelace Cast App +CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 161b4a6f3cb..9d259290956 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -940,7 +940,7 @@ async def test_trait_execute_adding_query_data(hass): "states": { "online": True, "cameraStreamAccessUrl": "https://example.com/api/streams/bla", - "cameraStreamReceiverAppId": "B12CE3CA", + "cameraStreamReceiverAppId": "B45F4572", }, } ] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 290aa00bb47..62a9d7a0120 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -146,7 +146,7 @@ async def test_camera_stream(hass): assert trt.query_attributes() == { "cameraStreamAccessUrl": "https://example.com/api/streams/bla", - "cameraStreamReceiverAppId": "B12CE3CA", + "cameraStreamReceiverAppId": "B45F4572", } From 4e1958c1bd80013f51cb4f826c9bf9063d4ebad4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 10 Nov 2021 20:34:35 +0100 Subject: [PATCH 0383/1452] Add Button platform to KNX integration (#59082) * add button platform * default values for payload and payload_length * allow `type` configuration for encoded payloads * add test for type configuration * move common constants to const.py - CONF_PAYLOAD - CONF_PAYLOAD_LENGTH * validate payload for payload_length or type * c&p errors * fix unique_id and pylint * fix validator --- homeassistant/components/knx/__init__.py | 2 + homeassistant/components/knx/button.py | 57 ++++++++++++ homeassistant/components/knx/const.py | 3 + homeassistant/components/knx/schema.py | 110 ++++++++++++++++++++--- homeassistant/components/knx/select.py | 6 +- tests/components/knx/test_button.py | 91 +++++++++++++++++++ tests/components/knx/test_select.py | 30 ++++--- 7 files changed, 273 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/knx/button.py create mode 100644 tests/components/knx/test_button.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index b61a825e97f..0cdd92abe64 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -50,6 +50,7 @@ from .const import ( from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure from .schema import ( BinarySensorSchema, + ButtonSchema, ClimateSchema, ConnectionSchema, CoverSchema, @@ -102,6 +103,7 @@ CONFIG_SCHEMA = vol.Schema( **EventSchema.SCHEMA, **ExposeSchema.platform_node(), **BinarySensorSchema.platform_node(), + **ButtonSchema.platform_node(), **ClimateSchema.platform_node(), **CoverSchema.platform_node(), **FanSchema.platform_node(), diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py new file mode 100644 index 00000000000..72bb807abb0 --- /dev/null +++ b/homeassistant/components/knx/button.py @@ -0,0 +1,57 @@ +"""Support for KNX/IP buttons.""" +from __future__ import annotations + +from xknx import XKNX +from xknx.devices import RawValue as XknxRawValue + +from homeassistant.components.button import ButtonEntity +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS +from .knx_entity import KnxEntity + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up buttons for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + + async_add_entities( + KNXButton(xknx, entity_config) for entity_config in platform_config + ) + + +class KNXButton(KnxEntity, ButtonEntity): + """Representation of a KNX button.""" + + _device: XknxRawValue + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX button.""" + super().__init__( + device=XknxRawValue( + xknx, + name=config[CONF_NAME], + payload_length=config[CONF_PAYLOAD_LENGTH], + group_address=config[KNX_ADDRESS], + ) + ) + self._payload = config[CONF_PAYLOAD] + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = ( + f"{self._device.remote_value.group_address}_{self._payload}" + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._device.set(self._payload) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 7eae5f1c19f..1058f5abe07 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -31,6 +31,8 @@ CONF_KNX_EXPOSE: Final = "expose" CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" CONF_KNX_ROUTING: Final = "routing" CONF_KNX_TUNNELING: Final = "tunneling" +CONF_PAYLOAD: Final = "payload" +CONF_PAYLOAD_LENGTH: Final = "payload_length" CONF_RESET_AFTER: Final = "reset_after" CONF_RESPOND_TO_READ: Final = "respond_to_read" CONF_STATE_ADDRESS: Final = "state_address" @@ -51,6 +53,7 @@ class SupportedPlatforms(Enum): """Supported platforms.""" BINARY_SENSOR = "binary_sensor" + BUTTON = "button" CLIMATE = "climate" COVER = "cover" FAN = "fan" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 6cd8fc6bc0d..d84531bbcaf 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -9,7 +9,7 @@ import voluptuous as vol from xknx import XKNX from xknx.devices.climate import SetpointShiftMode from xknx.dpt import DPTBase, DPTNumeric -from xknx.exceptions import CouldNotParseAddress +from xknx.exceptions import ConversionError, CouldNotParseAddress from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address @@ -40,6 +40,8 @@ from .const import ( CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_ROUTING, CONF_KNX_TUNNELING, + CONF_PAYLOAD, + CONF_PAYLOAD_LENGTH, CONF_RESET_AFTER, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -123,20 +125,49 @@ def numeric_type_validator(value: Any) -> str | int: raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.") +def _max_payload_value(payload_length: int) -> int: + if payload_length == 0: + return 0x3F + return int(256 ** payload_length) - 1 + + +def button_payload_sub_validator(entity_config: OrderedDict) -> OrderedDict: + """Validate a button entity payload configuration.""" + if _type := entity_config.get(CONF_TYPE): + _payload = entity_config[ButtonSchema.CONF_VALUE] + if (transcoder := DPTBase.parse_transcoder(_type)) is None: + raise vol.Invalid(f"'type: {_type}' is not a valid sensor type.") + entity_config[CONF_PAYLOAD_LENGTH] = transcoder.payload_length + try: + entity_config[CONF_PAYLOAD] = int.from_bytes( + transcoder.to_knx(_payload), byteorder="big" + ) + except ConversionError as ex: + raise vol.Invalid( + f"'payload: {_payload}' not valid for 'type: {_type}'" + ) from ex + return entity_config + + _payload = entity_config[CONF_PAYLOAD] + _payload_length = entity_config[CONF_PAYLOAD_LENGTH] + if _payload > (max_payload := _max_payload_value(_payload_length)): + raise vol.Invalid( + f"'payload: {_payload}' exceeds possible maximum for " + f"payload_length {_payload_length}: {max_payload}" + ) + return entity_config + + def select_options_sub_validator(entity_config: OrderedDict) -> OrderedDict: """Validate a select entity options configuration.""" options_seen = set() payloads_seen = set() - payload_length = entity_config[SelectSchema.CONF_PAYLOAD_LENGTH] - if payload_length == 0: - max_payload = 0x3F - else: - max_payload = 256 ** payload_length - 1 + payload_length = entity_config[CONF_PAYLOAD_LENGTH] for opt in entity_config[SelectSchema.CONF_OPTIONS]: option = opt[SelectSchema.CONF_OPTION] - payload = opt[SelectSchema.CONF_PAYLOAD] - if payload > max_payload: + payload = opt[CONF_PAYLOAD] + if payload > (max_payload := _max_payload_value(payload_length)): raise vol.Invalid( f"'payload: {payload}' for 'option: {option}' exceeds possible" f" maximum of 'payload_length: {payload_length}': {max_payload}" @@ -284,6 +315,67 @@ class BinarySensorSchema(KNXPlatformSchema): ) +class ButtonSchema(KNXPlatformSchema): + """Voluptuous schema for KNX buttons.""" + + PLATFORM_NAME = SupportedPlatforms.BUTTON.value + + CONF_VALUE = "value" + DEFAULT_NAME = "KNX Button" + + payload_or_value_msg = f"Please use only one of `{CONF_PAYLOAD}` or `{CONF_VALUE}`" + length_or_type_msg = ( + f"Please use only one of `{CONF_PAYLOAD_LENGTH}` or `{CONF_TYPE}`" + ) + + ENTITY_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(KNX_ADDRESS): ga_validator, + vol.Exclusive( + CONF_PAYLOAD, "payload_or_value", msg=payload_or_value_msg + ): object, + vol.Exclusive( + CONF_VALUE, "payload_or_value", msg=payload_or_value_msg + ): object, + vol.Exclusive( + CONF_PAYLOAD_LENGTH, "length_or_type", msg=length_or_type_msg + ): object, + vol.Exclusive( + CONF_TYPE, "length_or_type", msg=length_or_type_msg + ): object, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + } + ), + vol.Any( + vol.Schema( + # encoded value + { + vol.Required(CONF_VALUE): vol.Any(int, float, str), + vol.Required(CONF_TYPE): sensor_type_validator, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + # raw payload - default is DPT 1 style True + { + vol.Optional(CONF_PAYLOAD, default=1): cv.positive_int, + vol.Optional(CONF_PAYLOAD_LENGTH, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=14) + ), + vol.Optional(CONF_VALUE): None, + vol.Optional(CONF_TYPE): None, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + # calculate raw CONF_PAYLOAD and CONF_PAYLOAD_LENGTH + # from CONF_VALUE and CONF_TYPE if given and check payload size + button_payload_sub_validator, + ) + + class ClimateSchema(KNXPlatformSchema): """Voluptuous schema for KNX climate devices.""" @@ -733,8 +825,6 @@ class SelectSchema(KNXPlatformSchema): CONF_OPTION = "option" CONF_OPTIONS = "options" - CONF_PAYLOAD = "payload" - CONF_PAYLOAD_LENGTH = "payload_length" DEFAULT_NAME = "KNX Select" ENTITY_SCHEMA = vol.All( diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index e548ad27c8a..f002bad37ce 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -17,6 +17,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( + CONF_PAYLOAD, + CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, @@ -49,7 +51,7 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: return RawValue( xknx, name=config[CONF_NAME], - payload_length=config[SelectSchema.CONF_PAYLOAD_LENGTH], + payload_length=config[CONF_PAYLOAD_LENGTH], group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], @@ -66,7 +68,7 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): """Initialize a KNX select.""" super().__init__(_create_raw_value(xknx, config)) self._option_payloads: dict[str, int] = { - option[SelectSchema.CONF_OPTION]: option[SelectSchema.CONF_PAYLOAD] + option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] } self._attr_options = list(self._option_payloads) diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py new file mode 100644 index 00000000000..0e5f40670f7 --- /dev/null +++ b/tests/components/knx/test_button.py @@ -0,0 +1,91 @@ +"""Test KNX button.""" +from datetime import timedelta + +from homeassistant.components.knx.const import ( + CONF_PAYLOAD, + CONF_PAYLOAD_LENGTH, + KNX_ADDRESS, +) +from homeassistant.components.knx.schema import ButtonSchema +from homeassistant.const import CONF_NAME, CONF_TYPE +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_button_simple(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX button with default payload.""" + events = async_capture_events(hass, "state_changed") + await knx.setup_integration( + { + ButtonSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: "1/2/3", + } + } + ) + assert len(hass.states.async_all()) == 1 + assert len(events) == 1 + events.pop() + + # press button + await hass.services.async_call( + "button", "press", {"entity_id": "button.test"}, blocking=True + ) + await knx.assert_write("1/2/3", True) + assert len(events) == 1 + events.pop() + + # received telegrams on button GA are ignored by the entity + old_state = hass.states.get("button.test") + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await knx.receive_write("1/2/3", False) + await knx.receive_write("1/2/3", True) + new_state = hass.states.get("button.test") + assert old_state == new_state + assert len(events) == 0 + + # button does not respond to read + await knx.receive_read("1/2/3") + await knx.assert_telegram_count(0) + + +async def test_button_raw(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX button with raw payload.""" + await knx.setup_integration( + { + ButtonSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: "1/2/3", + CONF_PAYLOAD: False, + CONF_PAYLOAD_LENGTH: 0, + } + } + ) + # press button + await hass.services.async_call( + "button", "press", {"entity_id": "button.test"}, blocking=True + ) + await knx.assert_write("1/2/3", False) + + +async def test_button_type(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX button with encoded payload.""" + await knx.setup_integration( + { + ButtonSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: "1/2/3", + ButtonSchema.CONF_VALUE: 21.5, + CONF_TYPE: "2byte_float", + } + } + ) + # press button + await hass.services.async_call( + "button", "press", {"entity_id": "button.test"}, blocking=True + ) + await knx.assert_write("1/2/3", (0x0C, 0x33)) diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py index d8089976aca..f7f80c2ebf3 100644 --- a/tests/components/knx/test_select.py +++ b/tests/components/knx/test_select.py @@ -4,6 +4,8 @@ from unittest.mock import patch import pytest from homeassistant.components.knx.const import ( + CONF_PAYLOAD, + CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, @@ -19,9 +21,9 @@ from .conftest import KNXTestKit async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX select.""" _options = [ - {SelectSchema.CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, - {SelectSchema.CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, - {SelectSchema.CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, + {CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, + {CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, + {CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, ] test_address = "1/1/1" await knx.setup_integration( @@ -30,7 +32,7 @@ async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): CONF_NAME: "test", KNX_ADDRESS: test_address, CONF_SYNC_STATE: False, - SelectSchema.CONF_PAYLOAD_LENGTH: 0, + CONF_PAYLOAD_LENGTH: 0, SelectSchema.CONF_OPTIONS: _options, } } @@ -89,9 +91,9 @@ async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): """Test KNX select with passive_address and respond_to_read restoring state.""" _options = [ - {SelectSchema.CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, - {SelectSchema.CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, - {SelectSchema.CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, + {CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, + {CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, + {CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, ] test_address = "1/1/1" test_passive_address = "3/3/3" @@ -107,7 +109,7 @@ async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): CONF_NAME: "test", KNX_ADDRESS: [test_address, test_passive_address], CONF_RESPOND_TO_READ: True, - SelectSchema.CONF_PAYLOAD_LENGTH: 0, + CONF_PAYLOAD_LENGTH: 0, SelectSchema.CONF_OPTIONS: _options, } } @@ -129,11 +131,11 @@ async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): async def test_select_dpt_20_103_all_options(hass: HomeAssistant, knx: KNXTestKit): """Test KNX select with state_address, passive_address and respond_to_read.""" _options = [ - {SelectSchema.CONF_PAYLOAD: 0, SelectSchema.CONF_OPTION: "Auto"}, - {SelectSchema.CONF_PAYLOAD: 1, SelectSchema.CONF_OPTION: "Legio protect"}, - {SelectSchema.CONF_PAYLOAD: 2, SelectSchema.CONF_OPTION: "Normal"}, - {SelectSchema.CONF_PAYLOAD: 3, SelectSchema.CONF_OPTION: "Reduced"}, - {SelectSchema.CONF_PAYLOAD: 4, SelectSchema.CONF_OPTION: "Off"}, + {CONF_PAYLOAD: 0, SelectSchema.CONF_OPTION: "Auto"}, + {CONF_PAYLOAD: 1, SelectSchema.CONF_OPTION: "Legio protect"}, + {CONF_PAYLOAD: 2, SelectSchema.CONF_OPTION: "Normal"}, + {CONF_PAYLOAD: 3, SelectSchema.CONF_OPTION: "Reduced"}, + {CONF_PAYLOAD: 4, SelectSchema.CONF_OPTION: "Off"}, ] test_address = "1/1/1" test_state_address = "2/2/2" @@ -146,7 +148,7 @@ async def test_select_dpt_20_103_all_options(hass: HomeAssistant, knx: KNXTestKi KNX_ADDRESS: [test_address, test_passive_address], CONF_STATE_ADDRESS: test_state_address, CONF_RESPOND_TO_READ: True, - SelectSchema.CONF_PAYLOAD_LENGTH: 1, + CONF_PAYLOAD_LENGTH: 1, SelectSchema.CONF_OPTIONS: _options, } } From 34bc1298aa215c48e9e3d5e9a843bd780ce53b1a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 11 Nov 2021 00:15:23 +0000 Subject: [PATCH 0384/1452] [ci skip] Translation update --- .../alarm_control_panel/translations/tr.json | 6 +++--- .../components/awair/translations/tr.json | 2 +- .../components/blink/translations/tr.json | 2 +- .../components/climate/translations/tr.json | 2 +- .../components/cloudflare/translations/tr.json | 4 ++-- .../components/deconz/translations/tr.json | 2 +- .../components/directv/translations/tr.json | 4 ++++ .../components/esphome/translations/fr.json | 7 +++++++ .../evil_genius_labs/translations/bg.json | 15 +++++++++++++++ .../evil_genius_labs/translations/ca.json | 15 +++++++++++++++ .../evil_genius_labs/translations/nl.json | 15 +++++++++++++++ .../forecast_solar/translations/pl.json | 2 +- .../google_travel_time/translations/tr.json | 2 +- .../components/hyperion/translations/tr.json | 2 +- .../components/juicenet/translations/tr.json | 2 +- .../components/konnected/translations/tr.json | 6 +++--- .../components/litterrobot/translations/pl.json | 2 +- .../motion_blinds/translations/tr.json | 4 ++-- .../components/motioneye/translations/pl.json | 2 +- .../components/mqtt/translations/tr.json | 2 +- .../components/nanoleaf/translations/tr.json | 2 +- .../components/nut/translations/en.json | 17 ++++++++++++++--- .../components/roomba/translations/tr.json | 2 +- .../components/samsungtv/translations/pl.json | 2 +- .../components/shelly/translations/fr.json | 5 ++++- .../components/simplisafe/translations/tr.json | 2 +- .../components/tuya/translations/fr.json | 3 +++ .../components/tuya/translations/pl.json | 6 +++--- .../components/volumio/translations/tr.json | 2 +- .../components/watttime/translations/fr.json | 7 +++++++ .../wolflink/translations/sensor.tr.json | 2 +- .../components/xiaomi_miio/translations/tr.json | 10 +++++----- .../yale_smart_alarm/translations/pl.json | 2 +- 33 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/evil_genius_labs/translations/bg.json create mode 100644 homeassistant/components/evil_genius_labs/translations/ca.json create mode 100644 homeassistant/components/evil_genius_labs/translations/nl.json create mode 100644 homeassistant/components/watttime/translations/fr.json diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index 12796cb5a87..e07c4daba29 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -1,9 +1,9 @@ { "device_automation": { "action_type": { - "arm_away": "D\u0131\u015farda", - "arm_home": "Evde", - "arm_night": "Gece", + "arm_away": "{entity_name} Uzakta Alarm", + "arm_home": "{entity_name} Evde Alarm", + "arm_night": "{entity_name} Gece Alarm", "arm_vacation": "{entity_name} Alarm - Tatil Modu", "disarm": "Devre d\u0131\u015f\u0131 {entity_name}", "trigger": "Tetikle {entity_name}" diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 8463aadf009..e8fa8ca1027 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -6,7 +6,7 @@ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index aa42c87c2e8..1a7444cb644 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 469bebbf620..3e175e6f598 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -25,5 +25,5 @@ "off": "Kapal\u0131" } }, - "title": "\u0130klim" + "title": "\u0130klimlendirme" } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json index 773f74d7a95..1f0c68d7f90 100644 --- a/homeassistant/components/cloudflare/translations/tr.json +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -14,7 +14,7 @@ "step": { "reauth_confirm": { "data": { - "api_token": "API Belirteci", + "api_token": "API Anahtar\u0131", "description": "Cloudflare hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." } }, @@ -26,7 +26,7 @@ }, "user": { "data": { - "api_token": "API Belirteci" + "api_token": "API Anahtar\u0131" }, "description": "Bu entegrasyon, hesab\u0131n\u0131zdaki t\u00fcm b\u00f6lgeler i\u00e7in Zone:Zone:Read ve Zone:DNS:Edit izinleriyle olu\u015fturulmu\u015f bir API Simgesi gerektirir.", "title": "Cloudflare'ye ba\u011flan\u0131n" diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index f7688fcc101..5d259bd6be8 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -6,7 +6,7 @@ "no_bridges": "DeCONZ k\u00f6pr\u00fcs\u00fc bulunamad\u0131", "no_hardware_available": "deCONZ'a ba\u011fl\u0131 radyo donan\u0131m\u0131 yok", "not_deconz_bridge": "deCONZ k\u00f6pr\u00fcs\u00fc de\u011fil", - "updated_instance": "DeCONZ yeni ana bilgisayar adresiyle g\u00fcncelle\u015ftirildi" + "updated_instance": "DeCONZ yeni ana bilgisayar adresiyle g\u00fcncelle\u015ftirildi" }, "error": { "no_key": "API anahtar\u0131 al\u0131namad\u0131" diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json index 2e1f38c50d2..bcafc53ce66 100644 --- a/homeassistant/components/directv/translations/tr.json +++ b/homeassistant/components/directv/translations/tr.json @@ -10,6 +10,10 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, "description": "{name} kurmak istiyor musunuz?" }, "user": { diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 9f6f092afff..860755e97ba 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -7,6 +7,7 @@ "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", "invalid_auth": "Authentification invalide", + "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", @@ -21,6 +22,12 @@ "description": "Voulez-vous ajouter le n\u0153ud ESPHome ` {name} ` \u00e0 Home Assistant?", "title": "N\u0153ud ESPHome d\u00e9couvert" }, + "encryption_key": { + "data": { + "noise_psk": "Cl\u00e9 de chiffrement" + }, + "description": "Entrez la cl\u00e9 de chiffrement que vous avez d\u00e9finie dans votre configuration pour {name}." + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/evil_genius_labs/translations/bg.json b/homeassistant/components/evil_genius_labs/translations/bg.json new file mode 100644 index 00000000000..dcdcdcfc186 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/ca.json b/homeassistant/components/evil_genius_labs/translations/ca.json new file mode 100644 index 00000000000..e77c84008c7 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/nl.json b/homeassistant/components/evil_genius_labs/translations/nl.json new file mode 100644 index 00000000000..29eb87c44a0 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/pl.json b/homeassistant/components/forecast_solar/translations/pl.json index 8c1f96ea709..3fc782fe7c3 100644 --- a/homeassistant/components/forecast_solar/translations/pl.json +++ b/homeassistant/components/forecast_solar/translations/pl.json @@ -24,7 +24,7 @@ "declination": "Deklinacja (0 = Poziomo, 90 = Pionowo)", "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach" }, - "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." + "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." } } } diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json index 0b004c92986..421179ff1a0 100644 --- a/homeassistant/components/google_travel_time/translations/tr.json +++ b/homeassistant/components/google_travel_time/translations/tr.json @@ -12,7 +12,7 @@ "api_key": "API Anahtar\u0131", "destination": "Hedef", "name": "Ad", - "origin": "Men\u015fei" + "origin": "Kalk\u0131\u015f" }, "description": "Ba\u015flang\u0131\u00e7 ve var\u0131\u015f yerini belirtirken, bir adres, enlem/boylam koordinatlar\u0131 veya bir Google yer kimli\u011fi bi\u00e7iminde dikey \u00e7izgi karakteriyle ayr\u0131lm\u0131\u015f bir veya daha fazla konum sa\u011flayabilirsiniz. Bir Google yer kimli\u011fi kullanarak konumu belirtirken, kimli\u011fin \u00f6n\u00fcne 'place_id:' eklenmelidir." } diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index a3c6cd935ee..0368ce2dde3 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -12,7 +12,7 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131" }, "step": { "auth": { diff --git a/homeassistant/components/juicenet/translations/tr.json b/homeassistant/components/juicenet/translations/tr.json index 53890eb41e2..336a8308f0b 100644 --- a/homeassistant/components/juicenet/translations/tr.json +++ b/homeassistant/components/juicenet/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_token": "API Belirteci" + "api_token": "API Anahtar\u0131" }, "description": "API Belirtecine https://home.juice.net/Manage adresinden ihtiyac\u0131n\u0131z olacak.", "title": "JuiceNet'e ba\u011flan\u0131n" diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index 4ccb8b9c32f..be52b4ac92b 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -64,7 +64,7 @@ "5": "B\u00f6lge 5", "6": "B\u00f6lge 6", "7": "B\u00f6lge 7", - "out": "OUT" + "out": "DI\u015eARI" }, "description": "{host} bir {model} ke\u015ffetti. A\u015fa\u011f\u0131dan her G/\u00c7'\u0131n temel yap\u0131land\u0131rmas\u0131n\u0131 se\u00e7in - G/\u00c7'a ba\u011fl\u0131 olarak ikili sens\u00f6rlere (a\u00e7\u0131k/kapal\u0131 kontaklar), dijital sens\u00f6rlere (dht ve ds18b20) veya de\u011fi\u015ftirilebilir \u00e7\u0131k\u0131\u015flara izin verebilir. Sonraki ad\u0131mlarda ayr\u0131nt\u0131l\u0131 se\u00e7enekleri yap\u0131land\u0131rabileceksiniz.", "title": "G/\u00c7'\u0131 yap\u0131land\u0131r" @@ -77,8 +77,8 @@ "8": "B\u00f6lge 8", "9": "B\u00f6lge 9", "alarm1": "ALARM1", - "alarm2_out2": "OUT2/ALARM2", - "out1": "OUT1" + "alarm2_out2": "\u00c7IKI\u015e2/ALARM2", + "out1": "\u00c7IKI\u015e1" }, "description": "A\u015fa\u011f\u0131da kalan G/\u00c7'nin yap\u0131land\u0131rmas\u0131n\u0131 se\u00e7in. Sonraki ad\u0131mlarda ayr\u0131nt\u0131l\u0131 se\u00e7enekleri yap\u0131land\u0131rabileceksiniz.", "title": "Geni\u015fletilmi\u015f G/\u00c7'yi Yap\u0131land\u0131r" diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index 8558125c057..41654933a6f 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key::common::config_flow::abort::already_configured_account%]" + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index aab257b0a13..c9a0efe2538 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -17,7 +17,7 @@ "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc" }, "description": "16 karakterlik API Anahtar\u0131na ihtiyac\u0131n\u0131z olacak, talimatlar i\u00e7in https://www.home-assistant.io/integrations/motion_blinds/#retriving-the-key adresine bak\u0131n.", - "title": "Hareketli Panjurlar" + "title": "Hareketli Perdeler" }, "select": { "data": { @@ -32,7 +32,7 @@ "host": "IP Adresi" }, "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r", - "title": "Hareketli Panjurlar" + "title": "Hareketli Perdeler" } } }, diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json index 2488b2dae89..82bc7e8dcaa 100644 --- a/homeassistant/components/motioneye/translations/pl.json +++ b/homeassistant/components/motioneye/translations/pl.json @@ -20,7 +20,7 @@ "admin_password": "Has\u0142o administratora", "admin_username": "Nazwa u\u017cytkownika administratora", "surveillance_password": "Has\u0142o podgl\u0105du", - "surveillance_username": "[%key::common::config_flow::data::username%] podgl\u0105du", + "surveillance_username": "Nazwa u\u017cytkownika podgl\u0105du", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index e840c3aa2e1..d00aaf1c06b 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -41,7 +41,7 @@ "trigger_type": { "button_double_press": "\" {subtype} \" \u00e7ift t\u0131kland\u0131", "button_long_press": "\" {subtype} \" s\u00fcrekli olarak bas\u0131ld\u0131", - "button_long_release": "\"{alt t\u00fcr}\" uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", + "button_long_release": "\" {subtype} \" uzun bas\u0131\u015ftan sonra \u00e7\u0131kt\u0131", "button_quadruple_press": "\" {subtype} \" d\u00f6rt kez t\u0131kland\u0131", "button_quintuple_press": "\" {subtype} \" be\u015fli t\u0131kland\u0131", "button_short_press": "\" {subtype} \" bas\u0131ld\u0131", diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json index 2510264f599..55cbde518f2 100644 --- a/homeassistant/components/nanoleaf/translations/tr.json +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "invalid_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 90a1176a732..3d57189f7a5 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -1,14 +1,19 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "resources_not_available": "No known resources found" + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, "step": { + "resources": { + "data": { + "resources": "Resources" + }, + "title": "Choose the Resources to Monitor" + }, "ups": { "data": { "alias": "Alias", @@ -28,11 +33,17 @@ } }, "options": { + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, "step": { "init": { "data": { + "resources": "Resources", "scan_interval": "Scan Interval (seconds)" - } + }, + "description": "Choose Sensor Resources." } } } diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index 1bbbb2697bf..695ab709749 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -45,7 +45,7 @@ "host": "Ana bilgisayar", "password": "Parola" }, - "description": "\u015eu anda BLID ve parola alma manuel bir i\u015flemdir. L\u00fctfen a\u015fa\u011f\u0131daki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "Roomba veya Braava'y\u0131 se\u00e7in.", "title": "Cihaza ba\u011flan\u0131n" } } diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 00039239597..1f810248f92 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -12,7 +12,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "auth_missing": "[%key::component::samsungtv::config::abort::auth_missing%]" + "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant." }, "flow_title": "{device}", "step": { diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index e4bdc99db1e..d458d0212e6 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -33,14 +33,17 @@ "button": "Bouton", "button1": "Premier bouton", "button2": "Deuxi\u00e8me bouton", - "button3": "Troisi\u00e8me bouton" + "button3": "Troisi\u00e8me bouton", + "button4": "Quatri\u00e8me bouton" }, "trigger_type": { "double": "{subtype} double-cliqu\u00e9", "long": " {sous-type} long cliqu\u00e9", + "long_push": "{subtype} appui long", "long_single": "{subtype} clic long et simple clic", "single": "{subtype} simple clic", "single_long": "{subtype} simple clic, puis un clic long", + "single_push": "{subtype} simple pression", "triple": "{subtype} cliqu\u00e9 trois fois" } } diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 61a01c31292..6f8f07ccb9d 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -36,7 +36,7 @@ "password": "Parola", "username": "E-posta adresi" }, - "description": "SimpliSafe, 2021'den itibaren web uygulamas\u0131 \u00fczerinden yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle, bu s\u00fcrecin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](yetkilendirme kodu http://home assistant.io/integrations/simplisafe#getting) okudu\u011funuzdan emin olun.\n\nHaz\u0131r oldu\u011funuzda, SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]({url}) t\u0131klat\u0131n. \u0130\u015flem tamamland\u0131\u011f\u0131nda, buraya d\u00f6n\u00fcn ve G\u00f6nder'i t\u0131klat\u0131n.", + "description": "2021'den itibaren SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve G\u00f6nder'e t\u0131klay\u0131n.", "title": "Bilgilerinizi doldurun." } } diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 2dea3145558..f74ed38c18c 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -12,6 +12,9 @@ "step": { "login": { "data": { + "country_code": "Code pays", + "endpoint": "Zone de disponibilit\u00e9", + "password": "Mot de passe", "tuya_app_type": "Application mobile", "username": "Compte" }, diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index f8ce810e94b..a191c40ca3f 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -28,14 +28,14 @@ "data": { "access_id": "Identyfikator dost\u0119pu do Tuya IoT", "access_secret": "Has\u0142o dost\u0119pu do Tuya IoT", - "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", + "country_code": "Kraj", "password": "Has\u0142o", "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", "region": "Region", "tuya_project_type": "Typ projektu chmury Tuya", - "username": "Nazwa u\u017cytkownika" + "username": "Konto" }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce Tuya", "title": "Integracja Tuya" } } diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json index d651decbb3e..72732751732 100644 --- a/homeassistant/components/volumio/translations/tr.json +++ b/homeassistant/components/volumio/translations/tr.json @@ -10,7 +10,7 @@ }, "step": { "discovery_confirm": { - "description": "Home Assistant'a Volumio (` {name}", + "description": "Ev Asistan\u0131'na Volumio ('{name}') eklemek istiyor musunuz?", "title": "Bulunan Volumio" }, "user": { diff --git a/homeassistant/components/watttime/translations/fr.json b/homeassistant/components/watttime/translations/fr.json new file mode 100644 index 00000000000..c4bc0d48b1a --- /dev/null +++ b/homeassistant/components/watttime/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json index 2a8827f9bfd..8415adf6e6b 100644 --- a/homeassistant/components/wolflink/translations/sensor.tr.json +++ b/homeassistant/components/wolflink/translations/sensor.tr.json @@ -49,7 +49,7 @@ "nur_heizgerat": "Sadece kazan", "parallelbetrieb": "Paralel mod", "partymodus": "Parti modu", - "perm_cooling": "PermCooling", + "perm_cooling": "PermSo\u011futma", "permanent": "Kal\u0131c\u0131", "permanentbetrieb": "Kal\u0131c\u0131 mod", "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 0d073a291d9..097ccdab6ac 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -40,9 +40,9 @@ "host": "IP Adresi", "model": "Cihaz modeli (Opsiyonel)", "name": "Cihaz\u0131n ad\u0131", - "token": "API Belirteci" + "token": "API Anahtar\u0131" }, - "description": "32 karaktere API Belirteci , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", + "description": "32 karaktere API Anahtar\u0131 , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" }, "gateway": { @@ -51,15 +51,15 @@ "name": "A\u011f Ge\u00e7idinin Ad\u0131", "token": "API Belirteci" }, - "description": "32 karaktere API Belirteci , bkz. talimatlar i\u00e7in. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", + "description": "32 karaktere API Anahtar\u0131 , bkz. talimatlar i\u00e7in. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" }, "manual": { "data": { "host": "IP Adresi", - "token": "API Belirteci" + "token": "API Anahtar\u0131" }, - "description": "32 karaktere API Belirteci , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Belirteci \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", + "description": "32 karaktere API Anahtar\u0131 , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" }, "reauth_confirm": { diff --git a/homeassistant/components/yale_smart_alarm/translations/pl.json b/homeassistant/components/yale_smart_alarm/translations/pl.json index 553d05ee439..b409b7026c1 100644 --- a/homeassistant/components/yale_smart_alarm/translations/pl.json +++ b/homeassistant/components/yale_smart_alarm/translations/pl.json @@ -18,7 +18,7 @@ "user": { "data": { "area_id": "Identyfikator obszaru", - "name": "[%key::common::config_flow::data::name%]", + "name": "Nazwa", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" } From 6bb816d226cbf45487d3be74083e809a30ee56a6 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 11 Nov 2021 03:21:29 +0200 Subject: [PATCH 0385/1452] Fix wirelesstag switch arm/disarm (#59515) --- homeassistant/components/wirelesstag/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 201041ba616..b7de56d8eff 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -73,13 +73,13 @@ class WirelessTagPlatform: def arm(self, switch): """Arm entity sensor monitoring.""" - func_name = f"arm_{switch.sensor_type}" + func_name = f"arm_{switch.entity_description.key}" if (arm_func := getattr(self.api, func_name)) is not None: arm_func(switch.tag_id, switch.tag_manager_mac) def disarm(self, switch): """Disarm entity sensor monitoring.""" - func_name = f"disarm_{switch.sensor_type}" + func_name = f"disarm_{switch.entity_description.key}" if (disarm_func := getattr(self.api, func_name)) is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) From 751098c22061f2d9ab449d68f7ad065ed66ee890 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Nov 2021 22:01:59 -0600 Subject: [PATCH 0386/1452] Bump sqlalchemy to 1.4.26 (#59527) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9ea08cc1f64..e986c104f8b 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.23"], + "requirements": ["sqlalchemy==1.4.26"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 4796dac11a9..debcea9a737 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.23"], + "requirements": ["sqlalchemy==1.4.26"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b2b2f26bb2a..31cd7fb341f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.26.0 scapy==2.4.5 -sqlalchemy==1.4.23 +sqlalchemy==1.4.26 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index decfa186fda..7b80cee41c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2215,7 +2215,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.23 +sqlalchemy==1.4.26 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6e1707b4fd..4e25d14b60f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1297,7 +1297,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.23 +sqlalchemy==1.4.26 # homeassistant.components.srp_energy srpenergy==1.3.2 From ebb25ab0e604cf75ab5a038dc9d363d3894dd888 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 00:19:56 -0600 Subject: [PATCH 0387/1452] Make yaml file writes safer (#59384) --- homeassistant/components/config/__init__.py | 6 +- homeassistant/util/file.py | 54 +++++++++++++++++ homeassistant/util/json.py | 28 +-------- tests/components/config/test_group.py | 50 ++++++++++++++++ tests/util/test_file.py | 65 +++++++++++++++++++++ 5 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 homeassistant/util/file.py create mode 100644 tests/util/test_file.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index c39a79f3e4a..43c9cfabd08 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT +from homeassistant.util.file import write_utf8_file from homeassistant.util.yaml import dump, load_yaml DOMAIN = "config" @@ -252,6 +253,5 @@ def _write(path, data): """Write YAML helper.""" # Do it before opening file. If dump causes error it will now not # truncate the file. - data = dump(data) - with open(path, "w", encoding="utf-8") as outfile: - outfile.write(data) + contents = dump(data) + write_utf8_file(path, contents) diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py new file mode 100644 index 00000000000..9c5b11e4807 --- /dev/null +++ b/homeassistant/util/file.py @@ -0,0 +1,54 @@ +"""File utility functions.""" +from __future__ import annotations + +import logging +import os +import tempfile + +from homeassistant.exceptions import HomeAssistantError + +_LOGGER = logging.getLogger(__name__) + + +class WriteError(HomeAssistantError): + """Error writing the data.""" + + +def write_utf8_file( + filename: str, + utf8_data: str, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + + tmp_filename = "" + tmp_path = os.path.split(filename)[0] + try: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=tmp_path, delete=False + ) as fdesc: + fdesc.write(utf8_data) + tmp_filename = fdesc.name + if not private: + os.chmod(tmp_filename, 0o644) + os.replace(tmp_filename, filename) + except OSError as error: + _LOGGER.exception("Saving file failed: %s", filename) + raise WriteError(error) from error + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error( + "File replacement cleanup failed for %s while saving %s: %s", + tmp_filename, + filename, + err, + ) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index e82bd968754..e3bde277837 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -5,13 +5,13 @@ from collections import deque from collections.abc import Callable import json import logging -import os -import tempfile from typing import Any from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from .file import write_utf8_file + _LOGGER = logging.getLogger(__name__) @@ -61,29 +61,7 @@ def save_json( _LOGGER.error(msg) raise SerializationError(msg) from error - tmp_filename = "" - tmp_path = os.path.split(filename)[0] - try: - # Modern versions of Python tempfile create this file with mode 0o600 - with tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", dir=tmp_path, delete=False - ) as fdesc: - fdesc.write(json_data) - tmp_filename = fdesc.name - if not private: - os.chmod(tmp_filename, 0o644) - os.replace(tmp_filename, filename) - except OSError as error: - _LOGGER.exception("Saving JSON file failed: %s", filename) - raise WriteError(error) from error - finally: - if os.path.exists(tmp_filename): - try: - os.remove(tmp_filename) - except OSError as err: - # If we are cleaning up then something else went wrong, so - # we should suppress likely follow-on errors in the cleanup - _LOGGER.error("JSON replacement cleanup failed: %s", err) + write_utf8_file(filename, json_data, private) def format_unserializable_data(data: dict[str, Any]) -> str: diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 72a9a00cbea..4d1d28020bb 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,10 +1,14 @@ """Test Group config panel.""" from http import HTTPStatus import json +from pathlib import Path from unittest.mock import AsyncMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.config import group +from homeassistant.util.file import write_utf8_file +from homeassistant.util.yaml import dump, load_yaml VIEW_NAME = "api:config:group:config" @@ -113,3 +117,49 @@ async def test_update_device_config_invalid_json(hass, hass_client): resp = await client.post("/api/config/group/config/hello_beer", data="not json") assert resp.status == HTTPStatus.BAD_REQUEST + + +async def test_update_config_write_to_temp_file(hass, hass_client, tmpdir): + """Test config with a temp file.""" + test_dir = await hass.async_add_executor_job(tmpdir.mkdir, "files") + group_yaml = Path(test_dir / "group.yaml") + + with patch.object(group, "GROUP_CONFIG_PATH", group_yaml), patch.object( + config, "SECTIONS", ["group"] + ): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = { + "hello.beer": {"ignored": True}, + "other.entity": {"polling_intensity": 2}, + } + contents = dump(orig_data) + await hass.async_add_executor_job(write_utf8_file, group_yaml, contents) + + mock_call = AsyncMock() + + with patch.object(hass.services, "async_call", mock_call): + resp = await client.post( + "/api/config/group/config/hello_beer", + data=json.dumps( + {"name": "Beer", "entities": ["light.top", "light.bottom"]} + ), + ) + await hass.async_block_till_done() + + assert resp.status == HTTPStatus.OK + result = await resp.json() + assert result == {"result": "ok"} + + new_data = await hass.async_add_executor_job(load_yaml, group_yaml) + + assert new_data == { + **orig_data, + "hello_beer": { + "name": "Beer", + "entities": ["light.top", "light.bottom"], + }, + } + mock_call.assert_called_once_with("group", "reload") diff --git a/tests/util/test_file.py b/tests/util/test_file.py new file mode 100644 index 00000000000..109645a839a --- /dev/null +++ b/tests/util/test_file.py @@ -0,0 +1,65 @@ +"""Test Home Assistant file utility functions.""" +import os +from pathlib import Path +from unittest.mock import patch + +import pytest + +from homeassistant.util.file import WriteError, write_utf8_file + + +def test_write_utf8_file_private(tmpdir): + """Test files can be written as 0o600 or 0o644.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + write_utf8_file(test_file, '{"some":"data"}', False) + with open(test_file) as fh: + assert fh.read() == '{"some":"data"}' + assert os.stat(test_file).st_mode & 0o777 == 0o644 + + write_utf8_file(test_file, '{"some":"data"}', True) + with open(test_file) as fh: + assert fh.read() == '{"some":"data"}' + assert os.stat(test_file).st_mode & 0o777 == 0o600 + + +def test_write_utf8_file_fails_at_creation(tmpdir): + """Test that failed creation of the temp file does not create an empty file.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(WriteError), patch( + "homeassistant.util.file.tempfile.NamedTemporaryFile", side_effect=OSError + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) + + +def test_write_utf8_file_fails_at_rename(tmpdir, caplog): + """Test that if rename fails not not remove, we do not log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(WriteError), patch( + "homeassistant.util.file.os.replace", side_effect=OSError + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) + + assert "File replacement cleanup failed" not in caplog.text + + +def test_write_utf8_file_fails_at_rename_and_remove(tmpdir, caplog): + """Test that if rename and remove both fail, we log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(WriteError), patch( + "homeassistant.util.file.os.remove", side_effect=OSError + ), patch("homeassistant.util.file.os.replace", side_effect=OSError): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert "File replacement cleanup failed" in caplog.text From fae6c6fda15b2bdab725e2125bd48fa29659bd7d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Nov 2021 07:21:43 +0100 Subject: [PATCH 0388/1452] Upgrade pyupgrade to v2.29.0 (#59499) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57445820886..a9f42ed17ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.27.0 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8cc41279c13..319b6478655 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.27.0 +pyupgrade==2.29.0 yamllint==1.26.3 From 8447bbf5f0f68a5bb309ac976120f018a1a6d252 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Nov 2021 07:22:18 +0100 Subject: [PATCH 0389/1452] Add binary sensor platform to RDW Vehicle information (#59253) --- homeassistant/components/rdw/__init__.py | 3 +- homeassistant/components/rdw/binary_sensor.py | 101 ++++++++++++++++++ tests/components/rdw/test_binary_sensor.py | 51 +++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/rdw/binary_sensor.py create mode 100644 tests/components/rdw/test_binary_sensor.py diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index 32f5c81e86a..bde83fffad3 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from vehicle import RDW, Vehicle +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -11,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_LICENSE_PLATE, DOMAIN, LOGGER, SCAN_INTERVAL -PLATFORMS = (SENSOR_DOMAIN,) +PLATFORMS = (BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py new file mode 100644 index 00000000000..626afcf2b0a --- /dev/null +++ b/homeassistant/components/rdw/binary_sensor.py @@ -0,0 +1,101 @@ +"""Support for RDW binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from vehicle import Vehicle + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, ENTRY_TYPE_SERVICE + + +@dataclass +class RDWBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + is_on_fn: Callable[[Vehicle], bool | None] + + +@dataclass +class RDWBinarySensorEntityDescription( + BinarySensorEntityDescription, RDWBinarySensorEntityDescriptionMixin +): + """Describes RDW binary sensor entity.""" + + +BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( + RDWBinarySensorEntityDescription( + key="liability_insured", + name="Liability Insured", + icon="mdi:shield-car", + is_on_fn=lambda vehicle: vehicle.liability_insured, + ), + RDWBinarySensorEntityDescription( + key="pending_recall", + name="Pending Recall", + device_class=DEVICE_CLASS_PROBLEM, + is_on_fn=lambda vehicle: vehicle.pending_recall, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up RDW binary sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + RDWBinarySensorEntity( + coordinator=coordinator, + description=description, + ) + for description in BINARY_SENSORS + if description.is_on_fn(coordinator.data) is not None + ) + + +class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): + """Defines an RDW binary sensor.""" + + entity_description: RDWBinarySensorEntityDescription + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + description: RDWBinarySensorEntityDescription, + ) -> None: + """Initialize RDW binary sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.license_plate}_{description.key}" + + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, coordinator.data.license_plate)}, + manufacturer=coordinator.data.brand, + name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + model=coordinator.data.model, + configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", + ) + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return bool(self.entity_description.is_on_fn(self.coordinator.data)) diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py new file mode 100644 index 00000000000..39fc981424d --- /dev/null +++ b/tests/components/rdw/test_binary_sensor.py @@ -0,0 +1,51 @@ +"""Tests for the sensors provided by the RDW integration.""" +from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM +from homeassistant.components.rdw.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_vehicle_binary_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the RDW vehicle binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("binary_sensor.liability_insured") + entry = entity_registry.async_get("binary_sensor.liability_insured") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_liability_insured" + assert state.state == "off" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Liability Insured" + assert state.attributes.get(ATTR_ICON) == "mdi:shield-car" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.pending_recall") + entry = entity_registry.async_get("binary_sensor.pending_recall") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_pending_recall" + assert state.state == "off" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending Recall" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PROBLEM + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} + assert device_entry.manufacturer == "Skoda" + assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.model == "Citigo" + assert ( + device_entry.configuration_url + == "https://ovi.rdw.nl/default.aspx?kenteken=11ZKZ3" + ) + assert not device_entry.sw_version From 61e4ebf15541b7729d9d80f7f044d7a2312dede0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Nov 2021 07:22:52 +0100 Subject: [PATCH 0390/1452] Add button entity platform (restart button) to WLED (#59086) Co-authored-by: Tom Brien --- homeassistant/components/wled/__init__.py | 10 ++- homeassistant/components/wled/button.py | 41 ++++++++++ tests/components/wled/test_button.py | 94 +++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/wled/button.py create mode 100644 tests/components/wled/test_button.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index e7697676014..d54cfe3cd1c 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,6 +1,7 @@ """Support for WLED.""" from __future__ import annotations +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN @@ -12,7 +13,14 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator -PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, NUMBER_DOMAIN) +PLATFORMS = ( + BUTTON_DOMAIN, + LIGHT_DOMAIN, + SELECT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + NUMBER_DOMAIN, +) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py new file mode 100644 index 00000000000..7278495b3fa --- /dev/null +++ b/homeassistant/components/wled/button.py @@ -0,0 +1,41 @@ +"""Support for WLED button.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up WLED button based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([WLEDRestartButton(coordinator)]) + + +class WLEDRestartButton(WLEDEntity, ButtonEntity): + """Defines a WLED restart switch.""" + + _attr_icon = "mdi:restart" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize the button entity.""" + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Restart" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_restart" + + @wled_exception_handler + async def async_press(self) -> None: + """Send out a restart command.""" + await self.coordinator.wled.reset() diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py new file mode 100644 index 00000000000..9034632ae93 --- /dev/null +++ b/tests/components/wled/test_button.py @@ -0,0 +1,94 @@ +"""Tests for the WLED button platform.""" +from unittest.mock import MagicMock + +from freezegun import freeze_time +import pytest +from wled import WLEDConnectionError, WLEDError + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + ENTITY_CATEGORY_CONFIG, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_button( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the creation and values of the WLED button.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("button.wled_rgb_light_restart") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:restart" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.wled_rgb_light_restart") + assert entry + assert entry.unique_id == "aabbccddeeff_restart" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + # Restart + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.reset.call_count == 1 + mock_wled.reset.assert_called_with() + + +@freeze_time("2021-11-04 17:37:00", tz_offset=-1) +async def test_button_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED buttons.""" + mock_wled.reset.side_effect = WLEDError + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.wled_rgb_light_restart") + assert state + assert state.state == "2021-11-04T16:37:00+00:00" + assert "Invalid response from API" in caplog.text + + +async def test_button_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED buttons.""" + mock_wled.reset.side_effect = WLEDConnectionError + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.wled_rgb_light_restart") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text From f8f060b72ba415e88a94f0db590dd91bf568d7aa Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 11 Nov 2021 07:29:16 +0100 Subject: [PATCH 0391/1452] Make util.color._match_max_scale public (#59207) --- homeassistant/util/color.py | 12 ++++++------ tests/util/test_color.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 1cfd6447e8a..2daccf28915 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -404,8 +404,8 @@ def color_hs_to_xy( return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) -def _match_max_scale( - input_colors: tuple[int, ...], output_colors: tuple[int, ...] +def match_max_scale( + input_colors: tuple[int, ...], output_colors: tuple[float, ...] ) -> tuple[int, ...]: """Match the maximum value of the output to the input.""" max_in = max(input_colors) @@ -426,7 +426,7 @@ def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]: # Match the output maximum value to the input. This ensures the full # channel range is used. - return _match_max_scale((r, g, b), rgbw) # type: ignore + return match_max_scale((r, g, b), rgbw) # type: ignore[return-value] def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: @@ -436,7 +436,7 @@ def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: # Match the output maximum value to the input. This ensures the # output doesn't overflow. - return _match_max_scale((r, g, b, w), rgb) # type: ignore + return match_max_scale((r, g, b, w), rgb) # type: ignore[return-value] def color_rgb_to_rgbww( @@ -458,7 +458,7 @@ def color_rgb_to_rgbww( # Match the output maximum value to the input. This ensures the full # channel range is used. - return _match_max_scale((r, g, b), rgbww) # type: ignore + return match_max_scale((r, g, b), rgbww) # type: ignore[return-value] def color_rgbww_to_rgb( @@ -481,7 +481,7 @@ def color_rgbww_to_rgb( # Match the output maximum value to the input. This ensures the # output doesn't overflow. - return _match_max_scale((r, g, b, cw, ww), rgb) # type: ignore + return match_max_scale((r, g, b, cw, ww), rgb) # type: ignore[return-value] def color_rgb_to_hex(r: int, g: int, b: int) -> str: diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 8f520f4a7ec..db9ad988aee 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -279,6 +279,20 @@ def test_color_rgb_to_hex(): assert color_util.color_rgb_to_hex(255, 67.9204190, 0) == "ff4400" +def test_match_max_scale(): + """Test match_max_scale.""" + match_max_scale = color_util.match_max_scale + assert match_max_scale((255, 255, 255), (255, 255, 255)) == (255, 255, 255) + assert match_max_scale((0, 0, 0), (0, 0, 0)) == (0, 0, 0) + assert match_max_scale((255, 255, 255), (128, 128, 128)) == (255, 255, 255) + assert match_max_scale((0, 255, 0), (64, 128, 128)) == (128, 255, 255) + assert match_max_scale((0, 100, 0), (128, 64, 64)) == (100, 50, 50) + assert match_max_scale((10, 20, 33), (100, 200, 333)) == (10, 20, 33) + assert match_max_scale((255,), (100, 200, 333)) == (77, 153, 255) + assert match_max_scale((128,), (10.5, 20.9, 30.4)) == (44, 88, 128) + assert match_max_scale((10, 20, 30, 128), (100, 200, 333)) == (38, 77, 128) + + def test_gamut(): """Test gamut functions.""" assert color_util.check_valid_gamut(GAMUT) From cab9f821a1dfc3c80096ad3c60aab5b9f08ee2ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 00:31:08 -0600 Subject: [PATCH 0392/1452] Fix zeroconf with sonos v1 firmware (#59460) --- homeassistant/components/sonos/config_flow.py | 2 +- homeassistant/components/sonos/helpers.py | 7 ++- tests/components/sonos/test_config_flow.py | 50 +++++++++++++++++++ tests/components/sonos/test_helpers.py | 6 +++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 745d3db3890..98e1194ebd0 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -30,7 +30,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): ) -> FlowResult: """Handle a flow initialized by zeroconf.""" hostname = discovery_info["hostname"] - if hostname is None or not hostname.startswith("Sonos-"): + if hostname is None or not hostname.lower().startswith("sonos"): return self.async_abort(reason="not_sonos_device") await self.async_set_unique_id(self._domain, raise_on_progress=False) host = discovery_info[CONF_HOST] diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 01a75eb7747..490bcdefba5 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -44,5 +44,10 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" - baseuid = hostname.split("-")[1].replace(".local.", "") + if hostname.startswith("Sonos-"): + baseuid = hostname.split("-")[1].replace(".local.", "") + elif hostname.startswith("sonos"): + baseuid = hostname[5:].replace(".local.", "") + else: + raise ValueError(f"{hostname} is not a sonos device.") return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 39f3966e2ce..7d6fd02f51d 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -75,6 +75,56 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert len(mock_manager.mock_calls) == 2 +async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): + """Test we pass sonos devices to the discovery manager with v1 firmware devices.""" + + mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.107", + "port": 1443, + "hostname": "sonos5CAAFDE47AC8.local.", + "type": "_sonos._tcp.local.", + "name": "Sonos-5CAAFDE47AC8._sonos._tcp.local.", + "properties": { + "_raw": { + "info": b"/api/v1/players/RINCON_5CAAFDE47AC801400/info", + "vers": b"1", + "protovers": b"1.18.9", + }, + "info": "/api/v1/players/RINCON_5CAAFDE47AC801400/info", + "vers": "1", + "protovers": "1.18.9", + }, + }, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_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["title"] == "Sonos" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_manager.mock_calls) == 2 + + async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() diff --git a/tests/components/sonos/test_helpers.py b/tests/components/sonos/test_helpers.py index a52337f9455..be32d3a190b 100644 --- a/tests/components/sonos/test_helpers.py +++ b/tests/components/sonos/test_helpers.py @@ -1,9 +1,15 @@ """Test the sonos config flow.""" from __future__ import annotations +import pytest + from homeassistant.components.sonos.helpers import hostname_to_uid async def test_uid_to_hostname(): """Test we can convert a hostname to a uid.""" assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400" + assert hostname_to_uid("sonos5CAAFDE47AC8.local.") == "RINCON_5CAAFDE47AC801400" + + with pytest.raises(ValueError): + assert hostname_to_uid("notsonos5CAAFDE47AC8.local.") From ec9b5df7b3f7446f629c647eb8bceeefc8f4d9cb Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 11 Nov 2021 06:31:56 +0000 Subject: [PATCH 0393/1452] Ignore None state in state_change_event (#59485) --- homeassistant/components/integration/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 22e80e7879e..463cb3b4e05 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -155,6 +155,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): if ( old_state is None + or new_state is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): From a14131a6790a31b270d89daa3f33e1ab96ebd17c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 07:33:07 +0100 Subject: [PATCH 0394/1452] Suppress media status when the lovelace cast app is active (#59481) Co-authored-by: Bram Kragten --- homeassistant/components/cast/media_player.py | 36 ++++++++++++++----- tests/components/cast/test_media_player.py | 32 +++++++++++------ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index d8eacfeede4..dc3d20188e7 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -46,6 +46,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import lookup_plex_media from homeassistant.const import ( + CAST_APP_ID_HOMEASSISTANT_LOVELACE, CAST_APP_ID_HOMEASSISTANT_MEDIA, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, @@ -87,6 +88,7 @@ SUPPORT_CAST = ( | SUPPORT_TURN_ON ) +STATE_CASTING = "casting" ENTITY_SCHEMA = vol.All( vol.Schema( @@ -568,14 +570,18 @@ class CastDevice(MediaPlayerEntity): @property def state(self): """Return the state of the player.""" - if (media_status := self._media_status()[0]) is None: - return None - if media_status.player_is_playing: - return STATE_PLAYING - if media_status.player_is_paused: - return STATE_PAUSED - if media_status.player_is_idle: - return STATE_IDLE + # The lovelace app loops media to prevent timing out, don't show that + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return STATE_CASTING + if (media_status := self._media_status()[0]) is not None: + if media_status.player_is_playing: + return STATE_PLAYING + if media_status.player_is_paused: + return STATE_PAUSED + if media_status.player_is_idle: + return STATE_IDLE + if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: + return STATE_CASTING if self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF return None @@ -583,12 +589,18 @@ class CastDevice(MediaPlayerEntity): @property def media_content_id(self): """Content ID of current playing media.""" + # The lovelace app loops media to prevent timing out, don't show that + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return None media_status = self._media_status()[0] return media_status.content_id if media_status else None @property def media_content_type(self): """Content type of current playing media.""" + # The lovelace app loops media to prevent timing out, don't show that + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return None if (media_status := self._media_status()[0]) is None: return None if media_status.media_is_tvshow: @@ -602,6 +614,9 @@ class CastDevice(MediaPlayerEntity): @property def media_duration(self): """Duration of current playing media in seconds.""" + # The lovelace app loops media to prevent timing out, don't show that + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return None media_status = self._media_status()[0] return media_status.duration if media_status else None @@ -699,6 +714,9 @@ class CastDevice(MediaPlayerEntity): @property def media_position(self): """Position of current playing media in seconds.""" + # The lovelace app loops media to prevent timing out, don't show that + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return None media_status = self._media_status()[0] if media_status is None or not ( media_status.player_is_playing @@ -714,6 +732,8 @@ class CastDevice(MediaPlayerEntity): Returns value from homeassistant.util.dt.utcnow(). """ + if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: + return None media_status_recevied = self._media_status()[1] return media_status_recevied diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 3bb2b895c1a..48ed54cb76e 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -45,6 +45,7 @@ FakeGroupUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e3") def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" mock = MagicMock(uuid=info.uuid) + mock.app_id = None mock.media_controller.status = None return mock @@ -565,7 +566,7 @@ async def test_entity_availability(hass: HomeAssistant): conn_status_cb(connection_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "unknown" + assert state.state == "off" connection_status = MagicMock() connection_status.status = "DISCONNECTED" @@ -596,7 +597,7 @@ async def test_entity_cast_status(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) assert state.attributes.get("supported_features") == ( @@ -610,6 +611,17 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_VOLUME_SET ) + cast_status = MagicMock() + cast_status.volume_level = 0.5 + cast_status.volume_muted = False + cast_status_cb(cast_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + # Volume hidden if no app is active + assert state.attributes.get("volume_level") is None + assert not state.attributes.get("is_volume_muted") + + chromecast.app_id = "1234" cast_status = MagicMock() cast_status.volume_level = 0.5 cast_status.volume_muted = False @@ -665,7 +677,7 @@ async def test_entity_play_media(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) # Play_media @@ -694,7 +706,7 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) # Play_media - cast with app ID @@ -739,7 +751,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) # play_media - media_type cast with invalid JSON @@ -812,7 +824,7 @@ async def test_entity_media_content_type(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) media_status = MagicMock(images=None) @@ -866,7 +878,7 @@ async def test_entity_control(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) assert state.attributes.get("supported_features") == ( @@ -975,7 +987,7 @@ async def test_entity_media_states(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) media_status = MagicMock(images=None) @@ -1036,7 +1048,7 @@ async def test_group_media_states(hass, mz_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) group_media_status = MagicMock(images=None) @@ -1090,7 +1102,7 @@ async def test_group_media_control(hass, mz_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "unknown" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) group_media_status = MagicMock(images=None) From 5f8997471dca4e48dc8a72e41434a8e3b0f0a05c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 07:34:49 +0100 Subject: [PATCH 0395/1452] Minor refactor of template sensor (#59466) --- homeassistant/components/template/sensor.py | 77 ++++++--------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index ea203bdd879..a89a30af556 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,8 @@ """Allows the creation of a sensor that breaks out state_attributes.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.sensor import ( @@ -41,7 +43,7 @@ from .const import ( CONF_PICTURE, CONF_TRIGGER, ) -from .template_entity import TemplateEntity +from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity from .trigger_entity import TriggerEntity LEGACY_FIELDS = { @@ -57,18 +59,14 @@ LEGACY_FIELDS = { SENSOR_SCHEMA = vol.Schema( { - vol.Optional(CONF_NAME): cv.template, - vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_PICTURE): cv.template, - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } -) +).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) LEGACY_SENSOR_SCHEMA = vol.All( @@ -150,19 +148,7 @@ def _async_create_template_tracking_entities( sensors = [] for entity_conf in definitions: - # Still available on legacy - object_id = entity_conf.get(CONF_OBJECT_ID) - - state_template = entity_conf[CONF_STATE] - icon_template = entity_conf.get(CONF_ICON) - entity_picture_template = entity_conf.get(CONF_PICTURE) - availability_template = entity_conf.get(CONF_AVAILABILITY) - friendly_name_template = entity_conf.get(CONF_NAME) - unit_of_measurement = entity_conf.get(CONF_UNIT_OF_MEASUREMENT) - device_class = entity_conf.get(CONF_DEVICE_CLASS) - attribute_templates = entity_conf.get(CONF_ATTRIBUTES, {}) unique_id = entity_conf.get(CONF_UNIQUE_ID) - state_class = entity_conf.get(CONF_STATE_CLASS) if unique_id and unique_id_prefix: unique_id = f"{unique_id_prefix}-{unique_id}" @@ -170,17 +156,8 @@ def _async_create_template_tracking_entities( sensors.append( SensorTemplate( hass, - object_id, - friendly_name_template, - unit_of_measurement, - state_template, - icon_template, - entity_picture_template, - availability_template, - device_class, - attribute_templates, + entity_conf, unique_id, - state_class, ) ) @@ -219,47 +196,33 @@ class SensorTemplate(TemplateEntity, SensorEntity): def __init__( self, hass: HomeAssistant, - object_id: str | None, - friendly_name_template: template.Template | None, - unit_of_measurement: str | None, - state_template: template.Template, - icon_template: template.Template | None, - entity_picture_template: template.Template | None, - availability_template: template.Template | None, - device_class: str | None, - attribute_templates: dict[str, template.Template], + config: dict[str, Any], unique_id: str | None, - state_class: str | None, ) -> None: """Initialize the sensor.""" - super().__init__( - attribute_templates=attribute_templates, - availability_template=availability_template, - icon_template=icon_template, - entity_picture_template=entity_picture_template, - ) - if object_id is not None: + super().__init__(config=config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - self._friendly_name_template = friendly_name_template + self._friendly_name_template = config.get(CONF_NAME) self._attr_name = None # Try to render the name as it can influence the entity ID - if friendly_name_template: - friendly_name_template.hass = hass + if self._friendly_name_template: + self._friendly_name_template.hass = hass try: - self._attr_name = friendly_name_template.async_render( + self._attr_name = self._friendly_name_template.async_render( parse_result=False ) except template.TemplateError: pass - 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 + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._template = config.get(CONF_STATE) + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_state_class = config.get(CONF_STATE_CLASS) self._attr_unique_id = unique_id async def async_added_to_hass(self): From 65b1f0d9ebd0da4d222a9b3e8dfe003cc3a78168 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 07:38:15 +0100 Subject: [PATCH 0396/1452] Minor refactor of energy validator (#58209) --- homeassistant/components/energy/validate.py | 291 +++++++++++------- .../components/recorder/statistics.py | 4 +- tests/components/energy/test_validate.py | 22 +- 3 files changed, 204 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index b2a939bffce..d77ea75d36c 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -67,8 +67,10 @@ class EnergyPreferencesValidation: return dataclasses.asdict(self) -async def _async_validate_usage_stat( +@callback +def _async_validate_usage_stat( hass: HomeAssistant, + metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]], stat_id: str, allowed_device_classes: Sequence[str], allowed_units: Mapping[str, Sequence[str]], @@ -76,14 +78,6 @@ async def _async_validate_usage_stat( result: list[ValidationIssue], ) -> None: """Validate a statistic.""" - metadata = await hass.async_add_executor_job( - functools.partial( - recorder.statistics.get_metadata, - hass, - statistic_ids=(stat_id,), - ) - ) - if stat_id not in metadata: result.append(ValidationIssue("statistics_not_defined", stat_id)) @@ -201,18 +195,14 @@ def _async_validate_price_entity( result.append(ValidationIssue(unit_error, entity_id, unit)) -async def _async_validate_cost_stat( - hass: HomeAssistant, stat_id: str, result: list[ValidationIssue] +@callback +def _async_validate_cost_stat( + hass: HomeAssistant, + metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]], + stat_id: str, + result: list[ValidationIssue], ) -> None: """Validate that the cost stat is correct.""" - metadata = await hass.async_add_executor_job( - functools.partial( - recorder.statistics.get_metadata, - hass, - statistic_ids=(stat_id,), - ) - ) - if stat_id not in metadata: result.append(ValidationIssue("statistics_not_defined", stat_id)) @@ -266,154 +256,247 @@ def _async_validate_auto_generated_cost_entity( async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: """Validate the energy configuration.""" manager = await data.async_get_manager(hass) + statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {} + validate_calls = [] + wanted_statistics_metadata = set() result = EnergyPreferencesValidation() if manager.data is None: return result + # Create a list of validation checks 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"]: - await _async_validate_usage_stat( - hass, - flow["stat_energy_from"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(flow["stat_energy_from"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + flow["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) ) if flow.get("stat_cost") is not None: - await _async_validate_cost_stat( - hass, flow["stat_cost"], source_result + wanted_statistics_metadata.add(flow["stat_cost"]) + validate_calls.append( + functools.partial( + _async_validate_cost_stat, + hass, + statistics_metadata, + 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, - ENERGY_PRICE_UNITS, - ENERGY_PRICE_UNIT_ERROR, + validate_calls.append( + functools.partial( + _async_validate_price_entity, + hass, + flow["entity_energy_price"], + source_result, + ENERGY_PRICE_UNITS, + ENERGY_PRICE_UNIT_ERROR, + ) ) if flow.get("entity_energy_from") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): - _async_validate_auto_generated_cost_entity( - hass, - flow["entity_energy_from"], - source_result, + validate_calls.append( + functools.partial( + _async_validate_auto_generated_cost_entity, + hass, + flow["entity_energy_from"], + source_result, + ) ) for flow in source["flow_to"]: - await _async_validate_usage_stat( - hass, - flow["stat_energy_to"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(flow["stat_energy_to"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + flow["stat_energy_to"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) ) if flow.get("stat_compensation") is not None: - await _async_validate_cost_stat( - hass, flow["stat_compensation"], source_result + wanted_statistics_metadata.add(flow["stat_compensation"]) + validate_calls.append( + functools.partial( + _async_validate_cost_stat, + hass, + statistics_metadata, + 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, - ENERGY_PRICE_UNITS, - ENERGY_PRICE_UNIT_ERROR, + validate_calls.append( + functools.partial( + _async_validate_price_entity, + hass, + flow["entity_energy_price"], + source_result, + ENERGY_PRICE_UNITS, + ENERGY_PRICE_UNIT_ERROR, + ) ) if flow.get("entity_energy_to") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): - _async_validate_auto_generated_cost_entity( - hass, - flow["entity_energy_to"], - source_result, + validate_calls.append( + functools.partial( + _async_validate_auto_generated_cost_entity, + hass, + flow["entity_energy_to"], + source_result, + ) ) elif source["type"] == "gas": - await _async_validate_usage_stat( - hass, - source["stat_energy_from"], - GAS_USAGE_DEVICE_CLASSES, - GAS_USAGE_UNITS, - GAS_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(source["stat_energy_from"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + source["stat_energy_from"], + GAS_USAGE_DEVICE_CLASSES, + GAS_USAGE_UNITS, + GAS_UNIT_ERROR, + source_result, + ) ) if source.get("stat_cost") is not None: - await _async_validate_cost_stat( - hass, source["stat_cost"], source_result + wanted_statistics_metadata.add(source["stat_cost"]) + validate_calls.append( + functools.partial( + _async_validate_cost_stat, + hass, + statistics_metadata, + 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, - GAS_PRICE_UNITS, - GAS_PRICE_UNIT_ERROR, + validate_calls.append( + functools.partial( + _async_validate_price_entity, + hass, + source["entity_energy_price"], + source_result, + GAS_PRICE_UNITS, + GAS_PRICE_UNIT_ERROR, + ) ) if source.get("entity_energy_from") is not None and ( source.get("entity_energy_price") is not None or source.get("number_energy_price") is not None ): - _async_validate_auto_generated_cost_entity( - hass, - source["entity_energy_from"], - source_result, + validate_calls.append( + functools.partial( + _async_validate_auto_generated_cost_entity, + hass, + source["entity_energy_from"], + source_result, + ) ) elif source["type"] == "solar": - await _async_validate_usage_stat( - hass, - source["stat_energy_from"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(source["stat_energy_from"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + source["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) ) elif source["type"] == "battery": - await _async_validate_usage_stat( - hass, - source["stat_energy_from"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(source["stat_energy_from"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + source["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) ) - await _async_validate_usage_stat( - hass, - source["stat_energy_to"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - source_result, + wanted_statistics_metadata.add(source["stat_energy_to"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + source["stat_energy_to"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) ) for device in manager.data["device_consumption"]: device_result: list[ValidationIssue] = [] result.device_consumption.append(device_result) - await _async_validate_usage_stat( - hass, - device["stat_consumption"], - ENERGY_USAGE_DEVICE_CLASSES, - ENERGY_USAGE_UNITS, - ENERGY_UNIT_ERROR, - device_result, + wanted_statistics_metadata.add(device["stat_consumption"]) + validate_calls.append( + functools.partial( + _async_validate_usage_stat, + hass, + statistics_metadata, + device["stat_consumption"], + ENERGY_USAGE_DEVICE_CLASSES, + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + device_result, + ) ) + # Fetch the needed statistics metadata + statistics_metadata.update( + await hass.async_add_executor_job( + functools.partial( + recorder.statistics.get_metadata, + hass, + statistic_ids=list(wanted_statistics_metadata), + ) + ) + ) + + # Execute all the validation checks + for call in validate_calls: + call() + return result diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e5fe84cc874..83de258ea5d 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -478,7 +478,7 @@ def get_metadata_with_session( hass: HomeAssistant, session: scoped_session, *, - statistic_ids: Iterable[str] | None = None, + statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: @@ -533,7 +533,7 @@ def get_metadata_with_session( def get_metadata( hass: HomeAssistant, *, - statistic_ids: Iterable[str] | None = None, + statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 5e3ad5c4aff..78a61b1bf69 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -26,11 +26,19 @@ def mock_get_metadata(): """Mock recorder.statistics.get_metadata.""" mocks = {} + def _get_metadata(_hass, *, statistic_ids): + result = {} + for statistic_id in statistic_ids: + if statistic_id in mocks: + if mocks[statistic_id] is not None: + result[statistic_id] = mocks[statistic_id] + else: + result[statistic_id] = (1, {}) + return result + with patch( "homeassistant.components.recorder.statistics.get_metadata", - side_effect=lambda hass, statistic_ids: mocks.get( - statistic_ids[0], {statistic_ids[0]: (1, {})} - ), + wraps=_get_metadata, ): yield mocks @@ -361,8 +369,8 @@ async def test_validation_grid( """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 - mock_get_metadata["sensor.grid_cost_1"] = {} - mock_get_metadata["sensor.grid_compensation_1"] = {} + mock_get_metadata["sensor.grid_cost_1"] = None + mock_get_metadata["sensor.grid_compensation_1"] = None await mock_energy_manager.async_update( { "energy_sources": [ @@ -456,8 +464,8 @@ async def test_validation_grid_external_cost_compensation( hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata ): """Test validating grid with non entity stats for energy and cost/compensation.""" - mock_get_metadata["external:grid_cost_1"] = {} - mock_get_metadata["external:grid_compensation_1"] = {} + mock_get_metadata["external:grid_cost_1"] = None + mock_get_metadata["external:grid_compensation_1"] = None await mock_energy_manager.async_update( { "energy_sources": [ From 2a99ef20469b0bb786488168939bab8d6eec3997 Mon Sep 17 00:00:00 2001 From: Guess <24840952+mynameisdaniel32@users.noreply.github.com> Date: Thu, 11 Nov 2021 18:08:01 +1030 Subject: [PATCH 0397/1452] Set PARALLEL_UPDATES for ping binary sensor (#59524) Changing PARALLEL_UPDATES from 0 (unlimited) to 50 as with many sensors (above 500) successful pings weren't being recorded. Resolves https://github.com/home-assistant/core/issues/54860 --- homeassistant/components/ping/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 0c82c9ff8c4..ea07c3123e9 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -38,7 +38,7 @@ DEFAULT_PING_COUNT = 5 SCAN_INTERVAL = timedelta(minutes=5) -PARALLEL_UPDATES = 0 +PARALLEL_UPDATES = 50 PING_MATCHER = re.compile( r"(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)" From 7098260dee6bc127f7856b8f0ee903dc555fe595 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 11 Nov 2021 02:49:07 -0500 Subject: [PATCH 0398/1452] Fix state of sense net_production sensor (#59391) --- homeassistant/components/sense/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index a8695e32b57..08677cda8d0 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,7 @@ """Support for monitoring a Sense energy sensor.""" from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + STATE_CLASS_TOTAL, SensorEntity, ) from homeassistant.const import ( @@ -251,7 +251,7 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity): """Implementation of a Sense energy sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_state_class = STATE_CLASS_TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON From 731bec31458eac89e092c5cc4546e2d2edb84331 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 10:25:23 +0100 Subject: [PATCH 0399/1452] Improve test coverage (#59531) --- tests/components/cast/conftest.py | 1 + tests/components/cast/test_media_player.py | 73 +++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index a8118a94967..5078e4d34ad 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -40,6 +40,7 @@ def mz_mock(): def pycast_mock(castbrowser_mock, castbrowser_constructor_mock): """Mock pychromecast.""" pycast_mock = MagicMock() + pycast_mock.IDLE_APP_ID = pychromecast.IDLE_APP_ID pycast_mock.IGNORE_CEC = [] pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 48ed54cb76e..bbb648a835f 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -27,7 +27,11 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + CAST_APP_ID_HOMEASSISTANT_LOVELACE, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -1025,6 +1029,73 @@ async def test_entity_media_states(hass: HomeAssistant): assert state.state == "unknown" +async def test_entity_media_states_lovelace_app(hass: HomeAssistant): + """Test various entity media states when the lovelace app is active.""" + entity_id = "media_player.speaker" + reg = er.async_get(hass) + + info = get_fake_chromecast_info() + full_info = attr.evolve( + info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID + ) + + chromecast, _ = await async_setup_media_player_cast(hass, info) + cast_status_cb, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "off" + assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + + chromecast.app_id = CAST_APP_ID_HOMEASSISTANT_LOVELACE + cast_status = MagicMock() + cast_status_cb(cast_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "casting" + + media_status = MagicMock(images=None) + media_status.player_is_playing = True + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "casting" + + media_status.player_is_playing = False + media_status.player_is_paused = True + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "casting" + + media_status.player_is_paused = False + media_status.player_is_idle = True + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "casting" + + chromecast.app_id = pychromecast.IDLE_APP_ID + media_status.player_is_idle = False + chromecast.is_idle = True + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "off" + + chromecast.is_idle = False + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unknown" + + async def test_group_media_states(hass, mz_mock): """Test media states are read from group if entity has no state.""" entity_id = "media_player.speaker" From 85786fd987affcb462319a66365b0d36ef5460b6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 12:32:51 +0100 Subject: [PATCH 0400/1452] Make some device action tests more explicit (#59469) * Make some device action tests more explicit * Adjust tests --- .../alarm_control_panel/test_device_action.py | 2 ++ tests/components/cover/test_device_action.py | 31 ++++++++++++++++--- tests/components/light/test_device_action.py | 30 +++++++++++++----- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 1fdb908d2e6..75f1dc76aaf 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -160,6 +160,7 @@ async def test_get_action_capabilities( } actions = await async_get_device_automations(hass, "action", device_entry.id) assert len(actions) == 6 + assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -209,6 +210,7 @@ async def test_get_action_capabilities_arm_code( } actions = await async_get_device_automations(hass, "action", device_entry.id) assert len(actions) == 6 + assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index ab7b6b91699..491b59d9acb 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -11,6 +11,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + SUPPORT_STOP_TILT, ) from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import device_registry @@ -109,8 +110,22 @@ async def test_get_action_capabilities( ): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[2] + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockCover( + name="Set position cover", + is_on=True, + unique_id="unique_set_pos_cover", + current_cover_position=50, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT, + ), + ) + ent = platform.ENTITIES[0] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -126,7 +141,9 @@ async def test_get_action_capabilities( await hass.async_block_till_done() actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 4 # open, close, stop, set_position + assert len(actions) == 5 # open, close, open_tilt, close_tilt + action_types = {action["type"] for action in actions} + assert action_types == {"open", "close", "stop", "open_tilt", "close_tilt"} for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -169,6 +186,8 @@ async def test_get_action_capabilities_set_pos( } actions = await async_get_device_automations(hass, "action", device_entry.id) assert len(actions) == 1 # set_position + action_types = {action["type"] for action in actions} + assert action_types == {"set_position"} for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -185,7 +204,7 @@ async def test_get_action_capabilities_set_tilt_pos( """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -213,7 +232,9 @@ async def test_get_action_capabilities_set_tilt_pos( ] } actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 4 # open, close, stop, set_tilt_position + assert len(actions) == 3 + action_types = {action["type"] for action in actions} + assert action_types == {"open", "close", "set_tilt_position"} for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 5628861b72d..58743b1ae05 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -118,6 +118,8 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): ).entity_id actions = await async_get_device_automations(hass, "action", device_entry.id) assert len(actions) == 3 + action_types = {action["type"] for action in actions} + assert action_types == {"turn_on", "toggle", "turn_off"} for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -134,11 +136,17 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): @pytest.mark.parametrize( - "set_state,num_actions,supported_features_reg,supported_features_state,capabilities_reg,attributes_state,expected_capabilities", + "set_state,expected_actions,supported_features_reg,supported_features_state,capabilities_reg,attributes_state,expected_capabilities", [ ( False, - 5, + { + "turn_on", + "toggle", + "turn_off", + "brightness_increase", + "brightness_decrease", + }, 0, 0, {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS]}, @@ -157,7 +165,13 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): ), ( True, - 5, + { + "turn_on", + "toggle", + "turn_off", + "brightness_increase", + "brightness_decrease", + }, 0, 0, None, @@ -176,7 +190,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): ), ( False, - 4, + {"turn_on", "toggle", "turn_off", "flash"}, SUPPORT_FLASH, 0, None, @@ -194,7 +208,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): ), ( True, - 4, + {"turn_on", "toggle", "turn_off", "flash"}, 0, SUPPORT_FLASH, None, @@ -217,7 +231,7 @@ async def test_get_action_capabilities_features( device_reg, entity_reg, set_state, - num_actions, + expected_actions, supported_features_reg, supported_features_state, capabilities_reg, @@ -247,7 +261,9 @@ async def test_get_action_capabilities_features( ) actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == num_actions + assert len(actions) == len(expected_actions) + action_types = {action["type"] for action in actions} + assert action_types == expected_actions for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action From a29264518ca36cdfa17f49d340bf73648399c6c4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 12:33:15 +0100 Subject: [PATCH 0401/1452] Don't allow turning on audio only chromecasts (#59495) * Don't allow turning on audio only chromecasts * Improve tests * Adjust tests --- homeassistant/components/cast/media_player.py | 13 ++-- tests/components/cast/conftest.py | 1 + tests/components/cast/test_media_player.py | 61 +++++++++++++++++++ 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index dc3d20188e7..310431bb488 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -80,12 +80,7 @@ _LOGGER = logging.getLogger(__name__) CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = ( - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON + SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_TURN_OFF ) STATE_CASTING = "casting" @@ -694,6 +689,12 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] + if ( + self._chromecast + and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST + ): + support |= SUPPORT_TURN_ON + if ( self.cast_status and self.cast_status.volume_control_type != VOLUME_CONTROL_TYPE_FIXED diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 5078e4d34ad..dce5db51161 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -42,6 +42,7 @@ def pycast_mock(castbrowser_mock, castbrowser_constructor_mock): pycast_mock = MagicMock() pycast_mock.IDLE_APP_ID = pychromecast.IDLE_APP_ID pycast_mock.IGNORE_CEC = [] + pycast_mock.const = pychromecast.const pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock pycast_mock.discovery.AbstractCastListener = ( diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index bbb648a835f..3bb9cbf2316 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -591,6 +591,7 @@ async def test_entity_cast_status(hass: HomeAssistant): ) chromecast, _ = await async_setup_media_player_cast(hass, info) + chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -660,6 +661,65 @@ async def test_entity_cast_status(hass: HomeAssistant): ) +@pytest.mark.parametrize( + "cast_type,supported_features", + [ + ( + pychromecast.const.CAST_TYPE_AUDIO, + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, + ), + ( + pychromecast.const.CAST_TYPE_CHROMECAST, + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, + ), + ( + pychromecast.const.CAST_TYPE_GROUP, + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, + ), + ], +) +async def test_supported_features(hass: HomeAssistant, cast_type, supported_features): + """Test supported features.""" + entity_id = "media_player.speaker" + + info = get_fake_chromecast_info() + + chromecast, _ = await async_setup_media_player_cast(hass, info) + chromecast.cast_type = cast_type + _, conn_status_cb, _ = get_status_callbacks(chromecast) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "off" + + assert state.attributes.get("supported_features") == supported_features + + async def test_entity_play_media(hass: HomeAssistant): """Test playing media.""" entity_id = "media_player.speaker" @@ -872,6 +932,7 @@ async def test_entity_control(hass: HomeAssistant): ) chromecast, _ = await async_setup_media_player_cast(hass, info) + chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() From a079b4fd581fe5af93b708a2f1fe14d321c072d5 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Thu, 11 Nov 2021 04:20:16 -0800 Subject: [PATCH 0402/1452] Add tests to bring greeneye_monitor to 99% coverage (#58661) * Bring greeneye_monitor to 99% coverage. * Pass monitor into listeners on Monitors * Updates for changes in `dev`, create mock monitor * Remove logging left in after debugging * Remove xfails now that #58764 has merged --- .coveragerc | 2 - .../components/greeneye_monitor/__init__.py | 4 +- .../components/greeneye_monitor/sensor.py | 3 +- requirements_test_all.txt | 3 + tests/components/greeneye_monitor/__init__.py | 1 + tests/components/greeneye_monitor/common.py | 205 ++++++++++++++++++ tests/components/greeneye_monitor/conftest.py | 118 ++++++++++ .../components/greeneye_monitor/test_init.py | 199 +++++++++++++++++ .../greeneye_monitor/test_sensor.py | 165 ++++++++++++++ 9 files changed, 694 insertions(+), 6 deletions(-) create mode 100644 tests/components/greeneye_monitor/__init__.py create mode 100644 tests/components/greeneye_monitor/common.py create mode 100644 tests/components/greeneye_monitor/conftest.py create mode 100644 tests/components/greeneye_monitor/test_init.py create mode 100644 tests/components/greeneye_monitor/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 28a6eff3073..53012078f33 100644 --- a/.coveragerc +++ b/.coveragerc @@ -399,8 +399,6 @@ omit = homeassistant/components/google_travel_time/sensor.py homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py - homeassistant/components/greeneye_monitor/* - homeassistant/components/greeneye_monitor/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py homeassistant/components/growatt_server/sensor.py diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 3417d0c08dc..d2b0e7c307b 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from greeneye import Monitors +import greeneye import voluptuous as vol from homeassistant.const import ( @@ -123,7 +123,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the GreenEye Monitor component.""" - monitors = Monitors() + monitors = greeneye.Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors server_config = config[DOMAIN] diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 2a4692674c9..de71e3c27fa 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any, Generic, Optional, TypeVar, cast import greeneye -from greeneye import Monitors from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -145,7 +144,7 @@ class GEMSensor(Generic[T], SensorEntity): monitors = self.hass.data[DATA_GREENEYE_MONITOR] monitors.remove_listener(self._on_new_monitor) - def _try_connect_to_monitor(self, monitors: Monitors) -> bool: + def _try_connect_to_monitor(self, monitors: greeneye.Monitors) -> bool: monitor = monitors.monitors.get(self._monitor_serial_number) if not monitor: return False diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e25d14b60f..77ab56dc484 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -463,6 +463,9 @@ googlemaps==2.5.1 # homeassistant.components.gree greeclimate==0.12.3 +# homeassistant.components.greeneye_monitor +greeneye_monitor==2.1 + # homeassistant.components.growatt_server growattServer==1.1.0 diff --git a/tests/components/greeneye_monitor/__init__.py b/tests/components/greeneye_monitor/__init__.py new file mode 100644 index 00000000000..db9bcaee1f4 --- /dev/null +++ b/tests/components/greeneye_monitor/__init__.py @@ -0,0 +1 @@ +"""Tests for the GreenEye Monitor integration.""" diff --git a/tests/components/greeneye_monitor/common.py b/tests/components/greeneye_monitor/common.py new file mode 100644 index 00000000000..ac00ccbfc0b --- /dev/null +++ b/tests/components/greeneye_monitor/common.py @@ -0,0 +1,205 @@ +"""Common helpers for greeneye_monitor tests.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.greeneye_monitor import ( + CONF_CHANNELS, + CONF_COUNTED_QUANTITY, + CONF_COUNTED_QUANTITY_PER_PULSE, + CONF_MONITORS, + CONF_NET_METERING, + CONF_NUMBER, + CONF_PULSE_COUNTERS, + CONF_SERIAL_NUMBER, + CONF_TEMPERATURE_SENSORS, + CONF_TIME_UNIT, + CONF_VOLTAGE_SENSORS, + DOMAIN, +) +from homeassistant.const import ( + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_TEMPERATURE_UNIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +SINGLE_MONITOR_SERIAL_NUMBER = 110011 + + +def make_single_monitor_config_with_sensors(sensors: dict[str, Any]) -> dict[str, Any]: + """Wrap the given sensor config in the boilerplate for a single monitor with serial number SINGLE_MONITOR_SERIAL_NUMBER.""" + return { + DOMAIN: { + CONF_PORT: 7513, + CONF_MONITORS: [ + { + CONF_SERIAL_NUMBER: f"00{SINGLE_MONITOR_SERIAL_NUMBER}", + **sensors, + } + ], + } + } + + +SINGLE_MONITOR_CONFIG_NO_SENSORS = make_single_monitor_config_with_sensors({}) +SINGLE_MONITOR_CONFIG_PULSE_COUNTERS = make_single_monitor_config_with_sensors( + { + CONF_PULSE_COUNTERS: [ + { + CONF_NUMBER: 1, + CONF_NAME: "pulse_a", + CONF_COUNTED_QUANTITY: "pulses", + CONF_COUNTED_QUANTITY_PER_PULSE: 1.0, + CONF_TIME_UNIT: "s", + }, + { + CONF_NUMBER: 2, + CONF_NAME: "pulse_2", + CONF_COUNTED_QUANTITY: "gal", + CONF_COUNTED_QUANTITY_PER_PULSE: 0.5, + CONF_TIME_UNIT: "min", + }, + { + CONF_NUMBER: 3, + CONF_NAME: "pulse_3", + CONF_COUNTED_QUANTITY: "gal", + CONF_COUNTED_QUANTITY_PER_PULSE: 0.5, + CONF_TIME_UNIT: "h", + }, + { + CONF_NUMBER: 4, + CONF_NAME: "pulse_d", + CONF_COUNTED_QUANTITY: "pulses", + CONF_COUNTED_QUANTITY_PER_PULSE: 1.0, + CONF_TIME_UNIT: "s", + }, + ] + } +) + +SINGLE_MONITOR_CONFIG_POWER_SENSORS = make_single_monitor_config_with_sensors( + { + CONF_CHANNELS: [ + { + CONF_NUMBER: 1, + CONF_NAME: "channel 1", + }, + { + CONF_NUMBER: 2, + CONF_NAME: "channel two", + CONF_NET_METERING: True, + }, + ] + } +) + + +SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS = make_single_monitor_config_with_sensors( + { + CONF_TEMPERATURE_SENSORS: { + CONF_TEMPERATURE_UNIT: "F", + CONF_SENSORS: [ + {CONF_NUMBER: 1, CONF_NAME: "temp_a"}, + {CONF_NUMBER: 2, CONF_NAME: "temp_2"}, + {CONF_NUMBER: 3, CONF_NAME: "temp_c"}, + {CONF_NUMBER: 4, CONF_NAME: "temp_d"}, + {CONF_NUMBER: 5, CONF_NAME: "temp_5"}, + {CONF_NUMBER: 6, CONF_NAME: "temp_f"}, + {CONF_NUMBER: 7, CONF_NAME: "temp_g"}, + {CONF_NUMBER: 8, CONF_NAME: "temp_h"}, + ], + } + } +) + +SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS = make_single_monitor_config_with_sensors( + { + CONF_VOLTAGE_SENSORS: [ + { + CONF_NUMBER: 1, + CONF_NAME: "voltage 1", + }, + ] + } +) + + +async def setup_greeneye_monitor_component_with_config( + hass: HomeAssistant, config: ConfigType +) -> bool: + """Set up the greeneye_monitor component with the given config. Return True if successful, False otherwise.""" + result = await async_setup_component( + hass, + DOMAIN, + config, + ) + await hass.async_block_till_done() + + return result + + +def mock_with_listeners() -> MagicMock: + """Create a MagicMock with methods that follow the same pattern for working with listeners in the greeneye_monitor API.""" + mock = MagicMock() + add_listeners(mock) + return mock + + +def async_mock_with_listeners() -> AsyncMock: + """Create an AsyncMock with methods that follow the same pattern for working with listeners in the greeneye_monitor API.""" + mock = AsyncMock() + add_listeners(mock) + return mock + + +def add_listeners(mock: MagicMock | AsyncMock) -> None: + """Add add_listener and remove_listener methods to the given mock that behave like their counterparts on objects from the greeneye_monitor API, plus a notify_all_listeners method that calls all registered listeners.""" + mock.listeners = [] + mock.add_listener = mock.listeners.append + mock.remove_listener = mock.listeners.remove + + def notify_all_listeners(*args): + for listener in list(mock.listeners): + listener(*args) + + mock.notify_all_listeners = notify_all_listeners + + +def mock_pulse_counter() -> MagicMock: + """Create a mock GreenEye Monitor pulse counter.""" + pulse_counter = mock_with_listeners() + pulse_counter.pulses = 1000 + pulse_counter.pulses_per_second = 10 + return pulse_counter + + +def mock_temperature_sensor() -> MagicMock: + """Create a mock GreenEye Monitor temperature sensor.""" + temperature_sensor = mock_with_listeners() + temperature_sensor.temperature = 32.0 + return temperature_sensor + + +def mock_channel() -> MagicMock: + """Create a mock GreenEye Monitor CT channel.""" + channel = mock_with_listeners() + channel.absolute_watt_seconds = 1000 + channel.polarized_watt_seconds = -400 + channel.watts = None + return channel + + +def mock_monitor(serial_number: int) -> MagicMock: + """Create a mock GreenEye Monitor.""" + monitor = mock_with_listeners() + monitor.serial_number = serial_number + monitor.voltage = 120.0 + monitor.pulse_counters = [mock_pulse_counter() for i in range(0, 4)] + monitor.temperature_sensors = [mock_temperature_sensor() for i in range(0, 8)] + monitor.channels = [mock_channel() for i in range(0, 32)] + return monitor diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py new file mode 100644 index 00000000000..b6fa49032cc --- /dev/null +++ b/tests/components/greeneye_monitor/conftest.py @@ -0,0 +1,118 @@ +"""Common fixtures for testing greeneye_monitor.""" +from typing import Any, Dict +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.greeneye_monitor import DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + POWER_WATT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_get as get_entity_registry, +) + +from .common import add_listeners + + +def assert_sensor_state( + hass: HomeAssistant, + entity_id: str, + expected_state: str, + attributes: Dict[str, Any] = {}, +) -> None: + """Assert that the given entity has the expected state and at least the provided attributes.""" + state = hass.states.get(entity_id) + assert state + actual_state = state.state + assert actual_state == expected_state + for (key, value) in attributes.items(): + assert key in state.attributes + assert state.attributes[key] == value + + +def assert_temperature_sensor_registered( + hass: HomeAssistant, + serial_number: int, + number: int, + name: str, +): + """Assert that a temperature sensor entity was registered properly.""" + sensor = assert_sensor_registered(hass, serial_number, "temp", number, name) + assert sensor.device_class == DEVICE_CLASS_TEMPERATURE + + +def assert_pulse_counter_registered( + hass: HomeAssistant, + serial_number: int, + number: int, + name: str, + quantity: str, + per_time: str, +): + """Assert that a pulse counter entity was registered properly.""" + sensor = assert_sensor_registered(hass, serial_number, "pulse", number, name) + assert sensor.unit_of_measurement == f"{quantity}/{per_time}" + + +def assert_power_sensor_registered( + hass: HomeAssistant, serial_number: int, number: int, name: str +) -> None: + """Assert that a power sensor entity was registered properly.""" + sensor = assert_sensor_registered(hass, serial_number, "current", number, name) + assert sensor.unit_of_measurement == POWER_WATT + assert sensor.device_class == DEVICE_CLASS_POWER + + +def assert_voltage_sensor_registered( + hass: HomeAssistant, serial_number: int, number: int, name: str +) -> None: + """Assert that a voltage sensor entity was registered properly.""" + sensor = assert_sensor_registered(hass, serial_number, "volts", number, name) + assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT + assert sensor.device_class == DEVICE_CLASS_VOLTAGE + + +def assert_sensor_registered( + hass: HomeAssistant, + serial_number: int, + sensor_type: str, + number: int, + name: str, +) -> RegistryEntry: + """Assert that a sensor entity of a given type was registered properly.""" + registry = get_entity_registry(hass) + unique_id = f"{serial_number}-{sensor_type}-{number}" + + entity_id = registry.async_get_entity_id("sensor", DOMAIN, unique_id) + assert entity_id is not None + + sensor = registry.async_get(entity_id) + assert sensor + assert sensor.unique_id == unique_id + assert sensor.original_name == name + + return sensor + + +@pytest.fixture +def monitors() -> AsyncMock: + """Provide a mock greeneye.Monitors object that has listeners and can add new monitors.""" + with patch("greeneye.Monitors", new=AsyncMock) as mock_monitors: + add_listeners(mock_monitors) + mock_monitors.monitors = {} + + def add_monitor(monitor: MagicMock) -> None: + """Add the given mock monitor as a monitor with the given serial number, notifying any listeners on the Monitors object.""" + serial_number = monitor.serial_number + mock_monitors.monitors[serial_number] = monitor + mock_monitors.notify_all_listeners(monitor) + + mock_monitors.add_monitor = add_monitor + yield mock_monitors diff --git a/tests/components/greeneye_monitor/test_init.py b/tests/components/greeneye_monitor/test_init.py new file mode 100644 index 00000000000..143fb14f28c --- /dev/null +++ b/tests/components/greeneye_monitor/test_init.py @@ -0,0 +1,199 @@ +"""Tests for greeneye_monitor component initialization.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.greeneye_monitor import ( + CONF_MONITORS, + CONF_NUMBER, + CONF_SERIAL_NUMBER, + CONF_TEMPERATURE_SENSORS, + DOMAIN, +) +from homeassistant.const import ( + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_TEMPERATURE_UNIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .common import ( + SINGLE_MONITOR_CONFIG_NO_SENSORS, + SINGLE_MONITOR_CONFIG_POWER_SENSORS, + SINGLE_MONITOR_CONFIG_PULSE_COUNTERS, + SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS, + SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS, + SINGLE_MONITOR_SERIAL_NUMBER, + setup_greeneye_monitor_component_with_config, +) +from .conftest import ( + assert_power_sensor_registered, + assert_pulse_counter_registered, + assert_temperature_sensor_registered, + assert_voltage_sensor_registered, +) + + +async def test_setup_fails_if_no_sensors_defined( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup fails if there are no sensors defined in the YAML.""" + success = await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_NO_SENSORS + ) + assert not success + + +@pytest.mark.xfail(reason="Currently failing. Will fix in subsequent PR.") +async def test_setup_succeeds_no_config( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup succeeds if there is no config present in the YAML.""" + assert await async_setup_component(hass, DOMAIN, {}) + + +async def test_setup_creates_temperature_entities( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup registers temperature sensors properly.""" + assert await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS + ) + + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "temp_a" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "temp_2" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 3, "temp_c" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 4, "temp_d" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 5, "temp_5" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 6, "temp_f" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 7, "temp_g" + ) + assert_temperature_sensor_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 8, "temp_h" + ) + + +async def test_setup_creates_pulse_counter_entities( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup registers pulse counters properly.""" + assert await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS + ) + + assert_pulse_counter_registered( + hass, + SINGLE_MONITOR_SERIAL_NUMBER, + 1, + "pulse_a", + "pulses", + "s", + ) + assert_pulse_counter_registered( + hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "pulse_2", "gal", "min" + ) + assert_pulse_counter_registered( + hass, + SINGLE_MONITOR_SERIAL_NUMBER, + 3, + "pulse_3", + "gal", + "h", + ) + assert_pulse_counter_registered( + hass, + SINGLE_MONITOR_SERIAL_NUMBER, + 4, + "pulse_d", + "pulses", + "s", + ) + + +async def test_setup_creates_power_sensor_entities( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup registers power sensors correctly.""" + assert await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS + ) + + assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "channel 1") + assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "channel two") + + +async def test_setup_creates_voltage_sensor_entities( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that component setup registers voltage sensors properly.""" + assert await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + + assert_voltage_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "voltage 1") + + +async def test_multi_monitor_config(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that component setup registers entities from multiple monitors correctly.""" + assert await setup_greeneye_monitor_component_with_config( + hass, + { + DOMAIN: { + CONF_PORT: 7513, + CONF_MONITORS: [ + { + CONF_SERIAL_NUMBER: "00000001", + CONF_TEMPERATURE_SENSORS: { + CONF_TEMPERATURE_UNIT: "C", + CONF_SENSORS: [ + {CONF_NUMBER: 1, CONF_NAME: "unit_1_temp_1"} + ], + }, + }, + { + CONF_SERIAL_NUMBER: "00000002", + CONF_TEMPERATURE_SENSORS: { + CONF_TEMPERATURE_UNIT: "F", + CONF_SENSORS: [ + {CONF_NUMBER: 1, CONF_NAME: "unit_2_temp_1"} + ], + }, + }, + ], + } + }, + ) + + assert_temperature_sensor_registered(hass, 1, 1, "unit_1_temp_1") + assert_temperature_sensor_registered(hass, 2, 1, "unit_2_temp_1") + + +async def test_setup_and_shutdown(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that the component can set up and shut down cleanly, closing the underlying server on shutdown.""" + server = AsyncMock() + monitors.start_server = AsyncMock(return_value=server) + assert await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS + ) + + await hass.async_stop() + + assert server.close.called diff --git a/tests/components/greeneye_monitor/test_sensor.py b/tests/components/greeneye_monitor/test_sensor.py new file mode 100644 index 00000000000..63ab8b64423 --- /dev/null +++ b/tests/components/greeneye_monitor/test_sensor.py @@ -0,0 +1,165 @@ +"""Tests for greeneye_monitor sensors.""" +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.greeneye_monitor.sensor import ( + DATA_PULSES, + DATA_WATT_SECONDS, +) +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get as get_entity_registry + +from .common import ( + SINGLE_MONITOR_CONFIG_POWER_SENSORS, + SINGLE_MONITOR_CONFIG_PULSE_COUNTERS, + SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS, + SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS, + SINGLE_MONITOR_SERIAL_NUMBER, + mock_monitor, + setup_greeneye_monitor_component_with_config, +) +from .conftest import assert_sensor_state + + +async def test_disable_sensor_before_monitor_connected( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that a sensor disabled before its monitor connected stops listening for new monitors.""" + # The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + + assert len(monitors.listeners) == 1 + await disable_entity(hass, "sensor.voltage_1") + assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener + + +async def test_updates_state_when_monitor_connected( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that a sensor updates its state when its monitor first connects.""" + # The sensor base class handles updating the state on connection, so we test this with a single voltage sensor for ease + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + + assert_sensor_state(hass, "sensor.voltage_1", STATE_UNKNOWN) + assert len(monitors.listeners) == 1 + connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener + assert_sensor_state(hass, "sensor.voltage_1", "120.0") + + +async def test_disable_sensor_after_monitor_connected( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that a sensor disabled after its monitor connected stops listening for sensor changes.""" + # The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + + assert len(monitor.listeners) == 1 + await disable_entity(hass, "sensor.voltage_1") + assert len(monitor.listeners) == 0 + + +async def test_updates_state_when_sensor_pushes( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that a sensor entity updates its state when the underlying sensor pushes an update.""" + # The sensor base class handles triggering state updates, so we test this with a single voltage sensor for ease + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + assert_sensor_state(hass, "sensor.voltage_1", "120.0") + + monitor.voltage = 119.8 + monitor.notify_all_listeners() + assert_sensor_state(hass, "sensor.voltage_1", "119.8") + + +async def test_power_sensor_initially_unknown( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that the power sensor can handle its initial state being unknown (since the GEM API needs at least two packets to arrive before it can compute watts).""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS + ) + connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + assert_sensor_state( + hass, "sensor.channel_1", STATE_UNKNOWN, {DATA_WATT_SECONDS: 1000} + ) + # This sensor was configured with net metering on, so we should be taking the + # polarized value + assert_sensor_state( + hass, "sensor.channel_two", STATE_UNKNOWN, {DATA_WATT_SECONDS: -400} + ) + + +async def test_power_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that a power sensor reports its values correctly, including handling net metering.""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS + ) + monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + monitor.channels[0].watts = 120.0 + monitor.channels[1].watts = 120.0 + monitor.channels[0].notify_all_listeners() + monitor.channels[1].notify_all_listeners() + assert_sensor_state(hass, "sensor.channel_1", "120.0", {DATA_WATT_SECONDS: 1000}) + # This sensor was configured with net metering on, so we should be taking the + # polarized value + assert_sensor_state(hass, "sensor.channel_two", "120.0", {DATA_WATT_SECONDS: -400}) + + +async def test_pulse_counter(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that a pulse counter sensor reports its values properly, including calculating different units.""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS + ) + connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + assert_sensor_state(hass, "sensor.pulse_a", "10.0", {DATA_PULSES: 1000}) + # This counter was configured with each pulse meaning 0.5 gallons and + # wanting to show gallons per minute, so 10 pulses per second -> 300 gal/min + assert_sensor_state(hass, "sensor.pulse_2", "300.0", {DATA_PULSES: 1000}) + # This counter was configured with each pulse meaning 0.5 gallons and + # wanting to show gallons per hour, so 10 pulses per second -> 18000 gal/hr + assert_sensor_state(hass, "sensor.pulse_3", "18000.0", {DATA_PULSES: 1000}) + + +async def test_temperature_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that a temperature sensor reports its values properly, including proper handling of when its native unit is different from that configured in hass.""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS + ) + connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + # The config says that the sensor is reporting in Fahrenheit; if we set that up + # properly, HA will have converted that to Celsius by default. + assert_sensor_state(hass, "sensor.temp_a", "0.0") + + +async def test_voltage_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: + """Test that a voltage sensor reports its values properly.""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + assert_sensor_state(hass, "sensor.voltage_1", "120.0") + + +def connect_monitor(monitors: AsyncMock, serial_number: int) -> MagicMock: + """Simulate a monitor connecting to Home Assistant. Returns the mock monitor API object.""" + monitor = mock_monitor(serial_number) + monitors.add_monitor(monitor) + return monitor + + +async def disable_entity(hass: HomeAssistant, entity_id: str) -> None: + """Disable the given entity.""" + entity_registry = get_entity_registry(hass) + entity_registry.async_update_entity(entity_id, disabled_by="user") + await hass.async_block_till_done() From 6a21b241c0c56d29b20c5cb23c4690a436d80b93 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 11 Nov 2021 13:46:35 +0100 Subject: [PATCH 0403/1452] Velbus typing part 2 (#59148) --- .strict-typing | 1 + homeassistant/components/velbus/__init__.py | 32 +++++++------ .../components/velbus/binary_sensor.py | 4 ++ homeassistant/components/velbus/climate.py | 9 +++- .../components/velbus/config_flow.py | 17 ++++--- homeassistant/components/velbus/cover.py | 9 ++-- homeassistant/components/velbus/light.py | 47 ++++++++++++------- homeassistant/components/velbus/sensor.py | 43 +++++++++++------ homeassistant/components/velbus/switch.py | 4 ++ mypy.ini | 11 +++++ 10 files changed, 120 insertions(+), 57 deletions(-) diff --git a/.strict-typing b/.strict-typing index 4c9e626afa6..66a965b3221 100644 --- a/.strict-typing +++ b/.strict-typing @@ -137,6 +137,7 @@ homeassistant.components.uptime.* homeassistant.components.uptimerobot.* homeassistant.components.vacuum.* homeassistant.components.vallox.* +homeassistant.components.velbus.* homeassistant.components.vlc_telnet.* homeassistant.components.water_heater.* homeassistant.components.watttime.* diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index d907557bb01..be5ce04051c 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -3,16 +3,18 @@ from __future__ import annotations import logging +from velbusaio.channels import Channel as VelbusChannel from velbusaio.controller import Velbus import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_INTERFACE, @@ -32,7 +34,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Velbus platform.""" # Import from the configuration file if needed if DOMAIN not in config: @@ -97,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.services.has_service(DOMAIN, SERVICE_SCAN): return True - def check_entry_id(interface: str): + def check_entry_id(interface: str) -> str: for entry in hass.config_entries.async_entries(DOMAIN): if "port" in entry.data and entry.data["port"] == interface: return entry.entry_id @@ -105,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "The interface provided is not defined as a port in a Velbus integration" ) - async def scan(call): + async def scan(call: ServiceCall) -> None: await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].scan() hass.services.async_register( @@ -115,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}), ) - async def syn_clock(call): + async def syn_clock(call: ServiceCall) -> None: await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].sync_clock() hass.services.async_register( @@ -125,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}), ) - async def set_memo_text(call): + async def set_memo_text(call: ServiceCall) -> None: """Handle Memo Text service call.""" memo_text = call.data[CONF_MEMO_TEXT] memo_text.hass = hass @@ -167,32 +169,32 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class VelbusEntity(Entity): """Representation of a Velbus entity.""" - def __init__(self, channel): + def __init__(self, channel: VelbusChannel) -> None: """Initialize a Velbus entity.""" self._channel = channel @property - def unique_id(self): + def unique_id(self) -> str: """Get unique ID.""" - if (serial := self._channel.get_module_serial()) == 0: - serial = self._channel.get_module_address() + if (serial := self._channel.get_module_serial()) == "": + serial = str(self._channel.get_module_address()) return f"{serial}-{self._channel.get_channel_number()}" @property - def name(self): + def name(self) -> str: """Return the display name of this entity.""" return self._channel.get_name() @property - def should_poll(self): + def should_poll(self) -> bool: """Disable polling.""" return False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add listener for state changes.""" self._channel.on_status_update(self._on_update) - async def _on_update(self): + async def _on_update(self) -> None: self.async_write_ha_state() @property @@ -200,7 +202,7 @@ class VelbusEntity(Entity): """Return the device info.""" return DeviceInfo( identifiers={ - (DOMAIN, self._channel.get_module_address()), + (DOMAIN, str(self._channel.get_module_address())), }, manufacturer="Velleman", model=self._channel.get_module_type_name(), diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 014b1410fdc..8c67520dd9a 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Velbus Binary Sensors.""" +from velbusaio.channels import Button as VelbusButton + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -25,6 +27,8 @@ async def async_setup_entry( class VelbusBinarySensor(VelbusEntity, BinarySensorEntity): """Representation of a Velbus Binary Sensor.""" + _channel: VelbusButton + @property def is_on(self) -> bool: """Return true if the sensor is on.""" diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index a065679c4e5..c11698b1358 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,6 +1,10 @@ """Support for Velbus thermostat.""" from __future__ import annotations +from typing import Any + +from velbusaio.channels import Temperature as VelbusTemp + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, @@ -33,6 +37,7 @@ async def async_setup_entry( class VelbusClimate(VelbusEntity, ClimateEntity): """Representation of a Velbus thermostat.""" + _channel: VelbusTemp _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE _attr_temperature_unit = TEMP_CELSIUS _attr_hvac_mode = HVAC_MODE_HEAT @@ -40,7 +45,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): _attr_preset_modes = list(PRESET_MODES) @property - def target_temperature(self) -> int | None: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._channel.get_climate_target() @@ -56,7 +61,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): None, ) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 3ec5af14397..3facd8c6a33 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -1,6 +1,8 @@ """Config flow for the Velbus platform.""" from __future__ import annotations +from typing import Any + import velbusaio from velbusaio.exceptions import VelbusConnectionFailed import voluptuous as vol @@ -8,16 +10,17 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import DOMAIN @callback -def velbus_entries(hass: HomeAssistant): +def velbus_entries(hass: HomeAssistant) -> set[str]: """Return connections for Velbus domain.""" return { - (entry.data[CONF_PORT]) for entry in hass.config_entries.async_entries(DOMAIN) + entry.data[CONF_PORT] for entry in hass.config_entries.async_entries(DOMAIN) } @@ -30,11 +33,11 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the velbus config flow.""" self._errors: dict[str, str] = {} - def _create_device(self, name: str, prt: str): + def _create_device(self, name: str, prt: str) -> FlowResult: """Create an entry async.""" return self.async_create_entry(title=name, data={CONF_PORT: prt}) - async def _test_connection(self, prt): + async def _test_connection(self, prt: str) -> bool: """Try to connect to the velbus with the port specified.""" try: controller = velbusaio.controller.Velbus(prt) @@ -51,7 +54,9 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True return False - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -78,7 +83,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Import a config entry.""" user_input[CONF_NAME] = "Velbus Import" prt = user_input[CONF_PORT] diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 43cec0adb24..2e2ceb761a9 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from velbusaio.channels import Channel as VelbusChannel +from velbusaio.channels import Blind as VelbusBlind from homeassistant.components.cover import ( ATTR_POSITION, @@ -38,7 +38,9 @@ async def async_setup_entry( class VelbusCover(VelbusEntity, CoverEntity): """Representation a Velbus cover.""" - def __init__(self, channel: VelbusChannel) -> None: + _channel: VelbusBlind + + def __init__(self, channel: VelbusBlind) -> None: """Initialize the dimmer.""" super().__init__(channel) if self._channel.support_position(): @@ -60,8 +62,7 @@ class VelbusCover(VelbusEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open Velbus: 100 = closed, 0 = open """ - pos = self._channel.get_position() - return 100 - pos + return 100 - self._channel.get_position() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index 2c6cd8d8776..ad0b150bf22 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -1,4 +1,14 @@ """Support for Velbus light.""" +from __future__ import annotations + +from typing import Any + +from velbusaio.channels import ( + Button as VelbusButton, + Channel as VelbusChannel, + Dimmer as VelbusDimmer, +) + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_FLASH, @@ -10,16 +20,24 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] + entities: list[Entity] = [] for channel in cntrl.get_all("light"): entities.append(VelbusLight(channel)) for channel in cntrl.get_all("led"): @@ -30,24 +48,25 @@ async def async_setup_entry(hass, entry, async_add_entities): class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" + _channel: VelbusDimmer _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - def __init__(self, channel): + def __init__(self, channel: VelbusDimmer) -> None: """Initialize the dimmer.""" super().__init__(channel) self._attr_name = self._channel.get_name() @property - def is_on(self): + def is_on(self) -> bool: """Return true if the light is on.""" return self._channel.is_on() @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of the light.""" return int((self._channel.get_dimmer_state() * 255) / 100) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the Velbus light to turn on.""" if ATTR_BRIGHTNESS in kwargs: # Make sure a low but non-zero value is not rounded down to zero @@ -67,7 +86,7 @@ class VelbusLight(VelbusEntity, LightEntity): ) await getattr(self._channel, attr)(*args) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the velbus light to turn off.""" attr, *args = ( "set_dimmer_state", @@ -80,25 +99,21 @@ class VelbusLight(VelbusEntity, LightEntity): class VelbusButtonLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" + _channel: VelbusButton _attr_entity_registry_enabled_default = False _attr_supported_feature = SUPPORT_FLASH - def __init__(self, channel): + def __init__(self, channel: VelbusChannel) -> None: """Initialize the button light (led).""" super().__init__(channel) self._attr_name = f"LED {self._channel.get_name()}" @property - def is_on(self): + def is_on(self) -> Any: """Return true if the light is on.""" return self._channel.is_on() - @property - def brightness(self): - """Return the brightness of the light.""" - return int((self._channel.get_dimmer_state() * 255) / 100) - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the Velbus light to turn on.""" if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_LONG: @@ -111,7 +126,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): attr, *args = "set_led_state", "on" await getattr(self._channel, attr)(*args) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the velbus light to turn off.""" attr, *args = "set_led_state", "off" await getattr(self._channel, attr)(*args) diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 32f016b8ce3..8534a32080e 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,22 +1,31 @@ """Support for Velbus sensors.""" from __future__ import annotations +from velbusaio.channels import ButtonCounter, LightSensor, SensorNumber, Temperature + from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity from .const import DOMAIN -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 Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] @@ -31,13 +40,19 @@ async def async_setup_entry(hass, entry, async_add_entities): class VelbusSensor(VelbusEntity, SensorEntity): """Representation of a sensor.""" - def __init__(self, channel, counter=False): + _channel: ButtonCounter | Temperature | LightSensor | SensorNumber + + def __init__( + self, + channel: ButtonCounter | Temperature | LightSensor | SensorNumber, + counter: bool = False, + ) -> None: """Initialize a sensor Velbus entity.""" super().__init__(channel) - self._is_counter = counter + self._is_counter: bool = counter @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for counter sensors.""" unique_id = super().unique_id if self._is_counter: @@ -45,7 +60,7 @@ class VelbusSensor(VelbusEntity, SensorEntity): return unique_id @property - def name(self): + def name(self) -> str: """Return the name for the sensor.""" name = super().name if self._is_counter: @@ -53,7 +68,7 @@ class VelbusSensor(VelbusEntity, SensorEntity): return name @property - def device_class(self): + def device_class(self) -> str | None: """Return the device class of the sensor.""" if self._is_counter: return DEVICE_CLASS_ENERGY @@ -64,28 +79,28 @@ class VelbusSensor(VelbusEntity, SensorEntity): return None @property - def native_value(self): + def native_value(self) -> float | int | None: """Return the state of the sensor.""" if self._is_counter: - return self._channel.get_counter_state() - return self._channel.get_state() + return float(self._channel.get_counter_state()) + return float(self._channel.get_state()) @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" if self._is_counter: - return self._channel.get_counter_unit() - return self._channel.get_unit() + return str(self._channel.get_counter_unit()) + return str(self._channel.get_unit()) @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend.""" if self._is_counter: return "mdi:counter" return None @property - def state_class(self): + def state_class(self) -> str: """Return the state class of this device.""" if self._is_counter: return STATE_CLASS_TOTAL_INCREASING diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index c7b4a80f196..c3c4c8a5863 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -1,6 +1,8 @@ """Support for Velbus switches.""" from typing import Any +from velbusaio.channels import Relay as VelbusRelay + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -27,6 +29,8 @@ async def async_setup_entry( class VelbusSwitch(VelbusEntity, SwitchEntity): """Representation of a switch.""" + _channel: VelbusRelay + @property def is_on(self) -> bool: """Return true if the switch is on.""" diff --git a/mypy.ini b/mypy.ini index 6859b8b2d48..91bca58ba6b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1518,6 +1518,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.velbus.*] +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.vlc_telnet.*] check_untyped_defs = true disallow_incomplete_defs = true From 4ecbfe8646fa91503873758a73b3d5c96bad0548 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Nov 2021 16:28:46 +0100 Subject: [PATCH 0404/1452] Drop STEP_ID constants from data_entry_flow (#59497) Co-authored-by: epenet --- homeassistant/components/acmeda/config_flow.py | 4 ++-- homeassistant/components/control4/config_flow.py | 5 ++--- homeassistant/data_entry_flow.py | 5 +---- tests/components/acmeda/test_config_flow.py | 2 +- tests/components/control4/test_config_flow.py | 5 ++--- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 460357baa78..1db629e506a 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -8,7 +8,7 @@ import aiopulse import async_timeout import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_ID from .const import DOMAIN @@ -53,7 +53,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_hubs = {hub.id: hub for hub in hubs} return self.async_show_form( - step_id=data_entry_flow.STEP_ID_USER, + step_id="user", data_schema=vol.Schema( { vol.Required(CONF_ID): vol.In( diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 660d265758f..2cf1ca845f7 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import STEP_ID_INIT, STEP_ID_USER from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -132,7 +131,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id=STEP_ID_USER, data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=DATA_SCHEMA, errors=errors ) @staticmethod @@ -164,7 +163,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), } ) - return self.async_show_form(step_id=STEP_ID_INIT, data_schema=data_schema) + return self.async_show_form(step_id="init", data_schema=data_schema) class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 2bc1a6c2278..ae5feb3d198 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ import abc import asyncio from collections.abc import Iterable, Mapping from types import MappingProxyType -from typing import Any, Final, TypedDict +from typing import Any, TypedDict import uuid import voluptuous as vol @@ -21,9 +21,6 @@ RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" RESULT_TYPE_SHOW_PROGRESS = "progress" RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" -STEP_ID_INIT: Final = "init" -STEP_ID_USER: Final = "user" - # Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 6355d1b1adb..c50e0b9971f 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -92,7 +92,7 @@ async def test_show_form_two_hubs(hass, mock_hub_discover): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == data_entry_flow.STEP_ID_USER + assert result["step_id"] == "user" # Check we performed the discovery assert len(mock_hub_discover.mock_calls) == 1 diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 1ae69dbe36d..8a4791ab579 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.data_entry_flow import STEP_ID_INIT from tests.common import MockConfigEntry @@ -167,7 +166,7 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" - assert result["step_id"] == STEP_ID_INIT + assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -187,7 +186,7 @@ async def test_option_flow_defaults(hass): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" - assert result["step_id"] == STEP_ID_INIT + assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} From 90ee1f4783c38513f608420eee2df22e7803d643 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Thu, 11 Nov 2021 17:16:59 +0100 Subject: [PATCH 0405/1452] Add available property to statistics component (#59203) * Add available property to the statistics component * Add test for statistics sensor availability * Clean up availability check * Improve statistics source sensor tests * Revert variable rename * Improve comments --- homeassistant/components/statistics/sensor.py | 8 +++++- tests/components/statistics/test_sensor.py | 26 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 7b2951a1ab6..5b0ad3765b1 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -126,6 +126,7 @@ class StatisticsSensor(SensorEntity): self._entity_id = entity_id self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" self._name = name + self._available = False self._sampling_size = sampling_size self._max_age = max_age self._precision = precision @@ -176,6 +177,7 @@ class StatisticsSensor(SensorEntity): def _add_state_to_queue(self, new_state): """Add the state to the queue.""" + self._available = new_state.state != STATE_UNAVAILABLE if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE, None): return @@ -184,7 +186,6 @@ class StatisticsSensor(SensorEntity): self.states.append(new_state.state) else: self.states.append(float(new_state.state)) - self.ages.append(new_state.last_updated) except ValueError: _LOGGER.error( @@ -216,6 +217,11 @@ class StatisticsSensor(SensorEntity): """Return the unit the value is expressed in.""" return self._unit_of_measurement if not self.is_binary else None + @property + def available(self): + """Return the availability of the sensor linked to the source sensor.""" + return self._available + @property def should_poll(self): """No polling needed.""" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index c3b14ba360d..0aa1c116991 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.statistics.sensor import DOMAIN, StatisticsSensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, + STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, ) @@ -110,7 +111,6 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert str(self.mean) == state.state assert self.min == state.attributes.get("min_value") assert self.max == state.attributes.get("max_value") @@ -125,13 +125,29 @@ class TestStatisticsSensor(unittest.TestCase): assert self.change == state.attributes.get("change") assert self.average_change == state.attributes.get("average_change") - # Source sensor is unavailable, unit and state should not change - self.hass.states.set("sensor.test_monitored", "unavailable", {}) + # Source sensor turns unavailable, then available with valid value, + # statistics sensor should follow + state = self.hass.states.get("sensor.test") + self.hass.states.set( + "sensor.test_monitored", + STATE_UNAVAILABLE, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") - assert state == new_state + assert new_state.state == STATE_UNAVAILABLE + self.hass.states.set( + "sensor.test_monitored", + 0, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + new_state = self.hass.states.get("sensor.test") + assert new_state.state != STATE_UNAVAILABLE + assert new_state.attributes.get("count") == state.attributes.get("count") + 1 - # Source sensor has a non float state, unit and state should not change + # Source sensor has a non-float state, unit and state should not change + state = self.hass.states.get("sensor.test") self.hass.states.set("sensor.test_monitored", "beer", {}) self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") From 9ea338c121929fb30206da8851141dcf627c6e6e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 12 Nov 2021 00:59:13 +0800 Subject: [PATCH 0406/1452] Remove incomplete segment on stream restart (#59532) --- homeassistant/components/stream/hls.py | 10 ++++++++ homeassistant/components/stream/worker.py | 5 ++++ tests/components/stream/conftest.py | 10 ++++---- tests/components/stream/test_hls.py | 30 +++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index e1a0e6a8f67..44b19d2cc85 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -77,6 +77,16 @@ class HlsStreamOutput(StreamOutput): or self.stream_settings.min_segment_duration ) + def discontinuity(self) -> None: + """Remove incomplete segment from deque.""" + self._hass.loop.call_soon_threadsafe(self._async_discontinuity) + + @callback + def _async_discontinuity(self) -> None: + """Remove incomplete segment from deque in event loop.""" + if self._segments and not self._segments[-1].complete: + self._segments.pop() + class HlsMasterPlaylistView(StreamView): """Stream view used only for Chromecast compatibility.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e4be3168393..a0ab48290f5 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -18,6 +18,7 @@ from .const import ( ATTR_SETTINGS, AUDIO_CODECS, DOMAIN, + HLS_PROVIDER, MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, PACKETS_TO_WAIT_FOR_AUDIO, @@ -25,6 +26,7 @@ from .const import ( SOURCE_TIMEOUT, ) from .core import Part, Segment, StreamOutput, StreamSettings +from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -279,6 +281,9 @@ class SegmentBuffer: # the discontinuity sequence number. self._stream_id += 1 self._start_time = datetime.datetime.utcnow() + # Call discontinuity to remove incomplete segment from the HLS output + if hls_output := self._outputs_callback().get(HLS_PROVIDER): + cast(HlsStreamOutput, hls_output).discontinuity() def close(self) -> None: """Close stream buffer.""" diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 58c69218f14..62c62593c57 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -22,8 +22,8 @@ from aiohttp import web import async_timeout import pytest -from homeassistant.components.stream import Stream from homeassistant.components.stream.core import Segment, StreamOutput +from homeassistant.components.stream.worker import SegmentBuffer TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout @@ -34,7 +34,7 @@ class WorkerSync: def __init__(self): """Initialize WorkerSync.""" self._event = None - self._original = Stream._worker_finished + self._original = SegmentBuffer.discontinuity def pause(self): """Pause the worker before it finalizes the stream.""" @@ -45,7 +45,7 @@ class WorkerSync: logging.debug("waking blocked worker") self._event.set() - def blocking_finish(self, stream: Stream): + def blocking_discontinuity(self, stream: SegmentBuffer): """Intercept call to pause stream worker.""" # Worker is ending the stream, which clears all output buffers. # Block the worker thread until the test has a chance to verify @@ -63,8 +63,8 @@ def stream_worker_sync(hass): """Patch StreamOutput to allow test to synchronize worker stream end.""" sync = WorkerSync() with patch( - "homeassistant.components.stream.Stream._worker_finished", - side_effect=sync.blocking_finish, + "homeassistant.components.stream.worker.SegmentBuffer.discontinuity", + side_effect=sync.blocking_discontinuity, autospec=True, ): yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 07c8cc88a65..3bff13a936b 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -448,3 +448,33 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy stream_worker_sync.resume() stream.stop() + + +async def test_remove_incomplete_segment_on_exit(hass, stream_worker_sync): + """Test that the incomplete segment gets removed when the worker thread quits.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE, {}) + stream_worker_sync.pause() + stream.start() + hls = stream.add_provider(HLS_PROVIDER) + + segment = Segment(sequence=0, stream_id=0, duration=SEGMENT_DURATION) + hls.put(segment) + segment = Segment(sequence=1, stream_id=0, duration=SEGMENT_DURATION) + hls.put(segment) + segment = Segment(sequence=2, stream_id=0, duration=0) + hls.put(segment) + await hass.async_block_till_done() + + segments = hls._segments + assert len(segments) == 3 + assert not segments[-1].complete + stream_worker_sync.resume() + stream._thread_quit.set() + stream._thread.join() + stream._thread = None + await hass.async_block_till_done() + assert segments[-1].complete + assert len(segments) == 2 + stream.stop() From 6636287c59d1c75cef5d61735407915ac4216172 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Nov 2021 18:27:22 +0100 Subject: [PATCH 0407/1452] Bump cryptography 35.0.0 (#59541) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31cd7fb341f..2e22a5d5e82 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==3.4.8 +cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 home-assistant-frontend==20211109.0 diff --git a/requirements.txt b/requirements.txt index 1d4fe15979c..cc595332395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ ciso8601==2.2.0 httpx==0.19.0 jinja2==3.0.3 PyJWT==2.1.0 -cryptography==3.4.8 +cryptography==35.0.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 pyyaml==6.0 diff --git a/setup.py b/setup.py index 113a11d9c97..43ff77f6110 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ "jinja2==3.0.3", "PyJWT==2.1.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==3.4.8", + "cryptography==35.0.0", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pyyaml==6.0", From d1ee041997e3edc0c9288dd03b1ff1ec867e427c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Nov 2021 19:36:53 +0100 Subject: [PATCH 0408/1452] Test sensor unit conversion (#59546) --- tests/components/sensor/test_init.py | 49 +++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 7859d133c29..c3808b44b06 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,8 +1,55 @@ """The test for sensor device automation.""" +import pytest +from pytest import approx + from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + + +@pytest.mark.parametrize( + "unit_system,native_unit,state_unit,native_value,state_value", + [ + (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT, 100, 100), + (IMPERIAL_SYSTEM, TEMP_CELSIUS, TEMP_FAHRENHEIT, 38, 100), + (METRIC_SYSTEM, TEMP_FAHRENHEIT, TEMP_CELSIUS, 100, 38), + (METRIC_SYSTEM, TEMP_CELSIUS, TEMP_CELSIUS, 38, 38), + ], +) +async def test_temperature_conversion( + hass, + enable_custom_integrations, + unit_system, + native_unit, + state_unit, + native_value, + state_value, +): + """Test temperature conversion.""" + hass.config.units = unit_system + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_unit_of_measurement=native_unit, + device_class=DEVICE_CLASS_TEMPERATURE, + ) + + 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 float(state.state) == approx(float(state_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit async def test_deprecated_temperature_conversion( From 00b1c2bb70a781241f8a68b9429e4e1dccf0c379 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 11 Nov 2021 19:39:57 +0100 Subject: [PATCH 0409/1452] Fix KNX climate entities hvac action without controller_mode (#59522) * fix hvac action for climate entities without controller_mode * Update homeassistant/components/knx/climate.py Co-authored-by: Marvin Wichmann Co-authored-by: Marvin Wichmann --- homeassistant/components/knx/climate.py | 8 ++++---- homeassistant/components/knx/const.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 2099270f036..6d3194731f1 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -268,10 +268,10 @@ class KNXClimate(KnxEntity, ClimateEntity): return CURRENT_HVAC_OFF if self._device.is_active is False: return CURRENT_HVAC_IDLE - if self._device.mode is not None and self._device.mode.supports_controller_mode: - return CURRENT_HVAC_ACTIONS.get( - self._device.mode.controller_mode.value, CURRENT_HVAC_IDLE - ) + if ( + self._device.mode is not None and self._device.mode.supports_controller_mode + ) or self._device.is_active: + return CURRENT_HVAC_ACTIONS.get(self.hvac_mode, CURRENT_HVAC_IDLE) return None async def async_set_hvac_mode(self, hvac_mode: str) -> None: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 1058f5abe07..f3468fc8dc2 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -79,11 +79,11 @@ CONTROLLER_MODES: Final = { } CURRENT_HVAC_ACTIONS: Final = { - "Heat": CURRENT_HVAC_HEAT, - "Cool": CURRENT_HVAC_COOL, - "Off": CURRENT_HVAC_OFF, - "Fan only": CURRENT_HVAC_FAN, - "Dry": CURRENT_HVAC_DRY, + HVAC_MODE_HEAT: CURRENT_HVAC_HEAT, + HVAC_MODE_COOL: CURRENT_HVAC_COOL, + HVAC_MODE_OFF: CURRENT_HVAC_OFF, + HVAC_MODE_FAN_ONLY: CURRENT_HVAC_FAN, + HVAC_MODE_DRY: CURRENT_HVAC_DRY, } PRESET_MODES: Final = { From 3f2b1fa952748e2f3f4a69743957d8f3d65e7a79 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 12 Nov 2021 00:12:35 +0000 Subject: [PATCH 0410/1452] [ci skip] Translation update --- .../components/airthings/translations/ja.json | 13 +++++++ .../airvisual/translations/sensor.ja.json | 18 ++++++++++ .../components/ambee/translations/ja.json | 9 +++++ .../ambee/translations/sensor.ja.json | 10 ++++++ .../components/apple_tv/translations/ja.json | 5 +++ .../binary_sensor/translations/ja.json | 4 +++ .../components/bosch_shc/translations/ja.json | 13 +++++++ .../components/cast/translations/ja.json | 22 ++++++++++++ .../cert_expiry/translations/ja.json | 3 ++ .../cloudflare/translations/ja.json | 11 ++++++ .../components/co2signal/translations/ja.json | 10 ++++++ .../components/coinbase/translations/ja.json | 11 ++++++ .../crownstone/translations/ja.json | 3 +- .../components/daikin/translations/ja.json | 3 ++ .../components/dsmr/translations/ja.json | 34 +++++++++++++++++++ .../components/emonitor/translations/ja.json | 5 +++ .../enphase_envoy/translations/ja.json | 5 +++ .../components/esphome/translations/ja.json | 10 ++++++ .../fjaraskupan/translations/ja.json | 9 +++++ .../flick_electric/translations/ja.json | 12 +++++++ .../components/flipr/translations/ja.json | 14 ++++++++ .../forecast_solar/translations/ja.json | 12 ++++++- .../forked_daapd/translations/ja.json | 11 ++++++ .../components/foscam/translations/ja.json | 7 ++++ .../components/fritz/translations/ja.json | 5 +++ .../fritzbox_callmonitor/translations/ja.json | 5 +++ .../growatt_server/translations/ja.json | 16 +++++++++ .../huawei_lte/translations/ja.json | 16 +++++++++ .../translations/ja.json | 5 +++ .../components/ipp/translations/ja.json | 7 ++++ .../components/isy994/translations/ja.json | 8 +++++ .../keenetic_ndms2/translations/ja.json | 7 ++++ .../components/kodi/translations/ja.json | 28 +++++++++++++++ .../components/konnected/translations/ja.json | 11 ++++++ .../components/kraken/translations/ja.json | 11 ++++++ .../components/litejet/translations/ja.json | 11 ++++++ .../lutron_caseta/translations/ja.json | 5 +++ .../meteoclimatic/translations/ja.json | 12 +++++++ .../modem_callerid/translations/ja.json | 6 ++++ .../components/motioneye/translations/ja.json | 3 +- .../components/mqtt/translations/ja.json | 6 ++-- .../components/nam/translations/ja.json | 7 ++++ .../components/nexia/translations/ja.json | 11 ++++++ .../nmap_tracker/translations/ja.json | 26 ++++++++++++++ .../components/notion/translations/ja.json | 3 ++ .../components/omnilogic/translations/ja.json | 11 ++++++ .../components/onvif/translations/ja.json | 11 ++++++ .../ovo_energy/translations/ja.json | 5 +++ .../philips_js/translations/ja.json | 11 ++++++ .../components/plex/translations/ja.json | 5 +++ .../components/powerwall/translations/ja.json | 5 +++ .../components/prosegur/translations/ja.json | 16 +++++++++ .../pvpc_hourly_pricing/translations/ja.json | 21 ++++++++++++ .../components/rdw/translations/ja.json | 14 ++++++++ .../components/renault/translations/ja.json | 6 ++++ .../components/rfxtrx/translations/ja.json | 12 +++++++ .../components/roomba/translations/ja.json | 5 +++ .../components/samsungtv/translations/ja.json | 7 ++++ .../screenlogic/translations/ja.json | 5 +++ .../components/sensor/translations/ja.json | 17 +++++++++- .../components/shelly/translations/ja.json | 1 + .../components/sia/translations/ja.json | 23 +++++++++++++ .../simplisafe/translations/ja.json | 6 +++- .../components/soma/translations/ja.json | 7 ++++ .../somfy_mylink/translations/ja.json | 5 +++ .../components/sonos/translations/ja.json | 3 ++ .../components/switchbot/translations/ja.json | 24 ++++++++++++- .../components/syncthing/translations/ja.json | 3 ++ .../system_bridge/translations/ja.json | 3 ++ .../components/tplink/translations/ja.json | 1 + .../components/tuya/translations/ja.json | 5 ++- .../components/unifi/translations/ja.json | 1 + .../components/upnp/translations/ja.json | 11 ++++++ .../components/wallbox/translations/ja.json | 11 ++++++ .../components/wemo/translations/ja.json | 7 ++++ .../xiaomi_miio/translations/ja.json | 20 +++++++++++ .../yale_smart_alarm/translations/ja.json | 11 ++++++ .../components/yeelight/translations/ja.json | 11 ++++++ .../components/zha/translations/ja.json | 8 +++++ .../components/zwave/translations/ja.json | 7 ++++ .../components/zwave_js/translations/ja.json | 12 +++++++ 81 files changed, 785 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/airthings/translations/ja.json create mode 100644 homeassistant/components/airvisual/translations/sensor.ja.json create mode 100644 homeassistant/components/ambee/translations/ja.json create mode 100644 homeassistant/components/ambee/translations/sensor.ja.json create mode 100644 homeassistant/components/apple_tv/translations/ja.json create mode 100644 homeassistant/components/bosch_shc/translations/ja.json create mode 100644 homeassistant/components/cloudflare/translations/ja.json create mode 100644 homeassistant/components/co2signal/translations/ja.json create mode 100644 homeassistant/components/coinbase/translations/ja.json create mode 100644 homeassistant/components/dsmr/translations/ja.json create mode 100644 homeassistant/components/emonitor/translations/ja.json create mode 100644 homeassistant/components/enphase_envoy/translations/ja.json create mode 100644 homeassistant/components/fjaraskupan/translations/ja.json create mode 100644 homeassistant/components/flick_electric/translations/ja.json create mode 100644 homeassistant/components/flipr/translations/ja.json create mode 100644 homeassistant/components/forked_daapd/translations/ja.json create mode 100644 homeassistant/components/foscam/translations/ja.json create mode 100644 homeassistant/components/fritz/translations/ja.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/ja.json create mode 100644 homeassistant/components/growatt_server/translations/ja.json create mode 100644 homeassistant/components/huawei_lte/translations/ja.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/ja.json create mode 100644 homeassistant/components/ipp/translations/ja.json create mode 100644 homeassistant/components/isy994/translations/ja.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/ja.json create mode 100644 homeassistant/components/kodi/translations/ja.json create mode 100644 homeassistant/components/konnected/translations/ja.json create mode 100644 homeassistant/components/kraken/translations/ja.json create mode 100644 homeassistant/components/litejet/translations/ja.json create mode 100644 homeassistant/components/lutron_caseta/translations/ja.json create mode 100644 homeassistant/components/meteoclimatic/translations/ja.json create mode 100644 homeassistant/components/nam/translations/ja.json create mode 100644 homeassistant/components/nexia/translations/ja.json create mode 100644 homeassistant/components/nmap_tracker/translations/ja.json create mode 100644 homeassistant/components/omnilogic/translations/ja.json create mode 100644 homeassistant/components/onvif/translations/ja.json create mode 100644 homeassistant/components/ovo_energy/translations/ja.json create mode 100644 homeassistant/components/philips_js/translations/ja.json create mode 100644 homeassistant/components/powerwall/translations/ja.json create mode 100644 homeassistant/components/prosegur/translations/ja.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/ja.json create mode 100644 homeassistant/components/rdw/translations/ja.json create mode 100644 homeassistant/components/rfxtrx/translations/ja.json create mode 100644 homeassistant/components/roomba/translations/ja.json create mode 100644 homeassistant/components/samsungtv/translations/ja.json create mode 100644 homeassistant/components/screenlogic/translations/ja.json create mode 100644 homeassistant/components/sia/translations/ja.json create mode 100644 homeassistant/components/soma/translations/ja.json create mode 100644 homeassistant/components/somfy_mylink/translations/ja.json create mode 100644 homeassistant/components/syncthing/translations/ja.json create mode 100644 homeassistant/components/system_bridge/translations/ja.json create mode 100644 homeassistant/components/upnp/translations/ja.json create mode 100644 homeassistant/components/wallbox/translations/ja.json create mode 100644 homeassistant/components/wemo/translations/ja.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/ja.json create mode 100644 homeassistant/components/yeelight/translations/ja.json create mode 100644 homeassistant/components/zha/translations/ja.json diff --git a/homeassistant/components/airthings/translations/ja.json b/homeassistant/components/airthings/translations/ja.json new file mode 100644 index 00000000000..28c23934570 --- /dev/null +++ b/homeassistant/components/airthings/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "description": "{url} \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u8cc7\u683c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "id": "ID", + "secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.ja.json b/homeassistant/components/airvisual/translations/sensor.ja.json new file mode 100644 index 00000000000..20e524fc72b --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.ja.json @@ -0,0 +1,18 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u4e00\u9178\u5316\u70ad\u7d20", + "n2": "\u4e8c\u9178\u5316\u7a92\u7d20", + "o3": "\u30aa\u30be\u30f3", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u4e8c\u9178\u5316\u786b\u9ec4" + }, + "airvisual__pollutant_level": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u967a", + "moderate": "\u9069\u5ea6", + "unhealthy": "\u4e0d\u5065\u5eb7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json new file mode 100644 index 00000000000..3195f744824 --- /dev/null +++ b/homeassistant/components/ambee/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Ambee\u304cHome Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ja.json b/homeassistant/components/ambee/translations/sensor.ja.json new file mode 100644 index 00000000000..a750a257864 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.ja.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "\u9ad8\u3044", + "low": "\u4f4e\u3044", + "moderate": "\u9069\u5ea6", + "very high": "\u975e\u5e38\u306b\u9ad8\u3044" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index a4be88f4b12..474ae5b242e 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -164,6 +164,10 @@ "off": "\u672a\u691c\u51fa", "on": "\u691c\u51fa" }, + "update": { + "off": "\u6700\u65b0", + "on": "\u66f4\u65b0\u53ef\u80fd" + }, "vibration": { "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa" diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json new file mode 100644 index 00000000000..002d4e7178f --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "Bosch SHC: {name}", + "step": { + "credentials": { + "data": { + "password": "Smart Home Controller\u306e\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index b669a6e65b8..2e7141d1f03 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,9 +1,31 @@ { "config": { "step": { + "config": { + "description": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8 - Cast\u30c7\u30d0\u30a4\u30b9\u306e\u30db\u30b9\u30c8\u540d\u3001\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u306e\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8\u3002mDNS\u691c\u51fa\u304c\u6a5f\u80fd\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u4f7f\u7528\u3057\u307e\u3059\u3002", + "title": "Google Cast\u306e\u8a2d\u5b9a" + }, "confirm": { "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } + }, + "options": { + "step": { + "advanced_options": { + "data": { + "ignore_cec": "CEC\u3092\u7121\u8996\u3059\u308b", + "uuid": "\u8a31\u53ef\u3055\u308c\u305fUUID" + }, + "title": "Google Cast\u306e\u9ad8\u5ea6\u306a\u8a2d\u5b9a" + }, + "basic_options": { + "data": { + "known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8" + }, + "description": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8 - Cast\u30c7\u30d0\u30a4\u30b9\u306e\u30db\u30b9\u30c8\u540d\u3001\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u306e\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8\u3002mDNS\u691c\u51fa\u304c\u6a5f\u80fd\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u4f7f\u7528\u3057\u307e\u3059\u3002", + "title": "Google Cast\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/ja.json b/homeassistant/components/cert_expiry/translations/ja.json index 612122b2b2c..f10ff948420 100644 --- a/homeassistant/components/cert_expiry/translations/ja.json +++ b/homeassistant/components/cert_expiry/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "connection_timeout": "\u3053\u306e\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + }, "step": { "user": { "title": "\u30c6\u30b9\u30c8\u3059\u308b\u8a3c\u660e\u66f8\u3092\u5b9a\u7fa9\u3059\u308b" diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json new file mode 100644 index 00000000000..ab542270bc6 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "description": "Cloudflare\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/ja.json b/homeassistant/components/co2signal/translations/ja.json new file mode 100644 index 00000000000..768ebb8c233 --- /dev/null +++ b/homeassistant/components/co2signal/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f" + }, + "error": { + "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json new file mode 100644 index 00000000000..edce5477528 --- /dev/null +++ b/homeassistant/components/coinbase/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "exchange_base": "\u70ba\u66ff\u30ec\u30fc\u30c8\u30bb\u30f3\u30b5\u30fc\u306e\u57fa\u6e96\u901a\u8ca8\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json index 848514c7f7d..080a7c3a812 100644 --- a/homeassistant/components/crownstone/translations/ja.json +++ b/homeassistant/components/crownstone/translations/ja.json @@ -72,7 +72,8 @@ "data": { "usb_sphere": "Crownstone Sphere" }, - "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index fdbaf65af89..ff85e1c76e3 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "api_password": "\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002API\u30ad\u30fc\u307e\u305f\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u3044\u305a\u308c\u304b\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API Key\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json new file mode 100644 index 00000000000..a5f70b34180 --- /dev/null +++ b/homeassistant/components/dsmr/translations/ja.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e" + }, + "title": "\u63a5\u7d9a\u30a2\u30c9\u30ec\u30b9\u306e\u9078\u629e" + }, + "setup_serial": { + "data": { + "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e", + "port": "\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "title": "\u30c7\u30d0\u30a4\u30b9" + }, + "setup_serial_manual_path": { + "title": "\u30d1\u30b9" + }, + "user": { + "data": { + "type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + }, + "title": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ja.json b/homeassistant/components/emonitor/translations/ja.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/emonitor/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json new file mode 100644 index 00000000000..3dd80c3f60b --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{serial} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 4e980989720..79d246ac363 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -2,6 +2,7 @@ "config": { "error": { "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -13,7 +14,16 @@ "description": "ESPHome\u306e\u30ce\u30fc\u30c9 `{name}` \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "\u691c\u51fa\u3055\u308c\u305fESPHome\u306e\u30ce\u30fc\u30c9" }, + "encryption_key": { + "data": { + "noise_psk": "\u6697\u53f7\u5316\u30ad\u30fc" + }, + "description": "{name} \u3067\u8a2d\u5b9a\u3057\u305f\u6697\u53f7\u5316\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "reauth_confirm": { + "data": { + "noise_psk": "\u6697\u53f7\u5316\u30ad\u30fc" + }, "description": "ESPHome\u30c7\u30d0\u30a4\u30b9 {name} \u3001\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u3092\u6709\u52b9\u306b\u3057\u305f\u304b\u3001\u6697\u53f7\u5316\u306e\u30ad\u30fc\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f\u3002\u66f4\u65b0\u3055\u308c\u305f\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json new file mode 100644 index 00000000000..932bd3e30b4 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Fj\u00e4r\u00e5skupan\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ja.json b/homeassistant/components/flick_electric/translations/ja.json new file mode 100644 index 00000000000..54c94a474e1 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json new file mode 100644 index 00000000000..027577bb587 --- /dev/null +++ b/homeassistant/components/flipr/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + } + }, + "user": { + "title": "Flipr\u306b\u63a5\u7d9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json index b44ce74d2cb..ec85fe10466 100644 --- a/homeassistant/components/forecast_solar/translations/ja.json +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -1,9 +1,19 @@ { + "config": { + "step": { + "user": { + "description": "\u30bd\u30fc\u30e9\u30fc\u30d1\u30cd\u30eb\u306e\u30c7\u30fc\u30bf\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e0d\u660e\u306a\u5834\u5408\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + }, "options": { "step": { "init": { "data": { - "damping": "\u6e1b\u8870\u4fc2\u6570(\u30c0\u30f3\u30d4\u30f3\u30b0\u30d5\u30a1\u30af\u30bf\u30fc): \u671d\u3068\u5915\u65b9\u306e\u7d50\u679c\u3092\u8abf\u6574\u3059\u308b" + "api_key": "Forecast.Solar API\u30ad\u30fc(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", + "damping": "\u6e1b\u8870\u4fc2\u6570(\u30c0\u30f3\u30d4\u30f3\u30b0\u30d5\u30a1\u30af\u30bf\u30fc): \u671d\u3068\u5915\u65b9\u306e\u7d50\u679c\u3092\u8abf\u6574\u3059\u308b", + "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)" } } } diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json new file mode 100644 index 00000000000..ba9b5eae471 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "librespot-java\u30d1\u30a4\u30d7\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7528\u30dd\u30fc\u30c8(\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u5834\u5408)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json new file mode 100644 index 00000000000..1a6115bb697 --- /dev/null +++ b/homeassistant/components/foscam/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_response": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u7121\u52b9\u306a\u5fdc\u7b54" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/fritz/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json new file mode 100644 index 00000000000..38a843d14fa --- /dev/null +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "plant": { + "data": { + "plant_id": "\u30d7\u30e9\u30f3\u30c8" + }, + "title": "\u30d7\u30e9\u30f3\u30c8\u3092\u9078\u629e" + }, + "user": { + "title": "Growatt\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + }, + "title": "Growatt\u30b5\u30fc\u30d0\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json new file mode 100644 index 00000000000..a02253588df --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc" + } + }, + "options": { + "step": { + "init": { + "data": { + "unauthenticated_mode": "\u8a8d\u8a3c\u306a\u3057\u306e\u30e2\u30fc\u30c9(\u5909\u66f4\u306b\u306f\u30ea\u30ed\u30fc\u30c9\u304c\u5fc5\u8981)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json new file mode 100644 index 00000000000..eaec481025c --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json new file mode 100644 index 00000000000..8e7de9c3278 --- /dev/null +++ b/homeassistant/components/ipp/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json new file mode 100644 index 00000000000..750be17309a --- /dev/null +++ b/homeassistant/components/isy994/translations/ja.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "host_reachable": "\u30db\u30b9\u30c8\u5230\u9054\u53ef\u80fd", + "websocket_status": "\u30a4\u30d9\u30f3\u30c8\u30bd\u30b1\u30c3\u30c8 \u30b9\u30c6\u30fc\u30bf\u30b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json new file mode 100644 index 00000000000..cda97ffefbc --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json new file mode 100644 index 00000000000..63204bfae5c --- /dev/null +++ b/homeassistant/components/kodi/translations/ja.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u4e00\u610f(unique)\u306aID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "credentials": { + "description": "Kodi\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308a\u307e\u3059\u3002" + }, + "discovery_confirm": { + "description": "Home Assistant\u306bKodi (`{name}`) \u3092\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Kodi\u3092\u767a\u898b" + }, + "user": { + "description": "Kodi\u306e\u63a5\u7d9a\u60c5\u5831\u3067\u3059\u3002\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u3067 \"HTTP\u306b\u3088\u308bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u5fc5\u305a\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "ws_port": { + "description": "WebSocket\u30dd\u30fc\u30c8(Kodi\u3067\u306fTCP\u30dd\u30fc\u30c8\u3068\u547c\u3070\u308c\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059)\u3002WebSocket\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308b \"\u30d7\u30ed\u30b0\u30e9\u30e0\u306bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002WebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30dd\u30fc\u30c8\u3092\u524a\u9664\u3057\u3066\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} \u3092\u30aa\u30d5\u306b\u3059\u308b\u3088\u3046\u306b\u30af\u30a8\u30b9\u30c8\u3055\u308c\u307e\u3057\u305f", + "turn_on": "{entity_name} \u3092\u30aa\u30f3\u306b\u3059\u308b\u3088\u3046\u306b\u30ea\u30af\u30a8\u30b9\u30c8\u3055\u308c\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json new file mode 100644 index 00000000000..bb053e2bb5d --- /dev/null +++ b/homeassistant/components/konnected/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options_misc": { + "data": { + "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json new file mode 100644 index 00000000000..d516569d86a --- /dev/null +++ b/homeassistant/components/kraken/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json new file mode 100644 index 00000000000..9281796a351 --- /dev/null +++ b/homeassistant/components/litejet/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8 \u30c8\u30e9\u30f3\u30b8\u30b7\u30e7\u30f3(\u79d2)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json new file mode 100644 index 00000000000..eaec481025c --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ja.json b/homeassistant/components/meteoclimatic/translations/ja.json new file mode 100644 index 00000000000..f8e6a9c606b --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "code": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u30b3\u30fc\u30c9" + }, + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index c928c7e76c3..d330c5d9da3 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "\u6b8b\u308a\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { + "usb_confirm": { + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + }, "user": { "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", "title": "Phone\u30e2\u30c7\u30e0" diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 4c52169a7af..836d8d5987d 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -3,7 +3,8 @@ "step": { "init": { "data": { - "stream_url_template": "Stream URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" + "stream_url_template": "Stream URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", + "webhook_set_overwrite": "\u8a8d\u8b58\u3055\u308c\u3066\u3044\u306a\u3044Webhook\u3092\u4e0a\u66f8\u304d\u3059\u308b" } } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index cbe3e4d0fed..01f77dc1eef 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -23,10 +23,12 @@ "options": { "step": { "broker": { - "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Broker\u30aa\u30d7\u30b7\u30e7\u30f3" }, "options": { - "description": "\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(Discovery(\u691c\u51fa)) - \u691c\u51fa\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408(\u63a8\u5968)\u3001Home Assistant\u306f\u3001MQTT broker\u306b\u8a2d\u5b9a\u3092\u516c\u958b\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3084\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3057\u307e\u3059\u3002\u691c\u51fa\u3092\u7121\u52b9\u306b\u3057\u305f\u5834\u5408\u306f\u3001\u3059\u3079\u3066\u306e\u8a2d\u5b9a\u3092\u624b\u52d5\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d0\u30fc\u30b9(Birth(\u8a95\u751f))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u8a95\u751f\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304cMQTT broker\u306b\u3001(\u518d)\u63a5\u7d9a\u3059\u308b\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\n\u30a6\u30a3\u30eb(Will(\u610f\u601d))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u30a6\u30a3\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304c\u30d6\u30ed\u30fc\u30ab\u30fc(broker)\u3078\u306e\u63a5\u7d9a\u3092\u5931\u3046\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f\u3001\u30af\u30ea\u30fc\u30f3\u306a\u63a5\u7d9a(Home Assistant\u306e\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3\u306a\u3069)\u306e\u5834\u5408\u3068\u3001\u30af\u30ea\u30fc\u30f3\u3067\u306f\u306a\u3044\u63a5\u7d9a(Home Assistant\u306e\u30af\u30e9\u30c3\u30b7\u30e5\u3084\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u5931\u3063\u305f\u5834\u5408)\u306e\u3069\u3061\u3089\u306e\u5834\u5408\u3067\u3042\u3063\u3066\u3082\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002" + "description": "\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(Discovery(\u691c\u51fa)) - \u691c\u51fa\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408(\u63a8\u5968)\u3001Home Assistant\u306f\u3001MQTT broker\u306b\u8a2d\u5b9a\u3092\u516c\u958b\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3084\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3057\u307e\u3059\u3002\u691c\u51fa\u3092\u7121\u52b9\u306b\u3057\u305f\u5834\u5408\u306f\u3001\u3059\u3079\u3066\u306e\u8a2d\u5b9a\u3092\u624b\u52d5\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d0\u30fc\u30b9(Birth(\u8a95\u751f))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u8a95\u751f\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304cMQTT broker\u306b\u3001(\u518d)\u63a5\u7d9a\u3059\u308b\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\n\u30a6\u30a3\u30eb(Will(\u610f\u601d))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u30a6\u30a3\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304c\u30d6\u30ed\u30fc\u30ab\u30fc(broker)\u3078\u306e\u63a5\u7d9a\u3092\u5931\u3046\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f\u3001\u30af\u30ea\u30fc\u30f3\u306a\u63a5\u7d9a(Home Assistant\u306e\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3\u306a\u3069)\u306e\u5834\u5408\u3068\u3001\u30af\u30ea\u30fc\u30f3\u3067\u306f\u306a\u3044\u63a5\u7d9a(Home Assistant\u306e\u30af\u30e9\u30c3\u30b7\u30e5\u3084\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u5931\u3063\u305f\u5834\u5408)\u306e\u3069\u3061\u3089\u306e\u5834\u5408\u3067\u3042\u3063\u3066\u3082\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002", + "title": "MQTT\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json new file mode 100644 index 00000000000..a50e0a74f37 --- /dev/null +++ b/homeassistant/components/nam/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json new file mode 100644 index 00000000000..c681a7be2e2 --- /dev/null +++ b/homeassistant/components/nexia/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "brand": "\u30d6\u30e9\u30f3\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json new file mode 100644 index 00000000000..c225fb6bf6f --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "invalid_hosts": "\u7121\u52b9\u306a\u30db\u30b9\u30c8" + }, + "step": { + "user": { + "data": { + "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", + "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u69cb\u6210\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", + "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index bf28204a0df..a28acc92335 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -4,6 +4,9 @@ "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "step": { + "reauth_confirm": { + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json new file mode 100644 index 00000000000..62f5db806c1 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "ph_offset": "pH\u30aa\u30d5\u30bb\u30c3\u30c8(\u6b63\u307e\u305f\u306f\u8ca0)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json new file mode 100644 index 00000000000..4ddc01954f7 --- /dev/null +++ b/homeassistant/components/onvif/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json new file mode 100644 index 00000000000..ca9690c66f4 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{username}" + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json new file mode 100644 index 00000000000..7128b9694a0 --- /dev/null +++ b/homeassistant/components/philips_js/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "allow_notify": "\u30c7\u30fc\u30bf\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u306e\u4f7f\u7528\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 89df73b9221..8d172d6ea3f 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -4,6 +4,11 @@ "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, "step": { + "manual_setup": { + "data": { + "token": "\u30c8\u30fc\u30af\u30f3(\u30aa\u30d7\u30b7\u30e7\u30f3)" + } + }, "select_server": { "data": { "server": "\u30b5\u30fc\u30d0\u30fc" diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json new file mode 100644 index 00000000000..2a79188ae5b --- /dev/null +++ b/homeassistant/components/powerwall/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{ip_address}" + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/ja.json b/homeassistant/components/prosegur/translations/ja.json new file mode 100644 index 00000000000..76367501945 --- /dev/null +++ b/homeassistant/components/prosegur/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "description": "Prosegur\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" + } + }, + "user": { + "data": { + "country": "\u56fd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json new file mode 100644 index 00000000000..db9a55235e7 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "power": "\u5951\u7d04\u96fb\u529b (kW)" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u5951\u7d04\u96fb\u529b (kW)" + }, + "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/ja.json b/homeassistant/components/rdw/translations/ja.json new file mode 100644 index 00000000000..7114ff7d040 --- /dev/null +++ b/homeassistant/components/rdw/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown_license_plate": "\u4e0d\u660e\u306a\u30e9\u30a4\u30bb\u30f3\u30b9\u30d7\u30ec\u30fc\u30c8" + }, + "step": { + "user": { + "data": { + "license_plate": "\u30e9\u30a4\u30bb\u30f3\u30b9\u30d7\u30ec\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 3415f92f0c9..33c0087570d 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -1,6 +1,12 @@ { "config": { "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon\u30a2\u30ab\u30a6\u30f3\u30c8ID" + }, + "title": "Kamereon\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3092\u9078\u629e" + }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json new file mode 100644 index 00000000000..c4ba0aad4e4 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "send_command": "\u30b3\u30de\u30f3\u30c9\u3092\u9001\u4fe1: {subtype}", + "send_status": "\u30b9\u30c6\u30fc\u30bf\u30b9\u66f4\u65b0\u306e\u9001\u4fe1: {subtype}" + }, + "trigger_type": { + "command": "\u30b3\u30de\u30f3\u30c9\u3092\u53d7\u4fe1: {subtype}", + "status": "\u53d7\u4fe1\u30b9\u30c6\u30fc\u30bf\u30b9: {subtype}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json new file mode 100644 index 00000000000..eaec481025c --- /dev/null +++ b/homeassistant/components/roomba/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json new file mode 100644 index 00000000000..6fef3343c00 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "missing_config_entry": "\u3053\u306e\u30b5\u30e0\u30b9\u30f3\u88fd\u7aef\u672b\u306b\u306f\u8a2d\u5b9a\u9805\u76ee\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 9931bfd0a71..3c8b8c75a65 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,9 +1,24 @@ { "device_automation": { "condition_type": { - "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387" + "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", + "is_nitrogen_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_nitrous_oxide": "\u73fe\u5728\u306e {entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_ozone": "\u73fe\u5728\u306e {entity_name} \u30aa\u30be\u30f3\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_pm1": "\u73fe\u5728\u306e {entity_name} PM1\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_pm10": "\u73fe\u5728\u306e {entity_name} PM10\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_pm25": "\u73fe\u5728\u306e {entity_name} PM2.5\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", + "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb" }, "trigger_type": { + "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", + "nitrogen_monoxide": "{entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", + "nitrous_oxide": "{entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", + "ozone": "{entity_name} \u30aa\u30be\u30f3\u6fc3\u5ea6\u306e\u5909\u5316", + "pm1": "{entity_name} PM1\u6fc3\u5ea6\u306e\u5909\u5316", + "pm10": "{entity_name} PM10\u6fc3\u5ea6\u306e\u5909\u5316", + "pm25": "{entity_name} PM2.5\u6fc3\u5ea6\u306e\u5909\u5316", "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4" } }, diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index c6021ac5b23..be271bcd871 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -7,6 +7,7 @@ "btn_down": "{subtype} button down", "btn_up": "{subtype} button up", "double_push": "{subtype} double push", + "long": "{subtype} \u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af", "long_push": "{subtype} long push", "single_push": "{subtype} single push" } diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json new file mode 100644 index 00000000000..39e0558acd1 --- /dev/null +++ b/homeassistant/components/sia/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "additional_account": "\u8ffd\u52a0\u306e\u30a2\u30ab\u30a6\u30f3\u30c8", + "encryption_key": "\u6697\u53f7\u5316\u30ad\u30fc", + "ping_interval": "ping\u9593\u9694(\u5206)", + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" + } + } + } + }, + "options": { + "step": { + "options": { + "title": "SIA\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3002" + } + } + }, + "title": "SIA\u30a2\u30e9\u30fc\u30e0\u30b7\u30b9\u30c6\u30e0" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 503f0b2520f..6193be02ed5 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, + "error": { + "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f" + }, "step": { "input_auth_code": { "data": { @@ -12,7 +15,8 @@ "title": "\u627f\u8a8d\u7d42\u4e86" }, "user": { - "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066 Submit \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066 Submit \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", + "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json new file mode 100644 index 00000000000..8822fbc92ea --- /dev/null +++ b/homeassistant/components/soma/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json new file mode 100644 index 00000000000..f10941b3aac --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{mac} ({ip})" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json index 7867cbcbf98..390f487e69e 100644 --- a/homeassistant/components/sonos/translations/ja.json +++ b/homeassistant/components/sonos/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fSonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, "step": { "confirm": { "description": "Sonos\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 76788c484fe..8e977d57200 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -1,7 +1,29 @@ { "config": { "abort": { - "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", + "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9" + }, + "title": "Switchbot\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "\u518d\u8a66\u884c\u56de\u6570", + "retry_timeout": "\u518d\u8a66\u884c\u306e\u9593\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "scan_timeout": "\u5e83\u544a\u30c7\u30fc\u30bf\u3092\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u6642\u9593", + "update_time": "\u66f4\u65b0\u9593\u9694(\u79d2)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json new file mode 100644 index 00000000000..8f63633fe04 --- /dev/null +++ b/homeassistant/components/syncthing/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json new file mode 100644 index 00000000000..cf111a4063e --- /dev/null +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30b7\u30b9\u30c6\u30e0\u30d6\u30ea\u30c3\u30b8" +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 58fbc868d85..3be6be36350 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "TP-Link\u30b9\u30de\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 5c33fb91659..d63ac53889e 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -21,9 +21,12 @@ "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", + "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7" - } + }, + "description": "Tuya\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Tuya\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" } } }, diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 295874b751a..76bbf4a22bf 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json new file mode 100644 index 00000000000..a930173b0bf --- /dev/null +++ b/homeassistant/components/upnp/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "unique_id": "\u30c7\u30d0\u30a4\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json new file mode 100644 index 00000000000..c2bcf5f1c56 --- /dev/null +++ b/homeassistant/components/wallbox/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json new file mode 100644 index 00000000000..cfa7169b323 --- /dev/null +++ b/homeassistant/components/wemo/translations/ja.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "long_press": "Wemo\u30dc\u30bf\u30f3\u304c2\u79d2\u9593\u62bc\u3055\u308c\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index a138f746baf..2703b61759d 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -2,6 +2,26 @@ "config": { "error": { "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" + }, + "step": { + "connect": { + "data": { + "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" + } + }, + "select": { + "data": { + "select_device": "Miio\u30c7\u30d0\u30a4\u30b9" + } + } + } + }, + "options": { + "step": { + "init": { + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", + "title": "Xiaomi Miio" + } } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json new file mode 100644 index 00000000000..8325b797013 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "area_id": "\u30a8\u30ea\u30a2ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json new file mode 100644 index 00000000000..6168d342afb --- /dev/null +++ b/homeassistant/components/yeelight/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "model": "\u30e2\u30c7\u30eb(\u30aa\u30d7\u30b7\u30e7\u30f3)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json new file mode 100644 index 00000000000..49e9b6f9c21 --- /dev/null +++ b/homeassistant/components/zha/translations/ja.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index cbf5f7274a1..3398f27a713 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -2,6 +2,13 @@ "config": { "error": { "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)" + } + } } }, "state": { diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 42579491c85..79c0e6419ad 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, "step": { "configure_addon": { "data": { @@ -20,6 +23,15 @@ "set_config_parameter": "\u8a2d\u5b9a\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u3092\u8a2d\u5b9a", "set_lock_usercode": "{entity_name} \u306b\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u8a2d\u5b9a", "set_value": "Z-Wave\u5024\u306e\u8a2d\u5b9a\u5024" + }, + "condition_type": { + "node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9" + }, + "trigger_type": { + "event.notification.entry_control": "\u30a8\u30f3\u30c8\u30ea\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", + "event.notification.notification": "\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", + "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", + "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" } }, "options": { From 715aa86a35e3c069db7713bc22072d807d09674a Mon Sep 17 00:00:00 2001 From: Ricardo Steijn <61013287+RicArch97@users.noreply.github.com> Date: Fri, 12 Nov 2021 01:29:00 +0100 Subject: [PATCH 0411/1452] Bump crownstone-cloud to v1.4.9 (#59500) --- homeassistant/components/crownstone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index 4615d0b0329..585a44cfd14 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/crownstone", "requirements": [ - "crownstone-cloud==1.4.8", + "crownstone-cloud==1.4.9", "crownstone-sse==2.0.2", "crownstone-uart==2.1.0", "pyserial==3.5" diff --git a/requirements_all.txt b/requirements_all.txt index 7b80cee41c3..de03be393e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -502,7 +502,7 @@ coronavirus==1.1.1 croniter==1.0.6 # homeassistant.components.crownstone -crownstone-cloud==1.4.8 +crownstone-cloud==1.4.9 # homeassistant.components.crownstone crownstone-sse==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77ab56dc484..797d667699e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -310,7 +310,7 @@ coronavirus==1.1.1 croniter==1.0.6 # homeassistant.components.crownstone -crownstone-cloud==1.4.8 +crownstone-cloud==1.4.9 # homeassistant.components.crownstone crownstone-sse==2.0.2 From beb0650a813cfdfe4c5660a40cb38a84e91de5e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Nov 2021 02:31:00 +0100 Subject: [PATCH 0412/1452] Upgrade coverage to 6.1.2 (#59530) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 05b4b19c84c..246b52e0085 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.1.1 +coverage==6.1.2 freezegun==1.1.0 jsonpickle==1.4.1 mock-open==1.4.0 From 5d2eb8d3ff6aa4cc1bfd2adb725688c88ac6a871 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 22:31:58 -0500 Subject: [PATCH 0413/1452] Add tilt support to bond covers (#59505) --- homeassistant/components/bond/cover.py | 38 ++++++- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 20 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/test_cover.py | 114 +++++++++++++++++++- 6 files changed, 172 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 3a2777b09e8..2c5f0fe66c3 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -5,7 +5,16 @@ from typing import Any from bond_api import Action, BPUPSubscriptions, DeviceType -from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity +from homeassistant.components.cover import ( + DEVICE_CLASS_SHADE, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity @@ -45,6 +54,21 @@ class BondCover(BondEntity, CoverEntity): ) -> None: """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) + supported_features = 0 + if self._device.supports_open(): + supported_features |= SUPPORT_OPEN + if self._device.supports_close(): + supported_features |= SUPPORT_CLOSE + if self._device.supports_tilt_open(): + supported_features |= SUPPORT_OPEN_TILT + if self._device.supports_tilt_close(): + supported_features |= SUPPORT_CLOSE_TILT + if self._device.supports_hold(): + if self._device.supports_open() or self._device.supports_close(): + supported_features |= SUPPORT_STOP + if self._device.supports_tilt_open() or self._device.supports_tilt_close(): + supported_features |= SUPPORT_STOP_TILT + self._attr_supported_features = supported_features def _apply_state(self, state: dict) -> None: cover_open = state.get("open") @@ -63,3 +87,15 @@ class BondCover(BondEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Hold cover.""" await self._hub.bond.action(self._device.device_id, Action.hold()) + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + await self._hub.bond.action(self._device.device_id, Action.tilt_open()) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + await self._hub.bond.action(self._device.device_id, Action.tilt_close()) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._hub.bond.action(self._device.device_id, Action.hold()) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index a8395b68d60..cf5255e84a4 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.14"], + "requirements": ["bond-api==0.1.15"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 4f3de1bf1f0..abe3cc98002 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -82,6 +82,26 @@ class BondDevice: """Return True if this device supports any of the direction related commands.""" return self._has_any_action({Action.SET_DIRECTION}) + def supports_open(self) -> bool: + """Return True if this device supports opening.""" + return self._has_any_action({Action.OPEN}) + + def supports_close(self) -> bool: + """Return True if this device supports closing.""" + return self._has_any_action({Action.CLOSE}) + + def supports_tilt_open(self) -> bool: + """Return True if this device supports tilt opening.""" + return self._has_any_action({Action.TILT_OPEN}) + + def supports_tilt_close(self) -> bool: + """Return True if this device supports tilt closing.""" + return self._has_any_action({Action.TILT_CLOSE}) + + def supports_hold(self) -> bool: + """Return True if this device supports hold aka stop.""" + return self._has_any_action({Action.HOLD}) + def supports_light(self) -> bool: """Return True if this device supports any of the light related commands.""" return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF}) diff --git a/requirements_all.txt b/requirements_all.txt index de03be393e0..84adf1ec6cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -418,7 +418,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.14 +bond-api==0.1.15 # homeassistant.components.bosch_shc boschshcpy==0.2.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 797d667699e..f9902320929 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ blebox_uniapi==1.3.3 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.14 +bond-api==0.1.15 # homeassistant.components.bosch_shc boschshcpy==0.2.19 diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index f516d84d50a..7a27617d607 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -4,12 +4,16 @@ from datetime import timedelta from bond_api import Action, DeviceType from homeassistant import core -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, STATE_CLOSED from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -27,7 +31,29 @@ from tests.common import async_fire_time_changed def shades(name: str): """Create motorized shades with given name.""" - return {"name": name, "type": DeviceType.MOTORIZED_SHADES} + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["Open", "Close", "Hold"], + } + + +def tilt_only_shades(name: str): + """Create motorized shades that only tilt.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["TiltOpen", "TiltClose", "Hold"], + } + + +def tilt_shades(name: str): + """Create motorized shades with given name that can also tilt.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["Open", "Close", "Hold", "TiltOpen", "TiltClose", "Hold"], + } async def test_entity_registry(hass: core.HomeAssistant): @@ -99,6 +125,90 @@ async def test_stop_cover(hass: core.HomeAssistant): mock_hold.assert_called_once_with("test-device-id", Action.hold()) +async def test_tilt_open_cover(hass: core.HomeAssistant): + """Tests that tilt open cover command delegates to API.""" + await setup_platform( + hass, COVER_DOMAIN, tilt_only_shades("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_open, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_open.assert_called_once_with("test-device-id", Action.tilt_open()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_close_cover(hass: core.HomeAssistant): + """Tests that tilt close cover command delegates to API.""" + await setup_platform( + hass, COVER_DOMAIN, tilt_only_shades("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_close, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_close.assert_called_once_with("test-device-id", Action.tilt_close()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_stop_cover(hass: core.HomeAssistant): + """Tests that tilt stop cover command delegates to API.""" + await setup_platform( + hass, + COVER_DOMAIN, + tilt_only_shades("name-1"), + bond_device_id="test-device-id", + state={"counter1": 123}, + ) + + with patch_bond_action() as mock_hold, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.hold()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_and_open(hass: core.HomeAssistant): + """Tests that supports both tilt and open.""" + await setup_platform( + hass, + COVER_DOMAIN, + tilt_shades("name-1"), + bond_device_id="test-device-id", + state={"open": False}, + ) + + with patch_bond_action() as mock_open, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_open.assert_called_once_with("test-device-id", Action.tilt_open()) + assert hass.states.get("cover.name_1").state == STATE_CLOSED + + async def test_update_reports_open_cover(hass: core.HomeAssistant): """Tests that update command sets correct state when Bond API reports cover is open.""" await setup_platform(hass, COVER_DOMAIN, shades("name-1")) From 8de0c7204a3b9b4ecaeac7df3615ab01797bcfa4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 23:13:15 -0600 Subject: [PATCH 0414/1452] Bump async_timeout to 4.0.1 (#59565) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e22a5d5e82..47f5c4b6230 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiohttp==3.8.0 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.22.12 -async_timeout==4.0.0 +async_timeout==4.0.1 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/requirements.txt b/requirements.txt index cc595332395..a39c203fa0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # Home Assistant Core aiohttp==3.8.0 astral==2.2 -async_timeout==4.0.0 +async_timeout==4.0.1 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/setup.py b/setup.py index 43ff77f6110..2e416a3545e 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ "aiohttp==3.8.0", "astral==2.2", - "async_timeout==4.0.0", + "async_timeout==4.0.1", "attrs==21.2.0", "awesomeversion==21.10.1", 'backports.zoneinfo;python_version<"3.9"', From 6ef64f6b1c7dff01313301e3b90592f491fa0bb9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Nov 2021 22:11:41 -0800 Subject: [PATCH 0415/1452] Fix CORS error in emulated_hue (#59570) --- homeassistant/components/http/view.py | 14 +++++++++----- tests/components/emulated_hue/test_init.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index aeb610d265e..192d2d5d57b 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -94,11 +94,15 @@ class HomeAssistantView: for url in urls: routes.append(router.add_route(method, url, handler)) - allow_cors = ( - app["allow_all_cors"] if self.cors_allowed else app["allow_configured_cors"] - ) - for route in routes: - allow_cors(route) + # Use `get` because CORS middleware is not be loaded in emulated_hue + if self.cors_allowed: + allow_cors = app.get("allow_all_cors") + else: + allow_cors = app.get("allow_configured_cors") + + if allow_cors: + for route in routes: + allow_cors(route) def request_handler_factory( diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 2b0d6fe06c6..93bf8c0631f 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,5 +1,6 @@ """Test the Emulated Hue component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.emulated_hue import ( DATA_KEY, @@ -7,6 +8,7 @@ from homeassistant.components.emulated_hue import ( SAVE_DELAY, Config, ) +from homeassistant.setup import async_setup_component from homeassistant.util import utcnow from tests.common import async_fire_time_changed @@ -113,3 +115,12 @@ def test_config_alexa_entity_id_to_number(): entity_id = conf.number_to_entity_id("light.test") assert entity_id == "light.test" + + +async def test_setup_works(hass): + """Test setup works.""" + hass.config.components.add("network") + with patch( + "homeassistant.components.emulated_hue.create_upnp_datagram_endpoint" + ), patch("homeassistant.components.emulated_hue.async_get_source_ip"): + assert await async_setup_component(hass, "emulated_hue", {}) From 13067003cbffa9579d30b213afc6d48edd8a7267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 12 Nov 2021 09:39:30 +0100 Subject: [PATCH 0416/1452] Fix issue with ranges in requirements with hassfest (#59470) Co-authored-by: Martin Hjelmare --- script/hassfest/requirements.py | 23 +++++++++++++++-------- tests/hassfest/test_requirements.py | 9 ++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 26cb834e4e2..2da82762240 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -26,6 +26,7 @@ PACKAGE_REGEX = re.compile( r"^(?:--.+\s)?([-_\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") +PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") SUPPORTED_PYTHON_TUPLES = [ REQUIRED_PYTHON_VER[:2], tuple(map(operator.add, REQUIRED_PYTHON_VER, (0, 1, 0)))[:2], @@ -95,16 +96,22 @@ def validate_requirements_format(integration: Integration) -> bool: ) continue - if ( - version - and AwesomeVersion(version).strategy == AwesomeVersionStrategy.UNKNOWN - ): - integration.add_error( - "requirements", - f"Unable to parse package version ({version}) for {pkg}.", - ) + if not version: continue + for part in version.split(","): + version_part = PIP_VERSION_RANGE_SEPARATOR.match(part) + if ( + version_part + and AwesomeVersion(version_part.group(2)).strategy + == AwesomeVersionStrategy.UNKNOWN + ): + integration.add_error( + "requirements", + f"Unable to parse package version ({version}) for {pkg}.", + ) + continue + return len(integration.errors) == start_errors diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index 079e77f909b..91496b7fa6f 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -45,7 +45,14 @@ def test_validate_requirements_format_wrongly_pinned(integration: Integration): def test_validate_requirements_format_ignore_pin_for_custom(integration: Integration): """Test requirement ignore pinning for custom.""" - integration.manifest["requirements"] = ["test_package>=1", "test_package"] + integration.manifest["requirements"] = [ + "test_package>=1", + "test_package", + "test_package>=1.2.3,<3.2.1", + "test_package~=0.5.0", + "test_package>=1.4.2,<1.4.99,>=1.7,<1.8.99", + "test_package>=1.4.2,<1.9,!=1.5", + ] integration.path = Path("") assert validate_requirements_format(integration) assert len(integration.errors) == 0 From e30e4d5c6d7e560ceb7c3eca6d1d4d3b14b7b356 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Nov 2021 01:25:01 -0800 Subject: [PATCH 0417/1452] Block cloud explicitely from trusted networks (#59333) * Block cloud explicitely from trusted networks * Lint --- .../auth/providers/trusted_networks.py | 6 +++++ tests/auth/providers/test_trusted_networks.py | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 0f2b287a227..fa08dde139f 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -194,6 +194,12 @@ class TrustedNetworksAuthProvider(AuthProvider): if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies): raise InvalidAuthError("Can't allow access from a proxy server") + if "cloud" in self.hass.config.components: + from hass_nabucasa import remote # pylint: disable=import-outside-toplevel + + if remote.is_cloud_request.get(): + raise InvalidAuthError("Can't allow access from Home Assistant Cloud") + @callback def async_validate_refresh_token( self, refresh_token: RefreshToken, remote_ip: str | None = None diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index d7574bf0da1..406e9a033da 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -2,6 +2,7 @@ from ipaddress import ip_address, ip_network from unittest.mock import Mock, patch +from hass_nabucasa import remote import pytest import voluptuous as vol @@ -169,6 +170,27 @@ async def test_validate_access_proxy(hass, provider): provider.async_validate_access(ip_address("fd00::1")) +async def test_validate_access_cloud(hass, provider): + """Test validate access from trusted networks are blocked from cloud.""" + await async_setup_component( + hass, + "http", + { + "http": { + CONF_TRUSTED_PROXIES: ["192.168.128.0/31", "fd00::1"], + CONF_USE_X_FORWARDED_FOR: True, + } + }, + ) + hass.config.components.add("cloud") + + provider.async_validate_access(ip_address("192.168.128.2")) + + remote.is_cloud_request.set(True) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.2")) + + async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: From 363de374003221bc3a953bc049471ea237ef0498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 12 Nov 2021 10:26:17 +0100 Subject: [PATCH 0418/1452] Override api url in norway_air (#59573) --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/air_quality.py | 6 +++++- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 97edf8eb67f..4ebbdd3b1e7 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,7 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.3"], + "requirements": ["pyMetno==0.8.4"], "codeowners": ["@danielhiversen", "@thimic"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 8e829355ea0..f38897d62c8 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -24,6 +24,8 @@ CONF_FORECAST = "forecast" DEFAULT_FORECAST = 0 DEFAULT_NAME = "Air quality Norway" +OVERRIDE_URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/airqualityforecast/0.1/" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), @@ -72,7 +74,9 @@ class AirSensor(AirQualityEntity): def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" self._name = name - self._api = metno.AirQualityData(coordinates, forecast, session) + self._api = metno.AirQualityData( + coordinates, forecast, session, api_url=OVERRIDE_URL + ) @property def attribution(self) -> str: diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 69b2e85808b..87981f085a6 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,7 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.3"], + "requirements": ["pyMetno==0.8.4"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 84adf1ec6cc..fc5b787087c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1315,7 +1315,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.3 +pyMetno==0.8.4 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9902320929..86940d279dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -790,7 +790,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.3 +pyMetno==0.8.4 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 From 21f92f628698d1e42dd713cad676081b97cf0e5f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Fri, 12 Nov 2021 11:10:40 +0100 Subject: [PATCH 0419/1452] Add Nut device explicitly to the device registry (#59525) * Add Nut device explicitly to the device registry * Restore resources in data and remove unused string --- homeassistant/components/nut/__init__.py | 25 ++++++++++++++++++----- homeassistant/components/nut/strings.json | 3 +-- tests/components/nut/test_sensor.py | 4 ++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index e16e5824ff1..1a040b99f57 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -37,11 +38,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" - # strip out the stale setting CONF_RESOURCES from data & options - if CONF_RESOURCES in entry.data: - new_data = {k: v for k, v in entry.data.items() if k != CONF_RESOURCES} - new_opts = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} - hass.config_entries.async_update_entry(entry, data=new_data, options=new_opts) + # strip out the stale options CONF_RESOURCES, + # maintain the entry in data in case of version rollback + if CONF_RESOURCES in entry.options: + new_data = {**entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES]} + new_options = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} + hass.config_entries.async_update_entry( + entry, data=new_data, options=new_options + ) config = entry.data host = config[CONF_HOST] @@ -88,6 +92,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYNUT_UNIQUE_ID: unique_id, UNDO_UPDATE_LISTENER: undo_listener, } + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, unique_id)}, + name=data.name.title(), + manufacturer=data.device_info.get(ATTR_MANUFACTURER), + model=data.device_info.get(ATTR_MODEL), + sw_version=data.device_info.get(ATTR_SW_VERSION), + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index ad507411065..70ecbfb6d2e 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -13,8 +13,7 @@ "ups": { "title": "Choose the UPS to Monitor", "data": { - "alias": "Alias", - "resources": "Resources" + "alias": "Alias" } } }, diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 206bc6736f8..b36c6e8bcc4 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -270,7 +270,7 @@ async def test_stale_options(hass): data={ CONF_HOST: "mock", CONF_PORT: "mock", - CONF_RESOURCES: ["battery.charge"], + CONF_RESOURCES: ["ups.load"], }, options={CONF_RESOURCES: ["battery.charge"]}, ) @@ -291,7 +291,7 @@ async def test_stale_options(hass): entry = registry.async_get("sensor.ups1_battery_charge") assert entry assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" - assert CONF_RESOURCES not in config_entry.data + assert config_entry.data[CONF_RESOURCES] == ["battery.charge"] assert config_entry.options == {} state = hass.states.get("sensor.ups1_battery_charge") From c2f489266a3f3d3946db64ed531eaeccf632afd9 Mon Sep 17 00:00:00 2001 From: Radu Date: Fri, 12 Nov 2021 14:04:22 +0100 Subject: [PATCH 0420/1452] Add ZigStar Zigbee Coordinator audodiscovery (#59559) --- homeassistant/components/zha/manifest.json | 1 + homeassistant/generated/usb.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 500e1ceb02b..1fa15259821 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -18,6 +18,7 @@ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, {"vid":"10C4","pid":"EA60","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, {"vid":"1A86","pid":"7523","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, + {"vid":"1A86","pid":"7523","description":"*zigstar*","known_devices":["ZigStar Coordinators"]}, {"vid":"1CF1","pid":"0030","description":"*conbee*","known_devices":["Conbee II"]}, {"vid":"10C4","pid":"8A2A","description":"*zigbee*","known_devices":["Nortek HUSBZB-1"]}, {"vid":"10C4","pid":"8B34","description":"*bv 2010/10*","known_devices":["Bitron Video AV2010/10"]} diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 0fb3f52e1d7..57f2090fee2 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -29,6 +29,12 @@ USB = [ "pid": "7523", "description": "*tubeszb*" }, + { + "domain": "zha", + "vid": "1A86", + "pid": "7523", + "description": "*zigstar*" + }, { "domain": "zha", "vid": "1CF1", From 733193b5ad1b03f8551c3ff09b7b5e977b265648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 12 Nov 2021 14:54:15 +0100 Subject: [PATCH 0421/1452] Bump aiogithubapi from 21.8.0 to 21.11.0 (#59582) --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index ce2ad4e047b..b6b575ef7ce 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==21.8.0" + "aiogithubapi==21.11.0" ], "codeowners": [ "@timmo001", diff --git a/requirements_all.txt b/requirements_all.txt index fc5b787087c..c800a9a9902 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ aioflo==0.4.1 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==21.8.0 +aiogithubapi==21.11.0 # homeassistant.components.guardian aioguardian==2021.11.0 From 0ae5b9e880a5efacd16e66b52719dbaaf85e3cf5 Mon Sep 17 00:00:00 2001 From: j-stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 12 Nov 2021 14:57:40 +0100 Subject: [PATCH 0422/1452] Add Jellyfin integration (#44401) * Initial commit after scaffold setup * Add initial config flow * Create initial entity * Ready for testing * Can browse, no result yet * Further improvements. Browsing is working. Now need to work on proper stream URL * Two valid URLs. Do not play in HA * First working version for music * Add thumbnail * Includes Artist->Album hierarchy * Add sorting of artists, albums and tracks * Remove code for video libraries * Improved code styling * Optimize configuration flow * Fix unit tests for config flow * Fix import order * Conform to style requirements * Use empty string as media type for non playables * 100% code coverage config_flow * Type async_get_media_source * Final docsctring fix after rebase * Add __init__ and media_source files to .coveragerc * Fix testing issues after rebase * Fix string format issues and relative const import * Remove unused manifest entries * Raise ConfigEntry exceptions, not log errors * Upgrade dependency to avoid WARNING on startup * Change to builtin tuple and list (deprecation) * Log broad exceptions * Add strict typing * Further type fixes after rebase * Retry when cannot connect, otherwise fail setup * Remove unused CONFIG_SCHEMA * Enable strict typing checks * FlowResultDict -> FlowResult * Code quality improvements * Resolve mypy.ini merge conflict * Use unique userid generated by Jellyfin * Update homeassistant/components/jellyfin/config_flow.py Remove connection class from config flow Co-authored-by: Milan Meulemans * Minor changes for additional checks after rebase * Remove title from string and translations * Changes wrt review * Fixes based on rebase and review suggestions * Move client initialization to separate file * Remove persistent_notification, add test const.py Co-authored-by: Milan Meulemans --- .coveragerc | 2 + .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/jellyfin/__init__.py | 36 ++ .../components/jellyfin/client_wrapper.py | 94 +++++ .../components/jellyfin/config_flow.py | 62 ++++ homeassistant/components/jellyfin/const.py | 40 +++ .../components/jellyfin/manifest.json | 13 + .../components/jellyfin/media_source.py | 326 ++++++++++++++++++ .../components/jellyfin/strings.json | 21 ++ .../components/jellyfin/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/jellyfin/__init__.py | 1 + tests/components/jellyfin/const.py | 17 + tests/components/jellyfin/test_config_flow.py | 164 +++++++++ 18 files changed, 817 insertions(+) create mode 100644 homeassistant/components/jellyfin/__init__.py create mode 100644 homeassistant/components/jellyfin/client_wrapper.py create mode 100644 homeassistant/components/jellyfin/config_flow.py create mode 100644 homeassistant/components/jellyfin/const.py create mode 100644 homeassistant/components/jellyfin/manifest.json create mode 100644 homeassistant/components/jellyfin/media_source.py create mode 100644 homeassistant/components/jellyfin/strings.json create mode 100644 homeassistant/components/jellyfin/translations/en.json create mode 100644 tests/components/jellyfin/__init__.py create mode 100644 tests/components/jellyfin/const.py create mode 100644 tests/components/jellyfin/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 53012078f33..c891a73e290 100644 --- a/.coveragerc +++ b/.coveragerc @@ -518,6 +518,8 @@ omit = homeassistant/components/isy994/switch.py homeassistant/components/itach/remote.py homeassistant/components/itunes/media_player.py + homeassistant/components/jellyfin/__init__.py + homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/__init__.py homeassistant/components/juicenet/const.py diff --git a/.strict-typing b/.strict-typing index 66a965b3221..01295668a64 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.image_processing.* homeassistant.components.input_select.* homeassistant.components.integration.* homeassistant.components.iqvia.* +homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* homeassistant.components.knx.* homeassistant.components.kraken.* diff --git a/CODEOWNERS b/CODEOWNERS index ad69391ad29..a43b51d8b5b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,7 @@ homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 homeassistant/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig +homeassistant/components/jellyfin/* @j-stienstra homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py new file mode 100644 index 00000000000..a58108b05ab --- /dev/null +++ b/homeassistant/components/jellyfin/__init__.py @@ -0,0 +1,36 @@ +"""The Jellyfin integration.""" +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Jellyfin from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + client = create_client() + try: + await validate_input(hass, dict(entry.data), client) + except CannotConnect as ex: + raise ConfigEntryNotReady("Cannot connect to Jellyfin server") from ex + except InvalidAuth: + _LOGGER.error("Failed to login to Jellyfin server") + return False + else: + hass.data[DOMAIN][entry.entry_id] = {DATA_CLIENT: client} + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.data[DOMAIN].pop(entry.entry_id) + + return True diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py new file mode 100644 index 00000000000..9f6380e2181 --- /dev/null +++ b/homeassistant/components/jellyfin/client_wrapper.py @@ -0,0 +1,94 @@ +"""Utility methods for initializing a Jellyfin client.""" +from __future__ import annotations + +import socket +from typing import Any +import uuid + +from jellyfin_apiclient_python import Jellyfin, JellyfinClient +from jellyfin_apiclient_python.api import API +from jellyfin_apiclient_python.connection_manager import ( + CONNECTION_STATE, + ConnectionManager, +) + +from homeassistant import exceptions +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .const import CLIENT_VERSION, USER_AGENT, USER_APP_NAME + + +async def validate_input( + hass: HomeAssistant, user_input: dict[str, Any], client: JellyfinClient +) -> str: + """Validate that the provided url and credentials can be used to connect.""" + url = user_input[CONF_URL] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + userid = await hass.async_add_executor_job( + _connect, client, url, username, password + ) + + return userid + + +def create_client() -> JellyfinClient: + """Create a new Jellyfin client.""" + jellyfin = Jellyfin() + client = jellyfin.get_client() + _setup_client(client) + return client + + +def _setup_client(client: JellyfinClient) -> None: + """Configure the Jellyfin client with a number of required properties.""" + player_name = socket.gethostname() + client_uuid = str(uuid.uuid4()) + + client.config.app(USER_APP_NAME, CLIENT_VERSION, player_name, client_uuid) + client.config.http(USER_AGENT) + + +def _connect(client: JellyfinClient, url: str, username: str, password: str) -> str: + """Connect to the Jellyfin server and assert that the user can login.""" + client.config.data["auth.ssl"] = url.startswith("https") + + _connect_to_address(client.auth, url) + _login(client.auth, url, username, password) + return _get_id(client.jellyfin) + + +def _connect_to_address(connection_manager: ConnectionManager, url: str) -> None: + """Connect to the Jellyfin server.""" + state = connection_manager.connect_to_address(url) + if state["State"] != CONNECTION_STATE["ServerSignIn"]: + raise CannotConnect + + +def _login( + connection_manager: ConnectionManager, + url: str, + username: str, + password: str, +) -> None: + """Assert that the user can log in to the Jellyfin server.""" + response = connection_manager.login(url, username, password) + if "AccessToken" not in response: + raise InvalidAuth + + +def _get_id(api: API) -> str: + """Set the unique userid from a Jellyfin server.""" + settings: dict[str, Any] = api.get_user_settings() + userid: str = settings["Id"] + return userid + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate the server is unreachable.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate the credentials are invalid.""" diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py new file mode 100644 index 00000000000..55de7c12e44 --- /dev/null +++ b/homeassistant/components/jellyfin/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for the Jellyfin integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Jellyfin.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a user defined configuration.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + errors: dict[str, str] = {} + + if user_input is not None: + client = create_client() + try: + userid = await validate_input(self.hass, user_input, client) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception as ex: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception(ex) + else: + await self.async_set_unique_id(userid) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input[CONF_URL], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py new file mode 100644 index 00000000000..d8379859e54 --- /dev/null +++ b/homeassistant/components/jellyfin/const.py @@ -0,0 +1,40 @@ +"""Constants for the Jellyfin integration.""" + +from typing import Final + +DOMAIN: Final = "jellyfin" + +CLIENT_VERSION: Final = "1.0" + +COLLECTION_TYPE_MOVIES: Final = "movies" +COLLECTION_TYPE_TVSHOWS: Final = "tvshows" +COLLECTION_TYPE_MUSIC: Final = "music" + +DATA_CLIENT: Final = "client" + +ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType" +ITEM_KEY_ID: Final = "Id" +ITEM_KEY_IMAGE_TAGS: Final = "ImageTags" +ITEM_KEY_INDEX_NUMBER: Final = "IndexNumber" +ITEM_KEY_MEDIA_SOURCES: Final = "MediaSources" +ITEM_KEY_MEDIA_TYPE: Final = "MediaType" +ITEM_KEY_NAME: Final = "Name" + +ITEM_TYPE_ALBUM: Final = "MusicAlbum" +ITEM_TYPE_ARTIST: Final = "MusicArtist" +ITEM_TYPE_AUDIO: Final = "Audio" +ITEM_TYPE_LIBRARY: Final = "CollectionFolder" + +MAX_IMAGE_WIDTH: Final = 500 +MAX_STREAMING_BITRATE: Final = "140000000" + + +MEDIA_SOURCE_KEY_PATH: Final = "Path" + +MEDIA_TYPE_AUDIO: Final = "Audio" +MEDIA_TYPE_NONE: Final = "" + +SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC] + +USER_APP_NAME: Final = "Home Assistant" +USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json new file mode 100644 index 00000000000..345cecc2eb6 --- /dev/null +++ b/homeassistant/components/jellyfin/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "jellyfin", + "name": "Jellyfin", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/jellyfin", + "requirements": [ + "jellyfin-apiclient-python==1.7.2" + ], + "iot_class": "local_polling", + "codeowners": [ + "@j-stienstra" + ] +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py new file mode 100644 index 00000000000..55e849a1f14 --- /dev/null +++ b/homeassistant/components/jellyfin/media_source.py @@ -0,0 +1,326 @@ +"""The Media Source implementation for the Jellyfin integration.""" +from __future__ import annotations + +import logging +import mimetypes +from typing import Any +import urllib.parse + +from jellyfin_apiclient_python.api import jellyfin_url +from jellyfin_apiclient_python.client import JellyfinClient + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_TRACK, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.core import HomeAssistant + +from .const import ( + COLLECTION_TYPE_MUSIC, + DATA_CLIENT, + DOMAIN, + ITEM_KEY_COLLECTION_TYPE, + ITEM_KEY_ID, + ITEM_KEY_IMAGE_TAGS, + ITEM_KEY_INDEX_NUMBER, + ITEM_KEY_MEDIA_SOURCES, + ITEM_KEY_MEDIA_TYPE, + ITEM_KEY_NAME, + ITEM_TYPE_ALBUM, + ITEM_TYPE_ARTIST, + ITEM_TYPE_AUDIO, + ITEM_TYPE_LIBRARY, + MAX_IMAGE_WIDTH, + MAX_STREAMING_BITRATE, + MEDIA_SOURCE_KEY_PATH, + MEDIA_TYPE_AUDIO, + MEDIA_TYPE_NONE, + SUPPORTED_COLLECTION_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up Jellyfin media source.""" + # Currently only a single Jellyfin server is supported + entry = hass.config_entries.async_entries(DOMAIN)[0] + + data = hass.data[DOMAIN][entry.entry_id] + client: JellyfinClient = data[DATA_CLIENT] + + return JellyfinSource(hass, client) + + +class JellyfinSource(MediaSource): + """Represents a Jellyfin server.""" + + name: str = "Jellyfin" + + def __init__(self, hass: HomeAssistant, client: JellyfinClient) -> None: + """Initialize the Jellyfin media source.""" + super().__init__(DOMAIN) + + self.hass = hass + + self.client = client + self.api = client.jellyfin + self.url = jellyfin_url(client, "") + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Return a streamable URL and associated mime type.""" + media_item = await self.hass.async_add_executor_job( + self.api.get_item, item.identifier + ) + + stream_url = self._get_stream_url(media_item) + mime_type = _media_mime_type(media_item) + + return PlayMedia(stream_url, mime_type) + + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: + """Return a browsable Jellyfin media source.""" + if not item.identifier: + return await self._build_libraries() + + media_item = await self.hass.async_add_executor_job( + self.api.get_item, item.identifier + ) + + item_type = media_item["Type"] + if item_type == ITEM_TYPE_LIBRARY: + return await self._build_library(media_item, True) + if item_type == ITEM_TYPE_ARTIST: + return await self._build_artist(media_item, True) + if item_type == ITEM_TYPE_ALBUM: + return await self._build_album(media_item, True) + + raise BrowseError(f"Unsupported item type {item_type}") + + async def _build_libraries(self) -> BrowseMediaSource: + """Return all supported libraries the user has access to as media sources.""" + base = BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + libraries = await self._get_libraries() + + base.children = [] + + for library in libraries: + base.children.append(await self._build_library(library, False)) + + return base + + async def _get_libraries(self) -> list[dict[str, Any]]: + """Return all supported libraries a user has access to.""" + response = await self.hass.async_add_executor_job(self.api.get_media_folders) + libraries = response["Items"] + result = [] + for library in libraries: + if ITEM_KEY_COLLECTION_TYPE in library: + if library[ITEM_KEY_COLLECTION_TYPE] in SUPPORTED_COLLECTION_TYPES: + result.append(library) + return result + + async def _build_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single library as a browsable media source.""" + collection_type = library[ITEM_KEY_COLLECTION_TYPE] + + if collection_type == COLLECTION_TYPE_MUSIC: + return await self._build_music_library(library, include_children) + + raise BrowseError(f"Unsupported collection type {collection_type}") + + async def _build_music_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single music library as a browsable media source.""" + library_id = library[ITEM_KEY_ID] + library_name = library[ITEM_KEY_NAME] + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=library_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=library_name, + can_play=False, + can_expand=True, + ) + + if include_children: + result.children_media_class = MEDIA_CLASS_ARTIST + result.children = await self._build_artists(library_id) # type: ignore[assignment] + + return result + + async def _build_artists(self, library_id: str) -> list[BrowseMediaSource]: + """Return all artists in the music library.""" + artists = await self._get_children(library_id, ITEM_TYPE_ARTIST) + artists = sorted(artists, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_artist(artist, False) for artist in artists] + + async def _build_artist( + self, artist: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single artist as a browsable media source.""" + artist_id = artist[ITEM_KEY_ID] + artist_name = artist[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(artist) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=artist_id, + media_class=MEDIA_CLASS_ARTIST, + media_content_type=MEDIA_TYPE_NONE, + title=artist_name, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MEDIA_CLASS_ALBUM + result.children = await self._build_albums(artist_id) # type: ignore[assignment] + + return result + + async def _build_albums(self, artist_id: str) -> list[BrowseMediaSource]: + """Return all albums of a single artist as browsable media sources.""" + albums = await self._get_children(artist_id, ITEM_TYPE_ALBUM) + albums = sorted(albums, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_album(album, False) for album in albums] + + async def _build_album( + self, album: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single album as a browsable media source.""" + album_id = album[ITEM_KEY_ID] + album_title = album[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(album) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=album_id, + media_class=MEDIA_CLASS_ALBUM, + media_content_type=MEDIA_TYPE_NONE, + title=album_title, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MEDIA_CLASS_TRACK + result.children = await self._build_tracks(album_id) # type: ignore[assignment] + + return result + + async def _build_tracks(self, album_id: str) -> list[BrowseMediaSource]: + """Return all tracks of a single album as browsable media sources.""" + tracks = await self._get_children(album_id, ITEM_TYPE_AUDIO) + tracks = sorted(tracks, key=lambda k: k[ITEM_KEY_INDEX_NUMBER]) # type: ignore[no-any-return] + return [self._build_track(track) for track in tracks] + + def _build_track(self, track: dict[str, Any]) -> BrowseMediaSource: + """Return a single track as a browsable media source.""" + track_id = track[ITEM_KEY_ID] + track_title = track[ITEM_KEY_NAME] + mime_type = _media_mime_type(track) + thumbnail_url = self._get_thumbnail_url(track) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=track_id, + media_class=MEDIA_CLASS_TRACK, + media_content_type=mime_type, + title=track_title, + can_play=True, + can_expand=False, + thumbnail=thumbnail_url, + ) + + return result + + async def _get_children( + self, parent_id: str, item_type: str + ) -> list[dict[str, Any]]: + """Return all children for the parent_id whose item type is item_type.""" + params = { + "Recursive": "true", + "ParentId": parent_id, + "IncludeItemTypes": item_type, + } + if item_type == ITEM_TYPE_AUDIO: + params["Fields"] = ITEM_KEY_MEDIA_SOURCES + + result = await self.hass.async_add_executor_job(self.api.user_items, "", params) + return result["Items"] # type: ignore[no-any-return] + + def _get_thumbnail_url(self, media_item: dict[str, Any]) -> str | None: + """Return the URL for the primary image of a media item if available.""" + image_tags = media_item[ITEM_KEY_IMAGE_TAGS] + + if "Primary" not in image_tags: + return None + + item_id = media_item[ITEM_KEY_ID] + return str(self.api.artwork(item_id, "Primary", MAX_IMAGE_WIDTH)) + + def _get_stream_url(self, media_item: dict[str, Any]) -> str: + """Return the stream URL for a media item.""" + media_type = media_item[ITEM_KEY_MEDIA_TYPE] + + if media_type == MEDIA_TYPE_AUDIO: + return self._get_audio_stream_url(media_item) + + raise BrowseError(f"Unsupported media type {media_type}") + + def _get_audio_stream_url(self, media_item: dict[str, Any]) -> str: + """Return the stream URL for a music media item.""" + item_id = media_item[ITEM_KEY_ID] + user_id = self.client.config.data["auth.user_id"] + device_id = self.client.config.data["app.device_id"] + api_key = self.client.config.data["auth.token"] + + params = urllib.parse.urlencode( + { + "UserId": user_id, + "DeviceId": device_id, + "api_key": api_key, + "MaxStreamingBitrate": MAX_STREAMING_BITRATE, + } + ) + + return f"{self.url}Audio/{item_id}/universal?{params}" + + +def _media_mime_type(media_item: dict[str, Any]) -> str: + """Return the mime type of a media item.""" + media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + path = media_source[MEDIA_SOURCE_KEY_PATH] + mime_type, _ = mimetypes.guess_type(path) + + if mime_type is not None: + return mime_type + + raise BrowseError(f"Unable to determine mime type for path {path}") diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json new file mode 100644 index 00000000000..7587c1d86d9 --- /dev/null +++ b/homeassistant/components/jellyfin/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/en.json b/homeassistant/components/jellyfin/translations/en.json new file mode 100644 index 00000000000..05cdf6e9ea9 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single Jellyfin server is currently supported" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4af6b656745..b620366a31b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -147,6 +147,7 @@ FLOWS = [ "islamic_prayer_times", "isy994", "izone", + "jellyfin", "juicenet", "keenetic_ndms2", "kmtronic", diff --git a/mypy.ini b/mypy.ini index 91bca58ba6b..f879ed5799b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -737,6 +737,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.jellyfin.*] +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.jewish_calendar.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index c800a9a9902..fd482b7ddda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -899,6 +899,9 @@ iperf3==0.1.11 # homeassistant.components.gogogate2 ismartgate==4.0.4 +# homeassistant.components.jellyfin +jellyfin-apiclient-python==1.7.2 + # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86940d279dd..9421c4f3213 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -557,6 +557,9 @@ iotawattpy==0.1.0 # homeassistant.components.gogogate2 ismartgate==4.0.4 +# homeassistant.components.jellyfin +jellyfin-apiclient-python==1.7.2 + # homeassistant.components.rest jsonpath==0.82 diff --git a/tests/components/jellyfin/__init__.py b/tests/components/jellyfin/__init__.py new file mode 100644 index 00000000000..e5ff9ab3207 --- /dev/null +++ b/tests/components/jellyfin/__init__.py @@ -0,0 +1 @@ +"""Tests for the jellyfin integration.""" diff --git a/tests/components/jellyfin/const.py b/tests/components/jellyfin/const.py new file mode 100644 index 00000000000..b33f00818b7 --- /dev/null +++ b/tests/components/jellyfin/const.py @@ -0,0 +1,17 @@ +"""Constants for the Jellyfin integration tests.""" + +from typing import Final + +from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE + +TEST_URL: Final = "https://example.com" +TEST_USERNAME: Final = "test-username" +TEST_PASSWORD: Final = "test-password" + +MOCK_SUCCESFUL_CONNECTION_STATE: Final = {"State": CONNECTION_STATE["ServerSignIn"]} +MOCK_SUCCESFUL_LOGIN_RESPONSE: Final = {"AccessToken": "Test"} + +MOCK_UNSUCCESFUL_CONNECTION_STATE: Final = {"State": CONNECTION_STATE["Unavailable"]} +MOCK_UNSUCCESFUL_LOGIN_RESPONSE: Final = {""} + +MOCK_USER_SETTINGS: Final = {"Id": "123"} diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py new file mode 100644 index 00000000000..cc23265e011 --- /dev/null +++ b/tests/components/jellyfin/test_config_flow.py @@ -0,0 +1,164 @@ +"""Test the jellyfin config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .const import ( + MOCK_SUCCESFUL_CONNECTION_STATE, + MOCK_SUCCESFUL_LOGIN_RESPONSE, + MOCK_UNSUCCESFUL_CONNECTION_STATE, + MOCK_UNSUCCESFUL_LOGIN_RESPONSE, + MOCK_USER_SETTINGS, + TEST_PASSWORD, + TEST_URL, + TEST_USERNAME, +) + +from tests.common import MockConfigEntry + + +async def test_abort_if_existing_entry(hass: HomeAssistant): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_form(hass: HomeAssistant): + """Test the complete configuration form.""" + 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.jellyfin.client_wrapper.ConnectionManager.connect_to_address", + return_value=MOCK_SUCCESFUL_CONNECTION_STATE, + ) as mock_connect, patch( + "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.login", + return_value=MOCK_SUCCESFUL_LOGIN_RESPONSE, + ) as mock_login, patch( + "homeassistant.components.jellyfin.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.jellyfin.client_wrapper.API.get_user_settings", + return_value=MOCK_USER_SETTINGS, + ) as mock_set_id: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_URL + assert result2["data"] == { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + assert len(mock_connect.mock_calls) == 1 + assert len(mock_login.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_set_id.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant): + """Test we handle an unreachable server.""" + 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.jellyfin.client_wrapper.ConnectionManager.connect_to_address", + return_value=MOCK_UNSUCCESFUL_CONNECTION_STATE, + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + assert len(mock_connect.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant): + """Test that we can handle invalid credentials.""" + 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.jellyfin.client_wrapper.ConnectionManager.connect_to_address", + return_value=MOCK_SUCCESFUL_CONNECTION_STATE, + ) as mock_connect, patch( + "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.login", + return_value=MOCK_UNSUCCESFUL_LOGIN_RESPONSE, + ) as mock_login: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + assert len(mock_connect.mock_calls) == 1 + assert len(mock_login.mock_calls) == 1 + + +async def test_form_exception(hass: HomeAssistant): + """Test we handle an unexpected exception during server setup.""" + 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.jellyfin.client_wrapper.ConnectionManager.connect_to_address", + side_effect=Exception("UnknownException"), + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + assert len(mock_connect.mock_calls) == 1 From bcd9f3c05ff88cce888a5ef903cc5d5637f3bce0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 12 Nov 2021 15:01:40 +0100 Subject: [PATCH 0423/1452] Correct end time for monthly statistics summary (#59551) * Correct end time for monthly statistics summary * Add tests --- .../components/recorder/statistics.py | 10 +-- tests/components/recorder/test_statistics.py | 89 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 83de258ea5d..938519a119a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -751,12 +751,12 @@ def _reduce_statistics_per_month( def period_start_end(time: datetime) -> tuple[datetime, datetime]: """Return the start and end of the period (month) time is within.""" - start = dt_util.as_utc( - dt_util.as_local(time).replace( - day=1, hour=0, minute=0, second=0, microsecond=0 - ) + start_local = dt_util.as_local(time).replace( + day=1, hour=0, minute=0, second=0, microsecond=0 ) - end = (start + timedelta(days=31)).replace(day=1) + start = dt_util.as_utc(start_local) + end_local = (start_local + timedelta(days=31)).replace(day=1) + end = dt_util.as_utc(end_local) return (start, end) return _reduce_statistics(stats, same_period, period_start_end, timedelta(days=31)) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 5b93d1f567d..d510d6ef612 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -544,6 +544,95 @@ def test_external_statistics_errors(hass_recorder, caplog): assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} +@pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +def test_monthly_statistics(hass_recorder, caplog, timezone): + """Test inserting external statistics.""" + dt_util.set_default_time_zone(dt_util.get_time_zone(timezone)) + + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_statistics = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics(hass, external_metadata, external_statistics) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="month") + sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00")) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": sep_start.isoformat(), + "end": sep_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": oct_start.isoformat(), + "end": oct_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(3.0), + "sum": approx(5.0), + }, + ] + } + + dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) + + def record_states(hass): """Record some test states. From 5e6ad8977a45911fc2fd7d4b9c1b8fac53676b98 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Nov 2021 15:42:46 +0100 Subject: [PATCH 0424/1452] Update vehicle to 0.2.0 (#59583) --- homeassistant/components/rdw/manifest.json | 2 +- homeassistant/components/rdw/sensor.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rdw/test_sensor.py | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index d7614c5bfa9..cf4e457c0ac 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -3,7 +3,7 @@ "name": "RDW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", - "requirements": ["vehicle==0.1.0"], + "requirements": ["vehicle==0.2.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index c6fc7157494..1ce63dfaead 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -46,10 +46,10 @@ SENSORS: tuple[RDWSensorEntityDescription, ...] = ( value_fn=lambda vehicle: vehicle.apk_expiration.isoformat(), ), RDWSensorEntityDescription( - key="name_registration_date", - name="Name Registration Date", + key="ascription_date", + name="Ascription Date", device_class=DEVICE_CLASS_DATE, - value_fn=lambda vehicle: vehicle.name_registration_date.isoformat(), + value_fn=lambda vehicle: vehicle.ascription_date.isoformat(), ), ) diff --git a/requirements_all.txt b/requirements_all.txt index fd482b7ddda..5818a162dda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.rdw -vehicle==0.1.0 +vehicle==0.2.0 # homeassistant.components.velbus velbus-aio==2021.11.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9421c4f3213..e5f067ba3a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.rdw -vehicle==0.1.0 +vehicle==0.2.0 # homeassistant.components.velbus velbus-aio==2021.11.6 diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index c67a7459b0d..3f6d4b9b732 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -34,13 +34,13 @@ async def test_vehicle_sensors( assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.name_registration_date") - entry = entity_registry.async_get("sensor.name_registration_date") + state = hass.states.get("sensor.ascription_date") + entry = entity_registry.async_get("sensor.ascription_date") assert entry assert state - assert entry.unique_id == "11ZKZ3_name_registration_date" + assert entry.unique_id == "11ZKZ3_ascription_date" assert state.state == "2021-11-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Name Registration Date" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ascription Date" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes From 8249959eacc640f62c617bc30b4e46212923c733 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Nov 2021 15:43:19 +0100 Subject: [PATCH 0425/1452] Update elgato to 2.2.0 (#59585) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 7a095cb5917..ebc2aca6527 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==2.1.1"], + "requirements": ["elgato==2.2.0"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 5818a162dda..2ecfd428e8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ ebusdpy==0.0.16 ecoaliface==0.4.0 # homeassistant.components.elgato -elgato==2.1.1 +elgato==2.2.0 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5f067ba3a4..44f8ca345e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -354,7 +354,7 @@ dsmr_parser==0.30 dynalite_devices==0.1.46 # homeassistant.components.elgato -elgato==2.1.1 +elgato==2.2.0 # homeassistant.components.elkm1 elkm1-lib==1.0.0 From 2841e402b918faa75a73b4f2126b412735a324eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Nov 2021 18:03:57 +0100 Subject: [PATCH 0426/1452] Cleanup extra dict from hass data in Elgato (#59587) --- homeassistant/components/elgato/__init__.py | 5 ++--- homeassistant/components/elgato/const.py | 3 --- homeassistant/components/elgato/light.py | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 21b8de53c17..0c42e359d07 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DATA_ELGATO_CLIENT, DOMAIN +from .const import DOMAIN PLATFORMS = [LIGHT_DOMAIN] @@ -31,8 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: logging.getLogger(__name__).debug("Unable to connect: %s", exception) raise ConfigEntryNotReady from exception - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = elgato hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index 03a52b7e305..6e63b598346 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -3,9 +3,6 @@ # Integration domain DOMAIN = "elgato" -# Home Assistant data keys -DATA_ELGATO_CLIENT = "elgato_client" - # Attributes ATTR_ON = "on" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 8ed443b65e1..d8a6e74c41a 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import ( async_get_current_platform, ) -from .const import DATA_ELGATO_CLIENT, DOMAIN, SERVICE_IDENTIFY +from .const import DOMAIN, SERVICE_IDENTIFY _LOGGER = logging.getLogger(__name__) @@ -37,7 +37,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Elgato Light based on a config entry.""" - elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] + elgato: Elgato = hass.data[DOMAIN][entry.entry_id] info = await elgato.info() settings = await elgato.settings() async_add_entities([ElgatoLight(elgato, info, settings)], True) From 7fff0a9865b756751aa0328a86d88d69c535c2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 12 Nov 2021 19:06:50 +0100 Subject: [PATCH 0427/1452] Bump Airthings library (#59595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/airthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airthings/manifest.json b/homeassistant/components/airthings/manifest.json index 749a5e44992..24585804b45 100644 --- a/homeassistant/components/airthings/manifest.json +++ b/homeassistant/components/airthings/manifest.json @@ -3,7 +3,7 @@ "name": "Airthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings", - "requirements": ["airthings_cloud==0.0.1"], + "requirements": ["airthings_cloud==0.1.0"], "codeowners": [ "@danielhiversen" ], diff --git a/requirements_all.txt b/requirements_all.txt index 2ecfd428e8c..5b4dd964d71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -273,7 +273,7 @@ aioymaps==1.2.1 airly==1.1.0 # homeassistant.components.airthings -airthings_cloud==0.0.1 +airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44f8ca345e9..5f67bf4ece0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ aioymaps==1.2.1 airly==1.1.0 # homeassistant.components.airthings -airthings_cloud==0.0.1 +airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 From 44ae0e214ab4a8a3be35c3eb315ded9594f5faeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 12 Nov 2021 19:07:35 +0100 Subject: [PATCH 0428/1452] Bump Adax library (#59592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 3d2c9273d05..cf5cbbd02a5 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.1.1" + "adax==0.2.0" ], "codeowners": [ "@danielhiversen" diff --git a/requirements_all.txt b/requirements_all.txt index 5b4dd964d71..566ae6e772a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ adafruit-circuitpython-dht==3.6.0 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.adax -adax==0.1.1 +adax==0.2.0 # homeassistant.components.androidtv adb-shell[async]==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f67bf4ece0..5b229bc8bd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ abodepy==1.2.0 accuweather==0.3.0 # homeassistant.components.adax -adax==0.1.1 +adax==0.2.0 # homeassistant.components.androidtv adb-shell[async]==0.4.0 From 4cfac18e1ad3aebb7a2e3eafc78d9fe89004224a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 12 Nov 2021 19:09:03 +0100 Subject: [PATCH 0429/1452] Fix firmware status check for Fritz (#59578) --- homeassistant/components/fritz/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d6df32fa931..09926e5d9ac 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -257,10 +257,10 @@ class FritzBoxTools: def _update_device_info(self) -> tuple[bool, str | None]: """Retrieve latest device information from the FRITZ!Box.""" - userinterface = self.connection.call_action("UserInterface1", "GetInfo") - return userinterface.get("NewUpgradeAvailable"), userinterface.get( + version = self.connection.call_action("UserInterface1", "GetInfo").get( "NewX_AVM-DE_Version" ) + return bool(version), version def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" From 49b04571729a38a604041f9fc96974ca927e7258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 12 Nov 2021 22:09:07 +0100 Subject: [PATCH 0430/1452] Bump pyMetno to 0.9.0 (#59609) --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 4ebbdd3b1e7..b6c3e565dc0 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,7 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.4"], + "requirements": ["pyMetno==0.9.0"], "codeowners": ["@danielhiversen", "@thimic"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 87981f085a6..ade1a149590 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,7 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.4"], + "requirements": ["pyMetno==0.9.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 566ae6e772a..e4e651445e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1318,7 +1318,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.4 +pyMetno==0.9.0 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b229bc8bd0..e3c9f76ac58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -793,7 +793,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.4 +pyMetno==0.9.0 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 From f00effaba28b9cde99047a2e31955fae55fe8538 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 13 Nov 2021 00:11:56 +0000 Subject: [PATCH 0431/1452] [ci skip] Translation update --- .../accuweather/translations/ja.json | 25 ++++++++++++++++ .../components/agent_dvr/translations/ja.json | 9 ++++++ .../components/airtouch4/translations/ja.json | 7 +++++ .../airvisual/translations/sensor.ja.json | 4 ++- .../alarmdecoder/translations/ja.json | 17 +++++++++++ .../ambiclimate/translations/ja.json | 9 ++++++ .../components/august/translations/ja.json | 6 ++++ .../azure_devops/translations/ja.json | 17 +++++++++++ .../binary_sensor/translations/he.json | 29 ++++++++++++++++-- .../components/blebox/translations/ja.json | 9 ++++++ .../components/braviatv/translations/ja.json | 10 +++++++ .../components/broadlink/translations/ja.json | 9 ++++++ .../components/camera/translations/ja.json | 3 +- .../components/cloud/translations/ja.json | 2 ++ .../cloudflare/translations/ja.json | 6 ++++ .../components/co2signal/translations/ja.json | 13 ++++++++ .../components/coinbase/translations/ja.json | 24 +++++++++++++-- .../components/cover/translations/ja.json | 3 +- .../crownstone/translations/ja.json | 1 + .../demo/translations/select.ja.json | 8 +++++ .../components/denonavr/translations/ja.json | 30 +++++++++++++++++++ .../components/ecobee/translations/ja.json | 9 ++++++ .../components/energy/translations/ja.json | 3 ++ .../components/enocean/translations/ja.json | 16 ++++++++++ .../evil_genius_labs/translations/he.json | 15 ++++++++++ .../fireservicerota/translations/ja.json | 9 ++++++ .../components/flipr/translations/ja.json | 3 ++ .../components/foscam/translations/ja.json | 7 +++++ .../freedompro/translations/ja.json | 10 +++++++ .../components/geofency/translations/ja.json | 9 ++++++ .../google_travel_time/translations/ja.json | 3 ++ .../components/gpslogger/translations/ja.json | 9 ++++++ .../components/hangouts/translations/ja.json | 1 + .../components/harmony/translations/ja.json | 12 ++++++++ .../components/hive/translations/ja.json | 6 ++++ .../components/homekit/translations/ja.json | 5 ++++ .../components/honeywell/translations/ja.json | 10 +++++++ .../components/iotawatt/translations/ja.json | 3 +- .../components/jellyfin/translations/ca.json | 21 +++++++++++++ .../components/jellyfin/translations/de.json | 21 +++++++++++++ .../components/jellyfin/translations/en.json | 4 +-- .../components/jellyfin/translations/et.json | 21 +++++++++++++ .../components/jellyfin/translations/he.json | 21 +++++++++++++ .../components/jellyfin/translations/hu.json | 21 +++++++++++++ .../components/jellyfin/translations/pl.json | 21 +++++++++++++ .../keenetic_ndms2/translations/ja.json | 3 +- .../components/konnected/translations/ja.json | 20 +++++++++++++ .../components/locative/translations/ja.json | 9 ++++++ .../logi_circle/translations/ja.json | 12 ++++++++ .../components/met/translations/ja.json | 9 ++++++ .../met_eireann/translations/ja.json | 9 ++++++ .../mobile_app/translations/ja.json | 10 +++++++ .../modem_callerid/translations/ja.json | 3 +- .../components/motioneye/translations/ja.json | 7 +++++ .../components/nam/translations/ja.json | 5 ++++ .../components/nanoleaf/translations/ja.json | 9 ++++++ .../components/nest/translations/ja.json | 5 ++++ .../components/netatmo/translations/ja.json | 9 ++++++ .../nfandroidtv/translations/ja.json | 9 ++++++ .../nmap_tracker/translations/ja.json | 6 +++- .../components/nws/translations/ja.json | 9 ++++++ .../components/onvif/translations/ja.json | 3 ++ .../components/openuv/translations/ja.json | 11 +++++++ .../openweathermap/translations/ja.json | 10 +++++++ .../ovo_energy/translations/ja.json | 11 ++++++- .../components/owntracks/translations/ja.json | 9 ++++++ .../components/ozw/translations/ja.json | 11 +++++++ .../components/person/translations/ja.json | 3 +- .../components/plaato/translations/ja.json | 11 +++++++ .../components/point/translations/ja.json | 5 ++++ .../progettihwsw/translations/ja.json | 12 ++++++++ .../rainforest_eagle/translations/ja.json | 12 ++++++++ .../components/rdw/translations/he.json | 10 ++++++- .../components/renault/translations/ja.json | 6 +++- .../components/roon/translations/ja.json | 9 ++++++ .../screenlogic/translations/ja.json | 18 ++++++++++- .../components/select/translations/ja.json | 3 ++ .../components/sense/translations/he.json | 3 +- .../components/sense/translations/ja.json | 3 +- .../components/sensor/translations/ja.json | 1 + .../simplisafe/translations/ja.json | 3 ++ .../components/sma/translations/ja.json | 9 ++++++ .../smartthings/translations/ja.json | 9 ++++++ .../components/smarttub/translations/ja.json | 9 ++++++ .../somfy_mylink/translations/ja.json | 15 ++++++++++ .../srp_energy/translations/ja.json | 3 ++ .../synology_dsm/translations/ja.json | 6 ++++ .../components/tag/translations/ja.json | 3 ++ .../tellduslive/translations/ja.json | 3 ++ .../totalconnect/translations/ja.json | 11 +++++++ .../components/traccar/translations/ja.json | 9 ++++++ .../tuya/translations/select.he.json | 4 +++ .../components/unifi/translations/ja.json | 9 ++++++ .../components/wallbox/translations/ja.json | 3 +- .../xiaomi_miio/translations/ja.json | 8 +++++ .../yamaha_musiccast/translations/ja.json | 8 +++++ .../components/zha/translations/ja.json | 3 +- .../zoneminder/translations/ja.json | 16 ++++++++++ .../components/zwave_js/translations/ja.json | 4 +++ 99 files changed, 899 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/ja.json create mode 100644 homeassistant/components/agent_dvr/translations/ja.json create mode 100644 homeassistant/components/airtouch4/translations/ja.json create mode 100644 homeassistant/components/alarmdecoder/translations/ja.json create mode 100644 homeassistant/components/ambiclimate/translations/ja.json create mode 100644 homeassistant/components/azure_devops/translations/ja.json create mode 100644 homeassistant/components/blebox/translations/ja.json create mode 100644 homeassistant/components/braviatv/translations/ja.json create mode 100644 homeassistant/components/broadlink/translations/ja.json create mode 100644 homeassistant/components/demo/translations/select.ja.json create mode 100644 homeassistant/components/denonavr/translations/ja.json create mode 100644 homeassistant/components/ecobee/translations/ja.json create mode 100644 homeassistant/components/energy/translations/ja.json create mode 100644 homeassistant/components/enocean/translations/ja.json create mode 100644 homeassistant/components/evil_genius_labs/translations/he.json create mode 100644 homeassistant/components/fireservicerota/translations/ja.json create mode 100644 homeassistant/components/freedompro/translations/ja.json create mode 100644 homeassistant/components/geofency/translations/ja.json create mode 100644 homeassistant/components/google_travel_time/translations/ja.json create mode 100644 homeassistant/components/gpslogger/translations/ja.json create mode 100644 homeassistant/components/harmony/translations/ja.json create mode 100644 homeassistant/components/honeywell/translations/ja.json create mode 100644 homeassistant/components/jellyfin/translations/ca.json create mode 100644 homeassistant/components/jellyfin/translations/de.json create mode 100644 homeassistant/components/jellyfin/translations/et.json create mode 100644 homeassistant/components/jellyfin/translations/he.json create mode 100644 homeassistant/components/jellyfin/translations/hu.json create mode 100644 homeassistant/components/jellyfin/translations/pl.json create mode 100644 homeassistant/components/locative/translations/ja.json create mode 100644 homeassistant/components/logi_circle/translations/ja.json create mode 100644 homeassistant/components/met/translations/ja.json create mode 100644 homeassistant/components/met_eireann/translations/ja.json create mode 100644 homeassistant/components/mobile_app/translations/ja.json create mode 100644 homeassistant/components/nanoleaf/translations/ja.json create mode 100644 homeassistant/components/nfandroidtv/translations/ja.json create mode 100644 homeassistant/components/nws/translations/ja.json create mode 100644 homeassistant/components/openweathermap/translations/ja.json create mode 100644 homeassistant/components/owntracks/translations/ja.json create mode 100644 homeassistant/components/ozw/translations/ja.json create mode 100644 homeassistant/components/plaato/translations/ja.json create mode 100644 homeassistant/components/progettihwsw/translations/ja.json create mode 100644 homeassistant/components/rainforest_eagle/translations/ja.json create mode 100644 homeassistant/components/roon/translations/ja.json create mode 100644 homeassistant/components/select/translations/ja.json create mode 100644 homeassistant/components/sma/translations/ja.json create mode 100644 homeassistant/components/smarttub/translations/ja.json create mode 100644 homeassistant/components/srp_energy/translations/ja.json create mode 100644 homeassistant/components/tag/translations/ja.json create mode 100644 homeassistant/components/totalconnect/translations/ja.json create mode 100644 homeassistant/components/traccar/translations/ja.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/ja.json create mode 100644 homeassistant/components/zoneminder/translations/ja.json diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json new file mode 100644 index 00000000000..098640fa79a --- /dev/null +++ b/homeassistant/components/accuweather/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u5929\u6c17\u4e88\u5831" + }, + "description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002", + "title": "AccuWeather\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ja.json b/homeassistant/components/agent_dvr/translations/ja.json new file mode 100644 index 00000000000..d22d77ac94c --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Agent DVR\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/ja.json b/homeassistant/components/airtouch4/translations/ja.json new file mode 100644 index 00000000000..5d516ff94ed --- /dev/null +++ b/homeassistant/components/airtouch4/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "no_units": "AirTouch 4 Groups\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.ja.json b/homeassistant/components/airvisual/translations/sensor.ja.json index 20e524fc72b..91bd016b0ac 100644 --- a/homeassistant/components/airvisual/translations/sensor.ja.json +++ b/homeassistant/components/airvisual/translations/sensor.ja.json @@ -12,7 +12,9 @@ "good": "\u826f\u597d", "hazardous": "\u5371\u967a", "moderate": "\u9069\u5ea6", - "unhealthy": "\u4e0d\u5065\u5eb7" + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_sensitive": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u3068\u3066\u3082\u4e0d\u5065\u5eb7" } } } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json new file mode 100644 index 00000000000..d6d06d76c0c --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -0,0 +1,17 @@ +{ + "options": { + "step": { + "zone_details": { + "data": { + "zone_name": "\u30be\u30fc\u30f3\u540d", + "zone_type": "\u30be\u30fc\u30f3\u306e\u7a2e\u985e" + } + }, + "zone_select": { + "data": { + "zone_number": "\u30be\u30fc\u30f3\u756a\u53f7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/ja.json b/homeassistant/components/ambiclimate/translations/ja.json new file mode 100644 index 00000000000..7bf2e014e5e --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth": { + "title": "Ambiclimate\u3092\u8a8d\u8a3c\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index e410a949d7b..6944a72ef1d 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -1,6 +1,12 @@ { "config": { "step": { + "user_validate": { + "data": { + "login_method": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5" + }, + "description": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u5b50\u30e1\u30fc\u30eb\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u8a71\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f \"+NNNNNNNNN\" \u5f62\u5f0f\u306e\u96fb\u8a71\u756a\u53f7\u3067\u3059\u3002" + }, "validation": { "title": "2\u8981\u7d20\u8a8d\u8a3c" } diff --git a/homeassistant/components/azure_devops/translations/ja.json b/homeassistant/components/azure_devops/translations/ja.json new file mode 100644 index 00000000000..71f05935cb7 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "personal_access_token": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3(PAT)" + } + }, + "user": { + "data": { + "personal_access_token": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3(PAT)" + }, + "description": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u305f\u3081\u306bAzureDevOps\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u30d1\u30fc\u30bd\u30ca\u30eb\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306f\u3001\u30d7\u30e9\u30a4\u30d9\u30fc\u30c8\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u5834\u5408\u306e\u307f\u5fc5\u8981\u3067\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index f6018cfe08a..43b3c4ff246 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", "is_not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", "is_not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "is_not_running": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc", + "is_not_tampered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d7\u05d1\u05dc\u05d4", "is_not_unsafe": "{entity_name} \u05d1\u05d8\u05d5\u05d7", "is_occupied": "{entity_name} \u05ea\u05e4\u05d5\u05e1", "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", "is_present": "{entity_name} \u05e0\u05d5\u05db\u05d7", "is_problem": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d1\u05e2\u05d9\u05d4", + "is_running": "{entity_name} \u05e4\u05d5\u05e2\u05dc", "is_smoke": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e2\u05e9\u05df", "is_sound": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e6\u05dc\u05d9\u05dc", + "is_tampered": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d7\u05d1\u05dc\u05d4", "is_unsafe": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d1\u05d8\u05d5\u05d7", "is_update": "\u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df \u05e2\u05d1\u05d5\u05e8 {entity_name}", "is_vibration": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e8\u05d8\u05d8" @@ -52,6 +56,8 @@ "connected": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", "gas": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d2\u05d6", "hot": "{entity_name} \u05e0\u05e2\u05e9\u05d4 \u05d7\u05dd", + "is_not_tampered": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d7\u05d1\u05dc\u05d4", + "is_tampered": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d7\u05d1\u05dc\u05d4", "light": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", "locked": "{entity_name} \u05e0\u05e2\u05d5\u05dc", "moist": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d7", @@ -66,17 +72,18 @@ "no_update": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05de\u05e2\u05d5\u05d3\u05db\u05df", "no_vibration": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8", "not_bat_low": "{entity_name} \u05e1\u05d5\u05dc\u05dc\u05d4 \u05e8\u05d2\u05d9\u05dc\u05d4", - "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8", + "not_cold": "{entity_name} \u05e0\u05e2\u05e9\u05d4 \u05dc\u05d0 \u05e7\u05e8", "not_connected": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", "not_hot": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d0 \u05d7\u05dd", "not_locked": "{entity_name} \u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc", "not_moist": "{entity_name} \u05d4\u05ea\u05d9\u05d9\u05d1\u05e9", "not_moving": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d5\u05d6", "not_occupied": "{entity_name} \u05dc\u05d0 \u05e0\u05ea\u05e4\u05e1", - "not_opened": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "not_opened": "{entity_name} \u05e0\u05e1\u05d2\u05e8", "not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", "not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", "not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "not_running": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc \u05e2\u05d5\u05d3", "not_unsafe": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d1\u05d8\u05d5\u05d7", "occupied": "{entity_name} \u05e0\u05ea\u05e4\u05e1", "opened": "{entity_name} \u05e0\u05e4\u05ea\u05d7", @@ -84,6 +91,7 @@ "powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", "present": "{entity_name} \u05e0\u05d5\u05db\u05d7", "problem": "{entity_name} \u05d4\u05d7\u05dc\u05d4 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d1\u05e2\u05d9\u05d4", + "running": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05e4\u05e2\u05d5\u05dc", "smoke": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e2\u05e9\u05df", "sound": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e6\u05dc\u05d9\u05dc", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", @@ -93,6 +101,19 @@ "vibration": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8" } }, + "device_class": { + "cold": "\u05e7\u05d5\u05e8", + "gas": "\u05d2\u05d6", + "heat": "\u05d7\u05d5\u05dd", + "moisture": "\u05dc\u05d7\u05d5\u05ea", + "motion": "\u05ea\u05e0\u05d5\u05e2\u05d4", + "occupancy": "\u05ea\u05e4\u05d5\u05e1\u05d4", + "power": "\u05db\u05d7", + "problem": "\u05d1\u05e2\u05d9\u05d4", + "smoke": "\u05e2\u05e9\u05df", + "sound": "\u05e7\u05d5\u05dc", + "vibration": "\u05e8\u05d8\u05d8" + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", @@ -170,6 +191,10 @@ "off": "\u05ea\u05e7\u05d9\u05df", "on": "\u05d1\u05e2\u05d9\u05d4" }, + "running": { + "off": "\u05dc\u05d0 \u05e4\u05d5\u05e2\u05dc", + "on": "\u05e4\u05d5\u05e2\u05dc" + }, "safety": { "off": "\u05d1\u05d8\u05d5\u05d7", "on": "\u05dc\u05d0 \u05d1\u05d8\u05d5\u05d7" diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json new file mode 100644 index 00000000000..9fcabfc354a --- /dev/null +++ b/homeassistant/components/blebox/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "BleBox\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json new file mode 100644 index 00000000000..abc3a96b00f --- /dev/null +++ b/homeassistant/components/braviatv/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "authorize": { + "description": "\u30bd\u30cb\u30fc\u88fd\u306e\u30c6\u30ec\u30d3 \u30d6\u30e9\u30d3\u30a2\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Sony Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json new file mode 100644 index 00000000000..5bdedf46405 --- /dev/null +++ b/homeassistant/components/broadlink/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth": { + "title": "\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/camera/translations/ja.json b/homeassistant/components/camera/translations/ja.json index 4ab2b8ed3b6..27569fb66f3 100644 --- a/homeassistant/components/camera/translations/ja.json +++ b/homeassistant/components/camera/translations/ja.json @@ -1,7 +1,8 @@ { "state": { "_": { - "idle": "\u30a2\u30a4\u30c9\u30eb" + "idle": "\u30a2\u30a4\u30c9\u30eb", + "streaming": "\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0" } }, "title": "\u30ab\u30e1\u30e9" diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json index 5cd3600a13a..a2aee4b89e2 100644 --- a/homeassistant/components/cloud/translations/ja.json +++ b/homeassistant/components/cloud/translations/ja.json @@ -1,6 +1,8 @@ { "system_health": { "info": { + "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08", "remote_server": "\u30ea\u30e2\u30fc\u30c8\u30b5\u30fc\u30d0\u30fc" } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index ab542270bc6..99d952cb71f 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -5,6 +5,12 @@ "data": { "description": "Cloudflare\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" } + }, + "zone": { + "data": { + "zone": "\u30be\u30fc\u30f3" + }, + "title": "\u66f4\u65b0\u3059\u308b\u30be\u30fc\u30f3\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/co2signal/translations/ja.json b/homeassistant/components/co2signal/translations/ja.json index 768ebb8c233..00fb0c4eb0f 100644 --- a/homeassistant/components/co2signal/translations/ja.json +++ b/homeassistant/components/co2signal/translations/ja.json @@ -5,6 +5,19 @@ }, "error": { "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f" + }, + "step": { + "country": { + "data": { + "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9" + } + }, + "user": { + "data": { + "location": "\uff5e\u306e\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3059\u308b" + }, + "description": "\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001https://co2signal.com/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index edce5477528..ad0d0bfef01 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -1,10 +1,30 @@ { + "config": { + "step": { + "user": { + "data": { + "api_token": "API\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", + "currencies": "\u53e3\u5ea7\u6b8b\u9ad8 \u901a\u8ca8", + "exchange_rates": "\u70ba\u66ff\u30ec\u30fc\u30c8" + }, + "description": "Coinbase\u304b\u3089\u63d0\u4f9b\u3055\u308c\u305fAPI\u30ad\u30fc\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Coinbase API\u30ad\u30fc\u306e\u8a73\u7d30" + } + } + }, "options": { + "error": { + "currency_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u901a\u8ca8\u6b8b\u9ad8\u306e1\u3064\u4ee5\u4e0a\u304c\u3001Coinbase API\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "exchange_rate_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u70ba\u66ff\u30ec\u30fc\u30c8\u306e1\u3064\u4ee5\u4e0a\u304cCoinbase\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + }, "step": { "init": { "data": { - "exchange_base": "\u70ba\u66ff\u30ec\u30fc\u30c8\u30bb\u30f3\u30b5\u30fc\u306e\u57fa\u6e96\u901a\u8ca8\u3002" - } + "account_balance_currencies": "\u30a6\u30a9\u30ec\u30c3\u30c8\u306e\u6b8b\u9ad8\u3092\u5831\u544a\u3059\u308b\u3002", + "exchange_base": "\u70ba\u66ff\u30ec\u30fc\u30c8\u30bb\u30f3\u30b5\u30fc\u306e\u57fa\u6e96\u901a\u8ca8\u3002", + "exchange_rate_currencies": "\u30ec\u30dd\u30fc\u30c8\u3059\u3079\u304d\u70ba\u66ff\u30ec\u30fc\u30c8" + }, + "description": "Coinbase\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } } diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index 859240315bf..7d05d1616ea 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -4,5 +4,6 @@ "closed": "\u9589\u9396", "opening": "\u6249" } - } + }, + "title": "\u30ab\u30d0\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json index 080a7c3a812..22651f0cf8a 100644 --- a/homeassistant/components/crownstone/translations/ja.json +++ b/homeassistant/components/crownstone/translations/ja.json @@ -9,6 +9,7 @@ }, "step": { "usb_config": { + "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e\u3059\u308b\u304b\u3001USB\u30c9\u30f3\u30b0\u30eb\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u306a\u3044\u5834\u5408\u306f\u3001\"USB\u3092\u4f7f\u7528\u3057\u306a\u3044\" \u3092\u9078\u629e\u3057\u307e\u3059\u3002 \n\n VID 10C4 \u3067 PID EA60 \u306a\u30c7\u30d0\u30a4\u30b9\u3092\u898b\u3064\u3051\u307e\u3059\u3002", "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" }, "usb_manual_config": { diff --git a/homeassistant/components/demo/translations/select.ja.json b/homeassistant/components/demo/translations/select.ja.json new file mode 100644 index 00000000000..884adcdd5e1 --- /dev/null +++ b/homeassistant/components/demo/translations/select.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "demo__speed": { + "ludicrous_speed": "\u3070\u304b\u3052\u305f\u901f\u5ea6", + "ridiculous_speed": "\u3068\u3093\u3067\u3082\u306a\u3044\u901f\u5ea6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json new file mode 100644 index 00000000000..8d5fb06452b --- /dev/null +++ b/homeassistant/components/denonavr/translations/ja.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "not_denonavr_manufacturer": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u3055\u308c\u305f\u30e1\u30fc\u30ab\u30fc\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "not_denonavr_missing": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u60c5\u5831\u304c\u5b8c\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, + "error": { + "discovery_error": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "confirm": { + "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + }, + "user": { + "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "zone2": "\u30be\u30fc\u30f32\u306e\u8a2d\u5b9a", + "zone3": "\u30be\u30fc\u30f33\u306e\u8a2d\u5b9a" + }, + "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json new file mode 100644 index 00000000000..64ba77ca99e --- /dev/null +++ b/homeassistant/components/ecobee/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "authorize": { + "title": "ecobee.com\u306e\u30a2\u30d7\u30ea\u3092\u8a8d\u8a3c\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ja.json b/homeassistant/components/energy/translations/ja.json new file mode 100644 index 00000000000..6dd35f6b4d7 --- /dev/null +++ b/homeassistant/components/energy/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30a8\u30cd\u30eb\u30ae\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json new file mode 100644 index 00000000000..2989d7396d6 --- /dev/null +++ b/homeassistant/components/enocean/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "detect": { + "data": { + "path": "USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9" + } + }, + "manual": { + "data": { + "path": "USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/he.json b/homeassistant/components/evil_genius_labs/translations/he.json new file mode 100644 index 00000000000..00011f86933 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ja.json b/homeassistant/components/fireservicerota/translations/ja.json new file mode 100644 index 00000000000..fa8742d5a87 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth": { + "description": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u304c\u7121\u52b9\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u518d\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index 027577bb587..a3133a4d75b 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "no_flipr_id_found": "\u73fe\u5728\u3001\u3042\u306a\u305f\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u95a2\u9023\u4ed8\u3051\u3089\u308c\u3066\u3044\u308bflipr id\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u307e\u305a\u306f\u3001Flipr\u306e\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u3067\u52d5\u4f5c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "flipr_id": { "data": { diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 1a6115bb697..b59ab8ea799 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_response": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u7121\u52b9\u306a\u5fdc\u7b54" + }, + "step": { + "user": { + "data": { + "stream": "\u30b9\u30c8\u30ea\u30fc\u30e0" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/ja.json b/homeassistant/components/freedompro/translations/ja.json new file mode 100644 index 00000000000..4709477a760 --- /dev/null +++ b/homeassistant/components/freedompro/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "https://home.freedompro.eu \u304b\u3089\u53d6\u5f97\u3057\u305fAPI\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Freedompro API\u30ad\u30fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json new file mode 100644 index 00000000000..9db14d7eecd --- /dev/null +++ b/homeassistant/components/geofency/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Geofency Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json new file mode 100644 index 00000000000..f725fc427a6 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "Google\u30de\u30c3\u30d7\u306e\u79fb\u52d5\u6642\u9593(Travel Time)" +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json new file mode 100644 index 00000000000..a16b0cd96db --- /dev/null +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "GPSLogger Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json index a6ead972aee..dcfa29bcf54 100644 --- a/homeassistant/components/hangouts/translations/ja.json +++ b/homeassistant/components/hangouts/translations/ja.json @@ -15,6 +15,7 @@ }, "user": { "data": { + "authorization_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9(\u624b\u52d5\u8a8d\u8a3c\u6642\u306b\u5fc5\u8981)", "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json new file mode 100644 index 00000000000..d17b8c8fe15 --- /dev/null +++ b/homeassistant/components/harmony/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "link": { + "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "user": { + "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 91680d93274..bba52fff3c8 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -6,6 +6,12 @@ "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, "title": "\u30cf\u30a4\u30d6(Hive)2\u8981\u7d20\u8a8d\u8a3c\u3002" + }, + "reauth": { + "title": "Hive\u30ed\u30b0\u30a4\u30f3" + }, + "user": { + "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } } diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 35ada78aa74..5b17e41a8d1 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -1,6 +1,11 @@ { "options": { "step": { + "advanced": { + "data": { + "devices": "\u30c7\u30d0\u30a4\u30b9(\u30c8\u30ea\u30ac\u30fc)" + } + }, "cameras": { "data": { "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9" diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json new file mode 100644 index 00000000000..667f57a90ab --- /dev/null +++ b/homeassistant/components/honeywell/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "mytotalconnectcomfort.com \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3059\u308b\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json index 1c43df55349..0c352aab54e 100644 --- a/homeassistant/components/iotawatt/translations/ja.json +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -4,7 +4,8 @@ "auth": { "data": { "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "IoTawatt\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u8a8d\u8a3c\u304c\u5fc5\u8981\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3001\u9001\u4fe1 \u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/jellyfin/translations/ca.json b/homeassistant/components/jellyfin/translations/ca.json new file mode 100644 index 00000000000..feb588e0fd9 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/de.json b/homeassistant/components/jellyfin/translations/de.json new file mode 100644 index 00000000000..c94ba7e9662 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/en.json b/homeassistant/components/jellyfin/translations/en.json index 05cdf6e9ea9..fd85dc7acb6 100644 --- a/homeassistant/components/jellyfin/translations/en.json +++ b/homeassistant/components/jellyfin/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Only a single Jellyfin server is currently supported" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "url": "URL", "password": "Password", + "url": "URL", "username": "Username" } } diff --git a/homeassistant/components/jellyfin/translations/et.json b/homeassistant/components/jellyfin/translations/et.json new file mode 100644 index 00000000000..65665ff244f --- /dev/null +++ b/homeassistant/components/jellyfin/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks seadistus." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/he.json b/homeassistant/components/jellyfin/translations/he.json new file mode 100644 index 00000000000..89da99fcf9b --- /dev/null +++ b/homeassistant/components/jellyfin/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/hu.json b/homeassistant/components/jellyfin/translations/hu.json new file mode 100644 index 00000000000..d92411833c4 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/pl.json b/homeassistant/components/jellyfin/translations/pl.json new file mode 100644 index 00000000000..ffa33d7e6ed --- /dev/null +++ b/homeassistant/components/jellyfin/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index cda97ffefbc..8117d70f255 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093" + "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093", + "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic\u306e\u30eb\u30fc\u30bf\u30fc\u3067\u306f\u3042\u308a\u307e\u305b\u3093" } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index bb053e2bb5d..4a371f44c36 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -1,6 +1,26 @@ { "options": { "step": { + "options_io": { + "data": { + "1": "\u30be\u30fc\u30f31", + "2": "\u30be\u30fc\u30f32", + "3": "\u30be\u30fc\u30f33", + "4": "\u30be\u30fc\u30f34", + "5": "\u30be\u30fc\u30f35", + "6": "\u30be\u30fc\u30f36", + "7": "\u30be\u30fc\u30f37" + } + }, + "options_io_ext": { + "data": { + "10": "\u30be\u30fc\u30f310", + "11": "\u30be\u30fc\u30f311", + "12": "\u30be\u30fc\u30f312", + "8": "\u30be\u30fc\u30f38", + "9": "\u30be\u30fc\u30f39" + } + }, "options_misc": { "data": { "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json new file mode 100644 index 00000000000..f2c0b380c41 --- /dev/null +++ b/homeassistant/components/locative/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Locative Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ja.json b/homeassistant/components/logi_circle/translations/ja.json new file mode 100644 index 00000000000..ac4af42d1a3 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "auth": { + "title": "Logi Circle\u3067\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "title": "\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/ja.json b/homeassistant/components/met/translations/ja.json new file mode 100644 index 00000000000..686d04d1c11 --- /dev/null +++ b/homeassistant/components/met/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Meteorologisk institutt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/ja.json b/homeassistant/components/met_eireann/translations/ja.json new file mode 100644 index 00000000000..169c0bcb555 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Met \u00c9ireann Public Weather Forecast API\u306e\u6c17\u8c61\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3059\u308b\u5834\u6240\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json new file mode 100644 index 00000000000..c41021733c2 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea" +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index d330c5d9da3..d93d7aa4f0e 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -5,7 +5,8 @@ }, "step": { "usb_confirm": { - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", + "title": "Phone\u30e2\u30c7\u30e0" }, "user": { "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 836d8d5987d..416d79e77e8 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "hassio_confirm": { + "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306emotionEye" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index a50e0a74f37..4dbfbba3caf 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -2,6 +2,11 @@ "config": { "abort": { "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "Nettigo Air Monitor\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json new file mode 100644 index 00000000000..4484053cf9a --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "link": { + "title": "Nanoleaf\u3092\u30ea\u30f3\u30af\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 2bbd3b1b9fa..50db0a874e8 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -22,5 +22,10 @@ "title": "Nest\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u30ea\u30f3\u30af" } } + }, + "device_automation": { + "trigger_type": { + "camera_person": "\u691c\u51fa\u3055\u308c\u305f\u4eba" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 6fe56757002..277eca2fbdb 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -5,5 +5,14 @@ "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } + }, + "options": { + "step": { + "public_weather_areas": { + "data": { + "weather_areas": "\u5929\u6c17\u4e88\u5831\u306e\u30a8\u30ea\u30a2" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json new file mode 100644 index 00000000000..ec79a0aa382 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Android TV / Fire TV\u306e\u901a\u77e5" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index c225fb6bf6f..474b0c3e9aa 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { + "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u69cb\u6210\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" @@ -17,10 +19,12 @@ "step": { "init": { "data": { + "consider_home": "\u898b\u3089\u308c\u306a\u304b\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u3064\u79d2\u6570\u3002", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1" } } } - } + }, + "title": "Nmap\u30c8\u30e9\u30c3\u30ab\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json new file mode 100644 index 00000000000..29be7bcc491 --- /dev/null +++ b/homeassistant/components/nws/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30a2\u30e1\u30ea\u30ab\u56fd\u7acb\u6c17\u8c61\u5c40\u306b\u63a5\u7d9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 4ddc01954f7..809fd863fc9 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -1,6 +1,9 @@ { "config": { "step": { + "configure": { + "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" + }, "user": { "data": { "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" diff --git a/homeassistant/components/openuv/translations/ja.json b/homeassistant/components/openuv/translations/ja.json index b5d1f619ed1..69018ec5e5e 100644 --- a/homeassistant/components/openuv/translations/ja.json +++ b/homeassistant/components/openuv/translations/ja.json @@ -14,5 +14,16 @@ "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "\u30d7\u30ed\u30c6\u30af\u30b7\u30e7\u30f3\u30a6\u30a3\u30f3\u30c9\u30a6\u306eUV\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306e\u958b\u59cb", + "to_window": "\u30d7\u30ed\u30c6\u30af\u30b7\u30e7\u30f3\u30a6\u30a3\u30f3\u30c9\u30a6\u306eUV\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306e\u7d42\u4e86" + }, + "title": "OpenUV\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json new file mode 100644 index 00000000000..5d22a859d4a --- /dev/null +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "OpenWeatherMap" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json index ca9690c66f4..bda76d76231 100644 --- a/homeassistant/components/ovo_energy/translations/ja.json +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -1,5 +1,14 @@ { "config": { - "flow_title": "{username}" + "flow_title": "{username}", + "step": { + "reauth": { + "description": "OVO Energy\u306e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u73fe\u5728\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "user": { + "description": "OVO Energy\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u8a2d\u5b9a\u3057\u3066\u3001\u30a8\u30cd\u30eb\u30ae\u30fc\u4f7f\u7528\u91cf\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u3002", + "title": "OVO Energy\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3059\u308b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json new file mode 100644 index 00000000000..b24fadd37ee --- /dev/null +++ b/homeassistant/components/owntracks/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "OwnTracks\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json new file mode 100644 index 00000000000..4660eaa0e1c --- /dev/null +++ b/homeassistant/components/ozw/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "start_addon": { + "data": { + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/person/translations/ja.json b/homeassistant/components/person/translations/ja.json index 6679d6cca06..432fd29c964 100644 --- a/homeassistant/components/person/translations/ja.json +++ b/homeassistant/components/person/translations/ja.json @@ -4,5 +4,6 @@ "home": "\u5728\u5b85", "not_home": "\u5916\u51fa" } - } + }, + "title": "\u4eba" } \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json new file mode 100644 index 00000000000..2f5a90a0dfe --- /dev/null +++ b/homeassistant/components/plaato/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "api_method": { + "data": { + "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 09e474b989c..36653567d6a 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -2,6 +2,11 @@ "config": { "error": { "follow_link": "\u9001\u4fe1 \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "auth": { + "title": "\u8a8d\u8a3c\u30dd\u30a4\u30f3\u30c8" + } } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ja.json b/homeassistant/components/progettihwsw/translations/ja.json new file mode 100644 index 00000000000..881829af573 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "relay_modes": { + "title": "\u30ea\u30ec\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "user": { + "title": "\u30dc\u30fc\u30c9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/ja.json b/homeassistant/components/rainforest_eagle/translations/ja.json new file mode 100644 index 00000000000..b4e8b694fde --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cloud_id": "\u30af\u30e9\u30a6\u30c9ID", + "install_code": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u30b3\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/he.json b/homeassistant/components/rdw/translations/he.json index 067d7208d62..5acc1e1e9f3 100644 --- a/homeassistant/components/rdw/translations/he.json +++ b/homeassistant/components/rdw/translations/he.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown_license_plate": "\u05dc\u05d5\u05d7\u05d9\u05ea \u05e8\u05d9\u05e9\u05d5\u05d9 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "license_plate": "\u05dc\u05d5\u05d7\u05d9\u05ea \u05e8\u05d9\u05e9\u05d5\u05d9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 33c0087570d..4fc5fcc136d 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "kamereon_no_account": "Kamereon\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "kamereon": { "data": { @@ -10,7 +13,8 @@ "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { "data": { diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json new file mode 100644 index 00000000000..62ef7f1165e --- /dev/null +++ b/homeassistant/components/roon/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "link": { + "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index e8940bef26a..a48fe4e5ef6 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -1,5 +1,21 @@ { "config": { - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "gateway_entry": { + "description": "ScreenLogic\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "ScreenLogic" + }, + "gateway_select": { + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "title": "ScreenLogic" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/select/translations/ja.json b/homeassistant/components/select/translations/ja.json new file mode 100644 index 00000000000..71d0ae895b1 --- /dev/null +++ b/homeassistant/components/select/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u9078\u629e" +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json index ecb8a74bc6f..c4e87259193 100644 --- a/homeassistant/components/sense/translations/he.json +++ b/homeassistant/components/sense/translations/he.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "\u05d3\u05d5\u05d0\"\u05dc", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df" } } } diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 45721ebbdc1..190f6374790 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" - } + }, + "title": "Sense Energy Monitor\u306b\u63a5\u7d9a\u3059\u308b" } } } diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 3c8b8c75a65..38f961987e3 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -12,6 +12,7 @@ "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb" }, "trigger_type": { + "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrogen_monoxide": "{entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrous_oxide": "{entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 6193be02ed5..d645425297e 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -14,6 +14,9 @@ "description": "SimpliSafe Web\u30a2\u30d7\u30ea\u306eURL\u304b\u3089\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b:", "title": "\u627f\u8a8d\u7d42\u4e86" }, + "mfa": { + "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" + }, "user": { "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066 Submit \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json new file mode 100644 index 00000000000..654ca692761 --- /dev/null +++ b/homeassistant/components/sma/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "SMA Solar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 93db79466a4..19dfe5d9d0e 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -1,7 +1,16 @@ { "config": { "error": { + "token_forbidden": "\u30c8\u30fc\u30af\u30f3\u306b\u5fc5\u8981\u306aOAuth\u30b9\u30b3\u30fc\u30d7(OAuth scopes)\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "webhook_error": "SmartThings\u304cWebhook URL\u3092\u691c\u8a3c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002Webhook URL\u304c\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u53ef\u80fd\u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "authorize": { + "title": "Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "pat": { + "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } } } } \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json new file mode 100644 index 00000000000..7ade94ecd29 --- /dev/null +++ b/homeassistant/components/smarttub/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30ed\u30b0\u30a4\u30f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index f10941b3aac..d14696e3db3 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -1,5 +1,20 @@ { "config": { "flow_title": "{mac} ({ip})" + }, + "options": { + "step": { + "entity_config": { + "data": { + "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, + "target_config": { + "data": { + "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + }, + "title": "MyLink\u30ab\u30d0\u30fc\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json new file mode 100644 index 00000000000..400505d7385 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "SRP\u30a8\u30cd\u30eb\u30ae\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 1b69cbdf7c8..2553803cd7a 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "reconfigure_successful": "\u518d\u8a2d\u5b9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "step": { + "reauth": { + "description": "\u7406\u7531: {details}" + }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/tag/translations/ja.json b/homeassistant/components/tag/translations/ja.json new file mode 100644 index 00000000000..4639c5a2989 --- /dev/null +++ b/homeassistant/components/tag/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30bf\u30b0" +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index 8aa43369fee..ea2f8e7f82d 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -1,6 +1,9 @@ { "config": { "step": { + "auth": { + "title": "TelldusLive\u306b\u5bfe\u3057\u3066\u8a8d\u8a3c\u3059\u308b" + }, "user": { "description": "\u7a7a", "title": "\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u3092\u9078\u3076\u3002" diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json new file mode 100644 index 00000000000..c799eb7494e --- /dev/null +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "locations": { + "data": { + "usercode": "\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json new file mode 100644 index 00000000000..5c11725c884 --- /dev/null +++ b/homeassistant/components/traccar/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Traccar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index cdf4f19e72d..c9e9a0bd138 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -1,5 +1,9 @@ { "state": { + "tuya__basic_anti_flickr": { + "1": "50 \u05d4\u05e8\u05e5", + "2": "60 \u05d4\u05e8\u05e5" + }, "tuya__basic_nightvision": { "1": "\u05db\u05d1\u05d5\u05d9", "2": "\u05de\u05d5\u05e4\u05e2\u05dc" diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 76bbf4a22bf..e6f19a35999 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -9,5 +9,14 @@ "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u8a2d\u5b9a" } } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index c2bcf5f1c56..65203cc8c7e 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -7,5 +7,6 @@ } } } - } + }, + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 2703b61759d..90971677c2c 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -4,6 +4,14 @@ "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" }, "step": { + "cloud": { + "data": { + "cloud_country": "\u30af\u30e9\u30a6\u30c9\u30b5\u30fc\u30d0\u30fc\u306e\u56fd", + "cloud_password": "\u30af\u30e9\u30a6\u30c9\u30d1\u30b9\u30ef\u30fc\u30c9", + "cloud_username": "\u30af\u30e9\u30a6\u30c9\u306e\u30e6\u30fc\u30b6\u30fc\u540d", + "manual": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b(\u975e\u63a8\u5968)" + } + }, "connect": { "data": { "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" diff --git a/homeassistant/components/yamaha_musiccast/translations/ja.json b/homeassistant/components/yamaha_musiccast/translations/ja.json new file mode 100644 index 00000000000..c1dde7656af --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/ja.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "no_musiccast_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fMusicCast\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3088\u3046\u3067\u3059\u3002" + }, + "flow_title": "MusicCast: {name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 49e9b6f9c21..f5373fce98a 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}" } diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json new file mode 100644 index 00000000000..3f16215b0c0 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + }, + "create_entry": { + "default": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u304c\u8ffd\u52a0\u3055\u308c\u307e\u3057\u305f\u3002" + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "title": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 79c0e6419ad..5041e55ef5e 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -6,6 +6,7 @@ "step": { "configure_addon": { "data": { + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc", "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", @@ -35,6 +36,9 @@ } }, "options": { + "abort": { + "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3055\u308c\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "configure_addon": { "data": { From 9769a8c08e68a0c616004f651bbc36a6a44505f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Nov 2021 01:47:39 -0600 Subject: [PATCH 0432/1452] Revert "Bump async_timeout to 4.0.1" (#59601) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 47f5c4b6230..2e22a5d5e82 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiohttp==3.8.0 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.22.12 -async_timeout==4.0.1 +async_timeout==4.0.0 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/requirements.txt b/requirements.txt index a39c203fa0e..cc595332395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # Home Assistant Core aiohttp==3.8.0 astral==2.2 -async_timeout==4.0.1 +async_timeout==4.0.0 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/setup.py b/setup.py index 2e416a3545e..43ff77f6110 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ "aiohttp==3.8.0", "astral==2.2", - "async_timeout==4.0.1", + "async_timeout==4.0.0", "attrs==21.2.0", "awesomeversion==21.10.1", 'backports.zoneinfo;python_version<"3.9"', From 9d674af5660a325ed5834c8171bfe8b2144d11b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 13 Nov 2021 11:54:24 +0100 Subject: [PATCH 0433/1452] Bump open-garage to 0.2.0 (#59608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/opengarage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 929a0a0080d..cd7d0f48eec 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -6,7 +6,7 @@ "@danielhiversen" ], "requirements": [ - "open-garage==0.1.6" + "open-garage==0.2.0" ], "iot_class": "local_polling", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index e4e651445e1..02c0f9fa688 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==1.2.0 # homeassistant.components.opengarage -open-garage==0.1.6 +open-garage==0.2.0 # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3c9f76ac58..5b89e207593 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -685,7 +685,7 @@ ondilo==0.2.0 onvif-zeep-async==1.2.0 # homeassistant.components.opengarage -open-garage==0.1.6 +open-garage==0.2.0 # homeassistant.components.openerz openerz-api==0.1.0 From c70f06be480e20c79d982ec4b309e9d9d3a8ed74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Nov 2021 12:22:07 +0100 Subject: [PATCH 0434/1452] Upgrade twentemilieu to 0.4.2 (#59599) --- .../components/twentemilieu/config_flow.py | 2 +- .../components/twentemilieu/manifest.json | 2 +- .../components/twentemilieu/sensor.py | 66 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 870ce591788..6a70fbbdcbe 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -53,7 +53,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): twentemilieu = TwenteMilieu( post_code=user_input[CONF_POST_CODE], house_number=user_input[CONF_HOUSE_NUMBER], - house_letter=user_input.get(CONF_HOUSE_LETTER), + house_letter=user_input.get(CONF_HOUSE_LETTER, ""), session=session, ) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index a56154cba71..48aa908356a 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,7 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.3.0"], + "requirements": ["twentemilieu==0.4.2"], "codeowners": ["@frenck"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 4b76f3f475b..d13ae3a7165 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -1,14 +1,9 @@ """Support for Twente Milieu sensors.""" from __future__ import annotations -from twentemilieu import ( - WASTE_TYPE_NON_RECYCLABLE, - WASTE_TYPE_ORGANIC, - WASTE_TYPE_PAPER, - WASTE_TYPE_PLASTIC, - TwenteMilieu, - TwenteMilieuConnectionError, -) +from dataclasses import dataclass + +from twentemilieu import TwenteMilieu, TwenteMilieuConnectionError, WasteType from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -23,28 +18,47 @@ from .const import DATA_UPDATE, DOMAIN PARALLEL_UPDATES = 1 -SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=WASTE_TYPE_NON_RECYCLABLE, - name=f"{WASTE_TYPE_NON_RECYCLABLE} Waste Pickup", + +@dataclass +class TwenteMilieuSensorDescriptionMixin: + """Define an entity description mixin.""" + + waste_type: WasteType + + +@dataclass +class TwenteMilieuSensorDescription( + SensorEntityDescription, TwenteMilieuSensorDescriptionMixin +): + """Describe an Ambient PWS binary sensor.""" + + +SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( + TwenteMilieuSensorDescription( + key="Non-recyclable", + waste_type=WasteType.NON_RECYCLABLE, + name="Non-recyclable Waste Pickup", icon="mdi:delete-empty", device_class=DEVICE_CLASS_DATE, ), - SensorEntityDescription( - key=WASTE_TYPE_ORGANIC, - name=f"{WASTE_TYPE_ORGANIC} Waste Pickup", + TwenteMilieuSensorDescription( + key="Organic", + waste_type=WasteType.ORGANIC, + name="Organic Waste Pickup", icon="mdi:delete-empty", device_class=DEVICE_CLASS_DATE, ), - SensorEntityDescription( - key=WASTE_TYPE_PAPER, - name=f"{WASTE_TYPE_PAPER} Waste Pickup", + TwenteMilieuSensorDescription( + key="Paper", + waste_type=WasteType.PAPER, + name="Paper Waste Pickup", icon="mdi:delete-empty", device_class=DEVICE_CLASS_DATE, ), - SensorEntityDescription( - key=WASTE_TYPE_PLASTIC, - name=f"{WASTE_TYPE_PLASTIC} Waste Pickup", + TwenteMilieuSensorDescription( + key="Plastic", + waste_type=WasteType.PACKAGES, + name="Packages Waste Pickup", icon="mdi:delete-empty", device_class=DEVICE_CLASS_DATE, ), @@ -76,13 +90,14 @@ async def async_setup_entry( class TwenteMilieuSensor(SensorEntity): """Defines a Twente Milieu sensor.""" + entity_description: TwenteMilieuSensorDescription _attr_should_poll = False def __init__( self, twentemilieu: TwenteMilieu, unique_id: str, - description: SensorEntityDescription, + description: TwenteMilieuSensorDescription, ) -> None: """Initialize the Twente Milieu entity.""" self.entity_description = description @@ -104,6 +119,7 @@ class TwenteMilieuSensor(SensorEntity): async def async_update(self) -> None: """Update Twente Milieu entity.""" - next_pickup = await self._twentemilieu.next_pickup(self.entity_description.key) - if next_pickup is not None: - self._attr_native_value = next_pickup.date().isoformat() + pickups = await self._twentemilieu.update() + self._attr_native_value = None + if pickup := pickups.get(self.entity_description.waste_type): + self._attr_native_value = pickup.isoformat() diff --git a/requirements_all.txt b/requirements_all.txt index 02c0f9fa688..66b15e3d266 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ transmissionrpc==0.11 tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu -twentemilieu==0.3.0 +twentemilieu==0.4.2 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b89e207593..6b6468e9591 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ transmissionrpc==0.11 tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu -twentemilieu==0.3.0 +twentemilieu==0.4.2 # homeassistant.components.twilio twilio==6.32.0 From 8b6b4d7f8a5424705737ac189d00d69eed33f8bf Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 13 Nov 2021 12:59:48 +0100 Subject: [PATCH 0435/1452] Fix favorite RPM max value in Xiaomi Miio (#59631) --- homeassistant/components/xiaomi_miio/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 161a690a0df..87ccdd63d0f 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -177,7 +177,7 @@ NUMBER_TYPES = { icon="mdi:star-cog", unit_of_measurement="rpm", min_value=300, - max_value=2300, + max_value=2200, step=10, method="async_set_favorite_rpm", entity_category=ENTITY_CATEGORY_CONFIG, From 435e1fb092231774a24bb8c7b3ab1a6e4888a20d Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Sat, 13 Nov 2021 04:00:36 -0800 Subject: [PATCH 0436/1452] Always use a step size of 1 for z-wave js fans (#59622) --- homeassistant/components/zwave_js/fan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 6ee709893cb..4b4f23a85d2 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -103,6 +103,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) + @property + def percentage_step(self) -> float: + """Return the step size for percentage.""" + return 1 + @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" From 8ce3f182958ea89f65dcdd78181d95a212afa4b0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 13 Nov 2021 04:45:18 -0800 Subject: [PATCH 0437/1452] Remove unused stream_type attribute (#59625) Remove this stream type that was previously renamed to frontend_stream_type --- homeassistant/components/camera/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4aa443c2ed6..8534120a77f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -570,8 +570,6 @@ class Camera(Entity): if self.frontend_stream_type: attrs["frontend_stream_type"] = self.frontend_stream_type - # Remove after home-assistant/frontend#10298 is merged into nightly - attrs["stream_type"] = self.frontend_stream_type return attrs From 27b2aa04c9ce9330cbabbb29ccc84ca452c08282 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 13 Nov 2021 12:45:42 +0000 Subject: [PATCH 0438/1452] Add System Bridge keyboard services (#53893) * Add keyboard services * Extract to voluptuous validator * Cleanup * Lint * Catch StopIteration * Match validator Co-authored-by: Martin Hjelmare * Raise from Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 167 ++++++++++++------ .../components/system_bridge/services.yaml | 46 +++++ 2 files changed, 161 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index b3f481141b8..cf50368c34c 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -10,6 +10,7 @@ from systembridge import Bridge from systembridge.client import BridgeClient from systembridge.exceptions import BridgeAuthenticationException from systembridge.objects.command.response import CommandResponse +from systembridge.objects.keyboard.payload import KeyboardPayload import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -21,7 +22,11 @@ from homeassistant.const import ( CONF_PORT, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -39,20 +44,15 @@ PLATFORMS = ["binary_sensor", "sensor"] CONF_ARGUMENTS = "arguments" CONF_BRIDGE = "bridge" +CONF_KEY = "key" +CONF_MODIFIERS = "modifiers" +CONF_TEXT = "text" CONF_WAIT = "wait" SERVICE_SEND_COMMAND = "send_command" -SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( - { - vol.Required(CONF_BRIDGE): cv.string, - vol.Required(CONF_COMMAND): cv.string, - vol.Optional(CONF_ARGUMENTS, []): cv.string, - } -) SERVICE_OPEN = "open" -SERVICE_OPEN_SCHEMA = vol.Schema( - {vol.Required(CONF_BRIDGE): cv.string, vol.Required(CONF_PATH): cv.string} -) +SERVICE_SEND_KEYPRESS = "send_keypress" +SERVICE_SEND_TEXT = "send_text" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -113,25 +113,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): return True + def valid_device(device: str): + """Check device is valid.""" + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(device) + if device_entry is not None: + try: + return next( + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id in device_entry.config_entries + ) + except StopIteration as exception: + raise vol.Invalid from exception + raise vol.Invalid(f"Device {device} does not exist") + async def handle_send_command(call): """Handle the send_command service call.""" - device_registry = dr.async_get(hass) - device_id = call.data[CONF_BRIDGE] - device_entry = device_registry.async_get(device_id) - if device_entry is None: - _LOGGER.warning("Missing device: %s", device_id) - return + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + call.data[CONF_BRIDGE] + ] + bridge: Bridge = coordinator.bridge command = call.data[CONF_COMMAND] - arguments = shlex.split(call.data.get(CONF_ARGUMENTS, "")) - - entry_id = next( - entry.entry_id - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id in device_entry.config_entries - ) - coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id] - bridge: Bridge = coordinator.bridge + arguments = shlex.split(call.data[CONF_ARGUMENTS]) _LOGGER.debug( "Command payload: %s", @@ -141,55 +146,113 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: response: CommandResponse = await bridge.async_send_command( {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False} ) - if response.success: - _LOGGER.debug( - "Sent command. Response message was: %s", response.message - ) - else: - _LOGGER.warning( - "Error sending command. Response message was: %s", response.message + if not response.success: + raise HomeAssistantError( + f"Error sending command. Response message was: {response.message}" ) except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - _LOGGER.warning("Error sending command. Error was: %s", exception) + raise HomeAssistantError("Error sending command") from exception + _LOGGER.debug("Sent command. Response message was: %s", response.message) async def handle_open(call): """Handle the open service call.""" - device_registry = dr.async_get(hass) - device_id = call.data[CONF_BRIDGE] - device_entry = device_registry.async_get(device_id) - if device_entry is None: - _LOGGER.warning("Missing device: %s", device_id) - return + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + call.data[CONF_BRIDGE] + ] + bridge: Bridge = coordinator.bridge path = call.data[CONF_PATH] - entry_id = next( - entry.entry_id - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id in device_entry.config_entries - ) - coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id] - bridge: Bridge = coordinator.bridge - _LOGGER.debug("Open payload: %s", {CONF_PATH: path}) try: await bridge.async_open({CONF_PATH: path}) - _LOGGER.debug("Sent open request") except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - _LOGGER.warning("Error sending. Error was: %s", exception) + raise HomeAssistantError("Error sending") from exception + _LOGGER.debug("Sent open request") + + async def handle_send_keypress(call): + """Handle the send_keypress service call.""" + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + call.data[CONF_BRIDGE] + ] + bridge: Bridge = coordinator.data + + keyboard_payload: KeyboardPayload = { + CONF_KEY: call.data[CONF_KEY], + CONF_MODIFIERS: shlex.split(call.data.get(CONF_MODIFIERS, "")), + } + + _LOGGER.debug("Keypress payload: %s", keyboard_payload) + try: + await bridge.async_send_keypress(keyboard_payload) + except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: + raise HomeAssistantError("Error sending") from exception + _LOGGER.debug("Sent keypress request") + + async def handle_send_text(call): + """Handle the send_keypress service call.""" + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + call.data[CONF_BRIDGE] + ] + bridge: Bridge = coordinator.data + + keyboard_payload: KeyboardPayload = {CONF_TEXT: call.data[CONF_TEXT]} + + _LOGGER.debug("Text payload: %s", keyboard_payload) + try: + await bridge.async_send_keypress(keyboard_payload) + except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: + raise HomeAssistantError("Error sending") from exception + _LOGGER.debug("Sent text request") hass.services.async_register( DOMAIN, SERVICE_SEND_COMMAND, handle_send_command, - schema=SERVICE_SEND_COMMAND_SCHEMA, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_ARGUMENTS, ""): cv.string, + }, + ), ) hass.services.async_register( DOMAIN, SERVICE_OPEN, handle_open, - schema=SERVICE_OPEN_SCHEMA, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_PATH): cv.string, + }, + ), + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SEND_KEYPRESS, + handle_send_keypress, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_KEY): cv.string, + vol.Optional(CONF_MODIFIERS): cv.string, + }, + ), + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SEND_TEXT, + handle_send_text, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_TEXT): cv.string, + }, + ), ) # Reload entry when its updated. diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index 3f79f441415..aff0094501e 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -42,3 +42,49 @@ open: example: "https://www.home-assistant.io" selector: text: +send_keypress: + name: Send Keyboard Keypress + description: Sends a keyboard keypress. + fields: + bridge: + name: Bridge + description: The server to send the command to. + required: true + selector: + device: + integration: system_bridge + key: + name: Key + description: "Key to press. List available here: http://robotjs.io/docs/syntax#keys" + required: true + example: "audio_play" + selector: + text: + modifiers: + name: Modifiers + description: "List of modifier(s). Accepts alt, command/win, control, and shift." + required: false + default: "" + example: + - "control" + - "shift" + selector: + text: +send_text: + name: Send Keyboard Text + description: Sends text for the server to type. + fields: + bridge: + name: Bridge + description: The server to send the command to. + required: true + selector: + device: + integration: system_bridge + text: + name: Text + description: "Text to type." + required: true + example: "Hello world" + selector: + text: From 68e80f14318e1aafc3afe4ec42370fb0a8f30c20 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 13 Nov 2021 08:14:49 -0500 Subject: [PATCH 0439/1452] Remove yaml config from modem_callerid (#59526) --- .../components/modem_callerid/config_flow.py | 29 +----------- .../components/modem_callerid/const.py | 1 - .../components/modem_callerid/sensor.py | 45 +++---------------- .../modem_callerid/test_config_flow.py | 37 ++------------- 4 files changed, 10 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index da552b26beb..51671dcbe16 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -1,10 +1,9 @@ """Config flow for Modem Caller ID integration.""" from __future__ import annotations -import logging from typing import Any -from phone_modem import DEFAULT_PORT, PhoneModem +from phone_modem import PhoneModem import serial.tools.list_ports from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol @@ -16,8 +15,6 @@ from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DOMAIN, EXCEPTIONS -_LOGGER = logging.getLogger(__name__) - DATA_SCHEMA = vol.Schema({"name": str, "device": str}) @@ -102,30 +99,6 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(unused_ports)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - if self._async_current_entries(): - _LOGGER.warning( - "Loading Modem_callerid via platform setup is deprecated; Please remove it from your configuration" - ) - if CONF_DEVICE not in config: - config[CONF_DEVICE] = DEFAULT_PORT - ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - for port in ports: - if port.device == config[CONF_DEVICE]: - if ( - await self.validate_device_errors( - dev_path=port.device, - unique_id=_generate_unique_id(port), - ) - is None - ): - return self.async_create_entry( - title=config.get(CONF_NAME, DEFAULT_NAME), - data={CONF_DEVICE: port.device}, - ) - return self.async_abort(reason="cannot_connect") - async def validate_device_errors( self, dev_path: str, unique_id: str ) -> dict[str, str] | None: diff --git a/homeassistant/components/modem_callerid/const.py b/homeassistant/components/modem_callerid/const.py index b05623f8d8b..0b01c3b761f 100644 --- a/homeassistant/components/modem_callerid/const.py +++ b/homeassistant/components/modem_callerid/const.py @@ -5,7 +5,6 @@ from phone_modem import exceptions from serial import SerialException DATA_KEY_API = "api" -DATA_KEY_COORDINATOR = "coordinator" DEFAULT_NAME = "Phone Modem" DOMAIN = "modem_callerid" ICON = "mdi:phone-classic" diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index a1df80f6bcb..94c7c51ee80 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,48 +1,15 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" from __future__ import annotations -from phone_modem import DEFAULT_PORT, PhoneModem -import voluptuous as vol +from phone_modem import PhoneModem -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_DEVICE, - CONF_NAME, - EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP, STATE_IDLE from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers import entity_platform -from .const import CID, DATA_KEY_API, DEFAULT_NAME, DOMAIN, ICON, SERVICE_REJECT_CALL - -# Deprecated in Home Assistant 2021.10 -PLATFORM_SCHEMA = cv.deprecated( - vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE, default=DEFAULT_PORT): cv.string, - } - ) - ) -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigEntry, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Modem Caller ID component.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) +from .const import CID, DATA_KEY_API, DOMAIN, ICON, SERVICE_REJECT_CALL async def async_setup_entry( diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 5a2e4e5fd6d..4d76ae8a944 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -5,8 +5,8 @@ import phone_modem import serial.tools.list_ports from homeassistant.components import usb -from homeassistant.components.modem_callerid.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USB, SOURCE_USER +from homeassistant.components.modem_callerid.const import DOMAIN +from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -15,7 +15,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import CONF_DATA, IMPORT_DATA, _patch_config_flow_modem +from . import _patch_config_flow_modem DISCOVERY_INFO = { "device": phone_modem.DEFAULT_PORT, @@ -171,34 +171,3 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" - - -@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) -async def test_flow_import(hass: HomeAssistant): - """Test import step.""" - with _patch_config_flow_modem(AsyncMock()): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"] == CONF_DATA - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_flow_import_cannot_connect(hass: HomeAssistant): - """Test import connection error.""" - with _patch_config_flow_modem(AsyncMock()) as modemmock: - modemmock.side_effect = phone_modem.exceptions.SerialError - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" From cce441332fe71fbb28303d8ddef15b9c20bf7704 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Nov 2021 14:15:14 +0100 Subject: [PATCH 0440/1452] Add button platform to Elgato (#59628) Co-authored-by: Martin Hjelmare --- homeassistant/components/elgato/__init__.py | 3 +- homeassistant/components/elgato/button.py | 62 +++++++++++++++++ tests/components/elgato/test_button.py | 77 +++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elgato/button.py create mode 100644 tests/components/elgato/test_button.py diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 0c42e359d07..c074b3303e7 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -3,6 +3,7 @@ import logging from elgato import Elgato, ElgatoConnectionError +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT @@ -12,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -PLATFORMS = [LIGHT_DOMAIN] +PLATFORMS = [BUTTON_DOMAIN, LIGHT_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/elgato/button.py b/homeassistant/components/elgato/button.py new file mode 100644 index 00000000000..4e77f05e415 --- /dev/null +++ b/homeassistant/components/elgato/button.py @@ -0,0 +1,62 @@ +"""Support for Elgato button.""" +from __future__ import annotations + +import logging + +from elgato import Elgato, ElgatoError, Info + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Elgato button based on a config entry.""" + elgato: Elgato = hass.data[DOMAIN][entry.entry_id] + info = await elgato.info() + async_add_entities([ElgatoIdentifyButton(elgato, info)]) + + +class ElgatoIdentifyButton(ButtonEntity): + """Defines an Elgato identify button.""" + + def __init__(self, elgato: Elgato, info: Info) -> None: + """Initialize the button entity.""" + self.elgato = elgato + self._info = info + self.entity_description = ButtonEntityDescription( + key="identify", + name="Identify", + icon="mdi:help", + entity_category=ENTITY_CATEGORY_CONFIG, + ) + self._attr_unique_id = f"{info.serial_number}_{self.entity_description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Elgato Light.""" + return DeviceInfo( + identifiers={(DOMAIN, self._info.serial_number)}, + manufacturer="Elgato", + model=self._info.product_name, + name=self._info.product_name, + sw_version=f"{self._info.firmware_version} ({self._info.firmware_build_number})", + ) + + async def async_press(self) -> None: + """Identify the light, will make it blink.""" + try: + await self.elgato.identify() + except ElgatoError: + _LOGGER.exception("An error occurred while identifying the Elgato Light") diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py new file mode 100644 index 00000000000..21211596d0c --- /dev/null +++ b/tests/components/elgato/test_button.py @@ -0,0 +1,77 @@ +"""Tests for the Elgato Light button platform.""" +from unittest.mock import patch + +from elgato import ElgatoError +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + ENTITY_CATEGORY_CONFIG, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.elgato import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.mark.freeze_time("2021-11-13 11:48:00") +async def test_button_identify( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Elgato identify button.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("button.identify") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:help" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.identify") + assert entry + assert entry.unique_id == "CN11A1A00001_identify" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + with patch( + "homeassistant.components.elgato.light.Elgato.identify" + ) as mock_identify: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.identify"}, + blocking=True, + ) + + assert len(mock_identify.mock_calls) == 1 + mock_identify.assert_called_with() + + state = hass.states.get("button.identify") + assert state + assert state.state == "2021-11-13T11:48:00+00:00" + + +async def test_button_identify_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test an error occurs with the Elgato identify button.""" + await init_integration(hass, aioclient_mock) + + with patch( + "homeassistant.components.elgato.light.Elgato.identify", + side_effect=ElgatoError, + ) as mock_identify: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.identify"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(mock_identify.mock_calls) == 1 + assert "An error occurred while identifying the Elgato Light" in caplog.text From 303b6bc4f1d63e30c37d71e461e0e8b078028a33 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 13 Nov 2021 06:19:04 -0700 Subject: [PATCH 0441/1452] Add configuration URL for Ambient PWS (#59616) --- homeassistant/components/ambient_station/__init__.py | 5 +++++ homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 190ed6dc59e..c1ceb29fff1 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -5,6 +5,7 @@ from typing import Any from aioambient import Websocket from aioambient.errors import WebsocketError +from aioambient.util import get_public_device_id from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -220,11 +221,15 @@ class AmbientWeatherEntity(Entity): ) -> None: """Initialize the entity.""" self._ambient = ambient + + public_device_id = get_public_device_id(mac_address) self._attr_device_info = DeviceInfo( + configuration_url=f"https://ambientweather.net/dashboard/{public_device_id}", identifiers={(DOMAIN, mac_address)}, manufacturer="Ambient Weather", name=station_name, ) + self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 857ce6de585..33cb84706ff 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==2021.10.1"], + "requirements": ["aioambient==2021.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 66b15e3d266..a2fde3b7549 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.1 +aioambient==2021.11.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b6468e9591..4d96061c116 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.1 +aioambient==2021.11.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 From b2f2c76e5a45b5d8f2faf2ded551588fd5e918c5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 13 Nov 2021 06:20:26 -0700 Subject: [PATCH 0442/1452] Fix broken Ambient PWS config entry migration (#59618) --- homeassistant/components/ambient_station/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index c1ceb29fff1..3b318ba5a81 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -17,11 +17,13 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_call_later from .const import ( @@ -109,14 +111,15 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # 1 -> 2: Unique ID format changed, so delete and re-import: if version == 1: - dev_reg = await hass.helpers.device_registry.async_get_registry() - dev_reg.async_clear_config_entry(entry) + dev_reg = dr.async_get(hass) + dev_reg.async_clear_config_entry(entry.entry_id) - en_reg = await hass.helpers.entity_registry.async_get_registry() - en_reg.async_clear_config_entry(entry) + en_reg = er.async_get(hass) + en_reg.async_clear_config_entry(entry.entry_id) version = entry.version = 2 hass.config_entries.async_update_entry(entry) + LOGGER.info("Migration to version %s successful", version) return True From b5de99ebfcb3b9f641c812892b3637d5baf6d13c Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Sat, 13 Nov 2021 08:30:47 -0500 Subject: [PATCH 0443/1452] Fix inverted tuya doorcontact_state (#59427) --- homeassistant/components/tuya/const.py | 2 +- homeassistant/components/tuya/cover.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a9f7afb0ec5..f0707b1b37a 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -190,7 +190,7 @@ class DPCode(str, Enum): DEHUMIDITY_SET_VALUE = "dehumidify_set_value" DO_NOT_DISTURB = "do_not_disturb" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor - DOORCONTACT_STATE_2 = "doorcontact_state_3" + DOORCONTACT_STATE_2 = "doorcontact_state_2" DOORCONTACT_STATE_3 = "doorcontact_state_3" ELECTRICITY_LEFT = "electricity_left" FAN_DIRECTION = "fan_direction" # Fan direction diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 0b8a658fd7c..b5ac5645cd2 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -36,6 +36,7 @@ class TuyaCoverEntityDescription(CoverEntityDescription): """Describe an Tuya cover entity.""" current_state: DPCode | None = None + current_state_inverse: bool = False current_position: DPCode | None = None set_position: DPCode | None = None @@ -75,18 +76,21 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { key=DPCode.SWITCH_1, name="Door", current_state=DPCode.DOORCONTACT_STATE, + current_state_inverse=True, device_class=DEVICE_CLASS_GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_2, name="Door 2", current_state=DPCode.DOORCONTACT_STATE_2, + current_state_inverse=True, device_class=DEVICE_CLASS_GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_3, name="Door 3", current_state=DPCode.DOORCONTACT_STATE_3, + current_state_inverse=True, device_class=DEVICE_CLASS_GARAGE, ), ), @@ -262,7 +266,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): @property def is_closed(self) -> bool | None: - """Return is cover is closed.""" + """Return true if cover is closed.""" if ( self.entity_description.current_state is not None and ( @@ -272,7 +276,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): ) is not None ): - return current_state in (True, "fully_close") + return self.entity_description.current_state_inverse is not ( + current_state in (False, "fully_close") + ) if (position := self.current_cover_position) is not None: return position == 0 From aa89c670eb55abfc188cace8f51cae5824cae4f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Nov 2021 15:26:57 +0100 Subject: [PATCH 0444/1452] Upgrade wled to 0.9.0 (#59635) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 5ece2d4b9d8..c513f424e08 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.8.0"], + "requirements": ["wled==0.9.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index a2fde3b7549..a8843853985 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2424,7 +2424,7 @@ wirelesstagpy==0.5.0 withings-api==2.3.2 # homeassistant.components.wled -wled==0.8.0 +wled==0.9.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d96061c116..52212eee08d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1416,7 +1416,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.8.0 +wled==0.9.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 28a0ba4df33fa6ac0616715617868a20e19d7c30 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Nov 2021 15:34:09 +0100 Subject: [PATCH 0445/1452] Modernize/Simplify Twente Milieu (#59632) --- .coveragerc | 1 + .strict-typing | 1 + .../components/twentemilieu/__init__.py | 74 ++++++------------- .../components/twentemilieu/config_flow.py | 3 +- .../components/twentemilieu/const.py | 8 +- .../components/twentemilieu/sensor.py | 63 ++++++---------- .../components/twentemilieu/services.yaml | 11 --- mypy.ini | 11 +++ .../twentemilieu/test_config_flow.py | 5 +- 9 files changed, 70 insertions(+), 107 deletions(-) delete mode 100644 homeassistant/components/twentemilieu/services.yaml diff --git a/.coveragerc b/.coveragerc index c891a73e290..a34e137f61c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1140,6 +1140,7 @@ omit = homeassistant/components/tuya/switch.py homeassistant/components/tuya/util.py homeassistant/components/tuya/vacuum.py + homeassistant/components/twentemilieu/__init__.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py diff --git a/.strict-typing b/.strict-typing index 01295668a64..daed6f3434a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -133,6 +133,7 @@ homeassistant.components.tplink.* homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.tts.* +homeassistant.components.twentemilieu.* homeassistant.components.upcloud.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 81e0a040333..7b4e4084364 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -1,10 +1,9 @@ """Support for Twente Milieu.""" from __future__ import annotations -import asyncio -from datetime import timedelta +from datetime import date, timedelta -from twentemilieu import TwenteMilieu +from twentemilieu import TwenteMilieu, WasteType import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -12,17 +11,9 @@ from homeassistant.const import CONF_ID 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.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ( - CONF_HOUSE_LETTER, - CONF_HOUSE_NUMBER, - CONF_POST_CODE, - DATA_UPDATE, - DOMAIN, -) +from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN, LOGGER SCAN_INTERVAL = timedelta(seconds=3600) @@ -32,35 +23,6 @@ SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) PLATFORMS = ["sensor"] -async def _update_twentemilieu(hass: HomeAssistant, unique_id: str | None) -> None: - """Update Twente Milieu.""" - if unique_id is not None: - twentemilieu = hass.data[DOMAIN].get(unique_id) - if twentemilieu is not None: - await twentemilieu.update() - async_dispatcher_send(hass, DATA_UPDATE, unique_id) - else: - await asyncio.wait( - [twentemilieu.update() for twentemilieu in hass.data[DOMAIN].values()] - ) - - for uid in hass.data[DOMAIN]: - async_dispatcher_send(hass, DATA_UPDATE, uid) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Twente Milieu components.""" - - async def update(call) -> None: - """Service call to manually update the data.""" - unique_id = call.data.get(CONF_ID) - await _update_twentemilieu(hass, unique_id) - - hass.services.async_register(DOMAIN, SERVICE_UPDATE, update, schema=SERVICE_SCHEMA) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Twente Milieu from a config entry.""" session = async_get_clientsession(hass) @@ -71,24 +33,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - unique_id = entry.data[CONF_ID] - hass.data.setdefault(DOMAIN, {})[unique_id] = twentemilieu + coordinator: DataUpdateCoordinator[ + dict[WasteType, date | None] + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + update_method=twentemilieu.update, + ) + await coordinator.async_config_entry_first_refresh() + # For backwards compat, set unique ID + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=entry.data[CONF_ID]) + + hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) - async def _interval_update(now=None) -> None: - """Update Twente Milieu data.""" - await _update_twentemilieu(hass, unique_id) - - async_track_time_interval(hass, _interval_update, SCAN_INTERVAL) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Twente Milieu config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - del hass.data[DOMAIN][entry.data[CONF_ID]] - + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] return unload_ok diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 6a70fbbdcbe..7a00222fe9b 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -66,7 +66,8 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_address" return await self._show_setup_form(errors) - self._async_abort_entries_match({CONF_ID: unique_id}) + await self.async_set_unique_id(str(unique_id)) + self._abort_if_unique_id_configured() return self.async_create_entry( title=str(unique_id), diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 30f770efd25..95ab903cc17 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -1,8 +1,12 @@ """Constants for the Twente Milieu integration.""" +from datetime import timedelta +import logging +from typing import Final -DOMAIN = "twentemilieu" +DOMAIN: Final = "twentemilieu" -DATA_UPDATE = "twentemilieu_update" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index d13ae3a7165..04aa635b4a4 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -2,21 +2,23 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import date -from twentemilieu import TwenteMilieu, TwenteMilieuConnectionError, WasteType +from twentemilieu import WasteType from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from .const import DATA_UPDATE, DOMAIN - -PARALLEL_UPDATES = 1 +from .const import DOMAIN @dataclass @@ -71,55 +73,38 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Twente Milieu sensor based on a config entry.""" - twentemilieu = hass.data[DOMAIN][entry.data[CONF_ID]] - - try: - await twentemilieu.update() - except TwenteMilieuConnectionError as exception: - raise PlatformNotReady from exception - + coordinator = hass.data[DOMAIN][entry.data[CONF_ID]] async_add_entities( - [ - TwenteMilieuSensor(twentemilieu, entry.data[CONF_ID], description) - for description in SENSORS - ], - True, + TwenteMilieuSensor(coordinator, description, entry) for description in SENSORS ) -class TwenteMilieuSensor(SensorEntity): +class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): """Defines a Twente Milieu sensor.""" entity_description: TwenteMilieuSensorDescription - _attr_should_poll = False + coordinator: DataUpdateCoordinator[dict[WasteType, date | None]] def __init__( self, - twentemilieu: TwenteMilieu, - unique_id: str, + coordinator: DataUpdateCoordinator, description: TwenteMilieuSensorDescription, + entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" + super().__init__(coordinator=coordinator) self.entity_description = description - self._twentemilieu = twentemilieu - self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" + self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, + configuration_url="https://www.twentemilieu.nl", + identifiers={(DOMAIN, entry.data[CONF_ID])}, manufacturer="Twente Milieu", name="Twente Milieu", ) - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, DATA_UPDATE, self.async_schedule_update_ha_state - ) - ) - - async def async_update(self) -> None: - """Update Twente Milieu entity.""" - pickups = await self._twentemilieu.update() - self._attr_native_value = None - if pickup := pickups.get(self.entity_description.waste_type): - self._attr_native_value = pickup.isoformat() + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + if pickup := self.coordinator.data.get(self.entity_description.waste_type): + return pickup.isoformat() + return None diff --git a/homeassistant/components/twentemilieu/services.yaml b/homeassistant/components/twentemilieu/services.yaml deleted file mode 100644 index 6227bad1b6d..00000000000 --- a/homeassistant/components/twentemilieu/services.yaml +++ /dev/null @@ -1,11 +0,0 @@ -update: - name: Update - description: Update all entities with fresh data from Twente Milieu - fields: - id: - name: ID - description: Specific unique address ID to update - advanced: true - example: 1300012345 - selector: - text: diff --git a/mypy.ini b/mypy.ini index f879ed5799b..4e374c8b1ae 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1474,6 +1474,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.twentemilieu.*] +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.upcloud.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index e4e4b0c8335..ebded3cffdd 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -74,7 +74,10 @@ async def test_address_already_set_up( ) -> None: """Test we abort if address has already been set up.""" MockConfigEntry( - domain=DOMAIN, data={**FIXTURE_USER_INPUT, CONF_ID: "12345"}, title="12345" + domain=DOMAIN, + data={**FIXTURE_USER_INPUT, CONF_ID: "12345"}, + title="12345", + unique_id="12345", ).add_to_hass(hass) aioclient_mock.post( From 512bdac7245e85f2faabe20800e1ae0628faa808 Mon Sep 17 00:00:00 2001 From: jugla <59493499+jugla@users.noreply.github.com> Date: Sat, 13 Nov 2021 16:44:18 +0100 Subject: [PATCH 0446/1452] Air visual : robustness at startup when evaluate time interval (#59544) --- homeassistant/components/airvisual/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index ed687a84b27..15e5407138b 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -104,9 +104,10 @@ def async_get_cloud_coordinators_by_api_key( ) -> list[DataUpdateCoordinator]: """Get all DataUpdateCoordinator objects related to a particular API key.""" return [ - attrs[DATA_COORDINATOR] + coordinator for entry_id, attrs in hass.data[DOMAIN].items() if (entry := hass.config_entries.async_get_entry(entry_id)) + and (coordinator := attrs.get(DATA_COORDINATOR)) and entry.data.get(CONF_API_KEY) == api_key ] From f3a308458feaa1f5bc225b64768a8d7b52d5ed06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Nov 2021 18:09:10 +0100 Subject: [PATCH 0447/1452] Upgrade pytest-timeout to 2.0.1 (#59646) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 246b52e0085..5f20b10b4e1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -23,7 +23,7 @@ pytest-freezegun==0.4.2 pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 -pytest-timeout==1.4.2 +pytest-timeout==2.0.1 pytest-xdist==2.4.0 pytest==6.2.5 requests_mock==1.9.2 From 48024b6da0a3924ba87ffad8ad54045c38296e91 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sat, 13 Nov 2021 12:18:12 -0500 Subject: [PATCH 0448/1452] Bump greecliamate to 0.12.4 (#59645) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 62d5bec6bb8..e87a7c6a0bb 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.3"], + "requirements": ["greeclimate==0.12.4"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a8843853985..e37a5cd2380 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -753,7 +753,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.3 +greeclimate==0.12.4 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52212eee08d..1c4a88edc5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-nest-sdm==0.3.9 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.3 +greeclimate==0.12.4 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 From 0ba45e4db45def34aa9211792265d707b1f1f2fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Nov 2021 11:18:54 -0600 Subject: [PATCH 0449/1452] Bump zeroconf to 0.36.13 (#59644) - Closes #59415 - Fixes #58453 - Fixes #57678 - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.36.12...0.36.13 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 0c25a4c9860..95f9407661b 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.36.12"], + "requirements": ["zeroconf==0.36.13"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e22a5d5e82..fbdccb3822b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.26 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.12 +zeroconf==0.36.13 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index e37a5cd2380..c0e4041edec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2474,7 +2474,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.12 +zeroconf==0.36.13 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c4a88edc5d..ba9c7afa116 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1448,7 +1448,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.12 +zeroconf==0.36.13 # homeassistant.components.zha zha-quirks==0.0.63 From 674993073673039e730db3efc56c87b727f07cae Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sat, 13 Nov 2021 19:21:37 +0200 Subject: [PATCH 0450/1452] Switchbot "in memory" state for push mode switch (#58750) * Add in memory state tracking to Switchbot switch. * Switchbot assumed state * Add in memory state when Bot is in push mode. * Cleanup --- homeassistant/components/switchbot/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index a2064eb16d6..a09255d0392 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -130,6 +130,8 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): self._last_run_success = bool( await self.hass.async_add_executor_job(self._device.turn_on) ) + if self._last_run_success: + self._attr_is_on = True async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" @@ -139,6 +141,8 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): self._last_run_success = bool( await self.hass.async_add_executor_job(self._device.turn_off) ) + if self._last_run_success: + self._attr_is_on = False @property def assumed_state(self) -> bool: @@ -150,6 +154,8 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): @property def is_on(self) -> bool: """Return true if device is on.""" + if not self.data["data"]["switchMode"]: + return self._attr_is_on return self.data["data"]["isOn"] @property From f65af0f9d75b6bbf431551bd260fe8b69ffd8ad3 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sat, 13 Nov 2021 16:17:10 -0500 Subject: [PATCH 0451/1452] Update sense library version number (#59454) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index bb3ac2082f8..39a0a3da054 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.2"], + "requirements": ["sense_energy==0.9.3"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 940902851e2..b8d499c8522 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.2"], + "requirements": ["sense_energy==0.9.3"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index c0e4041edec..bfe78027d3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2125,7 +2125,7 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.2 +sense_energy==0.9.3 # homeassistant.components.sentry sentry-sdk==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba9c7afa116..00dfc6a8be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1245,7 +1245,7 @@ screenlogicpy==0.4.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.2 +sense_energy==0.9.3 # homeassistant.components.sentry sentry-sdk==1.4.3 From 2fca5a4b557d98903be1a279f1c837b27a38527a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 14 Nov 2021 00:05:32 +0100 Subject: [PATCH 0452/1452] Update xknx to 0.18.13 (#59658) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index bd79a815e7a..30dafe7d7f9 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.12"], + "requirements": ["xknx==0.18.13"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index bfe78027d3d..0dc5a8fbe9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.12 +xknx==0.18.13 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00dfc6a8be0..c81844aac11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1425,7 +1425,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.12 +xknx==0.18.13 # homeassistant.components.bluesound # homeassistant.components.fritz From e42bb244b7ac2f48001b67e4cfe6ebab096edf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Sat, 13 Nov 2021 18:50:37 -0500 Subject: [PATCH 0453/1452] Add TP-Link LED control for Kasa plugs and strips (#59621) --- homeassistant/components/tplink/entity.py | 6 +-- homeassistant/components/tplink/switch.py | 42 +++++++++++++++- tests/components/tplink/__init__.py | 2 + tests/components/tplink/test_switch.py | 61 +++++++++++++++++++++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 30c0fd60add..3038e93ac25 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -38,13 +38,9 @@ class CoordinatedTPLinkEntity(CoordinatorEntity): """Initialize the switch.""" super().__init__(coordinator) self.device: SmartDevice = device + self._attr_name = self.device.alias self._attr_unique_id = self.device.device_id - @property - def name(self) -> str: - """Return the name of the Smart Plug.""" - return cast(str, self.device.alias) - @property def device_info(self) -> DeviceInfo: """Return information about the device.""" diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index f0d299e21c8..927765d15f7 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -8,6 +8,7 @@ from kasa import SmartDevice from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -29,7 +30,7 @@ async def async_setup_entry( device = coordinator.device if not device.is_plug and not device.is_strip: return - entities = [] + entities: list = [] if device.is_strip: # Historically we only add the children if the device is a strip _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) @@ -38,9 +39,48 @@ async def async_setup_entry( else: entities.append(SmartPlugSwitch(device, coordinator)) + entities.append(SmartPlugLedSwitch(device, coordinator)) + async_add_entities(entities) +class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): + """Representation of switch for the LED of a TPLink Smart Plug.""" + + coordinator: TPLinkDataUpdateCoordinator + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__( + self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator + ) -> None: + """Initialize the LED switch.""" + super().__init__(device, coordinator) + + self._attr_name = f"{device.alias} LED" + self._attr_unique_id = f"{self.device.mac}_led" + + @property + def icon(self) -> str: + """Return the icon for the LED.""" + return "mdi:led-on" if self.is_on else "mdi:led-off" + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the LED switch on.""" + await self.device.set_led(True) + + @async_refresh_after + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the LED switch off.""" + await self.device.set_led(False) + + @property + def is_on(self) -> bool: + """Return true if LED switch is on.""" + return bool(self.device.led) + + class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 4e6dbb9dae7..50249f54f03 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -91,6 +91,7 @@ def _mocked_plug() -> SmartPlug: plug.hw_info = {"sw_ver": "1.0.0"} plug.turn_off = AsyncMock() plug.turn_on = AsyncMock() + plug.set_led = AsyncMock() plug.protocol = _mock_protocol() return plug @@ -111,6 +112,7 @@ def _mocked_strip() -> SmartStrip: strip.hw_info = {"sw_ver": "1.0.0"} strip.turn_off = AsyncMock() strip.turn_on = AsyncMock() + strip.set_led = AsyncMock() strip.protocol = _mock_protocol() plug0 = _mocked_plug() plug0.alias = "Plug0" diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 9e7f9189aab..03dc98f9799 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -53,6 +53,38 @@ async def test_plug(hass: HomeAssistant) -> None: plug.turn_on.reset_mock() +async def test_plug_led(hass: HomeAssistant) -> None: + """Test a smart plug LED.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_plug() + with _patch_discovery(device=plug), _patch_single_discovery(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.my_plug" + state = hass.states.get(entity_id) + + led_entity_id = f"{entity_id}_led" + led_state = hass.states.get(led_entity_id) + assert led_state.state == STATE_ON + assert led_state.name == f"{state.name} LED" + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + ) + plug.set_led.assert_called_once_with(False) + plug.set_led.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + ) + plug.set_led.assert_called_once_with(True) + plug.set_led.reset_mock() + + async def test_plug_unique_id(hass: HomeAssistant) -> None: """Test a plug unique id.""" already_migrated_config_entry = MockConfigEntry( @@ -124,6 +156,35 @@ async def test_strip(hass: HomeAssistant) -> None: strip.children[plug_id].turn_on.reset_mock() +async def test_strip_led(hass: HomeAssistant) -> None: + """Test a smart strip LED.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + strip = _mocked_strip() + with _patch_discovery(device=strip), _patch_single_discovery(device=strip): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # We should have a LED entity for the strip + led_entity_id = "switch.my_strip_led" + led_state = hass.states.get(led_entity_id) + assert led_state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + ) + strip.set_led.assert_called_once_with(False) + strip.set_led.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + ) + strip.set_led.assert_called_once_with(True) + strip.set_led.reset_mock() + + async def test_strip_unique_ids(hass: HomeAssistant) -> None: """Test a strip unique id.""" already_migrated_config_entry = MockConfigEntry( From fc539da42b910f357aa55d5a8231abb6e8aac5db Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 14 Nov 2021 00:12:48 +0000 Subject: [PATCH 0454/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 21 ++++++++++++++++++ .../accuweather/translations/ja.json | 7 +++++- .../accuweather/translations/sensor.ja.json | 9 ++++++++ .../components/acmeda/translations/ja.json | 12 ++++++++++ .../components/adax/translations/ja.json | 12 ++++++++++ .../components/adguard/translations/ja.json | 9 ++++++++ .../advantage_air/translations/ja.json | 11 ++++++++++ .../components/aemet/translations/ja.json | 19 ++++++++++++++++ .../components/agent_dvr/translations/ja.json | 3 +++ .../components/airly/translations/ja.json | 18 +++++++++++++++ .../components/airnow/translations/ja.json | 9 ++++++++ .../components/airtouch4/translations/ja.json | 8 +++++++ .../alarm_control_panel/translations/ja.json | 4 +++- .../alarmdecoder/translations/ja.json | 10 +++++++++ .../components/ambee/translations/ja.json | 7 +++++- .../components/apple_tv/translations/ja.json | 10 ++++++++- .../components/arcam_fmj/translations/ja.json | 13 +++++++++++ .../components/asuswrt/translations/ja.json | 22 +++++++++++++++++++ .../components/atag/translations/ja.json | 11 ++++++++++ .../components/axis/translations/ja.json | 5 ++++- .../components/blebox/translations/ja.json | 1 + .../components/bond/translations/ja.json | 11 ++++++++++ .../components/bosch_shc/translations/ja.json | 6 +++++ .../components/braviatv/translations/ja.json | 3 +++ .../components/broadlink/translations/ja.json | 5 +++++ .../components/brother/translations/ja.json | 12 ++++++++++ .../components/bsblan/translations/ja.json | 9 ++++++++ .../components/co2signal/translations/ja.json | 2 +- .../components/control4/translations/ja.json | 9 ++++++++ .../coolmaster/translations/ja.json | 11 ++++++++++ .../crownstone/translations/ja.json | 6 ++--- .../components/daikin/translations/ja.json | 3 +++ .../components/deconz/translations/ja.json | 5 +++++ .../components/denonavr/translations/ja.json | 7 ++++++ .../dialogflow/translations/ja.json | 2 +- .../components/dlna_dmr/translations/ja.json | 2 +- .../components/dsmr/translations/ja.json | 3 ++- .../components/dunehd/translations/ja.json | 12 ++++++++++ .../components/eafm/translations/ja.json | 12 ++++++++++ .../components/elgato/translations/ja.json | 9 ++++++++ .../components/elkm1/translations/ja.json | 11 ++++++++++ .../components/emonitor/translations/ja.json | 9 +++++++- .../enphase_envoy/translations/ja.json | 9 +++++++- .../components/epson/translations/ja.json | 7 ++++++ .../components/esphome/translations/ja.json | 5 ++++- .../evil_genius_labs/translations/it.json | 15 +++++++++++++ .../evil_genius_labs/translations/ja.json | 11 ++++++++++ .../fjaraskupan/translations/ja.json | 2 +- .../components/flo/translations/ja.json | 11 ++++++++++ .../forked_daapd/translations/ja.json | 10 +++++++++ .../components/foscam/translations/ja.json | 1 + .../components/freebox/translations/ja.json | 14 ++++++++++++ .../components/gios/translations/ja.json | 9 ++++++++ .../components/goalzero/translations/ja.json | 14 ++++++++++++ .../components/habitica/translations/ja.json | 9 ++++++++ .../components/hassio/translations/ja.json | 8 +++++++ .../components/heos/translations/ja.json | 12 ++++++++++ .../components/hlk_sw16/translations/ja.json | 11 ++++++++++ .../homekit_controller/translations/ja.json | 2 +- .../components/hue/translations/ja.json | 6 ++--- .../translations/ja.json | 7 +++++- .../hvv_departures/translations/ja.json | 11 ++++++++++ .../components/hyperion/translations/ja.json | 11 ++++++++++ .../components/ialarm/translations/ja.json | 11 ++++++++++ .../components/ifttt/translations/ja.json | 2 +- .../components/iotawatt/translations/ja.json | 5 +++++ .../components/ipp/translations/ja.json | 5 +++++ .../islamic_prayer_times/translations/ja.json | 9 ++++++++ .../components/isy994/translations/ja.json | 9 ++++++++ .../components/jellyfin/translations/it.json | 21 ++++++++++++++++++ .../components/jellyfin/translations/ru.json | 21 ++++++++++++++++++ .../keenetic_ndms2/translations/ja.json | 10 ++++++++- .../components/kmtronic/translations/ja.json | 11 ++++++++++ .../components/kodi/translations/ja.json | 3 +++ .../kostal_plenticore/translations/ja.json | 11 ++++++++++ .../components/local_ip/translations/ja.json | 10 +++++++++ .../components/lookin/translations/ja.json | 2 +- .../lutron_caseta/translations/ja.json | 10 ++++++++- .../components/mailgun/translations/ja.json | 2 +- .../components/mikrotik/translations/ja.json | 9 ++++++++ .../minecraft_server/translations/ja.json | 12 ++++++++++ .../modern_forms/translations/ja.json | 12 ++++++++++ .../motion_blinds/translations/ja.json | 3 +++ .../components/mullvad/translations/ja.json | 9 ++++++++ .../components/mutesync/translations/ja.json | 11 ++++++++++ .../components/mysensors/translations/ja.json | 17 ++++++++++++++ .../components/nam/translations/ja.json | 3 +++ .../components/nanoleaf/translations/ja.json | 5 +++++ .../components/netgear/translations/ja.json | 2 +- .../nfandroidtv/translations/ja.json | 4 ++++ .../nmap_tracker/translations/ja.json | 2 +- .../components/nuki/translations/ja.json | 11 ++++++++++ .../components/nut/translations/ja.json | 11 ++++++++++ .../components/nzbget/translations/ja.json | 11 ++++++++++ .../components/onewire/translations/ja.json | 14 ++++++++++++ .../components/onvif/translations/ja.json | 8 +++++++ .../openweathermap/translations/ja.json | 2 +- .../components/ozw/translations/ja.json | 3 +++ .../p1_monitor/translations/ja.json | 12 ++++++++++ .../philips_js/translations/ja.json | 9 ++++++++ .../components/pi_hole/translations/ja.json | 11 ++++++++++ .../components/plaato/translations/ja.json | 3 +++ .../progettihwsw/translations/ja.json | 3 +++ .../rainforest_eagle/translations/ja.json | 1 + .../components/roomba/translations/ja.json | 17 +++++++++++++- .../components/roon/translations/ja.json | 5 +++++ .../ruckus_unleashed/translations/ja.json | 11 ++++++++++ .../components/shelly/translations/ja.json | 9 ++++++++ .../components/sma/translations/ja.json | 3 +++ .../components/smappee/translations/ja.json | 14 ++++++++++++ .../somfy_mylink/translations/ja.json | 9 +++++++- .../components/sonarr/translations/ja.json | 11 ++++++++++ .../squeezebox/translations/ja.json | 16 ++++++++++++++ .../synology_dsm/translations/ja.json | 5 +++++ .../system_bridge/translations/ja.json | 9 ++++++++ .../tellduslive/translations/ja.json | 3 +++ .../transmission/translations/ja.json | 11 ++++++++++ .../twentemilieu/translations/ja.json | 3 ++- .../components/twilio/translations/ja.json | 2 +- .../components/twinkly/translations/ja.json | 12 ++++++++++ .../components/unifi/translations/ja.json | 3 ++- .../uptimerobot/translations/ja.json | 2 +- .../components/vilfo/translations/ja.json | 9 ++++++++ .../components/volumio/translations/ja.json | 11 ++++++++++ .../components/wled/translations/ja.json | 9 ++++++++ .../wolflink/translations/sensor.ja.json | 7 ++++++ .../xiaomi_aqara/translations/ja.json | 11 ++++++++++ .../xiaomi_miio/translations/ja.json | 10 +++++++++ .../yamaha_musiccast/translations/ja.json | 10 ++++++++- .../components/yeelight/translations/ja.json | 10 +++++++++ .../components/youless/translations/ja.json | 11 ++++++++++ .../zoneminder/translations/ja.json | 3 +++ .../components/zwave_js/translations/ja.json | 3 +++ 133 files changed, 1088 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/abode/translations/ja.json create mode 100644 homeassistant/components/accuweather/translations/sensor.ja.json create mode 100644 homeassistant/components/acmeda/translations/ja.json create mode 100644 homeassistant/components/adax/translations/ja.json create mode 100644 homeassistant/components/advantage_air/translations/ja.json create mode 100644 homeassistant/components/aemet/translations/ja.json create mode 100644 homeassistant/components/airly/translations/ja.json create mode 100644 homeassistant/components/airnow/translations/ja.json create mode 100644 homeassistant/components/arcam_fmj/translations/ja.json create mode 100644 homeassistant/components/asuswrt/translations/ja.json create mode 100644 homeassistant/components/atag/translations/ja.json create mode 100644 homeassistant/components/bond/translations/ja.json create mode 100644 homeassistant/components/brother/translations/ja.json create mode 100644 homeassistant/components/bsblan/translations/ja.json create mode 100644 homeassistant/components/control4/translations/ja.json create mode 100644 homeassistant/components/coolmaster/translations/ja.json create mode 100644 homeassistant/components/dunehd/translations/ja.json create mode 100644 homeassistant/components/eafm/translations/ja.json create mode 100644 homeassistant/components/elgato/translations/ja.json create mode 100644 homeassistant/components/elkm1/translations/ja.json create mode 100644 homeassistant/components/evil_genius_labs/translations/it.json create mode 100644 homeassistant/components/evil_genius_labs/translations/ja.json create mode 100644 homeassistant/components/flo/translations/ja.json create mode 100644 homeassistant/components/freebox/translations/ja.json create mode 100644 homeassistant/components/gios/translations/ja.json create mode 100644 homeassistant/components/goalzero/translations/ja.json create mode 100644 homeassistant/components/habitica/translations/ja.json create mode 100644 homeassistant/components/hassio/translations/ja.json create mode 100644 homeassistant/components/heos/translations/ja.json create mode 100644 homeassistant/components/hlk_sw16/translations/ja.json create mode 100644 homeassistant/components/hvv_departures/translations/ja.json create mode 100644 homeassistant/components/hyperion/translations/ja.json create mode 100644 homeassistant/components/ialarm/translations/ja.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/ja.json create mode 100644 homeassistant/components/jellyfin/translations/it.json create mode 100644 homeassistant/components/jellyfin/translations/ru.json create mode 100644 homeassistant/components/kmtronic/translations/ja.json create mode 100644 homeassistant/components/kostal_plenticore/translations/ja.json create mode 100644 homeassistant/components/local_ip/translations/ja.json create mode 100644 homeassistant/components/mikrotik/translations/ja.json create mode 100644 homeassistant/components/minecraft_server/translations/ja.json create mode 100644 homeassistant/components/modern_forms/translations/ja.json create mode 100644 homeassistant/components/mullvad/translations/ja.json create mode 100644 homeassistant/components/mutesync/translations/ja.json create mode 100644 homeassistant/components/mysensors/translations/ja.json create mode 100644 homeassistant/components/nuki/translations/ja.json create mode 100644 homeassistant/components/nut/translations/ja.json create mode 100644 homeassistant/components/nzbget/translations/ja.json create mode 100644 homeassistant/components/onewire/translations/ja.json create mode 100644 homeassistant/components/p1_monitor/translations/ja.json create mode 100644 homeassistant/components/pi_hole/translations/ja.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/ja.json create mode 100644 homeassistant/components/smappee/translations/ja.json create mode 100644 homeassistant/components/sonarr/translations/ja.json create mode 100644 homeassistant/components/squeezebox/translations/ja.json create mode 100644 homeassistant/components/transmission/translations/ja.json create mode 100644 homeassistant/components/twinkly/translations/ja.json create mode 100644 homeassistant/components/vilfo/translations/ja.json create mode 100644 homeassistant/components/volumio/translations/ja.json create mode 100644 homeassistant/components/wled/translations/ja.json create mode 100644 homeassistant/components/wolflink/translations/sensor.ja.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/ja.json create mode 100644 homeassistant/components/youless/translations/ja.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json new file mode 100644 index 00000000000..f10fb0798e5 --- /dev/null +++ b/homeassistant/components/abode/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "invalid_mfa_code": "\u7121\u52b9\u306aMFA\u30b3\u30fc\u30c9" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA\u30b3\u30fc\u30c9(6\u6841)" + }, + "title": "Abode\u306eMFA\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "reauth_confirm": { + "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "user": { + "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 098640fa79a..a79159a236e 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "error": { + "requests_exceeded": "Accuweather API\u3078\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u6570\u304c\u8a31\u53ef\u3055\u308c\u305f\u6570\u3092\u8d85\u3048\u307e\u3057\u305f\u3002\u6642\u9593\u3092\u7f6e\u304f\u304b\u3001API\u30ad\u30fc\u3092\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "step": { "user": { + "description": "\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/accuweather/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306e\u8a2d\u5b9a\u5f8c\u306b\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", "title": "AccuWeather" } } @@ -19,7 +23,8 @@ }, "system_health": { "info": { - "can_reach_server": "AccuWeather\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + "can_reach_server": "AccuWeather\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "remaining_requests": "\u6b8b\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8" } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.ja.json b/homeassistant/components/accuweather/translations/sensor.ja.json new file mode 100644 index 00000000000..9db8f685dfe --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u4e0b\u964d", + "rising": "\u4e0a\u6607", + "steady": "\u5b89\u5b9a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/ja.json b/homeassistant/components/acmeda/translations/ja.json new file mode 100644 index 00000000000..fd270e982e2 --- /dev/null +++ b/homeassistant/components/acmeda/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "\u30db\u30b9\u30c8ID" + }, + "title": "\u8ffd\u52a0\u3059\u308b\u30cf\u30d6\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json new file mode 100644 index 00000000000..5233293be0a --- /dev/null +++ b/homeassistant/components/adax/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "account_id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index e35071d99bf..71898e93b82 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -2,6 +2,15 @@ "config": { "abort": { "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "\u76e3\u8996\u3068\u5236\u5fa1\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001AdGuardHome\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u3057\u307e\u3059\u3002" + } } } } \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/ja.json b/homeassistant/components/advantage_air/translations/ja.json new file mode 100644 index 00000000000..3a61126ea94 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json new file mode 100644 index 00000000000..037bee5ceea --- /dev/null +++ b/homeassistant/components/aemet/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "AEMET weather station\u304b\u3089\u30c7\u30fc\u30bf\u3092\u53ce\u96c6\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ja.json b/homeassistant/components/agent_dvr/translations/ja.json index d22d77ac94c..4b063c675b2 100644 --- a/homeassistant/components/agent_dvr/translations/ja.json +++ b/homeassistant/components/agent_dvr/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "Agent DVR\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json new file mode 100644 index 00000000000..82eb7ff08e8 --- /dev/null +++ b/homeassistant/components/airly/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "wrong_location": "\u3053\u306e\u30a8\u30ea\u30a2\u306b\u306fAirly\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Airly\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json new file mode 100644 index 00000000000..e18aaaae05e --- /dev/null +++ b/homeassistant/components/airnow/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/ja.json b/homeassistant/components/airtouch4/translations/ja.json index 5d516ff94ed..e161df46ad2 100644 --- a/homeassistant/components/airtouch4/translations/ja.json +++ b/homeassistant/components/airtouch4/translations/ja.json @@ -2,6 +2,14 @@ "config": { "error": { "no_units": "AirTouch 4 Groups\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "AirTouch 4\u63a5\u7d9a\u306e\u8a73\u7d30\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002" + } } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 3eceb75b597..71625cf17b3 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -1,7 +1,9 @@ { "state": { "_": { + "pending": "\u4fdd\u7559\u4e2d", "triggered": "\u30c8\u30ea\u30ac\u30fc" } - } + }, + "title": "\u30a2\u30e9\u30fc\u30e0\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index d6d06d76c0c..36642646aa4 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "protocol": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } + } + }, "options": { "step": { "zone_details": { diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index 3195f744824..54754b49372 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -1,8 +1,13 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "description": "Ambee\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" + } + }, "user": { - "description": "Ambee\u304cHome Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index e8940bef26a..c32e4c566b3 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -1,5 +1,13 @@ { "config": { - "flow_title": "{name}" + "error": { + "no_usable_service": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u78ba\u7acb\u3059\u308b\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u5f15\u304d\u7d9a\u304d\u8868\u793a\u3055\u308c\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3059\u308b\u304b\u3001Apple TV\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "flow_title": "{name}", + "step": { + "user": { + "description": "\u307e\u305a\u3001\u8ffd\u52a0\u3057\u305f\u3044Apple TV\u306e\u30c7\u30d0\u30a4\u30b9\u540d(Kitchen \u3084 Bedroom\u306a\u3069)\u304bIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u898b\u3064\u304b\u3063\u305f\u5834\u5408\u306f\u3001\u4ee5\u4e0b\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n\n\u30c7\u30d0\u30a4\u30b9\u304c\u8868\u793a\u3055\u308c\u306a\u3044\u5834\u5408\u3084\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n\n{devices}" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ja.json b/homeassistant/components/arcam_fmj/translations/ja.json new file mode 100644 index 00000000000..54801fefa0d --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json new file mode 100644 index 00000000000..0b5c05b26f2 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ja.json b/homeassistant/components/atag/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/atag/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index d3091e20d35..15a22f76432 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -5,7 +5,10 @@ }, "step": { "user": { - "title": "Axis\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "Axis\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index 9fcabfc354a..ab0faf90da9 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -2,6 +2,7 @@ "config": { "step": { "user": { + "description": "BleBox\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", "title": "BleBox\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/bond/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 002d4e7178f..88c11c531b4 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -6,6 +6,12 @@ "data": { "password": "Smart Home Controller\u306e\u30d1\u30b9\u30ef\u30fc\u30c9" } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Bosch Smart Home Controller\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3067\u76e3\u8996\u304a\u3088\u3073\u5236\u5fa1\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index abc3a96b00f..28168e63cfe 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -4,6 +4,9 @@ "authorize": { "description": "\u30bd\u30cb\u30fc\u88fd\u306e\u30c6\u30ec\u30d3 \u30d6\u30e9\u30d3\u30a2\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Sony Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index 5bdedf46405..e7a65b8d84b 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -3,6 +3,11 @@ "step": { "auth": { "title": "\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u8a8d\u8a3c" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json new file mode 100644 index 00000000000..45aafc0b296 --- /dev/null +++ b/homeassistant/components/brother/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "wrong_host": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" + }, + "step": { + "user": { + "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json new file mode 100644 index 00000000000..cd2ddf38c39 --- /dev/null +++ b/homeassistant/components/bsblan/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "BSB-Lan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/ja.json b/homeassistant/components/co2signal/translations/ja.json index 00fb0c4eb0f..b17dc55ac62 100644 --- a/homeassistant/components/co2signal/translations/ja.json +++ b/homeassistant/components/co2signal/translations/ja.json @@ -16,7 +16,7 @@ "data": { "location": "\uff5e\u306e\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3059\u308b" }, - "description": "\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001https://co2signal.com/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001https://co2signal.com/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json new file mode 100644 index 00000000000..c3fd320ad24 --- /dev/null +++ b/homeassistant/components/control4/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Control4\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8a73\u7d30\u3068\u3001\u30ed\u30fc\u30ab\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json index 22651f0cf8a..fd336ad42a4 100644 --- a/homeassistant/components/crownstone/translations/ja.json +++ b/homeassistant/components/crownstone/translations/ja.json @@ -10,7 +10,7 @@ "step": { "usb_config": { "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e\u3059\u308b\u304b\u3001USB\u30c9\u30f3\u30b0\u30eb\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u306a\u3044\u5834\u5408\u306f\u3001\"USB\u3092\u4f7f\u7528\u3057\u306a\u3044\" \u3092\u9078\u629e\u3057\u307e\u3059\u3002 \n\n VID 10C4 \u3067 PID EA60 \u306a\u30c7\u30d0\u30a4\u30b9\u3092\u898b\u3064\u3051\u307e\u3059\u3002", - "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, "usb_manual_config": { "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", @@ -45,11 +45,11 @@ "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", - "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, "usb_config_option": { "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", - "title": "Crownstone USB\u30f3\u30b0\u30eb\u306e\u69cb\u6210" + "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, "usb_manual_config": { "data": { diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index ff85e1c76e3..bf3fb1bdc19 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API Key\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306e\u8a2d\u5b9a" } diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 5798618f463..65cb1e46428 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -10,6 +10,11 @@ "step": { "link": { "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" + }, + "manual_input": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } }, diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 8d5fb06452b..92442f15392 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e3b\u96fb\u6e90\u30b1\u30fc\u30d6\u30eb\u3068\u30a4\u30fc\u30b5\u30cd\u30c3\u30c8\u30b1\u30fc\u30d6\u30eb\u3092\u53d6\u308a\u5916\u3057\u3066\u3001\u518d\u63a5\u7d9a\u3059\u308b\u3068\u554f\u984c\u304c\u89e3\u6c7a\u3059\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002", "not_denonavr_manufacturer": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u3055\u308c\u305f\u30e1\u30fc\u30ab\u30fc\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", "not_denonavr_missing": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u60c5\u5831\u304c\u5b8c\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, @@ -11,7 +12,13 @@ "confirm": { "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" }, + "select": { + "data": { + "select_host": "\u53d7\u4fe1\u6a5f\u306eIP\u30a2\u30c9\u30ec\u30b9" + } + }, "user": { + "description": "\u53d7\u4fe1\u6a5f\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" } } diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index ae8909faec9..a81ee0eda0c 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -3,7 +3,7 @@ "step": { "user": { "description": "Dialogflow\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", - "title": "Dialogflow Webhook\u3092\u8a2d\u5b9a" + "title": "Dialogflow Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 6c041c865da..b02784f8a2b 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -4,7 +4,7 @@ "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308b DLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", - "incomplete_config": "\u69cb\u6210\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", + "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", "non_unique_id": "\u540c\u4e00\u306eID\u3067\u8907\u6570\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index a5f70b34180..fabdf8bc3af 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -9,7 +9,8 @@ "step": { "setup_network": { "data": { - "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e" + "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e", + "host": "\u30db\u30b9\u30c8" }, "title": "\u63a5\u7d9a\u30a2\u30c9\u30ec\u30b9\u306e\u9078\u629e" }, diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json new file mode 100644 index 00000000000..28351c5215a --- /dev/null +++ b/homeassistant/components/dunehd/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/ja.json b/homeassistant/components/eafm/translations/ja.json new file mode 100644 index 00000000000..24b479a0fe7 --- /dev/null +++ b/homeassistant/components/eafm/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_stations": "\u6d2a\u6c34\u76e3\u8996(Track a flood monitoring)\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, + "step": { + "user": { + "title": "\u6d2a\u6c34\u76e3\u8996(Track a flood monitoring)\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8ffd\u8de1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json new file mode 100644 index 00000000000..e244ae5f1d2 --- /dev/null +++ b/homeassistant/components/elgato/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Elgato Key Light\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json new file mode 100644 index 00000000000..4611ef75ac0 --- /dev/null +++ b/homeassistant/components/elkm1/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ja.json b/homeassistant/components/emonitor/translations/ja.json index e8940bef26a..ad4cf1544ef 100644 --- a/homeassistant/components/emonitor/translations/ja.json +++ b/homeassistant/components/emonitor/translations/ja.json @@ -1,5 +1,12 @@ { "config": { - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index 3dd80c3f60b..850e34f88bf 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -1,5 +1,12 @@ { "config": { - "flow_title": "{serial} ({host})" + "flow_title": "{serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/epson/translations/ja.json b/homeassistant/components/epson/translations/ja.json index f11e8609e01..4d81a0612b5 100644 --- a/homeassistant/components/epson/translations/ja.json +++ b/homeassistant/components/epson/translations/ja.json @@ -2,6 +2,13 @@ "config": { "error": { "powered_off": "\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u306f\u5165\u3063\u3066\u3044\u307e\u3059\u304b\uff1f\u521d\u671f\u8a2d\u5b9a\u3092\u884c\u3046\u305f\u3081\u306b\u306f\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u304a\u304f\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 79d246ac363..41484fdf874 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "authenticate": { - "description": "{name} \u306e\u69cb\u6210\u3067\u8a2d\u5b9a\u3057\u305f\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "{name} \u3067\u8a2d\u5b9a\u3057\u305f\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "discovery_confirm": { "description": "ESPHome\u306e\u30ce\u30fc\u30c9 `{name}` \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", @@ -27,6 +27,9 @@ "description": "ESPHome\u30c7\u30d0\u30a4\u30b9 {name} \u3001\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u3092\u6709\u52b9\u306b\u3057\u305f\u304b\u3001\u6697\u53f7\u5316\u306e\u30ad\u30fc\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f\u3002\u66f4\u65b0\u3055\u308c\u305f\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "\u3042\u306a\u305f\u306e[ESPHome](https://esphomelib.com/)\u306e\u30ce\u30fc\u30c9\u306e\u63a5\u7d9a\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } diff --git a/homeassistant/components/evil_genius_labs/translations/it.json b/homeassistant/components/evil_genius_labs/translations/it.json new file mode 100644 index 00000000000..8e3e5b34899 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/ja.json b/homeassistant/components/evil_genius_labs/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json index 932bd3e30b4..de08aede9ff 100644 --- a/homeassistant/components/fjaraskupan/translations/ja.json +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -2,7 +2,7 @@ "config": { "step": { "confirm": { - "description": "Fj\u00e4r\u00e5skupan\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + "description": "Fj\u00e4r\u00e5skupan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/flo/translations/ja.json b/homeassistant/components/flo/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/flo/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index ba9b5eae471..0c549fbee01 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "forked-daapd\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index b59ab8ea799..5e0b6473557 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "host": "\u30db\u30b9\u30c8", "stream": "\u30b9\u30c8\u30ea\u30fc\u30e0" } } diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json new file mode 100644 index 00000000000..ee911e0a3a9 --- /dev/null +++ b/homeassistant/components/freebox/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "link": { + "title": "Freebox router\u306b\u30ea\u30f3\u30af" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json new file mode 100644 index 00000000000..abc8feb8a32 --- /dev/null +++ b/homeassistant/components/gios/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json new file mode 100644 index 00000000000..60af7c9c461 --- /dev/null +++ b/homeassistant/components/goalzero/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "confirm_discovery": { + "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ja.json b/homeassistant/components/habitica/translations/ja.json new file mode 100644 index 00000000000..9b979b7c31f --- /dev/null +++ b/homeassistant/components/habitica/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Habitica profile\u306b\u63a5\u7d9a\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3068\u30bf\u30b9\u30af\u3092\u76e3\u8996\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002 \u6ce8\u610f: api_id\u3068api_key\u306f\u3001https://habitica.com/user/settings/api \u304b\u3089\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json new file mode 100644 index 00000000000..88a27fb36bb --- /dev/null +++ b/homeassistant/components/hassio/translations/ja.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "healthy": "\u5143\u6c17", + "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json new file mode 100644 index 00000000000..954da767d10 --- /dev/null +++ b/homeassistant/components/heos/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Heos\u30c7\u30d0\u30a4\u30b9(\u3067\u304d\u308c\u3070\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u6709\u7dda\u3067\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9)\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/ja.json b/homeassistant/components/hlk_sw16/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 248404e363a..c4581a44488 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 1a430a03c97..ce391b43101 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -13,9 +13,6 @@ }, "step": { "init": { - "data": { - "host": "\u30db\u30b9\u30c8" - }, "title": "Hue bridge\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" }, "link": { @@ -23,6 +20,9 @@ "title": "\u30ea\u30f3\u30af\u30cf\u30d6" }, "manual": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "Hue bridges\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json index eaec481025c..ae96a3282ce 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ja.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -1,5 +1,10 @@ { "config": { - "flow_title": "{name} ({host})" + "flow_title": "{name} ({host})", + "step": { + "link": { + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/hyperion/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/ja.json b/homeassistant/components/ialarm/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/ialarm/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 795beb33c9e..1b0f51d7f11 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -3,7 +3,7 @@ "step": { "user": { "description": "IFTTT\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", - "title": "IFTTT\u306eWebhook\u30a2\u30d7\u30ec\u30c3\u30c8\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b" + "title": "IFTTT\u306eWebhook\u30a2\u30d7\u30ec\u30c3\u30c8\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json index 0c352aab54e..c93ce47e3f0 100644 --- a/homeassistant/components/iotawatt/translations/ja.json +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -6,6 +6,11 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "IoTawatt\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u8a8d\u8a3c\u304c\u5fc5\u8981\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3001\u9001\u4fe1 \u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 8e7de9c3278..0ae87941243 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -2,6 +2,11 @@ "config": { "abort": { "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "user": { + "description": "\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u5370\u5237\u30d7\u30ed\u30c8\u30b3\u30eb(IPP)\u3092\u4ecb\u3057\u3066\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } } } } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/ja.json b/homeassistant/components/islamic_prayer_times/translations/ja.json new file mode 100644 index 00000000000..f75f5acc402 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Islamic Prayer Times\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 750be17309a..85cb735a884 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "URL" + } + } + } + }, "system_health": { "info": { "host_reachable": "\u30db\u30b9\u30c8\u5230\u9054\u53ef\u80fd", diff --git a/homeassistant/components/jellyfin/translations/it.json b/homeassistant/components/jellyfin/translations/it.json new file mode 100644 index 00000000000..e00681ee6df --- /dev/null +++ b/homeassistant/components/jellyfin/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/ru.json b/homeassistant/components/jellyfin/translations/ru.json new file mode 100644 index 00000000000..b94d8def5e3 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index 8117d70f255..6edbf1013b0 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -2,7 +2,15 @@ "config": { "abort": { "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093", - "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic\u306e\u30eb\u30fc\u30bf\u30fc\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic router\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "Keenetic NDMS2\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 63204bfae5c..5633ba40f97 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -12,6 +12,9 @@ "title": "Kodi\u3092\u767a\u898b" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "Kodi\u306e\u63a5\u7d9a\u60c5\u5831\u3067\u3059\u3002\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u3067 \"HTTP\u306b\u3088\u308bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u5fc5\u305a\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "ws_port": { diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json new file mode 100644 index 00000000000..46274c545ee --- /dev/null +++ b/homeassistant/components/local_ip/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30ed\u30fc\u30ab\u30ebIP\u30a2\u30c9\u30ec\u30b9" + } + } + }, + "title": "\u30ed\u30fc\u30ab\u30ebIP\u30a2\u30c9\u30ec\u30b9" +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/ja.json b/homeassistant/components/lookin/translations/ja.json index e4ac6a11f08..04b723e15e0 100644 --- a/homeassistant/components/lookin/translations/ja.json +++ b/homeassistant/components/lookin/translations/ja.json @@ -8,7 +8,7 @@ } }, "discovery_confirm": { - "description": "{name} ({host}) \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + "description": "{name} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index eaec481025c..2de21e912a2 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -1,5 +1,13 @@ { "config": { - "flow_title": "{name} ({host})" + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 8e896108021..8193746de5b 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -3,7 +3,7 @@ "step": { "user": { "description": "Mailgun\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", - "title": "Mailgun Webhook\u306e\u8a2d\u5b9a" + "title": "Mailgun Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json new file mode 100644 index 00000000000..e4511a43cb7 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Mikrotik\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json new file mode 100644 index 00000000000..c6ee304e29d --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_ip": "IP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059(MAC\u30a2\u30c9\u30ec\u30b9\u3092\u7279\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f)\u3002\u4fee\u6b63\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "user": { + "description": "\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001Minecraft Server\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json new file mode 100644 index 00000000000..a943b0046d3 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Modern Forms fan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index a290e97a70e..21621989af3 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -8,6 +8,9 @@ "data": { "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" } + }, + "user": { + "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059" } } }, diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json new file mode 100644 index 00000000000..79235ee835e --- /dev/null +++ b/homeassistant/components/mullvad/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Mullvad VPN\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/ja.json b/homeassistant/components/mutesync/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/mutesync/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json new file mode 100644 index 00000000000..af277704c10 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9" + }, + "error": { + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9" + }, + "step": { + "gw_tcp": { + "data": { + "device": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306eIP\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 4dbfbba3caf..5a3b47683ef 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "Nettigo Air Monitor\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index 4484053cf9a..50eb1d2c609 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -3,6 +3,11 @@ "step": { "link": { "title": "Nanoleaf\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index 65132c94377..eb184ba0aa8 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", + "host": "\u30db\u30b9\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d (\u30aa\u30d7\u30b7\u30e7\u30f3)" diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index ec79a0aa382..324e2e4d71b 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -2,6 +2,10 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", "title": "Android TV / Fire TV\u306e\u901a\u77e5" } } diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index 474b0c3e9aa..bbaa3ffa341 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -11,7 +11,7 @@ "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, - "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u69cb\u6210\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" + "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" } } }, diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/nuki/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/nut/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/nzbget/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json new file mode 100644 index 00000000000..58af1684052 --- /dev/null +++ b/homeassistant/components/onewire/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "owserver": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "user": { + "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 809fd863fc9..4ac3f9c01e4 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -2,8 +2,16 @@ "config": { "step": { "configure": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, + "manual_input": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, "user": { "data": { "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 5d22a859d4a..cbe8ba65321 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json index 4660eaa0e1c..84a0a8c5fb8 100644 --- a/homeassistant/components/ozw/translations/ja.json +++ b/homeassistant/components/ozw/translations/ja.json @@ -1,6 +1,9 @@ { "config": { "step": { + "hassio_confirm": { + "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u3068OpenZWave\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, "start_addon": { "data": { "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc" diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json new file mode 100644 index 00000000000..ffda5204c1c --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "P1 Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index 7128b9694a0..375375a4787 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 2f5a90a0dfe..e99532c81ed 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -5,6 +5,9 @@ "data": { "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059" } + }, + "user": { + "title": "Plaato Wdebhookvices\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/progettihwsw/translations/ja.json b/homeassistant/components/progettihwsw/translations/ja.json index 881829af573..bcfc9c3ce4f 100644 --- a/homeassistant/components/progettihwsw/translations/ja.json +++ b/homeassistant/components/progettihwsw/translations/ja.json @@ -5,6 +5,9 @@ "title": "\u30ea\u30ec\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "\u30dc\u30fc\u30c9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/rainforest_eagle/translations/ja.json b/homeassistant/components/rainforest_eagle/translations/ja.json index b4e8b694fde..0ddb56bb22a 100644 --- a/homeassistant/components/rainforest_eagle/translations/ja.json +++ b/homeassistant/components/rainforest_eagle/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "cloud_id": "\u30af\u30e9\u30a6\u30c9ID", + "host": "\u30db\u30b9\u30c8", "install_code": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u30b3\u30fc\u30c9" } } diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index eaec481025c..ac7cef536d7 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -1,5 +1,20 @@ { "config": { - "flow_title": "{name} ({host})" + "flow_title": "{name} ({host})", + "step": { + "init": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "link_manual": { + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u81ea\u52d5\u7684\u306b\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b21\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u308b\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044: {auth_help_url}" + }, + "manual": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 62ef7f1165e..859d3d16017 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -3,6 +3,11 @@ "step": { "link": { "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/ruckus_unleashed/translations/ja.json b/homeassistant/components/ruckus_unleashed/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index be271bcd871..509eb2ec443 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, "device_automation": { "trigger_subtype": { "button4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json index 654ca692761..8145ff4cbaa 100644 --- a/homeassistant/components/sma/translations/ja.json +++ b/homeassistant/components/sma/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "SMA Solar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json new file mode 100644 index 00000000000..aa0d07e8551 --- /dev/null +++ b/homeassistant/components/smappee/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "environment": { + "description": "Smappee\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + }, + "local": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index d14696e3db3..38b1c2e9606 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -1,6 +1,13 @@ { "config": { - "flow_title": "{mac} ({ip})" + "flow_title": "{mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } }, "options": { "step": { diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/sonarr/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json new file mode 100644 index 00000000000..2b5dffa0ba7 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 2553803cd7a..156540d319f 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -12,6 +12,11 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index cf111a4063e..23bb84493b1 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -1,3 +1,12 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, "title": "\u30b7\u30b9\u30c6\u30e0\u30d6\u30ea\u30c3\u30b8" } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index ea2f8e7f82d..fb920c2969c 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -5,6 +5,9 @@ "title": "TelldusLive\u306b\u5bfe\u3057\u3066\u8a8d\u8a3c\u3059\u308b" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "\u7a7a", "title": "\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u3092\u9078\u3076\u3002" } diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/transmission/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json index 5ff4d54e766..a0ef9e88923 100644 --- a/homeassistant/components/twentemilieu/translations/ja.json +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "post_code": "\u90f5\u4fbf\u756a\u53f7" - } + }, + "description": "\u3042\u306a\u305f\u306e\u4f4f\u6240\u306eTwente Milieu providing waste collection(\u30b4\u30df\u53ce\u96c6\u60c5\u5831)\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index e19844627aa..850f61751cc 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Twilio Webhook\u306e\u8a2d\u5b9a" + "title": "Twilio Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json new file mode 100644 index 00000000000..5eeb2e30bd7 --- /dev/null +++ b/homeassistant/components/twinkly/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Twinkly device\u306e\u30db\u30b9\u30c8(\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9)" + }, + "description": "Twinkly led string\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index e6f19a35999..1766534faa7 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -4,9 +4,10 @@ "step": { "user": { "data": { + "host": "\u30db\u30b9\u30c8", "site": "\u30b5\u30a4\u30c8ID" }, - "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u8a2d\u5b9a" + "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 210be855fc5..73352d646cf 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -4,7 +4,7 @@ "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u65e2\u5b58\u306e\u69cb\u6210\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" + "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json new file mode 100644 index 00000000000..71c0bbbb177 --- /dev/null +++ b/homeassistant/components/vilfo/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/volumio/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json new file mode 100644 index 00000000000..d49599e7eb5 --- /dev/null +++ b/homeassistant/components/wled/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "WLED\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json new file mode 100644 index 00000000000..64f7286e08b --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -0,0 +1,7 @@ +{ + "state": { + "wolflink__state": { + "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json new file mode 100644 index 00000000000..59ea458ea2a --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9(\u30aa\u30d7\u30b7\u30e7\u30f3)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 90971677c2c..02027c6bce3 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -17,6 +17,16 @@ "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" } }, + "device": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, + "manual": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "select": { "data": { "select_device": "Miio\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/yamaha_musiccast/translations/ja.json b/homeassistant/components/yamaha_musiccast/translations/ja.json index c1dde7656af..346c1b50ee0 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ja.json +++ b/homeassistant/components/yamaha_musiccast/translations/ja.json @@ -3,6 +3,14 @@ "error": { "no_musiccast_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fMusicCast\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3088\u3046\u3067\u3059\u3002" }, - "flow_title": "MusicCast: {name}" + "flow_title": "MusicCast: {name}", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "MusicCast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index 6168d342afb..bfbd3e225ee 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "flow_title": "{model} {id} ({host})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/youless/translations/ja.json b/homeassistant/components/youless/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/youless/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json index 3f16215b0c0..fd2d82c0544 100644 --- a/homeassistant/components/zoneminder/translations/ja.json +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -9,6 +9,9 @@ "flow_title": "ZoneMinder", "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8(\u4f8b: 10.10.0.4:8010)" + }, "title": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 5041e55ef5e..50edb776841 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -12,6 +12,9 @@ "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" } + }, + "hassio_confirm": { + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, From 5860932635fa3d1463af6b4d4bfbdcebc9727748 Mon Sep 17 00:00:00 2001 From: Andrey Gorbunov Date: Sun, 14 Nov 2021 05:12:27 +0300 Subject: [PATCH 0455/1452] Bump pymysensors to 0.22.1 (#59521) * Bump pymysensors to 0.22.0 (#51265) * Bump pymysensors to 0.22.1 (#51265) --- homeassistant/components/mysensors/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index 3b7695146ba..6e7a4f9cded 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -2,7 +2,7 @@ "domain": "mysensors", "name": "MySensors", "documentation": "https://www.home-assistant.io/integrations/mysensors", - "requirements": ["pymysensors==0.21.0"], + "requirements": ["pymysensors==0.22.1"], "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 0dc5a8fbe9c..f9fd2ce34a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1658,7 +1658,7 @@ pymsteams==0.1.12 pymyq==3.1.4 # homeassistant.components.mysensors -pymysensors==0.21.0 +pymysensors==0.22.1 # homeassistant.components.netgear pynetgear==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c81844aac11..28abac33b50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pymonoprice==0.3 pymyq==3.1.4 # homeassistant.components.mysensors -pymysensors==0.21.0 +pymysensors==0.22.1 # homeassistant.components.netgear pynetgear==0.7.0 From 570f80a73c28914472661a5cd086d108c465e282 Mon Sep 17 00:00:00 2001 From: ericvb Date: Sun, 14 Nov 2021 03:22:36 +0100 Subject: [PATCH 0456/1452] Check early for empty passages in delijn (#59612) * Add a check to verify if there is a passage Late in the evening and at night, there can be no passages anymore, so check it to avoid an unnecessary exception * One passage is enough! Requesting minimum 2 passages was an error due to counting from 1 and not zero * Invert check and put it out of the try-catch code Adding also the KeyError in the log message * Clean up * Putting comment in the correct python syntax * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/delijn/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 395c2d93dff..310e6a7f7fc 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -82,6 +82,10 @@ class DeLijnPublicTransportSensor(SensorEntity): self._attributes["stopname"] = self._name + if not self.line.passages: + self._available = False + return + try: first = self.line.passages[0] if first["due_at_realtime"] is not None: @@ -97,8 +101,8 @@ class DeLijnPublicTransportSensor(SensorEntity): self._attributes["is_realtime"] = first["is_realtime"] self._attributes["next_passages"] = self.line.passages self._available = True - except (KeyError, IndexError): - _LOGGER.error("Invalid data received from De Lijn") + except (KeyError) as error: + _LOGGER.error("Invalid data received from De Lijn: %s", error) self._available = False @property From c3238157604f9b78664e956347cc875463eee4d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Nov 2021 23:55:23 -0600 Subject: [PATCH 0457/1452] Ensure flux_led bulbs turn on even if brightness is 0 (#59661) --- homeassistant/components/flux_led/light.py | 14 ++++++++--- tests/components/flux_led/test_light.py | 28 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 15f9716b52a..fedf31743da 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -328,13 +328,21 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: + brightness = self.brightness + if not self.is_on: await self._device.async_turn_on() if not kwargs: return - - if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: - brightness = self.brightness + # If the brightness was previously 0, the light + # will not turn on unless brightness is at least 1 + if not brightness: + brightness = 1 + elif not brightness: + # If the device was on and brightness was not + # set, it means it was masked by an effect + brightness = 255 # Handle switch to CCT Color Mode if ATTR_COLOR_TEMP in kwargs: diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 8819852f6b2..ce0320f7717 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -214,11 +214,26 @@ async def test_rgb_light(hass: HomeAssistant) -> None: await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_on.assert_called_once() bulb.async_turn_on.reset_mock() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, @@ -229,6 +244,19 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) bulb.async_set_levels.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 it means we could not read it because + # an effect is in progress so we use 255 + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 await hass.services.async_call( LIGHT_DOMAIN, "turn_on", From f596cb19fd7c5e60d89f474aafff95f4e4c6616d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 14 Nov 2021 10:53:19 +0100 Subject: [PATCH 0458/1452] Handle KeyError in getuser (#59667) --- homeassistant/helpers/system_info.py | 6 +++++- tests/helpers/test_system_info.py | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 766fa90af96..7e65ab858ad 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -23,13 +23,17 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: "virtualenv": is_virtual_env(), "python_version": platform.python_version(), "docker": False, - "user": getuser(), "arch": platform.machine(), "timezone": str(hass.config.time_zone), "os_name": platform.system(), "os_version": platform.release(), } + try: + info_object["user"] = getuser() + except KeyError: + info_object["user"] = None + if platform.system() == "Windows": info_object["os_version"] = platform.win32_ver()[0] elif platform.system() == "Darwin": diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index fd9d488596f..f4cb70f421a 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -27,3 +27,10 @@ async def test_container_installationtype(hass): ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): info = await hass.helpers.system_info.async_get_system_info() assert info["installation_type"] == "Unknown" + + +async def test_getuser_keyerror(hass): + """Test getuser keyerror.""" + with patch("homeassistant.helpers.system_info.getuser", side_effect=KeyError): + info = await hass.helpers.system_info.async_get_system_info() + assert info["user"] is None From da8bfed793cd7a9c194029ae80df209d469ce664 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sun, 14 Nov 2021 05:09:32 -0500 Subject: [PATCH 0459/1452] Remove mychevy component (#59629) --- .coveragerc | 1 - homeassistant/components/mychevy/__init__.py | 155 --------------- .../components/mychevy/binary_sensor.py | 79 -------- .../components/mychevy/manifest.json | 8 - homeassistant/components/mychevy/sensor.py | 186 ------------------ requirements_all.txt | 3 - 6 files changed, 432 deletions(-) delete mode 100644 homeassistant/components/mychevy/__init__.py delete mode 100644 homeassistant/components/mychevy/binary_sensor.py delete mode 100644 homeassistant/components/mychevy/manifest.json delete mode 100644 homeassistant/components/mychevy/sensor.py diff --git a/.coveragerc b/.coveragerc index a34e137f61c..95bf05eb8b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -668,7 +668,6 @@ omit = homeassistant/components/mutesync/binary_sensor.py homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py - homeassistant/components/mychevy/* homeassistant/components/mycroft/* homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/binary_sensor.py diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py deleted file mode 100644 index 5ea5b142657..00000000000 --- a/homeassistant/components/mychevy/__init__.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Support for MyChevy.""" -from datetime import timedelta -import logging -import threading -import time - -import mychevy.mychevy as mc -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.util import Throttle - -DOMAIN = "mychevy" -UPDATE_TOPIC = DOMAIN -ERROR_TOPIC = f"{DOMAIN}_error" - -MYCHEVY_SUCCESS = "success" -MYCHEVY_ERROR = "error" - -NOTIFICATION_ID = "mychevy_website_notification" -NOTIFICATION_TITLE = "MyChevy website status" - -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -ERROR_SLEEP_TIME = timedelta(minutes=30) - -CONF_COUNTRY = "country" -DEFAULT_COUNTRY = "us" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): vol.All( - cv.string, vol.In(["us", "ca"]) - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -class EVSensorConfig: - """The EV sensor configuration.""" - - def __init__( - self, name, attr, unit_of_measurement=None, icon=None, extra_attrs=None - ): - """Create new sensor configuration.""" - self.name = name - self.attr = attr - self.extra_attrs = extra_attrs or [] - self.unit_of_measurement = unit_of_measurement - self.icon = icon - - -class EVBinarySensorConfig: - """The EV binary sensor configuration.""" - - def __init__(self, name, attr, device_class=None): - """Create new binary sensor configuration.""" - self.name = name - self.attr = attr - self.device_class = device_class - - -def setup(hass, base_config): - """Set up the mychevy component.""" - config = base_config.get(DOMAIN) - - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - country = config.get(CONF_COUNTRY) - hass.data[DOMAIN] = MyChevyHub( - mc.MyChevy(email, password, country), hass, base_config - ) - hass.data[DOMAIN].start() - - return True - - -class MyChevyHub(threading.Thread): - """MyChevy Hub. - - Connecting to the mychevy website is done through a selenium - webscraping process. That can only run synchronously. In order to - prevent blocking of other parts of Home Assistant the architecture - launches a polling loop in a thread. - - When new data is received, sensors are updated, and hass is - signaled that there are updates. Sensors are not created until the - first update, which will be 60 - 120 seconds after the platform - starts. - """ - - def __init__(self, client, hass, hass_config): - """Initialize MyChevy Hub.""" - super().__init__() - self._client = client - self.hass = hass - self.hass_config = hass_config - self.cars = [] - self.status = None - self.ready = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update sensors from mychevy website. - - This is a synchronous polling call that takes a very long time - (like 2 to 3 minutes long time) - - """ - self._client.login() - self._client.get_cars() - self.cars = self._client.cars - if self.ready is not True: - discovery.load_platform(self.hass, "sensor", DOMAIN, {}, self.hass_config) - discovery.load_platform( - self.hass, "binary_sensor", DOMAIN, {}, self.hass_config - ) - self.ready = True - self.cars = self._client.update_cars() - - def get_car(self, vid): - """Compatibility to work with one car.""" - if self.cars: - for car in self.cars: - if car.vid == vid: - return car - return None - - def run(self): - """Thread run loop.""" - # We add the status device first outside of the loop - - # And then busy wait on threads - while True: - try: - _LOGGER.info("Starting mychevy loop") - self.update() - self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(MIN_TIME_BETWEEN_UPDATES.total_seconds()) - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Error updating mychevy data. " - "This probably means the OnStar link is down again" - ) - self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) - time.sleep(ERROR_SLEEP_TIME.total_seconds()) diff --git a/homeassistant/components/mychevy/binary_sensor.py b/homeassistant/components/mychevy/binary_sensor.py deleted file mode 100644 index eb28dd26b06..00000000000 --- a/homeassistant/components/mychevy/binary_sensor.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Support for MyChevy binary sensors.""" -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorEntity, -) -from homeassistant.core import callback -from homeassistant.util import slugify - -from . import DOMAIN as MYCHEVY_DOMAIN, UPDATE_TOPIC, EVBinarySensorConfig - -SENSORS = [EVBinarySensorConfig("Plugged In", "plugged_in", "plug")] - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the MyChevy sensors.""" - if discovery_info is None: - return - - sensors = [] - hub = hass.data[MYCHEVY_DOMAIN] - for sconfig in SENSORS: - for car in hub.cars: - sensors.append(EVBinarySensor(hub, sconfig, car.vid)) - - async_add_entities(sensors) - - -class EVBinarySensor(BinarySensorEntity): - """Base EVSensor class. - - The only real difference between sensors is which units and what - attribute from the car object they are returning. All logic can be - built with just setting subclass attributes. - """ - - def __init__(self, connection, config, car_vid): - """Initialize sensor with car connection.""" - self._conn = connection - self._name = config.name - self._attr = config.attr - self._type = config.device_class - self._is_on = None - self._car_vid = car_vid - self.entity_id = f"{BINARY_SENSOR_DOMAIN}.{MYCHEVY_DOMAIN}_{slugify(self._car.name)}_{slugify(self._name)}" - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def is_on(self): - """Return if on.""" - return self._is_on - - @property - def _car(self): - """Return the car.""" - return self._conn.get_car(self._car_vid) - - async def async_added_to_hass(self): - """Register callbacks.""" - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback - ) - ) - - @callback - def async_update_callback(self): - """Update state.""" - if self._car is not None: - self._is_on = getattr(self._car, self._attr, None) - self.async_write_ha_state() - - @property - def should_poll(self): - """Return the polling state.""" - return False diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json deleted file mode 100644 index e726d49bb64..00000000000 --- a/homeassistant/components/mychevy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "mychevy", - "name": "myChevrolet", - "documentation": "https://www.home-assistant.io/integrations/mychevy", - "requirements": ["mychevy==2.1.1"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py deleted file mode 100644 index 1a5613d8864..00000000000 --- a/homeassistant/components/mychevy/sensor.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Support for MyChevy sensors.""" -import logging - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity -from homeassistant.const import PERCENTAGE -from homeassistant.core import callback -from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.util import slugify - -from . import ( - DOMAIN as MYCHEVY_DOMAIN, - ERROR_TOPIC, - MYCHEVY_ERROR, - MYCHEVY_SUCCESS, - UPDATE_TOPIC, - EVSensorConfig, -) - -_LOGGER = logging.getLogger(__name__) - -BATTERY_SENSOR = "batteryLevel" - -SENSORS = [ - EVSensorConfig("Mileage", "totalMiles", "miles", "mdi:speedometer"), - EVSensorConfig("Electric Range", "electricRange", "miles", "mdi:speedometer"), - EVSensorConfig("Charged By", "estimatedFullChargeBy"), - EVSensorConfig("Charge Mode", "chargeMode"), - EVSensorConfig( - "Battery Level", BATTERY_SENSOR, PERCENTAGE, "mdi:battery", ["charging"] - ), -] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the MyChevy sensors.""" - if discovery_info is None: - return - - hub = hass.data[MYCHEVY_DOMAIN] - sensors = [MyChevyStatus()] - for sconfig in SENSORS: - for car in hub.cars: - sensors.append(EVSensor(hub, sconfig, car.vid)) - - add_entities(sensors) - - -class MyChevyStatus(SensorEntity): - """A string representing the charge mode.""" - - _name = "MyChevy Status" - _icon = "mdi:car-connected" - - def __init__(self): - """Initialize sensor with car connection.""" - self._state = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.success - ) - ) - - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - ERROR_TOPIC, self.error - ) - ) - - @callback - def success(self): - """Update state, trigger updates.""" - if self._state != MYCHEVY_SUCCESS: - _LOGGER.debug("Successfully connected to mychevy website") - self._state = MYCHEVY_SUCCESS - self.async_write_ha_state() - - @callback - def error(self): - """Update state, trigger updates.""" - _LOGGER.error( - "Connection to mychevy website failed. " - "This probably means the mychevy to OnStar link is down" - ) - self._state = MYCHEVY_ERROR - self.async_write_ha_state() - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def native_value(self): - """Return the state.""" - return self._state - - @property - def should_poll(self): - """Return the polling state.""" - return False - - -class EVSensor(SensorEntity): - """Base EVSensor class. - - The only real difference between sensors is which units and what - attribute from the car object they are returning. All logic can be - built with just setting subclass attributes. - """ - - def __init__(self, connection, config, car_vid): - """Initialize sensor with car connection.""" - self._conn = connection - self._name = config.name - self._attr = config.attr - self._extra_attrs = config.extra_attrs - self._unit_of_measurement = config.unit_of_measurement - self._icon = config.icon - self._state = None - self._state_attributes = {} - self._car_vid = car_vid - - self.entity_id = f"{SENSOR_DOMAIN}.{MYCHEVY_DOMAIN}_{slugify(self._car.name)}_{slugify(self._name)}" - - async def async_added_to_hass(self): - """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback - ) - - @property - def _car(self): - """Return the car.""" - return self._conn.get_car(self._car_vid) - - @property - def icon(self): - """Return the icon.""" - if self._attr == BATTERY_SENSOR: - charging = self._state_attributes.get("charging", False) - return icon_for_battery_level(self.state, charging) - return self._icon - - @property - def name(self): - """Return the name.""" - return self._name - - @callback - def async_update_callback(self): - """Update state.""" - if self._car is not None: - self._state = getattr(self._car, self._attr, None) - if self._unit_of_measurement == "miles": - self._state = round(self._state) - for attr in self._extra_attrs: - self._state_attributes[attr] = getattr(self._car, attr) - self.async_write_ha_state() - - @property - def native_value(self): - """Return the state.""" - return self._state - - @property - def extra_state_attributes(self): - """Return all the state attributes.""" - return self._state_attributes - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement the state is expressed in.""" - return self._unit_of_measurement - - @property - def should_poll(self): - """Return the polling state.""" - return False diff --git a/requirements_all.txt b/requirements_all.txt index f9fd2ce34a7..e58f0b1409f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1034,9 +1034,6 @@ mutagen==1.45.1 # homeassistant.components.mutesync mutesync==0.0.1 -# homeassistant.components.mychevy -mychevy==2.1.1 - # homeassistant.components.mycroft mycroftapi==2.0 From 458bc92124a349e0738036602c871a79505964de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 14 Nov 2021 11:11:29 +0100 Subject: [PATCH 0460/1452] Add test coverage to Twente Milieu (#59640) --- .coveragerc | 3 - .../components/twentemilieu/__init__.py | 6 +- .../components/twentemilieu/const.py | 2 + .../components/twentemilieu/manifest.json | 1 + .../components/twentemilieu/sensor.py | 5 +- tests/components/twentemilieu/conftest.py | 88 ++++++++ .../twentemilieu/test_config_flow.py | 205 ++++++++++-------- tests/components/twentemilieu/test_init.py | 60 +++++ tests/components/twentemilieu/test_sensor.py | 78 +++++++ 9 files changed, 351 insertions(+), 97 deletions(-) create mode 100644 tests/components/twentemilieu/conftest.py create mode 100644 tests/components/twentemilieu/test_init.py create mode 100644 tests/components/twentemilieu/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 95bf05eb8b0..872e707a864 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1139,9 +1139,6 @@ omit = homeassistant/components/tuya/switch.py homeassistant/components/tuya/util.py homeassistant/components/tuya/vacuum.py - homeassistant/components/twentemilieu/__init__.py - homeassistant/components/twentemilieu/const.py - homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py homeassistant/components/twilio_sms/notify.py homeassistant/components/twitter/notify.py diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 7b4e4084364..60b7d07808b 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -46,7 +46,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # For backwards compat, set unique ID if entry.unique_id is None: - hass.config_entries.async_update_entry(entry, unique_id=entry.data[CONF_ID]) + hass.config_entries.async_update_entry( + entry, unique_id=str(entry.data[CONF_ID]) + ) hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -58,5 +60,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Twente Milieu config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - del hass.data[DOMAIN][entry.entry_id] + del hass.data[DOMAIN][entry.data[CONF_ID]] return unload_ok diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 95ab903cc17..8717d26b0f3 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -11,3 +11,5 @@ SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" + +ENTRY_TYPE_SERVICE: Final = "service" diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index 48aa908356a..b4f0a3730a9 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/twentemilieu", "requirements": ["twentemilieu==0.4.2"], "codeowners": ["@frenck"], + "quality_scale": "platinum", "iot_class": "cloud_polling" } diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 04aa635b4a4..bb3176e5834 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN +from .const import DOMAIN, ENTRY_TYPE_SERVICE @dataclass @@ -97,7 +97,8 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" self._attr_device_info = DeviceInfo( configuration_url="https://www.twentemilieu.nl", - identifiers={(DOMAIN, entry.data[CONF_ID])}, + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, manufacturer="Twente Milieu", name="Twente Milieu", ) diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py new file mode 100644 index 00000000000..d540658787b --- /dev/null +++ b/tests/components/twentemilieu/conftest.py @@ -0,0 +1,88 @@ +"""Fixtures for the Twente Milieu integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from datetime import date +from unittest.mock import MagicMock, patch + +import pytest +from twentemilieu import WasteType + +from homeassistant.components.twentemilieu.const import ( + CONF_HOUSE_LETTER, + CONF_HOUSE_NUMBER, + CONF_POST_CODE, + DOMAIN, +) +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="1234AB 1", + domain=DOMAIN, + data={ + CONF_ID: 12345, + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: "A", + }, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.twentemilieu.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture +def mock_twentemilieu_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Twente Milieu client.""" + with patch( + "homeassistant.components.twentemilieu.config_flow.TwenteMilieu", autospec=True + ) as twentemilieu_mock: + twentemilieu = twentemilieu_mock.return_value + twentemilieu.unique_id.return_value = 12345 + yield twentemilieu + + +@pytest.fixture +def mock_twentemilieu() -> Generator[None, MagicMock, None]: + """Return a mocked Twente Milieu client.""" + with patch( + "homeassistant.components.twentemilieu.TwenteMilieu", autospec=True + ) as twentemilieu_mock: + twentemilieu = twentemilieu_mock.return_value + twentemilieu.unique_id.return_value = 12345 + twentemilieu.update.return_value = { + WasteType.NON_RECYCLABLE: date(2021, 11, 1), + WasteType.ORGANIC: date(2021, 11, 2), + WasteType.PACKAGES: date(2021, 11, 3), + WasteType.PAPER: None, + } + yield twentemilieu + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twentemilieu: MagicMock, +) -> MockConfigEntry: + """Set up the TwenteMilieu integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index ebded3cffdd..aec0f29e590 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for the Twente Milieu config flow.""" -import aiohttp +from unittest.mock import MagicMock -from homeassistant import config_entries, data_entry_flow +from twentemilieu import TwenteMilieuAddressError, TwenteMilieuConnectionError + +from homeassistant import config_entries from homeassistant.components.twentemilieu import config_flow from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, @@ -10,116 +12,139 @@ from homeassistant.components.twentemilieu.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker - -FIXTURE_USER_INPUT = { - CONF_POST_CODE: "1234AB", - CONF_HOUSE_NUMBER: "1", - CONF_HOUSE_LETTER: "A", -} -async def test_show_set_form(hass: HomeAssistant) -> None: - """Test that the setup form is served.""" +async def test_full_user_flow( + hass: HomeAssistant, + mock_twentemilieu_config_flow: MagicMock, + mock_setup_entry: MagicMock, +) -> None: + """Test registering an integration and finishing flow works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result - -async def test_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we show user form on Twente Milieu connection error.""" - aioclient_mock.post( - "https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: "A", + }, ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "12345" + assert result2.get("data") == { + CONF_ID: 12345, + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: "A", + } async def test_invalid_address( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_twentemilieu_config_flow: MagicMock, + mock_setup_entry: MagicMock, ) -> None: - """Test we show user form on Twente Milieu invalid address error.""" - aioclient_mock.post( - "https://twentemilieuapi.ximmio.com/api/FetchAdress", - json={"dataList": []}, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_address"} - - -async def test_address_already_set_up( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort if address has already been set up.""" - MockConfigEntry( - domain=DOMAIN, - data={**FIXTURE_USER_INPUT, CONF_ID: "12345"}, - title="12345", - unique_id="12345", - ).add_to_hass(hass) - - aioclient_mock.post( - "https://twentemilieuapi.ximmio.com/api/FetchAdress", - json={"dataList": [{"UniqueId": "12345"}]}, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_full_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test registering an integration and finishing flow works.""" - aioclient_mock.post( - "https://twentemilieuapi.ximmio.com/api/FetchAdress", - json={"dataList": [{"UniqueId": "12345"}]}, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) + """Test full user flow when the user enters an incorrect address. + This tests also tests if the user recovers from it by entering a valid + address in the second attempt. + """ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result - result = await hass.config_entries.flow.async_configure( + mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuAddressError + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - FIXTURE_USER_INPUT, + user_input={ + CONF_POST_CODE: "1234", + CONF_HOUSE_NUMBER: "1", + }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "12345" - assert result["data"][CONF_POST_CODE] == FIXTURE_USER_INPUT[CONF_POST_CODE] - assert result["data"][CONF_HOUSE_NUMBER] == FIXTURE_USER_INPUT[CONF_HOUSE_NUMBER] - assert result["data"][CONF_HOUSE_LETTER] == FIXTURE_USER_INPUT[CONF_HOUSE_LETTER] + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "invalid_address"} + assert "flow_id" in result2 + + mock_twentemilieu_config_flow.unique_id.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "12345" + assert result3.get("data") == { + CONF_ID: 12345, + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: None, + } + + +async def test_connection_error( + hass: HomeAssistant, + mock_twentemilieu_config_flow: MagicMock, +) -> None: + """Test we show user form on Twente Milieu connection error.""" + mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: "A", + }, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert result.get("errors") == {"base": "cannot_connect"} + + +async def test_address_already_set_up( + hass: HomeAssistant, + mock_twentemilieu_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we abort if address has already been set up.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_POST_CODE: "1234AB", + CONF_HOUSE_NUMBER: "1", + CONF_HOUSE_LETTER: "A", + }, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" diff --git a/tests/components/twentemilieu/test_init.py b/tests/components/twentemilieu/test_init.py new file mode 100644 index 00000000000..d5fd108b67a --- /dev/null +++ b/tests/components/twentemilieu/test_init.py @@ -0,0 +1,60 @@ +"""Tests for the Twente Milieu integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.twentemilieu.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twentemilieu: AsyncMock, +) -> None: + """Test the Twente Milieu configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.twentemilieu.TwenteMilieu.update", + side_effect=RuntimeError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Twente Milieu configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_update_config_entry_unique_id( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twentemilieu: AsyncMock, +) -> None: + """Test the we update old config entries with an unique ID.""" + mock_config_entry.unique_id = None + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.unique_id == "12345" diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py new file mode 100644 index 00000000000..11717d3e285 --- /dev/null +++ b/tests/components/twentemilieu/test_sensor.py @@ -0,0 +1,78 @@ +"""Tests for the Twente Milieu sensors.""" +from homeassistant.components.twentemilieu.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_DATE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_waste_pickup_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Twente Milieu waste pickup sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.non_recyclable_waste_pickup") + entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup") + assert entry + assert state + assert entry.unique_id == "twentemilieu_12345_Non-recyclable" + assert state.state == "2021-11-01" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.organic_waste_pickup") + entry = entity_registry.async_get("sensor.organic_waste_pickup") + assert entry + assert state + assert entry.unique_id == "twentemilieu_12345_Organic" + assert state.state == "2021-11-02" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.packages_waste_pickup") + entry = entity_registry.async_get("sensor.packages_waste_pickup") + assert entry + assert state + assert entry.unique_id == "twentemilieu_12345_Plastic" + assert state.state == "2021-11-03" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.paper_waste_pickup") + entry = entity_registry.async_get("sensor.paper_waste_pickup") + assert entry + assert state + assert entry.unique_id == "twentemilieu_12345_Paper" + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "12345")} + assert device_entry.manufacturer == "Twente Milieu" + assert device_entry.name == "Twente Milieu" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.configuration_url == "https://www.twentemilieu.nl" + assert not device_entry.model + assert not device_entry.sw_version From c6c8c1293f172fc523013ca05d8122ebacd57b22 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sun, 14 Nov 2021 12:20:02 +0200 Subject: [PATCH 0461/1452] Bump pyezviz to 0.1.9.8 (#58873) * always create sensors for ezviz * fix ezviz sensors --- homeassistant/components/ezviz/manifest.json | 2 +- homeassistant/components/ezviz/sensor.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 1108f1a6f83..0c6fe4e9dcf 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], - "requirements": ["pyezviz==0.1.9.4"], + "requirements": ["pyezviz==0.1.9.8"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 3ea650154f0..5197982a2c5 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -36,6 +36,8 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="PIR_Status", device_class=DEVICE_CLASS_MOTION, ), + "last_alarm_type_code": SensorEntityDescription(key="last_alarm_type_code"), + "last_alarm_type_name": SensorEntityDescription(key="last_alarm_type_name"), } diff --git a/requirements_all.txt b/requirements_all.txt index e58f0b1409f..edaaeac74d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1471,7 +1471,7 @@ pyeverlights==0.1.0 pyevilgenius==1.0.0 # homeassistant.components.ezviz -pyezviz==0.1.9.4 +pyezviz==0.1.9.8 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28abac33b50..4964d8e3e09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ pyeverlights==0.1.0 pyevilgenius==1.0.0 # homeassistant.components.ezviz -pyezviz==0.1.9.4 +pyezviz==0.1.9.8 # homeassistant.components.fido pyfido==2.1.1 From 2ca874a15cda422046d5024ff5c798161cc169d3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 14 Nov 2021 11:49:02 +0100 Subject: [PATCH 0462/1452] Add configuration_url to deCONZ device when entry source is addon (#59598) --- homeassistant/components/deconz/gateway.py | 2 +- tests/components/deconz/test_gateway.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index fd18be76011..679c1498e44 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -152,7 +152,7 @@ class DeconzGateway: # Gateway service configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}" if self.config_entry.source == SOURCE_HASSIO: - configuration_url = None + configuration_url = "homeassistant://hassio/ingress/core_deconz" device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, configuration_url=configuration_url, diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 2cb73102bf1..8016d2327ef 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -179,7 +179,7 @@ async def test_gateway_setup(hass, aioclient_mock): assert gateway_entry.entry_type == "service" -async def test_gateway_device_no_configuration_url_when_addon(hass, aioclient_mock): +async def test_gateway_device_configuration_url_when_addon(hass, aioclient_mock): """Successful setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", @@ -195,7 +195,9 @@ async def test_gateway_device_no_configuration_url_when_addon(hass, aioclient_mo identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} ) - assert not gateway_entry.configuration_url + assert ( + gateway_entry.configuration_url == "homeassistant://hassio/ingress/core_deconz" + ) async def test_gateway_retry(hass): From 3175bca37d1af22327ca506ea214157af57acbc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Nov 2021 04:56:06 -0600 Subject: [PATCH 0463/1452] Bump flux_led to 0.24.21 (#59662) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index ef87f220a80..203acb77385 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.18"], + "requirements": ["flux_led==0.24.21"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index edaaeac74d4..0b121b64f52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.18 +flux_led==0.24.21 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4964d8e3e09..3c2fb1a0db9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.18 +flux_led==0.24.21 # homeassistant.components.homekit fnvhash==0.1.0 From e5129042add7d4eca652bcd9dda172bf67e409b0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 15 Nov 2021 04:49:45 +1300 Subject: [PATCH 0464/1452] Fix ESPHome state watching when new state is None (#59528) --- homeassistant/components/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0c07d0732ef..8e7f2524a35 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -225,7 +225,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Forward Home Assistant states updates to ESPHome.""" # Only communicate changes to the state or attribute tracked - if ( + if event.data.get("new_state") is None or ( event.data.get("old_state") is not None and "new_state" in event.data and ( From ed6c70a026ec8ff36a9815affa252a2777ab4bbd Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sun, 14 Nov 2021 16:56:09 +0100 Subject: [PATCH 0465/1452] Pass exit code to s6-init (#59545) --- rootfs/etc/services.d/home-assistant/finish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 119a90ea3c6..3691583ec81 100644 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -13,8 +13,12 @@ ifelse { s6-test ${1} -eq ${SIGNAL_EXIT_CODE} } { # Process terminated by a signal define signal ${2} foreground { s6-echo "[finish] process received signal ${signal}" } + backtick -n new_exit_code { s6-expr 128 + ${signal} } + importas -ui new_exit_code new_exit_code + foreground { redirfd -w 1 /var/run/s6/env-stage3/S6_STAGE2_EXITED s6-echo -n -- ${new_exit_code} } if { s6-test ${signal} -ne ${SIGTERM} } s6-svscanctl -t /var/run/s6/services } +foreground { redirfd -w 1 /var/run/s6/env-stage3/S6_STAGE2_EXITED s6-echo -n -- ${1} } s6-svscanctl -t /var/run/s6/services From ee07e9b379ad1c39bc51d0b1052cda53950ae2bf Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 14 Nov 2021 10:57:50 -0500 Subject: [PATCH 0466/1452] Fix unpatched timeout in gree config flow tests (#59449) --- tests/components/gree/test_config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index a3e881d6daf..27d290e3b90 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from .common import FakeDiscovery +@patch("homeassistant.components.gree.config_flow.DISCOVERY_TIMEOUT", 0) async def test_creating_entry_sets_up_climate(hass): """Test setting up Gree creates the climate components.""" with patch( @@ -32,6 +33,7 @@ async def test_creating_entry_sets_up_climate(hass): assert len(setup.mock_calls) == 1 +@patch("homeassistant.components.gree.config_flow.DISCOVERY_TIMEOUT", 0) async def test_creating_entry_has_no_devices(hass): """Test setting up Gree creates the climate components.""" with patch( From 8b676f4252a37130b7f1206c7a460220aee16f52 Mon Sep 17 00:00:00 2001 From: tizzen33 <53906250+tizzen33@users.noreply.github.com> Date: Sun, 14 Nov 2021 16:59:09 +0100 Subject: [PATCH 0467/1452] Add Toon Humidity Sensor (#59488) Co-authored-by: Franck Nijhof --- homeassistant/components/toon/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 35f317a2638..30cde4632a9 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -141,6 +142,17 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( state_class=STATE_CLASS_MEASUREMENT, cls=ToonDisplayDeviceSensor, ), + ToonSensorEntityDescription( + key="current_humidity", + name="Humidity", + section="thermostat", + measurement="current_humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + cls=ToonDisplayDeviceSensor, + ), ToonSensorEntityDescription( key="gas_average", name="Average Gas Usage", From 46160c2a8966783bea5186c2091c9d5216b650d2 Mon Sep 17 00:00:00 2001 From: Arthur Zapparoli <2992+arthurgeek@users.noreply.github.com> Date: Sun, 14 Nov 2021 17:00:33 +0100 Subject: [PATCH 0468/1452] Add Camila, a missing Amazon Polly voice for Brazilian Portuguese (#59346) --- homeassistant/components/amazon_polly/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py index 5ae8c73881d..91882dd386c 100644 --- a/homeassistant/components/amazon_polly/const.py +++ b/homeassistant/components/amazon_polly/const.py @@ -78,6 +78,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Maja", # Polish "Ricardo", "Vitoria", # Portuguese, Brazilian + "Camila", # Portuguese, Brazilian "Cristiano", "Ines", # Portuguese, European "Carmen", # Romanian From 5ec4a502b045c0ea1348dc46f998ffb39df454ba Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 14 Nov 2021 16:04:04 +0000 Subject: [PATCH 0469/1452] Check Honeywell Lyric token is valid before updating data (#59310) --- homeassistant/components/lyric/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 8b80fa61d2b..a9d5cbdec7d 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -93,6 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> Lyric: """Fetch data from Lyric.""" + await oauth_session.async_ensure_token_valid() try: async with async_timeout.timeout(60): await lyric.get_locations() From d5f85f393d8a3c4427deaad7c6f0f632c40ce287 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 14 Nov 2021 17:05:18 +0100 Subject: [PATCH 0470/1452] Add entity categorisation to AVM Fritz!Smarthome devices (#59287) --- homeassistant/components/fritzbox/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 0745ddc8331..6ffa8bc8560 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, @@ -54,6 +55,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, suitable=lambda device: ( device.has_temperature_sensor and not device.has_thermostat ), @@ -73,6 +75,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, suitable=lambda device: device.battery_level is not None, native_value=lambda device: device.battery_level, # type: ignore[no-any-return] ), From 26f3d50a32cebc0266f04919f3c69afcde0c2623 Mon Sep 17 00:00:00 2001 From: csgitmeup <87123764+csgitmeup@users.noreply.github.com> Date: Sun, 14 Nov 2021 17:06:06 +0100 Subject: [PATCH 0471/1452] Bump pykodi to 0.2.7 (#59251) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index f88a893c7fa..6e46b0883d9 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.2.6"], + "requirements": ["pykodi==0.2.7"], "codeowners": ["@OnFreund", "@cgtobi"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 0b121b64f52..1398085ca0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1574,7 +1574,7 @@ pykira==0.1.1 pykmtronic==0.3.0 # homeassistant.components.kodi -pykodi==0.2.6 +pykodi==0.2.7 # homeassistant.components.kraken pykrakenapi==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c2fb1a0db9..d74fbbdb939 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ pykira==0.1.1 pykmtronic==0.3.0 # homeassistant.components.kodi -pykodi==0.2.6 +pykodi==0.2.7 # homeassistant.components.kraken pykrakenapi==0.1.8 From afa7ca1222d47be44d40fdaa369b1fb589607caa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Nov 2021 08:11:12 -0800 Subject: [PATCH 0472/1452] Drop block on local proxies from HA Cloud (#59334) --- homeassistant/components/cloud/const.py | 8 -- homeassistant/components/cloud/http_api.py | 10 -- homeassistant/components/cloud/prefs.py | 47 ------- tests/components/cloud/test_http_api.py | 144 --------------------- 4 files changed, 209 deletions(-) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index d0417e0d38d..f24f172be36 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -63,13 +63,5 @@ MODE_PROD = "production" DISPATCHER_REMOTE_UPDATE = "cloud_remote_update" -class InvalidTrustedNetworks(Exception): - """Raised when invalid trusted networks config.""" - - -class InvalidTrustedProxies(Exception): - """Raised when invalid trusted proxies config.""" - - class RequireRelink(Exception): """The skill needs to be relinked.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 64d3943dda7..c18db2ac683 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -33,8 +33,6 @@ from .const import ( PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, - InvalidTrustedNetworks, - InvalidTrustedProxies, RequireRelink, ) @@ -42,14 +40,6 @@ _LOGGER = logging.getLogger(__name__) _CLOUD_ERRORS = { - InvalidTrustedNetworks: ( - HTTPStatus.INTERNAL_SERVER_ERROR, - "Remote UI not compatible with 127.0.0.1/::1 as a trusted network.", - ), - InvalidTrustedProxies: ( - HTTPStatus.INTERNAL_SERVER_ERROR, - "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", - ), asyncio.TimeoutError: ( HTTPStatus.BAD_GATEWAY, "Unable to reach the Home Assistant cloud.", diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 816ebe26d24..bb76ca3b669 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,8 +1,6 @@ """Preference management for cloud.""" from __future__ import annotations -from ipaddress import ip_address - from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User from homeassistant.core import callback @@ -34,8 +32,6 @@ from .const import ( PREF_SHOULD_EXPOSE, PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, - InvalidTrustedNetworks, - InvalidTrustedProxies, ) STORAGE_KEY = DOMAIN @@ -110,14 +106,6 @@ class CloudPreferences: if value is not UNDEFINED: prefs[key] = value - if remote_enabled is True and self._has_local_trusted_network: - prefs[PREF_ENABLE_REMOTE] = False - raise InvalidTrustedNetworks - - if remote_enabled is True and self._has_local_trusted_proxies: - prefs[PREF_ENABLE_REMOTE] = False - raise InvalidTrustedProxies - await self._save_prefs(prefs) async def async_update_google_entity_config( @@ -217,9 +205,6 @@ class CloudPreferences: if not self._prefs.get(PREF_ENABLE_REMOTE, False): return False - if self._has_local_trusted_network or self._has_local_trusted_proxies: - return False - return True @property @@ -310,38 +295,6 @@ class CloudPreferences: # an image was restored without restoring the cloud prefs. return await self._hass.auth.async_get_user(user_id) - @property - def _has_local_trusted_network(self) -> bool: - """Return if we allow localhost to bypass auth.""" - local4 = ip_address("127.0.0.1") - local6 = ip_address("::1") - - for prv in self._hass.auth.auth_providers: - if prv.type != "trusted_networks": - continue - - for network in prv.trusted_networks: - if local4 in network or local6 in network: - return True - - return False - - @property - def _has_local_trusted_proxies(self) -> bool: - """Return if we allow localhost to be a proxy and use its data.""" - if not hasattr(self._hass, "http"): - return False - - local4 = ip_address("127.0.0.1") - local6 = ip_address("::1") - - if any( - local4 in nwk or local6 in nwk for nwk in self._hass.http.trusted_proxies - ): - return True - - return False - async def _save_prefs(self, prefs): """Save preferences to disk.""" self._prefs = prefs diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 947a8e0125c..42a498528ce 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,7 +1,6 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from http import HTTPStatus -from ipaddress import ip_network from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp @@ -11,7 +10,6 @@ from hass_nabucasa.const import STATE_CONNECTED from jose import jwt import pytest -from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.alexa import errors as alexa_errors from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink @@ -558,100 +556,6 @@ async def test_enabling_remote(hass, hass_ws_client, setup_api, mock_cloud_login assert len(mock_disconnect.mock_calls) == 1 -async def test_enabling_remote_trusted_networks_local4( - hass, hass_ws_client, setup_api, mock_cloud_login -): - """Test we cannot enable remote UI when trusted networks active.""" - # pylint: disable=protected-access - hass.auth._providers[ - ("trusted_networks", None) - ] = tn_auth.TrustedNetworksAuthProvider( - hass, - None, - tn_auth.CONFIG_SCHEMA( - {"type": "trusted_networks", "trusted_networks": ["127.0.0.1"]} - ), - ) - - client = await hass_ws_client(hass) - - with patch( - "hass_nabucasa.remote.RemoteUI.connect", side_effect=AssertionError - ) as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR - assert ( - response["error"]["message"] - == "Remote UI not compatible with 127.0.0.1/::1 as a trusted network." - ) - - assert len(mock_connect.mock_calls) == 0 - - -async def test_enabling_remote_trusted_networks_local6( - hass, hass_ws_client, setup_api, mock_cloud_login -): - """Test we cannot enable remote UI when trusted networks active.""" - # pylint: disable=protected-access - hass.auth._providers[ - ("trusted_networks", None) - ] = tn_auth.TrustedNetworksAuthProvider( - hass, - None, - tn_auth.CONFIG_SCHEMA( - {"type": "trusted_networks", "trusted_networks": ["::1"]} - ), - ) - - client = await hass_ws_client(hass) - - with patch( - "hass_nabucasa.remote.RemoteUI.connect", side_effect=AssertionError - ) as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR - assert ( - response["error"]["message"] - == "Remote UI not compatible with 127.0.0.1/::1 as a trusted network." - ) - - assert len(mock_connect.mock_calls) == 0 - - -async def test_enabling_remote_trusted_networks_other( - hass, hass_ws_client, setup_api, mock_cloud_login -): - """Test we can enable remote UI when trusted networks active.""" - # pylint: disable=protected-access - hass.auth._providers[ - ("trusted_networks", None) - ] = tn_auth.TrustedNetworksAuthProvider( - hass, - None, - tn_auth.CONFIG_SCHEMA( - {"type": "trusted_networks", "trusted_networks": ["192.168.0.0/24"]} - ), - ) - - client = await hass_ws_client(hass) - cloud = hass.data[DOMAIN] - - with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() - - assert response["success"] - assert cloud.client.remote_autostart - - assert len(mock_connect.mock_calls) == 1 - - async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_login): """Test that we can list Google entities.""" client = await hass_ws_client(hass) @@ -729,54 +633,6 @@ async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_ } -async def test_enabling_remote_trusted_proxies_local4( - hass, hass_ws_client, setup_api, mock_cloud_login -): - """Test we cannot enable remote UI when trusted networks active.""" - hass.http.trusted_proxies.append(ip_network("127.0.0.1")) - - client = await hass_ws_client(hass) - - with patch( - "hass_nabucasa.remote.RemoteUI.connect", side_effect=AssertionError - ) as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR - assert ( - response["error"]["message"] - == "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies." - ) - - assert len(mock_connect.mock_calls) == 0 - - -async def test_enabling_remote_trusted_proxies_local6( - hass, hass_ws_client, setup_api, mock_cloud_login -): - """Test we cannot enable remote UI when trusted networks active.""" - hass.http.trusted_proxies.append(ip_network("::1")) - - client = await hass_ws_client(hass) - - with patch( - "hass_nabucasa.remote.RemoteUI.connect", side_effect=AssertionError - ) as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR - assert ( - response["error"]["message"] - == "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies." - ) - - assert len(mock_connect.mock_calls) == 0 - - async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_login): """Test that we can list Alexa entities.""" client = await hass_ws_client(hass) From d9f3cff606912d57a84903acf8d8ab8cef363217 Mon Sep 17 00:00:00 2001 From: JumpmanJunior Date: Sun, 14 Nov 2021 17:30:03 +0100 Subject: [PATCH 0473/1452] Fix IAD and vis for DS2438 1wire sensor (#59126) * Fix vis and IAD for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 * Fix IAD and vis for DS2438 --- homeassistant/components/onewire/sensor.py | 10 ++++------ homeassistant/components/onewire/switch.py | 11 ++++++++++- tests/components/onewire/const.py | 21 ++++++++++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 54528cf05f3..50d1a8e0c8e 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -24,13 +24,11 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_TYPE, - DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, @@ -183,11 +181,11 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=STATE_CLASS_MEASUREMENT, ), OneWireSensorEntityDescription( - key="IAD", - device_class=DEVICE_CLASS_CURRENT, + key="vis", + device_class=DEVICE_CLASS_VOLTAGE, entity_registry_enabled_default=False, - name="Current", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + name="vis", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, read_mode=READ_MODE_FLOAT, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 712077c62bd..3146f4fb8a6 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,6 +58,15 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { for id in DEVICE_KEYS_A_B ] ), + "26": ( + OneWireSwitchEntityDescription( + key="IAD", + entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, + name="IAD", + read_mode=READ_MODE_BOOL, + ), + ), "29": tuple( [ OneWireSwitchEntityDescription( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 55663c65d36..537c93cbe11 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -26,13 +26,11 @@ from homeassistant.const import ( ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, ATTR_VIA_DEVICE, - DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, @@ -399,13 +397,22 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_ENTITY_ID: "sensor.26_111111111111_current", - ATTR_INJECT_READS: b" 1", - ATTR_STATE: "1.0", + ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_ENTITY_ID: "sensor.26_111111111111_vis", + ATTR_INJECT_READS: b" 0.12", + ATTR_STATE: "0.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/vis", + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + }, + ], + SWITCH_DOMAIN: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "switch.26_111111111111_iad", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/26.111111111111/IAD", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, ], }, From 70de7db1973b5c24779dc6d1baa19035648267ef Mon Sep 17 00:00:00 2001 From: flyize Date: Sun, 14 Nov 2021 12:33:34 -0500 Subject: [PATCH 0474/1452] Update surepetcare services.yaml (#58892) --- homeassistant/components/surepetcare/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml index fc352aeb6ab..57b1ef22008 100644 --- a/homeassistant/components/surepetcare/services.yaml +++ b/homeassistant/components/surepetcare/services.yaml @@ -33,7 +33,7 @@ set_pet_location: text: location: description: Pet location (Inside or Outside) - example: inside + example: Inside required: true selector: select: From 5b5b79f0e5a0c8f800f08b527e23850ad0e541f3 Mon Sep 17 00:00:00 2001 From: Peeter N Date: Sun, 14 Nov 2021 19:52:35 +0200 Subject: [PATCH 0475/1452] Set MaxCube Climate properties using class _attr_ properties (#58910) --- homeassistant/components/maxcube/climate.py | 57 +++++---------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 2b2395acfc6..cba13ac5f09 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -68,29 +68,22 @@ class MaxCubeClimate(ClimateEntity): def __init__(self, handler, device): """Initialize MAX! Cube ClimateEntity.""" room = handler.cube.room_by_id(device.room_id) - self._name = f"{room.name} {device.name}" + self._attr_name = f"{room.name} {device.name}" self._cubehandle = handler self._device = device - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._device.serial + self._attr_supported_features = SUPPORT_FLAGS + self._attr_should_poll = True + self._attr_unique_id = self._device.serial + self._attr_temperature_unit = TEMP_CELSIUS + self._attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT] + self._attr_preset_modes = [ + PRESET_NONE, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_AWAY, + PRESET_ON, + ] @property def min_temp(self): @@ -105,11 +98,6 @@ class MaxCubeClimate(ClimateEntity): """Return the maximum temperature.""" return self._device.max_temperature or MAX_TEMPERATURE - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" @@ -129,11 +117,6 @@ class MaxCubeClimate(ClimateEntity): return HVAC_MODE_HEAT - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT] - def set_hvac_mode(self, hvac_mode: str): """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: @@ -222,18 +205,6 @@ class MaxCubeClimate(ClimateEntity): return PRESET_AWAY return PRESET_NONE - @property - def preset_modes(self): - """Return available preset modes.""" - return [ - PRESET_NONE, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_ECO, - PRESET_AWAY, - PRESET_ON, - ] - def set_preset_mode(self, preset_mode): """Set new operation mode.""" if preset_mode == PRESET_COMFORT: From 568df3d972cda73a2d4fb8b1243fd919b0a391bf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:05:38 -0700 Subject: [PATCH 0476/1452] Perform some RainMachine code cleanup (#58865) --- .../components/rainmachine/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index b72fe0fb25d..786895dc99b 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -13,7 +13,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_DEVICE_ID, CONF_IP_ADDRESS, CONF_PASSWORD, @@ -166,9 +165,6 @@ async def async_update_programs_and_zones( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -184,9 +180,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: - controller = hass.data[DOMAIN][entry.entry_id][ - DATA_CONTROLLER - ] = get_client_controller(client) + controller = get_client_controller(client) entry_updates: dict[str, Any] = {} if not entry.unique_id or is_ip_address(entry.unique_id): @@ -244,7 +238,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*controller_init_tasks) - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinators + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_CONTROLLER: controller, + DATA_COORDINATOR: coordinators, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -354,7 +353,7 @@ class RainMachineEntity(CoordinatorEntity): ), sw_version=controller.software_version, ) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_name = f"{controller.name} {description.name}" # The colons are removed from the device MAC simply because that value # (unnecessarily) makes up the existing unique ID formula and we want to avoid From 198b18dd0004ffbc93efb142b375b3f582d1944e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:06:27 -0700 Subject: [PATCH 0477/1452] Perform some OpenUV code cleanup (#58864) --- homeassistant/components/openuv/__init__.py | 11 ++++------- homeassistant/components/openuv/binary_sensor.py | 10 ++-------- homeassistant/components/openuv/const.py | 1 - homeassistant/components/openuv/sensor.py | 3 +-- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index e38d95a6101..20ef5211c23 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -9,7 +9,6 @@ from pyopenuv.errors import OpenUvError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, @@ -30,7 +29,6 @@ from homeassistant.helpers.service import verify_domain_control from .const import ( CONF_FROM_WINDOW, CONF_TO_WINDOW, - DATA_CLIENT, DATA_PROTECTION_WINDOW, DATA_UV, DEFAULT_FROM_WINDOW, @@ -51,9 +49,6 @@ PLATFORMS = ["binary_sensor", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - _verify_domain_control = verify_domain_control(hass, DOMAIN) try: @@ -73,7 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = openuv + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = openuv + hass.config_entries.async_setup_platforms(entry, PLATFORMS) @_verify_domain_control @@ -175,7 +172,7 @@ class OpenUvEntity(Entity): def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_should_poll = False self._attr_unique_id = ( f"{openuv.client.latitude}_{openuv.client.longitude}_{description.key}" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 913d844a7c3..503d82d32f2 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -9,13 +9,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, parse_datetime, utcnow from . import OpenUvEntity -from .const import ( - DATA_CLIENT, - DATA_PROTECTION_WINDOW, - DOMAIN, - LOGGER, - TYPE_PROTECTION_WINDOW, -) +from .const import DATA_PROTECTION_WINDOW, DOMAIN, LOGGER, TYPE_PROTECTION_WINDOW ATTR_PROTECTION_WINDOW_ENDING_TIME = "end_time" ATTR_PROTECTION_WINDOW_ENDING_UV = "end_uv" @@ -33,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up an OpenUV sensor based on a config entry.""" - openuv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + openuv = hass.data[DOMAIN][entry.entry_id] async_add_entities( [OpenUvBinarySensor(openuv, BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW)] ) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index 975511c7297..b03726d5749 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -7,7 +7,6 @@ LOGGER = logging.getLogger(__package__) CONF_FROM_WINDOW = "from_window" CONF_TO_WINDOW = "to_window" -DATA_CLIENT = "data_client" DATA_PROTECTION_WINDOW = "protection_window" DATA_UV = "uv" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index bcfac6e3684..0660ca740ac 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -14,7 +14,6 @@ from homeassistant.util.dt import as_local, parse_datetime from . import OpenUvEntity from .const import ( - DATA_CLIENT, DATA_UV, DOMAIN, TYPE_CURRENT_OZONE_LEVEL, @@ -122,7 +121,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up a OpenUV sensor based on a config entry.""" - openuv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + openuv = hass.data[DOMAIN][entry.entry_id] async_add_entities( [OpenUvSensor(openuv, description) for description in SENSOR_DESCRIPTIONS] ) From 56942504453a24fdaba2fe47f80c78a8dbbc72b1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:07:34 -0700 Subject: [PATCH 0478/1452] Perform some Guardian code cleanup (#58861) --- homeassistant/components/guardian/__init__.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 3610d3d3d80..0edd7088b89 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -8,7 +8,7 @@ from typing import cast from aioguardian import Client from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription @@ -40,12 +40,6 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_COORDINATOR: {}, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - } - client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) # The valve controller's UDP-based API can't handle concurrent requests very well, @@ -53,6 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_lock = asyncio.Lock() # Set up DataUpdateCoordinators for the valve controller: + coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( (API_SENSOR_PAIR_DUMP, client.sensor.pair_dump), @@ -61,9 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - api - ] = GuardianDataUpdateCoordinator( + coordinator = coordinators[api] = GuardianDataUpdateCoordinator( hass, client=client, api_name=api, @@ -74,15 +67,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: init_valve_controller_tasks.append(coordinator.async_refresh()) await asyncio.gather(*init_valve_controller_tasks) - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ] = PairedSensorManager(hass, entry, client, api_lock) + paired_sensor_manager = PairedSensorManager(hass, entry, client, api_lock) await paired_sensor_manager.async_process_latest_paired_sensor_uids() + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: client, + DATA_COORDINATOR: coordinators, + DATA_COORDINATOR_PAIRED_SENSOR: {}, + DATA_PAIRED_SENSOR_MANAGER: paired_sensor_manager, + } + @callback def async_process_paired_sensor_uids() -> None: """Define a callback for when new paired sensor data is received.""" @@ -90,9 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: paired_sensor_manager.async_process_latest_paired_sensor_uids() ) - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - API_SENSOR_PAIR_DUMP - ].async_add_listener(async_process_paired_sensor_uids) + coordinators[API_SENSOR_PAIR_DUMP].async_add_listener( + async_process_paired_sensor_uids + ) # Set up all of the Guardian entity platforms: hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -204,7 +202,7 @@ class GuardianEntity(CoordinatorEntity): ) -> None: """Initialize.""" self._attr_device_info = DeviceInfo(manufacturer="Elexa") - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} + self._attr_extra_state_attributes = {} self._entry = entry self.entity_description = description From 595184aa5562de763b36a533dd166f1f0e867518 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:08:35 -0700 Subject: [PATCH 0479/1452] Perform some Notion code cleanup (#58863) --- homeassistant/components/notion/__init__.py | 12 +++++------- homeassistant/components/notion/binary_sensor.py | 3 +-- homeassistant/components/notion/config_flow.py | 13 ++++++------- homeassistant/components/notion/const.py | 2 -- homeassistant/components/notion/sensor.py | 4 ++-- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index d06827ffd7d..4aa8447eb9a 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -9,7 +9,7 @@ from aionotion import async_get_client from aionotion.errors import InvalidCredentialsError, NotionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -24,7 +24,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DATA_COORDINATOR, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER PLATFORMS = ["binary_sensor", "sensor"] @@ -39,9 +39,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notion as a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - if not entry.unique_id: hass.config_entries.async_update_entry( entry, unique_id=entry.data[CONF_USERNAME] @@ -99,7 +96,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -157,7 +155,7 @@ class NotionEntity(CoordinatorEntity): via_device=(DOMAIN, bridge.get("hardware_id")), ) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_name = f'{sensor["name"]}: {description.name}' self._attr_unique_id = ( f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index ab3d0775436..42e32b1e19c 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -22,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity from .const import ( - DATA_COORDINATOR, DOMAIN, LOGGER, SENSOR_BATTERY, @@ -122,7 +121,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 84fe69eb61a..cdaab389dc7 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -44,23 +44,22 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assert self._username assert self._password + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) try: await async_get_client(self._username, self._password, session=session) except InvalidCredentialsError: - return self.async_show_form( - step_id=step_id, - data_schema=schema, - errors={"base": "invalid_auth"}, - description_placeholders={CONF_USERNAME: self._username}, - ) + errors["base"] = "invalid_auth" except NotionError as err: LOGGER.error("Unknown Notion error: %s", err) + errors["base"] = "unknown" + + if errors: return self.async_show_form( step_id=step_id, data_schema=schema, - errors={"base": "unknown"}, + errors=errors, description_placeholders={CONF_USERNAME: self._username}, ) diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 5541cfedc70..339d3020734 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -4,8 +4,6 @@ import logging DOMAIN = "notion" LOGGER = logging.getLogger(__package__) -DATA_COORDINATOR = "coordinator" - SENSOR_BATTERY = "low_battery" SENSOR_DOOR = "door" SENSOR_GARAGE_DOOR = "garage_door" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index efb33944990..2e7260080bb 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity -from .const import DATA_COORDINATOR, DOMAIN, LOGGER, SENSOR_TEMPERATURE +from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE SENSOR_DESCRIPTIONS = ( SensorEntityDescription( @@ -27,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ From 24c899cf50837bcdc330f8ed3b2e47cf0fdc2066 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:10:05 -0700 Subject: [PATCH 0480/1452] Perform some Flu Near You code cleanup (#58860) --- homeassistant/components/flunearyou/__init__.py | 3 +-- homeassistant/components/flunearyou/sensor.py | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 86a86e440c9..beb7bec2c2f 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -26,8 +26,6 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" - hass.data.setdefault(DOMAIN, {}) - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -64,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*data_init_tasks) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index a30c2423253..0017d868964 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -10,12 +10,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_STATE, - CONF_LATITUDE, - CONF_LONGITUDE, -) +from homeassistant.const import ATTR_STATE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -34,8 +29,6 @@ ATTR_STATE_REPORTS_LAST_WEEK = "state_reports_last_week" ATTR_STATE_REPORTS_THIS_WEEK = "state_reports_this_week" ATTR_ZIP_CODE = "zip_code" -DEFAULT_ATTRIBUTION = "Data provided by Flu Near You" - SENSOR_TYPE_CDC_LEVEL = "level" SENSOR_TYPE_CDC_LEVEL2 = "level2" SENSOR_TYPE_USER_CHICK = "chick" @@ -140,8 +133,6 @@ async def async_setup_entry( class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" - DEFAULT_EXTRA_STATE_ATTRIBUTES = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - def __init__( self, coordinator: DataUpdateCoordinator, @@ -166,7 +157,6 @@ class CdcSensor(FluNearYouSensor): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" return { - **self.DEFAULT_EXTRA_STATE_ATTRIBUTES, ATTR_REPORTED_DATE: self.coordinator.data["week_date"], ATTR_STATE: self.coordinator.data["name"], } @@ -186,7 +176,6 @@ class UserSensor(FluNearYouSensor): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" attrs = { - **self.DEFAULT_EXTRA_STATE_ATTRIBUTES, ATTR_CITY: self.coordinator.data["local"]["city"].split("(")[0], ATTR_REPORTED_LATITUDE: self.coordinator.data["local"]["latitude"], ATTR_REPORTED_LONGITUDE: self.coordinator.data["local"]["longitude"], From 03176dad7d7fee1371d685d0ceada967af760e0c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:11:36 -0700 Subject: [PATCH 0481/1452] Perform some IQVIA code cleanup (#58862) --- homeassistant/components/iqvia/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 8d556251e4e..38d29aeffb2 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -11,7 +11,6 @@ from pyiqvia import Client from pyiqvia.errors import IQVIAError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -43,9 +42,6 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IQVIA as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: hass.config_entries.async_update_entry( @@ -94,7 +90,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # API calls fail: raise ConfigEntryNotReady() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -121,7 +119,7 @@ class IQVIAEntity(CoordinatorEntity): """Initialize.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_unique_id = f"{entry.data[CONF_ZIP_CODE]}_{description.key}" self._entry = entry self.entity_description = description From dbfe0cad5294e166ffab6e097a15591d3e882d81 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 14 Nov 2021 10:12:25 -0800 Subject: [PATCH 0482/1452] Refactor nest test ConfigEntry setup in config flow tests (#59459) * Refactor nest ConfigEntry creation in tests into a helper This is pulled out of PR#59260 to make that overall diff smaller. * Add typing consistently on new functions --- tests/components/nest/test_config_flow_sdm.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 2b7ac71d44c..75ce8bcf939 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -8,6 +8,7 @@ from homeassistant import config_entries, setup from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN 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 from .common import MockConfigEntry @@ -34,13 +35,24 @@ WEB_REDIRECT_URL = "https://example.com/auth/external/callback" APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" -def get_config_entry(hass): +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: """Return a single config entry.""" entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 return entries[0] +def create_config_entry(hass: HomeAssistant, data: dict) -> ConfigEntry: + """Create the ConfigEntry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + return entry + + class OAuthFixture: """Simulate the oauth flow used by the config flow.""" @@ -165,9 +177,9 @@ async def test_web_reauth(hass, oauth): assert await setup.async_setup_component(hass, DOMAIN, CONFIG) - old_entry = MockConfigEntry( - domain=DOMAIN, - data={ + old_entry = create_config_entry( + hass, + { "auth_implementation": WEB_AUTH_DOMAIN, "token": { # Verify this is replaced at end of the test @@ -175,9 +187,7 @@ async def test_web_reauth(hass, oauth): }, "sdm": {}, }, - unique_id=DOMAIN, ) - old_entry.add_to_hass(hass) entry = get_config_entry(hass) assert entry.data["token"] == { @@ -211,10 +221,7 @@ async def test_web_reauth(hass, oauth): async def test_single_config_entry(hass): """Test that only a single config entry is allowed.""" - old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} - ) - old_entry.add_to_hass(hass) + create_config_entry(hass, {"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}}) assert await setup.async_setup_component(hass, DOMAIN, CONFIG) @@ -299,9 +306,9 @@ async def test_app_reauth(hass, oauth): assert await setup.async_setup_component(hass, DOMAIN, CONFIG) - old_entry = MockConfigEntry( - domain=DOMAIN, - data={ + old_entry = create_config_entry( + hass, + { "auth_implementation": APP_AUTH_DOMAIN, "token": { # Verify this is replaced at end of the test @@ -309,9 +316,7 @@ async def test_app_reauth(hass, oauth): }, "sdm": {}, }, - unique_id=DOMAIN, ) - old_entry.add_to_hass(hass) entry = get_config_entry(hass) assert entry.data["token"] == { From 20fbb5b95136b6f089e2fa04198c56d2b7f224d6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Nov 2021 11:12:34 -0700 Subject: [PATCH 0483/1452] Perform some ReCollect Waste code cleanup (#58866) --- homeassistant/components/recollect_waste/__init__.py | 8 +++----- homeassistant/components/recollect_waste/const.py | 2 -- homeassistant/components/recollect_waste/sensor.py | 9 ++++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index c32b4119c67..25900345205 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) @@ -21,9 +21,6 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - session = aiohttp_client.async_get_clientsession(hass) client = Client( entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session @@ -49,7 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py index 4a6c9dbda6c..5589507d4ac 100644 --- a/homeassistant/components/recollect_waste/const.py +++ b/homeassistant/components/recollect_waste/const.py @@ -7,5 +7,3 @@ LOGGER = logging.getLogger(__package__) CONF_PLACE_ID = "place_id" CONF_SERVICE_ID = "service_id" - -DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 619a12a42f7..eb4cacb38e2 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -5,7 +5,7 @@ from aiorecollect.client import PickupType from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE +from homeassistant.const import CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,14 +14,13 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN ATTR_PICKUP_TYPES = "pickup_types" ATTR_AREA_NAME = "area_name" ATTR_NEXT_PICKUP_TYPES = "next_pickup_types" ATTR_NEXT_PICKUP_DATE = "next_pickup_date" -DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "Waste Pickup" PLATFORM_SCHEMA = cv.deprecated(DOMAIN) @@ -44,7 +43,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([ReCollectWasteSensor(coordinator, entry)]) @@ -57,7 +56,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {} self._attr_name = DEFAULT_NAME self._attr_unique_id = ( f"{entry.data[CONF_PLACE_ID]}{entry.data[CONF_SERVICE_ID]}" From c98172f9c13468ffbdaa73910007219f03a0f32b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 14 Nov 2021 19:47:15 +0100 Subject: [PATCH 0484/1452] Add typing to deCONZ Scene platform and deCONZ Services (#59603) Co-authored-by: Matthias Alphart --- homeassistant/components/deconz/scene.py | 27 ++++++++++++++++----- homeassistant/components/deconz/services.py | 23 +++++++++++------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 69f3d48c82c..3d8e1aa27ba 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -1,19 +1,34 @@ """Support for deCONZ scenes.""" + +from __future__ import annotations + +from collections.abc import ValuesView from typing import Any +from pydeconz.group import DeconzScene as PydeconzScene + from homeassistant.components.scene import Scene -from homeassistant.core import callback +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 .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up scenes for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) @callback - def async_add_scene(scenes=gateway.api.scenes.values()): + def async_add_scene( + scenes: list[PydeconzScene] + | ValuesView[PydeconzScene] = gateway.api.scenes.values(), + ) -> None: """Add scene from deCONZ.""" entities = [DeconzScene(scene, gateway) for scene in scenes] @@ -34,14 +49,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzScene(Scene): """Representation of a deCONZ scene.""" - def __init__(self, scene, gateway): + def __init__(self, scene: PydeconzScene, gateway: DeconzGateway) -> None: """Set up a scene.""" self._scene = scene self.gateway = gateway self._attr_name = scene.full_name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to sensors events.""" self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 535dd9807fb..63252aa7787 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,9 +1,12 @@ """deCONZ services.""" +from types import MappingProxyType + from pydeconz.utils import normalize_bridge_id import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.deconz.gateway import DeconzGateway +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -55,10 +58,10 @@ SERVICE_TO_SCHEMA = { @callback -def async_setup_services(hass): +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for deCONZ integration.""" - async def async_call_deconz_service(service_call): + async def async_call_deconz_service(service_call: ServiceCall) -> None: """Call correct deCONZ service.""" service = service_call.service service_data = service_call.data @@ -97,13 +100,15 @@ def async_setup_services(hass): @callback -def async_unload_services(hass): +def async_unload_services(hass: HomeAssistant) -> None: """Unload deCONZ services.""" for service in SUPPORTED_SERVICES: hass.services.async_remove(DOMAIN, service) -async def async_configure_service(gateway, data): +async def async_configure_service( + gateway: DeconzGateway, data: MappingProxyType +) -> None: """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). @@ -133,7 +138,7 @@ async def async_configure_service(gateway, data): await gateway.api.request("put", field, json=data) -async def async_refresh_devices_service(gateway): +async def async_refresh_devices_service(gateway: DeconzGateway) -> None: """Refresh available devices from deCONZ.""" gateway.ignore_state_updates = True await gateway.api.refresh_state() @@ -143,7 +148,7 @@ async def async_refresh_devices_service(gateway): gateway.async_add_device_callback(resource_type, force=True) -async def async_remove_orphaned_entries_service(gateway): +async def async_remove_orphaned_entries_service(gateway: DeconzGateway) -> None: """Remove orphaned deCONZ entries from device and entity registries.""" device_registry = dr.async_get(gateway.hass) entity_registry = er.async_get(gateway.hass) @@ -164,14 +169,14 @@ async def async_remove_orphaned_entries_service(gateway): connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, identifiers=set(), ) - if gateway_host.id in devices_to_be_removed: + if gateway_host and gateway_host.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_host.id) # Don't remove the Gateway service entry gateway_service = device_registry.async_get_device( identifiers={(DOMAIN, gateway.api.config.bridge_id)}, connections=set() ) - if gateway_service.id in devices_to_be_removed: + if gateway_service and gateway_service.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_service.id) # Don't remove devices belonging to available events From cd988bded0bc842fdf8454e1bb23b92c83d4cacc Mon Sep 17 00:00:00 2001 From: Anton Malko Date: Sun, 14 Nov 2021 22:36:14 +0300 Subject: [PATCH 0485/1452] Update aiolookin to 0.0.4 version (#59684) --- homeassistant/components/lookin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 046b0e482a1..7260985654a 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", "codeowners": ["@ANMalko"], - "requirements": ["aiolookin==0.0.3"], + "requirements": ["aiolookin==0.0.4"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1398085ca0c..43f1d7b3e92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -207,7 +207,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.3 +aiolookin==0.0.4 # homeassistant.components.lyric aiolyric==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d74fbbdb939..a84863a4cca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiokafka==0.6.0 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.3 +aiolookin==0.0.4 # homeassistant.components.lyric aiolyric==1.0.8 From 85aeee7cc73e55ce30b5bf609e0251c9758265b7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 14 Nov 2021 14:37:40 -0500 Subject: [PATCH 0486/1452] Add smartthings configuration_url (#58676) --- .../components/smartthings/__init__.py | 17 +++++++++-------- .../smartthings/test_binary_sensor.py | 2 ++ tests/components/smartthings/test_climate.py | 2 ++ tests/components/smartthings/test_cover.py | 2 ++ tests/components/smartthings/test_fan.py | 2 ++ tests/components/smartthings/test_light.py | 2 ++ tests/components/smartthings/test_lock.py | 2 ++ tests/components/smartthings/test_sensor.py | 12 ++++++++++++ tests/components/smartthings/test_switch.py | 2 ++ 9 files changed, 35 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index eb3aa9cb0f0..3f10758076f 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -428,14 +428,15 @@ class SmartThingsEntity(Entity): self._dispatcher_remove() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Get attributes about the device.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.label, - "model": self._device.device_type_name, - "manufacturer": "Unavailable", - } + return DeviceInfo( + configuration_url="https://account.smartthings.com", + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer="Unavailable", + model=self._device.device_type_name, + name=self._device.label, + ) @property def name(self) -> str: diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index efc34424ae0..7f4748bc215 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -65,6 +65,8 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry.unique_id == f"{device.device_id}.{Attribute.motion}" entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index dc8f2acc9fa..17443b72029 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -579,6 +579,8 @@ async def test_entity_and_device_attributes(hass, thermostat): entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, thermostat.device_id)} assert entry.name == thermostat.label assert entry.model == thermostat.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index aad7a4b037e..4111d21b25b 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -44,6 +44,8 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 2a66fc646c7..16b0360f9eb 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -70,6 +70,8 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 81062adf934..166b0606b66 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -123,6 +123,8 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 86c8d534a71..f1ab6640058 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -32,6 +32,8 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 049666baf99..0e22a1facba 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -98,6 +98,8 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" @@ -125,6 +127,8 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): assert entry.entity_category is None entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" @@ -138,6 +142,8 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): assert entry.entity_category is None entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" @@ -175,6 +181,8 @@ async def test_power_consumption_sensor(hass, device_factory): assert entry.unique_id == f"{device.device_id}.energy_meter" entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" @@ -187,6 +195,8 @@ async def test_power_consumption_sensor(hass, device_factory): assert entry.unique_id == f"{device.device_id}.power_meter" entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" @@ -209,6 +219,8 @@ async def test_power_consumption_sensor(hass, device_factory): assert entry.unique_id == f"{device.device_id}.energy_meter" entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index c884d601baf..77c7f4d2c7e 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -31,6 +31,8 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry + assert entry.configuration_url == "https://account.smartthings.com" + assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" From 305d25b5109bbb97bc0ab9106e6904b094567f81 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 14 Nov 2021 13:41:23 -0600 Subject: [PATCH 0487/1452] Set measurement entity_class for darksky temp & humidity (#59512) --- homeassistant/components/darksky/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 3de47136d45..6d1711b4d0e 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -181,6 +182,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { key="temperature", name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -192,6 +194,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { key="apparent_temperature", name="Apparent Temperature", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -203,6 +206,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { key="dew_point", name="Dew Point", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -258,6 +262,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { key="humidity", name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, si_unit=PERCENTAGE, us_unit=PERCENTAGE, ca_unit=PERCENTAGE, From 9f2ec5c9068debdefacfa0914c44bd7844fbf8ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 14 Nov 2021 21:03:00 +0100 Subject: [PATCH 0488/1452] Upgrade wled to 0.10.0 (#59669) * Upgrade wled to 0.10.0 * DeviceInfo expects a string, not a AwesomeVersion object --- homeassistant/components/wled/manifest.json | 2 +- homeassistant/components/wled/models.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index c513f424e08..8c87880ad38 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.9.0"], + "requirements": ["wled==0.10.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index 93b04b9f49b..a71491daf7a 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -19,6 +19,6 @@ class WLEDEntity(CoordinatorEntity): name=self.coordinator.data.info.name, manufacturer=self.coordinator.data.info.brand, model=self.coordinator.data.info.product, - sw_version=self.coordinator.data.info.version, + sw_version=str(self.coordinator.data.info.version), configuration_url=f"http://{self.coordinator.wled.host}", ) diff --git a/requirements_all.txt b/requirements_all.txt index 43f1d7b3e92..52c4f3bf7c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2421,7 +2421,7 @@ wirelesstagpy==0.5.0 withings-api==2.3.2 # homeassistant.components.wled -wled==0.9.0 +wled==0.10.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a84863a4cca..838723cb619 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1416,7 +1416,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.9.0 +wled==0.10.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 9c2bff3b3b433a1fb77adcc0c2192e684c93c664 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Sun, 14 Nov 2021 15:06:42 -0500 Subject: [PATCH 0489/1452] Use octoprint printer flag status to check if printer is printing (#59663) --- homeassistant/components/octoprint/sensor.py | 23 ++++++++++- tests/components/octoprint/test_sensor.py | 40 +++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index af8ce9bc3c0..2a3e8c773ff 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -23,6 +23,17 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +JOB_PRINTING_STATES = ["Printing from SD", "Printing"] + + +def _is_printer_printing(printer: OctoprintPrinterInfo) -> bool: + return ( + printer + and printer.state + and printer.state.flags + and printer.state.flags.printing + ) + async def async_setup_entry( hass: HomeAssistant, @@ -151,7 +162,11 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): def native_value(self): """Return sensor state.""" job: OctoprintJobInfo = self.coordinator.data["job"] - if not job or not job.progress.print_time_left or job.state != "Printing": + if ( + not job + or not job.progress.print_time_left + or not _is_printer_printing(self.coordinator.data["printer"]) + ): return None read_time = self.coordinator.data["last_read_time"] @@ -175,7 +190,11 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): """Return sensor state.""" job: OctoprintJobInfo = self.coordinator.data["job"] - if not job or not job.progress.print_time or job.state != "Printing": + if ( + not job + or not job.progress.print_time + or not _is_printer_printing(self.coordinator.data["printer"]) + ): return None read_time = self.coordinator.data["last_read_time"] diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index 136674f3465..ccc7dfacaf2 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -11,7 +11,7 @@ async def test_sensors(hass): """Test the underlying sensors.""" printer = { "state": { - "flags": {}, + "flags": {"printing": True}, "text": "Operational", }, "temperature": {"tool1": {"actual": 18.83136, "target": 37.83136}}, @@ -82,7 +82,7 @@ async def test_sensors_no_target_temp(hass): """Test the underlying sensors.""" printer = { "state": { - "flags": {}, + "flags": {"printing": True, "paused": False}, "text": "Operational", }, "temperature": {"tool1": {"actual": 18.83136, "target": None}}, @@ -107,3 +107,39 @@ async def test_sensors_no_target_temp(hass): assert state.name == "OctoPrint target tool1 temp" entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") assert entry.unique_id == "target tool1 temp-uuid" + + +async def test_sensors_paused(hass): + """Test the underlying sensors.""" + printer = { + "state": { + "flags": {"printing": False}, + "text": "Operational", + }, + "temperature": {"tool1": {"actual": 18.83136, "target": None}}, + } + job = { + "job": {}, + "progress": {"completion": 50, "printTime": 600, "printTimeLeft": 6000}, + "state": "Paused", + } + with patch( + "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) + ): + await init_integration(hass, "sensor", printer=printer, job=job) + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.octoprint_start_time") + assert state is not None + assert state.state == "unknown" + assert state.name == "OctoPrint Start Time" + entry = entity_registry.async_get("sensor.octoprint_start_time") + assert entry.unique_id == "Start Time-uuid" + + state = hass.states.get("sensor.octoprint_estimated_finish_time") + assert state is not None + assert state.state == "unknown" + assert state.name == "OctoPrint Estimated Finish Time" + entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") + assert entry.unique_id == "Estimated Finish Time-uuid" From 0991a3012596543c996ff77ec84e3f44712d1cd3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 14 Nov 2021 16:08:22 -0800 Subject: [PATCH 0490/1452] Pre-factor nest subscriber to library (#59462) * Pre-factor nest subscriber to library Move the nest subscriber to a library that can be reused in a future PR: - From ConfigFlow for creating subscriptions - On nest removal to delete subscriptions This is pulled out of PR #59260 to make that easier to review. * Resolve pylint error in nest api subscriber * Remove duplicate constants --- homeassistant/components/nest/__init__.py | 32 +++---------------- homeassistant/components/nest/api.py | 39 +++++++++++++++++++++-- homeassistant/components/nest/const.py | 4 +++ tests/components/nest/common.py | 3 +- tests/components/nest/test_init_sdm.py | 14 +++++--- 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1cbe97dc3b7..37901d060e1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -8,7 +8,6 @@ from google_nest_sdm.exceptions import ( ConfigurationException, GoogleNestException, ) -from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -22,15 +21,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - config_entry_oauth2_flow, - config_validation as cv, -) +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 ( + CONF_PROJECT_ID, + CONF_SUBSCRIBER_ID, + DATA_NEST_CONFIG, DATA_SDM, DATA_SUBSCRIBER, DOMAIN, @@ -43,9 +41,6 @@ from .legacy import async_setup_legacy, async_setup_legacy_entry _LOGGER = logging.getLogger(__name__) -CONF_PROJECT_ID = "project_id" -CONF_SUBSCRIBER_ID = "subscriber_id" -DATA_NEST_CONFIG = "nest_config" DATA_NEST_UNAVAILABLE = "nest_unavailable" NEST_SETUP_NOTIFICATION = "nest_setup" @@ -199,24 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_SDM not in entry.data: return await async_setup_legacy_entry(hass, entry) - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) - - config = hass.data[DOMAIN][DATA_NEST_CONFIG] - - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) - auth = api.AsyncConfigEntryAuth( - aiohttp_client.async_get_clientsession(hass), - session, - config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET], - ) - subscriber = GoogleNestSubscriber( - auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] - ) + subscriber = await api.new_subscriber(hass, entry) callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 426a651461a..17b473dbeaa 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -6,10 +6,22 @@ from typing import cast from aiohttp import ClientSession from google.oauth2.credentials import Credentials from google_nest_sdm.auth import AbstractAuth +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber -from homeassistant.helpers import config_entry_oauth2_flow +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 aiohttp_client, config_entry_oauth2_flow -from .const import API_URL, OAUTH2_TOKEN, SDM_SCOPES +from .const import ( + API_URL, + CONF_PROJECT_ID, + CONF_SUBSCRIBER_ID, + DATA_NEST_CONFIG, + DOMAIN, + OAUTH2_TOKEN, + SDM_SCOPES, +) # See https://developers.google.com/nest/device-access/registration @@ -55,3 +67,26 @@ class AsyncConfigEntryAuth(AbstractAuth): ) creds.expiry = datetime.datetime.fromtimestamp(token["expires_at"]) return creds + + +async def new_subscriber( + hass: HomeAssistant, entry: ConfigEntry +) -> GoogleNestSubscriber: + """Create a GoogleNestSubscriber.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + config = hass.data[DOMAIN][DATA_NEST_CONFIG] + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + auth = AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), + session, + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], + ) + return GoogleNestSubscriber( + auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] + ) diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 25b43de1032..6fcd74299ba 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -3,6 +3,10 @@ DOMAIN = "nest" DATA_SDM = "sdm" DATA_SUBSCRIBER = "subscriber" +DATA_NEST_CONFIG = "nest_config" + +CONF_PROJECT_ID = "project_id" +CONF_SUBSCRIBER_ID = "subscriber_id" SIGNAL_NEST_UPDATE = "nest_update" diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index d6dc730ec11..c9572c528bb 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -107,7 +107,8 @@ async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch("homeassistant.components.nest.PLATFORMS", [platform]), patch( - "homeassistant.components.nest.GoogleNestSubscriber", return_value=subscriber + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, ): assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 205cc34fe20..59d1cbd0d69 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -60,7 +60,7 @@ async def test_setup_configuration_failure(hass, caplog): async def test_setup_susbcriber_failure(hass, caplog): """Test configuration error.""" with patch( - "homeassistant.components.nest.GoogleNestSubscriber.start_async", + "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", side_effect=GoogleNestException(), ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): result = await async_setup_sdm(hass) @@ -74,10 +74,14 @@ async def test_setup_susbcriber_failure(hass, caplog): async def test_setup_device_manager_failure(hass, caplog): """Test configuration error.""" - with patch("homeassistant.components.nest.GoogleNestSubscriber.start_async"), patch( - "homeassistant.components.nest.GoogleNestSubscriber.async_get_device_manager", + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.start_async" + ), patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", side_effect=GoogleNestException(), - ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + ), caplog.at_level( + logging.ERROR, logger="homeassistant.components.nest" + ): result = await async_setup_sdm(hass) assert result assert len(caplog.messages) == 1 @@ -91,7 +95,7 @@ async def test_setup_device_manager_failure(hass, caplog): async def test_subscriber_auth_failure(hass, caplog): """Test configuration error.""" with patch( - "homeassistant.components.nest.GoogleNestSubscriber.start_async", + "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", side_effect=AuthException(), ): result = await async_setup_sdm(hass, CONFIG) From f0c9f443d183d74ea95ffc2739d22500b3df5fe2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 15 Nov 2021 00:12:46 +0000 Subject: [PATCH 0491/1452] [ci skip] Translation update --- .../advantage_air/translations/ja.json | 4 ++- .../components/aemet/translations/ja.json | 3 ++ .../components/airly/translations/ja.json | 4 ++- .../components/airnow/translations/ja.json | 3 ++ .../components/airvisual/translations/ja.json | 26 ++++++++++++++++ .../alarm_control_panel/translations/ja.json | 3 ++ .../alarmdecoder/translations/ja.json | 3 ++ .../automation/translations/ca.json | 4 +-- .../binary_sensor/translations/ca.json | 4 +-- .../binary_sensor/translations/ja.json | 4 +++ .../components/calendar/translations/ca.json | 4 +-- .../components/climate/translations/ca.json | 2 +- .../demo/translations/select.ja.json | 1 + .../devolo_home_control/translations/ja.json | 7 +++++ .../dialogflow/translations/ja.json | 3 ++ .../components/fan/translations/ca.json | 4 +-- .../components/flipr/translations/ja.json | 5 ++- .../forecast_solar/translations/ja.json | 11 +++++-- .../components/geofency/translations/ja.json | 4 +++ .../components/gpslogger/translations/ja.json | 4 +++ .../components/group/translations/ca.json | 4 +-- .../components/hive/translations/ja.json | 3 ++ .../components/homekit/translations/ja.json | 3 ++ .../homekit_controller/translations/ja.json | 3 +- .../humidifier/translations/ca.json | 4 +-- .../components/ifttt/translations/ja.json | 3 ++ .../input_boolean/translations/ca.json | 4 +-- .../components/isy994/translations/ja.json | 6 +++- .../components/jellyfin/translations/tr.json | 21 +++++++++++++ .../jellyfin/translations/zh-Hant.json | 21 +++++++++++++ .../components/kodi/translations/tr.json | 2 +- .../components/konnected/translations/ja.json | 7 +++++ .../components/life360/translations/ja.json | 3 ++ .../components/light/translations/ca.json | 4 +-- .../components/litejet/translations/ja.json | 3 +- .../components/locative/translations/ja.json | 3 ++ .../lutron_caseta/translations/ca.json | 4 +-- .../components/mailgun/translations/ja.json | 3 ++ .../media_player/translations/ca.json | 4 +-- .../modern_forms/translations/ja.json | 7 ++++- .../modern_forms/translations/tr.json | 2 +- .../components/motioneye/translations/ja.json | 3 +- .../components/nanoleaf/translations/ja.json | 5 +++ .../components/owntracks/translations/ja.json | 3 ++ .../components/plaato/translations/ja.json | 17 +++++++++- .../components/remote/translations/ca.json | 4 +-- .../components/samsungtv/translations/ja.json | 2 +- .../components/script/translations/ca.json | 4 +-- .../components/select/translations/ja.json | 11 +++++++ .../components/sensor/translations/ca.json | 4 +-- .../components/sensor/translations/ja.json | 8 +++-- .../components/sentry/translations/ja.json | 31 +++++++++++++++++++ .../components/spotify/translations/ja.json | 7 +++++ .../components/switch/translations/ca.json | 4 +-- .../synology_dsm/translations/ja.json | 3 +- .../components/traccar/translations/ja.json | 3 ++ .../components/tractive/translations/ja.json | 7 +++++ .../tuya/translations/select.ca.json | 14 ++++----- .../components/twilio/translations/ja.json | 3 ++ .../uptimerobot/translations/ja.json | 2 +- .../components/vacuum/translations/ca.json | 4 +-- .../components/volumio/translations/tr.json | 2 +- .../water_heater/translations/ca.json | 2 +- .../components/withings/translations/ja.json | 7 +++++ .../xiaomi_miio/translations/ja.json | 3 +- .../xiaomi_miio/translations/select.ca.json | 2 +- .../components/zha/translations/ja.json | 7 ++++- .../components/zwave_js/translations/ja.json | 10 +++++- 68 files changed, 327 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/ja.json create mode 100644 homeassistant/components/devolo_home_control/translations/ja.json create mode 100644 homeassistant/components/jellyfin/translations/tr.json create mode 100644 homeassistant/components/jellyfin/translations/zh-Hant.json create mode 100644 homeassistant/components/sentry/translations/ja.json create mode 100644 homeassistant/components/spotify/translations/ja.json create mode 100644 homeassistant/components/tractive/translations/ja.json create mode 100644 homeassistant/components/withings/translations/ja.json diff --git a/homeassistant/components/advantage_air/translations/ja.json b/homeassistant/components/advantage_air/translations/ja.json index 3a61126ea94..50cfb370444 100644 --- a/homeassistant/components/advantage_air/translations/ja.json +++ b/homeassistant/components/advantage_air/translations/ja.json @@ -4,7 +4,9 @@ "user": { "data": { "port": "\u30dd\u30fc\u30c8" - } + }, + "description": "Advantage Air wall mounted tablet\u306eAPI\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002", + "title": "\u63a5\u7d9a" } } } diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index 037bee5ceea..f8035e1ef47 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + }, "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", "title": "AEMET OpenData" } diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 82eb7ff08e8..ee44e93d394 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -12,7 +12,9 @@ }, "system_health": { "info": { - "can_reach_server": "Airly\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + "can_reach_server": "Airly\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "requests_per_day": "1\u65e5\u3042\u305f\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8", + "requests_remaining": "\u6b8b\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8" } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index e18aaaae05e..80779208ca9 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_location": "\u305d\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u7d50\u679c\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "user": { "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json new file mode 100644 index 00000000000..5568b2c6fa2 --- /dev/null +++ b/homeassistant/components/airvisual/translations/ja.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "geography_by_coords": { + "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u7def\u5ea6/\u7d4c\u5ea6\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", + "title": "Geography\u306e\u8a2d\u5b9a" + }, + "geography_by_name": { + "data": { + "city": "\u90fd\u5e02", + "country": "\u56fd", + "state": "\u5dde" + }, + "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u90fd\u5e02/\u5dde/\u56fd\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", + "title": "Geography\u306e\u8a2d\u5b9a" + }, + "node_pro": { + "description": "\u500b\u4eba\u306eAirVisual\u30e6\u30cb\u30c3\u30c8\u3092\u76e3\u8996\u3057\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u672c\u4f53\u306eUI\u304b\u3089\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002", + "title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a" + }, + "reauth_confirm": { + "title": "AirVisual\u3092\u518d\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 71625cf17b3..01dfcc9f8b8 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -1,6 +1,9 @@ { "state": { "_": { + "armed": "\u8b66\u6212", + "disarmed": "\u89e3\u9664", + "disarming": "\u89e3\u9664", "pending": "\u4fdd\u7559\u4e2d", "triggered": "\u30c8\u30ea\u30ac\u30fc" } diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index 36642646aa4..7c75ed677ba 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "AlarmDecoder\u3068\u306e\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index 7d96a6a466d..c1d35331e2b 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Automatitzaci\u00f3" diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index b68400fd679..2aa89dfc134 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -116,8 +116,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" }, "battery": { "off": "Normal", diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 474ae5b242e..cdb7445d7dc 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -9,6 +9,7 @@ "is_light": "{\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u540d} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "is_moist": "{entity_name} \u306f\u6e7f\u3063\u3066\u3044\u307e\u3059", + "is_no_update": "{entity_name} \u306f\u6700\u65b0\u3067\u3059", "is_no_vibration": "{entity_name} \u306f\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u305b\u3093", "is_not_bat_low": "{entity_name} \u30d0\u30c3\u30c6\u30ea\u30fc\u306f\u6b63\u5e38\u3067\u3059", "is_not_cold": "{entity_name} \u51b7\u3048\u3066\u3044\u307e\u305b\u3093", @@ -38,6 +39,7 @@ "is_sound": "{entity_name} \u304c\u97f3\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", "is_unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "is_update": "{entity_name} \u306b\u5229\u7528\u53ef\u80fd\u306a\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u304c\u3042\u308a\u307e\u3059", "is_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u3059" }, "trigger_type": { @@ -48,6 +50,7 @@ "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", "is_not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "no_update": "{entity_name} \u304c\u6700\u65b0\u306b\u306a\u308a\u307e\u3057\u305f", "no_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u306a\u304f\u306a\u3063\u305f", "not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", "not_hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u305b\u3093", @@ -72,6 +75,7 @@ "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", "unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u306a\u304f\u306a\u308a\u307e\u3057\u305f", + "update": "{\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u540d} \u306f\u3001\u5229\u7528\u53ef\u80fd\u306a\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u53d6\u5f97\u3057\u307e\u3057\u305f\u3002", "vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u59cb\u3081\u307e\u3057\u305f" } }, diff --git a/homeassistant/components/calendar/translations/ca.json b/homeassistant/components/calendar/translations/ca.json index 63cffd7063f..f1b3279a4cb 100644 --- a/homeassistant/components/calendar/translations/ca.json +++ b/homeassistant/components/calendar/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Calendari" diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index 89720be754e..3eb99744751 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -22,7 +22,7 @@ "fan_only": "Nom\u00e9s ventilador", "heat": "Escalfa", "heat_cool": "Escalfa/Refreda", - "off": "off" + "off": "OFF" } }, "title": "Climatitzaci\u00f3" diff --git a/homeassistant/components/demo/translations/select.ja.json b/homeassistant/components/demo/translations/select.ja.json index 884adcdd5e1..92f56768485 100644 --- a/homeassistant/components/demo/translations/select.ja.json +++ b/homeassistant/components/demo/translations/select.ja.json @@ -1,6 +1,7 @@ { "state": { "demo__speed": { + "light_speed": "\u30e9\u30a4\u30c8\u306e\u901f\u5ea6", "ludicrous_speed": "\u3070\u304b\u3052\u305f\u901f\u5ea6", "ridiculous_speed": "\u3068\u3093\u3067\u3082\u306a\u3044\u901f\u5ea6" } diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json new file mode 100644 index 00000000000..def5524cdbb --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "reauth_failed": "\u4ee5\u524d\u306e\u3068\u540c\u3058mydevolo\u30e6\u30fc\u30b6\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index a81ee0eda0c..6b8e68a96c9 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "description": "Dialogflow\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", diff --git a/homeassistant/components/fan/translations/ca.json b/homeassistant/components/fan/translations/ca.json index da5296b34f0..7c1789aeb24 100644 --- a/homeassistant/components/fan/translations/ca.json +++ b/homeassistant/components/fan/translations/ca.json @@ -15,8 +15,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Ventilador" diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index a3133a4d75b..06337da8b6d 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -7,9 +7,12 @@ "flipr_id": { "data": { "flipr_id": "Flipr ID" - } + }, + "description": "\u30ea\u30b9\u30c8\u306e\u4e2d\u304b\u3089FliprID\u3092\u9078\u3076", + "title": "Flipr\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { + "description": "\u3042\u306a\u305f\u306eFlipr\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3057\u307e\u3059\u3002", "title": "Flipr\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json index ec85fe10466..881d2bfeb80 100644 --- a/homeassistant/components/forecast_solar/translations/ja.json +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -2,6 +2,11 @@ "config": { "step": { "user": { + "data": { + "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", + "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)", + "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b" + }, "description": "\u30bd\u30fc\u30e9\u30fc\u30d1\u30cd\u30eb\u306e\u30c7\u30fc\u30bf\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e0d\u660e\u306a\u5834\u5408\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } @@ -13,8 +18,10 @@ "api_key": "Forecast.Solar API\u30ad\u30fc(\u30aa\u30d7\u30b7\u30e7\u30f3)", "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", "damping": "\u6e1b\u8870\u4fc2\u6570(\u30c0\u30f3\u30d4\u30f3\u30b0\u30d5\u30a1\u30af\u30bf\u30fc): \u671d\u3068\u5915\u65b9\u306e\u7d50\u679c\u3092\u8abf\u6574\u3059\u308b", - "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)" - } + "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)", + "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b" + }, + "description": "\u3053\u308c\u3089\u306e\u5024\u306b\u3088\u308a\u3001Solar.Forecast\u306e\u7d50\u679c\u3092\u5fae\u8abf\u6574\u3067\u304d\u307e\u3059\u3002\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u4e0d\u660e\u306a\u5834\u5408\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index 9db14d7eecd..69592711efe 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Geofency\u306ewebhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { + "description": "Geofency Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", "title": "Geofency Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index a16b0cd96db..1be2d7c79ed 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001GPSLogger\u3067webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { + "description": "GPSLogger Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", "title": "GPSLogger Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 5cb406727e8..552a2c9677e 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -5,9 +5,9 @@ "home": "A casa", "locked": "Bloquejat", "not_home": "Fora", - "off": "off", + "off": "OFF", "ok": "OK", - "on": "on", + "on": "ON", "open": "Obert/a", "problem": "Problema", "unlocked": "Desbloquejat" diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index bba52fff3c8..5de338fbb7b 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown_entry": "\u65e2\u5b58\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + }, "step": { "2fa": { "data": { diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 5b17e41a8d1..f8b122b3643 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -10,6 +10,9 @@ "data": { "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9" } + }, + "yaml": { + "description": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u306fYAML\u3092\u4ecb\u3057\u3066\u5236\u5fa1\u3055\u308c\u307e\u3059" } } } diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index c4581a44488..4f2de96bc8d 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ca.json b/homeassistant/components/humidifier/translations/ca.json index 5040fd4b419..bf0c1d805f6 100644 --- a/homeassistant/components/humidifier/translations/ca.json +++ b/homeassistant/components/humidifier/translations/ca.json @@ -20,8 +20,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Humidificador" diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 1b0f51d7f11..c0820401f21 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[IFTTT Webhook applet]({applet_url})\u306e\"Make a web request\"\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/json\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "description": "IFTTT\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", diff --git a/homeassistant/components/input_boolean/translations/ca.json b/homeassistant/components/input_boolean/translations/ca.json index 8e3e86e9166..23600285d58 100644 --- a/homeassistant/components/input_boolean/translations/ca.json +++ b/homeassistant/components/input_boolean/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Entrada booleana" diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 85cb735a884..6b33f35444a 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -1,10 +1,14 @@ { "config": { + "error": { + "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80" + }, "step": { "user": { "data": { "host": "URL" - } + }, + "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80" } } }, diff --git a/homeassistant/components/jellyfin/translations/tr.json b/homeassistant/components/jellyfin/translations/tr.json new file mode 100644 index 00000000000..bb1d5df64fa --- /dev/null +++ b/homeassistant/components/jellyfin/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/zh-Hant.json b/homeassistant/components/jellyfin/translations/zh-Hant.json new file mode 100644 index 00000000000..3f24589c235 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json index 715b3e1ac79..a8d11ddcb9b 100644 --- a/homeassistant/components/kodi/translations/tr.json +++ b/homeassistant/components/kodi/translations/tr.json @@ -22,7 +22,7 @@ "description": "L\u00fctfen Kodi kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bunlar Sistem / Ayarlar / A\u011f / Hizmetler'de bulunabilir." }, "discovery_confirm": { - "description": "Ev Asistan\u0131'na Kodi ('{name}') eklemek istiyor musunuz?", + "description": "Ev Asistan\u0131'na Kodi \"{name}\" eklemek istiyor musunuz?", "title": "Ke\u015ffedilen Kodi" }, "user": { diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 4a371f44c36..aa0c2ef2303 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "import_confirm": { + "description": "ID {id} \u306eKonnected\u30a2\u30e9\u30fc\u30e0\u30d1\u30cd\u30eb\u304c\u8a2d\u5b9a\u3067\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u30fc\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u304d\u307e\u3059\u3002" + } + } + }, "options": { "step": { "options_io": { diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 7a239efcd3d..a70c2e14efd 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "\u8a73\u7d30\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001[Life360\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "error": { "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d" } diff --git a/homeassistant/components/light/translations/ca.json b/homeassistant/components/light/translations/ca.json index 788c4c3aa5c..1e91f5005ce 100644 --- a/homeassistant/components/light/translations/ca.json +++ b/homeassistant/components/light/translations/ca.json @@ -19,8 +19,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Llum" diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index 9281796a351..0a0c82ea072 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -4,7 +4,8 @@ "init": { "data": { "default_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8 \u30c8\u30e9\u30f3\u30b8\u30b7\u30e7\u30f3(\u79d2)" - } + }, + "title": "LiteJet\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index f2c0b380c41..3ce29779b8d 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u5834\u6240\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Locative app\u3067webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "title": "Locative Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index b56f0976119..de714fea726 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -48,8 +48,8 @@ "lower_3": "Baixa 3", "lower_4": "Baixa 4", "lower_all": "Baixa-ho tot", - "off": "off", - "on": "on", + "off": "OFF", + "on": "ON", "open_1": "Obre 1", "open_2": "Obre 2", "open_3": "Obre 3", diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 8193746de5b..033747d6263 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Mailgun]({mailgun_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-fjsorm-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "description": "Mailgun\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", diff --git a/homeassistant/components/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json index 5887685e119..e1fce334053 100644 --- a/homeassistant/components/media_player/translations/ca.json +++ b/homeassistant/components/media_player/translations/ca.json @@ -18,8 +18,8 @@ "state": { "_": { "idle": "Inactiu", - "off": "off", - "on": "on", + "off": "OFF", + "on": "ON", "paused": "Pausat/ada", "playing": "Reproduint", "standby": "En espera" diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json index a943b0046d3..341cc32781d 100644 --- a/homeassistant/components/modern_forms/translations/ja.json +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -6,7 +6,12 @@ "host": "\u30db\u30b9\u30c8" }, "description": "Modern Forms fan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + }, + "zeroconf_confirm": { + "description": "Home Assistant\u306b `{name}` \u3068\u3044\u3046\u540d\u524d\u306eModern Forms fan\u3092\u8ffd\u52a0\u3057\u307e\u3059\u304b?", + "title": "Modern Forms fan device\u3092\u767a\u898b" } } - } + }, + "title": "Modern Forms" } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json index aa17461c063..b0f884ef68f 100644 --- a/homeassistant/components/modern_forms/translations/tr.json +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -19,7 +19,7 @@ "description": "Modern Forms fan\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." }, "zeroconf_confirm": { - "description": "'{name}' adl\u0131 Modern Formlar fan\u0131n\u0131 Ev Asistan\u0131'na eklemek istiyor musunuz?", + "description": "\"{name}\" adl\u0131 Modern Formlar fan\u0131n\u0131 Ev Asistan\u0131'na eklemek istiyor musunuz?", "title": "Ke\u015ffedilen Modern Formlar fan cihaz\u0131" } } diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 416d79e77e8..54e8a6315bb 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -11,7 +11,8 @@ "init": { "data": { "stream_url_template": "Stream URL\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", - "webhook_set_overwrite": "\u8a8d\u8b58\u3055\u308c\u3066\u3044\u306a\u3044Webhook\u3092\u4e0a\u66f8\u304d\u3059\u308b" + "webhook_set": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u5831\u544a\u3059\u308b\u3088\u3046\u306b\u3001motionEye webhooks\u3092\u8a2d\u5b9a\u3059\u308b", + "webhook_set_overwrite": "\u8a8d\u8b58\u3055\u308c\u3066\u3044\u306a\u3044Webhooks\u3092\u4e0a\u66f8\u304d\u3059\u308b" } } } diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index 50eb1d2c609..d57d1949630 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -1,7 +1,12 @@ { "config": { + "error": { + "not_allowing_new_tokens": "Nanoleaf\u306f\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u8a31\u53ef\u3057\u3066\u3044\u306a\u3044\u306e\u3067\u3001\u4e0a\u8a18\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "flow_title": "{name}", "step": { "link": { + "description": "Nanoleaf\u306e\u96fb\u6e90\u30dc\u30bf\u30f3\u3092\u30dc\u30bf\u30f3\u306eLED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u30675\u79d2\u9593\u62bc\u3057\u7d9a\u3051\u3066\u304b\u3089\u300130\u79d2\u4ee5\u5185\u306b **\u9001\u4fe1** \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Nanoleaf\u3092\u30ea\u30f3\u30af\u3059\u308b" }, "user": { diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index b24fadd37ee..e9d91d3238b 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\niOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "title": "OwnTracks\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index e99532c81ed..c2a6e29432a 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -1,14 +1,29 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Plaato Airlock\u3067Webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({doevice_type} \u306e\u540d\u524d\u304c **{device_name}** \u3067 was_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u3067\u3046\u307e\u304f\u3044\u304f\u30cf\u30ba\u3067\u3059\uff01\n\u539f\u6587: See [the documentation]({doevice_type} with name **{device_name}** was_url}) for successfurtherlly dsetails.up!" + }, + "error": { + "no_api_method": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8ffd\u52a0\u3059\u308b\u304b\u3001webhook\u3092\u9078\u629e\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, "step": { "api_method": { "data": { - "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059" + "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059", + "use_webhook": "webhook\u3092\u4f7f\u7528" } }, "user": { "title": "Plaato Wdebhookvices\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "webhook": { + "description": "Webhook\u60c5\u5831:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n", + "title": "Plaato Airlock\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index b8a317c60d0..7e001059f14 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Comandament" diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 6fef3343c00..60f77922a81 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_config_entry": "\u3053\u306e\u30b5\u30e0\u30b9\u30f3\u88fd\u7aef\u672b\u306b\u306f\u8a2d\u5b9a\u9805\u76ee\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/script/translations/ca.json b/homeassistant/components/script/translations/ca.json index 905805e21fe..4369856606f 100644 --- a/homeassistant/components/script/translations/ca.json +++ b/homeassistant/components/script/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Programa (script)" diff --git a/homeassistant/components/select/translations/ja.json b/homeassistant/components/select/translations/ja.json index 71d0ae895b1..808684aeb6f 100644 --- a/homeassistant/components/select/translations/ja.json +++ b/homeassistant/components/select/translations/ja.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "{entity_name} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u5909\u66f4" + }, + "condition_type": { + "selected_option": "\u73fe\u5728\u9078\u629e\u3055\u308c\u3066\u3044\u308b {entity_name} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "trigger_type": { + "current_option_changed": "{entity_name} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" + } + }, "title": "\u9078\u629e" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index ddf8f50a010..c90ed273a67 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -55,8 +55,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Sensor" diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 38f961987e3..546d0d2d1df 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", + "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrogen_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrous_oxide": "\u73fe\u5728\u306e {entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_ozone": "\u73fe\u5728\u306e {entity_name} \u30aa\u30be\u30f3\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -9,7 +10,8 @@ "is_pm10": "\u73fe\u5728\u306e {entity_name} PM10\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_pm25": "\u73fe\u5728\u306e {entity_name} PM2.5\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", - "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb" + "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb" }, "trigger_type": { "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", @@ -20,7 +22,9 @@ "pm1": "{entity_name} PM1\u6fc3\u5ea6\u306e\u5909\u5316", "pm10": "{entity_name} PM10\u6fc3\u5ea6\u306e\u5909\u5316", "pm25": "{entity_name} PM2.5\u6fc3\u5ea6\u306e\u5909\u5316", - "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4" + "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4", + "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", + "volatile_organic_compounds": "{entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u6fc3\u5ea6\u306e\u5909\u5316" } }, "state": { diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json new file mode 100644 index 00000000000..a68fdc41be2 --- /dev/null +++ b/homeassistant/components/sentry/translations/ja.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "bad_dsn": "\u7121\u52b9\u306aDSN" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + }, + "description": "Sentry DSN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Sentry" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\u74b0\u5883\u540d(\u7701\u7565\u53ef\u80fd)", + "event_custom_components": "\u30ab\u30b9\u30bf\u30e0\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304b\u3089\u306e\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b", + "event_handled": "\u51e6\u7406\u3055\u308c\u305f\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1", + "event_third_party_packages": "\u30b5\u30fc\u30c9\u30d1\u30fc\u30c6\u30a3\u88fd\u30d1\u30c3\u30b1\u30fc\u30b8\u304b\u3089\u306e\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b", + "logging_event_level": "Sentry\u304c\u30a4\u30d9\u30f3\u30c8\u3092\u767b\u9332\u3059\u308b\u969b\u306e\u30ed\u30b0\u30ec\u30d9\u30eb", + "tracing": "\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u30c8\u30ec\u30fc\u30b9\u3092\u6709\u52b9\u306b\u3059\u308b", + "tracing_sample_rate": "\u30c8\u30ec\u30fc\u30b9\u306e\u30b5\u30f3\u30d7\u30eb\u30ec\u30fc\u30c8; 0.0 \u304b\u3089 1.0(1.0 = 100%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json new file mode 100644 index 00000000000..ec197ac35cb --- /dev/null +++ b/homeassistant/components/spotify/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Spotify\u306e\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index 999373799f0..e39386a680f 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "off", - "on": "on" + "off": "OFF", + "on": "ON" } }, "title": "Interruptor" diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 156540d319f..0e8464ccf72 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -5,7 +5,8 @@ }, "step": { "reauth": { - "description": "\u7406\u7531: {details}" + "description": "\u7406\u7531: {details}", + "title": "Synology DSM\u518d\u8a8d\u8a3c\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index 5c11725c884..6d5e2d060c8 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Traccar\u3067Webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306eURL\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044: `{webhook_url}`\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "title": "Traccar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json new file mode 100644 index 00000000000..213e642271a --- /dev/null +++ b/homeassistant/components/tractive/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ca.json b/homeassistant/components/tuya/translations/select.ca.json index 8aa03aa4bfa..f55c56a112b 100644 --- a/homeassistant/components/tuya/translations/select.ca.json +++ b/homeassistant/components/tuya/translations/select.ca.json @@ -7,8 +7,8 @@ }, "tuya__basic_nightvision": { "0": "Autom\u00e0tic", - "1": "off", - "2": "on" + "1": "OFF", + "2": "ON" }, "tuya__decibel_sensitivity": { "0": "Sensibilitat baixa", @@ -24,7 +24,7 @@ "led": "LED" }, "tuya__light_mode": { - "none": "off", + "none": "OFF", "pos": "Indica la ubicaci\u00f3 de l'interruptor", "relay": "Indiqueu l'estat, activat/desactivat" }, @@ -40,10 +40,10 @@ "tuya__relay_status": { "last": "Recorda l'\u00faltim estat", "memory": "Recorda l'\u00faltim estat", - "off": "off", - "on": "on", - "power_off": "off", - "power_on": "on" + "off": "OFF", + "on": "ON", + "power_off": "OFF", + "power_on": "ON" } } } \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 850f61751cc..53dfe543506 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Twilio]({twilio_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-form-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "title": "Twilio Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 73352d646cf..8aeafef4efa 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/vacuum/translations/ca.json b/homeassistant/components/vacuum/translations/ca.json index 11d431a1810..d98a51a5363 100644 --- a/homeassistant/components/vacuum/translations/ca.json +++ b/homeassistant/components/vacuum/translations/ca.json @@ -19,8 +19,8 @@ "docked": "Aparcat", "error": "Error", "idle": "Inactiu", - "off": "off", - "on": "on", + "off": "OFF", + "on": "ON", "paused": "Pausat/ada", "returning": "Retornant a base" } diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json index 72732751732..1d06eb89d56 100644 --- a/homeassistant/components/volumio/translations/tr.json +++ b/homeassistant/components/volumio/translations/tr.json @@ -10,7 +10,7 @@ }, "step": { "discovery_confirm": { - "description": "Ev Asistan\u0131'na Volumio ('{name}') eklemek istiyor musunuz?", + "description": "Home Asistan\u0131'na Volumio \"{name}\" eklemek istiyor musunuz?", "title": "Bulunan Volumio" }, "user": { diff --git a/homeassistant/components/water_heater/translations/ca.json b/homeassistant/components/water_heater/translations/ca.json index 6033868ccaa..022b5e887d8 100644 --- a/homeassistant/components/water_heater/translations/ca.json +++ b/homeassistant/components/water_heater/translations/ca.json @@ -12,7 +12,7 @@ "gas": "Gas", "heat_pump": "Bomba de calor", "high_demand": "Alta demanda", - "off": "off", + "off": "OFF", "performance": "Rendiment" } } diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json new file mode 100644 index 00000000000..bca8f6606ca --- /dev/null +++ b/homeassistant/components/withings/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u9078\u629e\u3057\u305fWithings\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 02027c6bce3..d98e34886de 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -30,7 +30,8 @@ "select": { "data": { "select_device": "Miio\u30c7\u30d0\u30a4\u30b9" - } + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308bXiaomi Miio\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json index eb54883ffa6..bc96de04645 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ca.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -3,7 +3,7 @@ "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Atenua", - "off": "off" + "off": "OFF" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index f5373fce98a..599d27b807b 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -4,6 +4,11 @@ "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "confirm": { + "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 50edb776841..2da13d21b64 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002" }, + "flow_title": "{name}", "step": { "configure_addon": { "data": { @@ -15,6 +16,9 @@ }, "hassio_confirm": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "usb_confirm": { + "description": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } }, @@ -40,11 +44,15 @@ }, "options": { "abort": { - "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3055\u308c\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3057\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "error": { + "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" }, "step": { "configure_addon": { "data": { + "emulate_hardware": "\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u306e\u30a8\u30df\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3", "log_level": "\u30ed\u30b0\u30ec\u30d9\u30eb", "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af", "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", From 4963bb97d083365f79194dfbdc241d01ba494ce2 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 14 Nov 2021 21:56:49 -0800 Subject: [PATCH 0492/1452] bump total_connect_client to 2021.11.4 (#59695) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index c70eafd9c31..0eec41968cc 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2021.11.2"], + "requirements": ["total_connect_client==2021.11.4"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 52c4f3bf7c9..55f3f46116e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2317,7 +2317,7 @@ todoist-python==8.0.0 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.2 +total_connect_client==2021.11.4 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 838723cb619..5d261d41f84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1345,7 +1345,7 @@ tesla-powerwall==0.3.12 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.2 +total_connect_client==2021.11.4 # homeassistant.components.transmission transmissionrpc==0.11 From c2d66956b09a3377c290eba741d8202e32f6048c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 15 Nov 2021 06:58:07 +0100 Subject: [PATCH 0493/1452] Bump philips js to 2.7.6 (#59690) --- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 3bea3ff7337..60bc862406d 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.5"], + "requirements": ["ha-philipsjs==2.7.6"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 55f3f46116e..cf1f2f7a6dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ ha-av==8.0.4-rc.1 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.5 +ha-philipsjs==2.7.6 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d261d41f84..bceeb3d283a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -479,7 +479,7 @@ ha-av==8.0.4-rc.1 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.5 +ha-philipsjs==2.7.6 # homeassistant.components.habitica habitipy==0.2.0 From 04a258bf21ee215dc2b00cd5102c75902f0ead6c Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Mon, 15 Nov 2021 09:51:57 +0100 Subject: [PATCH 0494/1452] fix(luftdaten): air pressure is reported in pascal instead of hecto pascal (#59687) --- homeassistant/components/luftdaten/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ea09c9208ee..a87c67620cd 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, - PRESSURE_HPA, + PRESSURE_PA, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -68,14 +68,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_PRESSURE, name="Pressure", icon="mdi:arrow-down-bold", - native_unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_PA, device_class=DEVICE_CLASS_PRESSURE, ), SensorEntityDescription( key=SENSOR_PRESSURE_AT_SEALEVEL, name="Pressure at sealevel", icon="mdi:download", - native_unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_PA, device_class=DEVICE_CLASS_PRESSURE, ), SensorEntityDescription( From 96f7b0d91054d6383322ebfef7e154dbf664b120 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Nov 2021 04:19:31 -0600 Subject: [PATCH 0495/1452] Use atomicwrites for mission critical core files (#59606) --- homeassistant/auth/auth_store.py | 2 +- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/auth/providers/homeassistant.py | 2 +- homeassistant/components/config/__init__.py | 4 +-- homeassistant/components/network/network.py | 4 ++- homeassistant/core.py | 4 +-- homeassistant/helpers/area_registry.py | 4 ++- homeassistant/helpers/device_registry.py | 4 ++- homeassistant/helpers/entity_registry.py | 4 ++- homeassistant/helpers/storage.py | 10 +++++- homeassistant/package_constraints.txt | 1 + homeassistant/util/file.py | 33 +++++++++++++++++-- homeassistant/util/json.py | 8 +++-- requirements.txt | 1 + requirements_test.txt | 1 + setup.py | 1 + tests/util/test_file.py | 22 ++++++++++--- tests/util/test_json.py | 7 ++-- 19 files changed, 92 insertions(+), 24 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index c935a0da7d0..114329eda1e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -42,7 +42,7 @@ class AuthStore: self._groups: dict[str, models.Group] | None = None self._perm_lookup: PermissionLookup | None = None self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True + STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._lock = asyncio.Lock() diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 0f53ddc900d..0378d3aeaa8 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -100,7 +100,7 @@ class NotifyAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._user_settings: _UsersDict | None = None self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True + STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index e0e2b9522d9..c979ba05b5a 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -77,7 +77,7 @@ class TotpAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._users: dict[str, str] | None = None self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True + STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._init_lock = asyncio.Lock() diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 1ffed6f87fd..8d682670e01 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -63,7 +63,7 @@ class Data: """Initialize the user data store.""" self.hass = hass self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True + STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._data: dict[str, Any] | None = None # Legacy mode will allow usernames to start/end with whitespace diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 43c9cfabd08..96dbd79da1e 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.util.file import write_utf8_file +from homeassistant.util.file import write_utf8_file_atomic from homeassistant.util.yaml import dump, load_yaml DOMAIN = "config" @@ -254,4 +254,4 @@ def _write(path, data): # Do it before opening file. If dump causes error it will now not # truncate the file. contents = dump(data) - write_utf8_file(path, contents) + write_utf8_file_atomic(path, contents) diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index ffe3406e28e..5e2e7b8ad6b 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -25,7 +25,9 @@ class Network: def __init__(self, hass: HomeAssistant) -> None: """Initialize the Network class.""" - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._data: dict[str, Any] = {} self.adapters: list[Adapter] = [] diff --git a/homeassistant/core.py b/homeassistant/core.py index 891d718a81f..2f5783de443 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1715,7 +1715,7 @@ class Config: async def async_load(self) -> None: """Load [homeassistant] core config.""" store = self.hass.helpers.storage.Store( - CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True, atomic_writes=True ) if not (data := await store.async_load()): @@ -1763,7 +1763,7 @@ class Config: } store = self.hass.helpers.storage.Store( - CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True, atomic_writes=True ) await store.async_save(data) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index f08fbe36511..11b7e5a78bd 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -49,7 +49,9 @@ class AreaRegistry: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._normalized_name_area_idx: dict[str, str] = {} @callback diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index b41df3d6aa0..e28f01b9e7a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -162,7 +162,9 @@ class DeviceRegistry: def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" self.hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._clear_index() @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b7b0eed2f32..7d3bdf58a92 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -155,7 +155,9 @@ class EntityRegistry: self.hass = hass self.entities: dict[str, RegistryEntry] self._index: dict[tuple[str, str, str], str] = {} - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified ) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 4d69d3be070..59cf4ff8c22 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -76,6 +76,7 @@ class Store: private: bool = False, *, encoder: type[JSONEncoder] | None = None, + atomic_writes: bool = False, ) -> None: """Initialize storage class.""" self.version = version @@ -88,6 +89,7 @@ class Store: self._write_lock = asyncio.Lock() self._load_task: asyncio.Future | None = None self._encoder = encoder + self._atomic_writes = atomic_writes @property def path(self): @@ -238,7 +240,13 @@ class Store: os.makedirs(os.path.dirname(path)) _LOGGER.debug("Writing data for %s to %s", self.key, path) - json_util.save_json(path, data, self._private, encoder=self._encoder) + json_util.save_json( + path, + data, + self._private, + encoder=self._encoder, + atomic_writes=self._atomic_writes, + ) async def _async_migrate_func(self, old_version, old_data): """Migrate to the new version.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbdccb3822b..be20a0b027f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,6 +6,7 @@ aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.22.12 async_timeout==4.0.0 +atomicwrites==1.4.0 attrs==21.2.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index 9c5b11e4807..cb5969b3079 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -5,6 +5,8 @@ import logging import os import tempfile +from atomicwrites import AtomicWriter + from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -14,6 +16,33 @@ class WriteError(HomeAssistantError): """Error writing the data.""" +def write_utf8_file_atomic( + filename: str, + utf8_data: str, + private: bool = False, +) -> None: + """Write a file and rename it into place using atomicwrites. + + Writes all or nothing. + + This function uses fsync under the hood. It should + only be used to write mission critical files as + fsync can block for a few seconds or longer is the + disk is busy. + + Using this function frequently will significantly + negatively impact performance. + """ + try: + with AtomicWriter(filename, overwrite=True).open() as fdesc: + if not private: + os.fchmod(fdesc.fileno(), 0o644) + fdesc.write(utf8_data) + except OSError as error: + _LOGGER.exception("Saving file failed: %s", filename) + raise WriteError(error) from error + + def write_utf8_file( filename: str, utf8_data: str, @@ -33,8 +62,8 @@ def write_utf8_file( ) as fdesc: fdesc.write(utf8_data) tmp_filename = fdesc.name - if not private: - os.chmod(tmp_filename, 0o644) + if not private: + os.fchmod(fdesc.fileno(), 0o644) os.replace(tmp_filename, filename) except OSError as error: _LOGGER.exception("Saving file failed: %s", filename) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index e3bde277837..9c98691c605 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -10,7 +10,7 @@ from typing import Any from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError -from .file import write_utf8_file +from .file import write_utf8_file, write_utf8_file_atomic _LOGGER = logging.getLogger(__name__) @@ -49,6 +49,7 @@ def save_json( private: bool = False, *, encoder: type[json.JSONEncoder] | None = None, + atomic_writes: bool = False, ) -> None: """Save JSON data to a file. @@ -61,7 +62,10 @@ def save_json( _LOGGER.error(msg) raise SerializationError(msg) from error - write_utf8_file(filename, json_data, private) + if atomic_writes: + write_utf8_file_atomic(filename, json_data, private) + else: + write_utf8_file(filename, json_data, private) def format_unserializable_data(data: dict[str, Any]) -> str: diff --git a/requirements.txt b/requirements.txt index cc595332395..fe35d852aef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ aiohttp==3.8.0 astral==2.2 async_timeout==4.0.0 attrs==21.2.0 +atomicwrites==1.4.0 awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 diff --git a/requirements_test.txt b/requirements_test.txt index 5f20b10b4e1..36cb9785ef6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,6 +31,7 @@ responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 tqdm==4.49.0 +types-atomicwrites==1.4.1 types-croniter==1.0.0 types-backports==0.1.3 types-certifi==0.1.4 diff --git a/setup.py b/setup.py index 43ff77f6110..38a48fb5f0f 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ REQUIRES = [ "astral==2.2", "async_timeout==4.0.0", "attrs==21.2.0", + "atomicwrites==1.4.0", "awesomeversion==21.10.1", 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", diff --git a/tests/util/test_file.py b/tests/util/test_file.py index 109645a839a..41d104cdd8a 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -5,20 +5,21 @@ from unittest.mock import patch import pytest -from homeassistant.util.file import WriteError, write_utf8_file +from homeassistant.util.file import WriteError, write_utf8_file, write_utf8_file_atomic -def test_write_utf8_file_private(tmpdir): +@pytest.mark.parametrize("func", [write_utf8_file, write_utf8_file_atomic]) +def test_write_utf8_file_atomic_private(tmpdir, func): """Test files can be written as 0o600 or 0o644.""" test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - write_utf8_file(test_file, '{"some":"data"}', False) + func(test_file, '{"some":"data"}', False) with open(test_file) as fh: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o644 - write_utf8_file(test_file, '{"some":"data"}', True) + func(test_file, '{"some":"data"}', True) with open(test_file) as fh: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o600 @@ -63,3 +64,16 @@ def test_write_utf8_file_fails_at_rename_and_remove(tmpdir, caplog): write_utf8_file(test_file, '{"some":"data"}', False) assert "File replacement cleanup failed" in caplog.text + + +def test_write_utf8_file_atomic_fails(tmpdir): + """Test OSError from write_utf8_file_atomic is rethrown as WriteError.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(WriteError), patch( + "homeassistant.util.file.AtomicWriter.open", side_effect=OSError + ): + write_utf8_file_atomic(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 1d82f5972a3..752e93b39cd 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -67,11 +67,12 @@ def test_save_and_load_private(): assert stats.st_mode & 0o77 == 0 -def test_overwrite_and_reload(): +@pytest.mark.parametrize("atomic_writes", [True, False]) +def test_overwrite_and_reload(atomic_writes): """Test that we can overwrite an existing file and read back.""" fname = _path_for("test3") - save_json(fname, TEST_JSON_A) - save_json(fname, TEST_JSON_B) + save_json(fname, TEST_JSON_A, atomic_writes=atomic_writes) + save_json(fname, TEST_JSON_B, atomic_writes=atomic_writes) data = load_json(fname) assert data == TEST_JSON_B From 5370dd81226853c7c8e6b2c5b7310c38a3e0948a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Nov 2021 03:23:25 -0700 Subject: [PATCH 0496/1452] Bump simplisafe-python to 2021.11.0 (#59692) --- .../components/simplisafe/__init__.py | 56 +++++++++++-------- .../simplisafe/alarm_control_panel.py | 35 ++++-------- .../components/simplisafe/binary_sensor.py | 32 +++++------ homeassistant/components/simplisafe/lock.py | 6 +- .../components/simplisafe/manifest.json | 2 +- homeassistant/components/simplisafe/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 66 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a25f76bcbc8..404c6e96fd4 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -16,11 +16,16 @@ from simplipy.errors import ( from simplipy.system import SystemNotification from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import ( - VOLUME_HIGH, - VOLUME_LOW, - VOLUME_MEDIUM, - VOLUME_OFF, + MAX_ALARM_DURATION, + MAX_ENTRY_DELAY_AWAY, + MAX_ENTRY_DELAY_HOME, + MAX_EXIT_DELAY_AWAY, + MAX_EXIT_DELAY_HOME, + MIN_ALARM_DURATION, + MIN_ENTRY_DELAY_AWAY, + MIN_EXIT_DELAY_AWAY, SystemV3, + Volume, ) from simplipy.websocket import ( EVENT_AUTOMATIC_TEST, @@ -120,10 +125,10 @@ PLATFORMS = ( ) VOLUME_MAP = { - "high": VOLUME_HIGH, - "low": VOLUME_LOW, - "medium": VOLUME_MEDIUM, - "off": VOLUME_OFF, + "high": Volume.HIGH, + "low": Volume.LOW, + "medium": Volume.MEDIUM, + "off": Volume.OFF, } SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -141,25 +146,29 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( vol.Optional(ATTR_ALARM_DURATION): vol.All( cv.time_period, lambda value: value.total_seconds(), - vol.Range(min=30, max=480), + vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), ), vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( cv.time_period, lambda value: value.total_seconds(), - vol.Range(min=30, max=255), + vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), ), vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, lambda value: value.total_seconds(), vol.Range(max=255) + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_ENTRY_DELAY_HOME), ), vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( cv.time_period, lambda value: value.total_seconds(), - vol.Range(min=45, max=255), + vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), ), vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, lambda value: value.total_seconds(), vol.Range(max=255) + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_EXIT_DELAY_HOME), ), vol.Optional(ATTR_LIGHT): cv.boolean, vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( @@ -457,8 +466,8 @@ class SimpliSafe: assert self._api.refresh_token assert self._api.websocket - self._api.websocket.add_connect_listener(self._async_websocket_on_connect) - self._api.websocket.add_event_listener(self._async_websocket_on_event) + self._api.websocket.add_connect_callback(self._async_websocket_on_connect) + self._api.websocket.add_event_callback(self._async_websocket_on_event) asyncio.create_task(self._api.websocket.async_connect()) async def async_websocket_disconnect_listener(_: Event) -> None: @@ -510,7 +519,7 @@ class SimpliSafe: ) self.entry.async_on_unload( - self._api.add_refresh_token_listener(async_save_refresh_token) + self._api.add_refresh_token_callback(async_save_refresh_token) ) async_save_refresh_token(self._api.refresh_token) @@ -566,15 +575,16 @@ class SimpliSafeEntity(CoordinatorEntity): device_name = DEFAULT_ENTITY_NAME serial = system.serial - try: - device_type = DeviceTypes( - simplisafe.initial_event_to_use[system.system_id].get("sensorType") - ) - except ValueError: - device_type = DeviceTypes.unknown - event = simplisafe.initial_event_to_use[system.system_id] + if raw_type := event.get("sensorType"): + try: + device_type = DeviceTypes(raw_type) + except ValueError: + device_type = DeviceTypes.UNKNOWN + else: + device_type = DeviceTypes.UNKNOWN + self._attr_extra_state_attributes = { ATTR_LAST_EVENT_INFO: event.get("info"), ATTR_LAST_EVENT_SENSOR_NAME: event.get("sensorName"), diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index de571f1291d..7a09db18b07 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -6,13 +6,7 @@ from typing import TYPE_CHECKING from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v2 import SystemV2 -from simplipy.system.v3 import ( - VOLUME_HIGH, - VOLUME_LOW, - VOLUME_MEDIUM, - VOLUME_OFF, - SystemV3, -) +from simplipy.system.v3 import SystemV3 from simplipy.websocket import ( EVENT_ALARM_CANCELED, EVENT_ALARM_TRIGGERED, @@ -71,20 +65,13 @@ ATTR_RF_JAMMING = "rf_jamming" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -VOLUME_STRING_MAP = { - VOLUME_HIGH: "high", - VOLUME_LOW: "low", - VOLUME_MEDIUM: "medium", - VOLUME_OFF: "off", -} - STATE_MAP_FROM_REST_API = { - SystemStates.alarm: STATE_ALARM_TRIGGERED, - SystemStates.away: STATE_ALARM_ARMED_AWAY, - SystemStates.away_count: STATE_ALARM_ARMING, - SystemStates.exit_delay: STATE_ALARM_ARMING, - SystemStates.home: STATE_ALARM_ARMED_HOME, - SystemStates.off: STATE_ALARM_DISARMED, + SystemStates.ALARM: STATE_ALARM_TRIGGERED, + SystemStates.AWAY: STATE_ALARM_ARMED_AWAY, + SystemStates.AWAY_COUNT: STATE_ALARM_ARMING, + SystemStates.EXIT_DELAY: STATE_ALARM_ARMING, + SystemStates.HOME: STATE_ALARM_ARMED_HOME, + SystemStates.OFF: STATE_ALARM_DISARMED, } STATE_MAP_FROM_WEBSOCKET_EVENT = { @@ -226,9 +213,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_extra_state_attributes.update( { ATTR_ALARM_DURATION: self._system.alarm_duration, - ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], + ATTR_ALARM_VOLUME: self._system.alarm_volume.name.lower(), ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, - ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], + ATTR_CHIME_VOLUME: self._system.chime_volume.name.lower(), ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, @@ -236,9 +223,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): ATTR_GSM_STRENGTH: self._system.gsm_strength, ATTR_LIGHT: self._system.light, ATTR_RF_JAMMING: self._system.rf_jamming, - ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ - self._system.voice_prompt_volume - ], + ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name.lower(), ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, ATTR_WIFI_STRENGTH: self._system.wifi_strength, } diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index eef38ffe003..240ff24c6c8 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -24,25 +24,25 @@ from . import SimpliSafe, SimpliSafeEntity from .const import DOMAIN, LOGGER SUPPORTED_BATTERY_SENSOR_TYPES = [ - DeviceTypes.carbon_monoxide, - DeviceTypes.entry, - DeviceTypes.glass_break, - DeviceTypes.leak, - DeviceTypes.lock_keypad, - DeviceTypes.motion, - DeviceTypes.siren, - DeviceTypes.smoke, - DeviceTypes.temperature, + DeviceTypes.CARBON_MONOXIDE, + DeviceTypes.ENTRY, + DeviceTypes.GLASS_BREAK, + DeviceTypes.LEAK, + DeviceTypes.LOCK_KEYPAD, + DeviceTypes.MOTION, + DeviceTypes.SIREN, + DeviceTypes.SMOKE, + DeviceTypes.TEMPERATURE, ] TRIGGERED_SENSOR_TYPES = { - DeviceTypes.carbon_monoxide: DEVICE_CLASS_GAS, - DeviceTypes.entry: DEVICE_CLASS_DOOR, - DeviceTypes.glass_break: DEVICE_CLASS_SAFETY, - DeviceTypes.leak: DEVICE_CLASS_MOISTURE, - DeviceTypes.motion: DEVICE_CLASS_MOTION, - DeviceTypes.siren: DEVICE_CLASS_SAFETY, - DeviceTypes.smoke: DEVICE_CLASS_SMOKE, + DeviceTypes.CARBON_MONOXIDE: DEVICE_CLASS_GAS, + DeviceTypes.ENTRY: DEVICE_CLASS_DOOR, + DeviceTypes.GLASS_BREAK: DEVICE_CLASS_SAFETY, + DeviceTypes.LEAK: DEVICE_CLASS_MOISTURE, + DeviceTypes.MOTION: DEVICE_CLASS_MOTION, + DeviceTypes.SIREN: DEVICE_CLASS_SAFETY, + DeviceTypes.SMOKE: DEVICE_CLASS_SMOKE, } diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 263fd54f9d6..12d06c1028a 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -90,6 +90,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" + self._attr_is_jammed = self._device.state == LockStates.JAMMED + self._attr_is_locked = self._device.state == LockStates.LOCKED + self._attr_extra_state_attributes.update( { ATTR_LOCK_LOW_BATTERY: self._device.lock_low_battery, @@ -97,9 +100,6 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): } ) - self._attr_is_jammed = self._device.state == LockStates.jammed - self._attr_is_locked = self._device.state == LockStates.locked - @callback def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: """Update the entity when new data comes from the websocket.""" diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 97968e124b1..ecc4578d878 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==12.0.2"], + "requirements": ["simplisafe-python==2021.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 0fb9c129a7c..b2b0a432bd6 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -28,7 +28,7 @@ async def async_setup_entry( continue for sensor in system.sensors.values(): - if sensor.type == DeviceTypes.temperature: + if sensor.type == DeviceTypes.TEMPERATURE: sensors.append(SimplisafeFreezeSensor(simplisafe, system, sensor)) async_add_entities(sensors) diff --git a/requirements_all.txt b/requirements_all.txt index cf1f2f7a6dd..5f47b64934e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2143,7 +2143,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==12.0.2 +simplisafe-python==2021.11.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bceeb3d283a..62a484980f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1257,7 +1257,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==12.0.2 +simplisafe-python==2021.11.0 # homeassistant.components.slack slackclient==2.5.0 From acf58111c69c0c78c67351c0ee9911d81d70c5dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Nov 2021 13:00:46 +0100 Subject: [PATCH 0497/1452] Correct initial update of async_track_template_result (#59705) --- homeassistant/components/template/light.py | 18 +++--- homeassistant/helpers/event.py | 4 +- tests/components/template/test_light.py | 1 + tests/helpers/test_event.py | 67 ++++++++++++++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index fce35f312f8..5d172489840 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -510,7 +510,7 @@ class LightTemplate(TemplateEntity, LightEntity): def _update_brightness(self, brightness): """Update the brightness from the template.""" try: - if brightness in ("None", ""): + if brightness in (None, "None", ""): self._brightness = None return if 0 <= int(brightness) <= 255: @@ -531,7 +531,7 @@ class LightTemplate(TemplateEntity, LightEntity): def _update_white_value(self, white_value): """Update the white value from the template.""" try: - if white_value in ("None", ""): + if white_value in (None, "None", ""): self._white_value = None return if 0 <= int(white_value) <= 255: @@ -551,7 +551,7 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_effect_list(self, effect_list): """Update the effect list from the template.""" - if effect_list in ("None", ""): + if effect_list in (None, "None", ""): self._effect_list = None return @@ -572,7 +572,7 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_effect(self, effect): """Update the effect from the template.""" - if effect in ("None", ""): + if effect in (None, "None", ""): self._effect = None return @@ -617,7 +617,7 @@ class LightTemplate(TemplateEntity, LightEntity): def _update_temperature(self, render): """Update the temperature from the template.""" try: - if render in ("None", ""): + if render in (None, "None", ""): self._temperature = None return temperature = int(render) @@ -643,7 +643,7 @@ class LightTemplate(TemplateEntity, LightEntity): """Update the hs_color from the template.""" h_str = s_str = None if isinstance(render, str): - if render in ("None", ""): + if render in (None, "None", ""): self._color = None return h_str, s_str = map( @@ -675,7 +675,7 @@ class LightTemplate(TemplateEntity, LightEntity): """Update the max mireds from the template.""" try: - if render in ("None", ""): + if render in (None, "None", ""): self._max_mireds = None return self._max_mireds = int(render) @@ -690,7 +690,7 @@ class LightTemplate(TemplateEntity, LightEntity): def _update_min_mireds(self, render): """Update the min mireds from the template.""" try: - if render in ("None", ""): + if render in (None, "None", ""): self._min_mireds = None return self._min_mireds = int(render) @@ -704,7 +704,7 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_supports_transition(self, render): """Update the supports transition from the template.""" - if render in ("None", ""): + if render in (None, "None", ""): self._supports_transition = False return self._supports_transition = bool(render) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b157840db0d..d9fdce8b1f3 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -923,8 +923,8 @@ class _TrackTemplateResultInfo: last_result = self._last_result.get(template) - # Check to see if the result has changed - if result == last_result: + # Check to see if the result has changed or is new + if result == last_result and template in self._last_result: return True if isinstance(result, TemplateError) and isinstance(last_result, TemplateError): diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index e0ee5422439..255cea6348f 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -685,6 +685,7 @@ async def test_level_action_no_template(hass, start_ha, calls): (None, {"replace4": '"{{x - 12}}"'}), (None, {"replace4": '"{{ none }}"'}), (None, {"replace4": '""'}), + (None, {"replace4": "\"{{ state_attr('light.nolight', 'brightness') }}\""}), ], ) @pytest.mark.parametrize( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 2e8f6264f1a..6d78ae089da 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1022,6 +1022,73 @@ async def test_track_template_result(hass): assert len(wildercard_runs) == 4 +async def test_track_template_result_none(hass): + """Test tracking template.""" + specific_runs = [] + wildcard_runs = [] + wildercard_runs = [] + + template_condition = Template("{{state_attr('sensor.test', 'battery')}}", hass) + template_condition_var = Template( + "{{(state_attr('sensor.test', 'battery')|int) + test }}", hass + ) + + def specific_run_callback(event, updates): + track_result = updates.pop() + result = int(track_result.result) if track_result.result is not None else None + specific_runs.append(result) + + async_track_template_result( + hass, [TrackTemplate(template_condition, None)], specific_run_callback + ) + + @ha.callback + def wildcard_run_callback(event, updates): + track_result = updates.pop() + last_result = ( + int(track_result.last_result) + if track_result.last_result is not None + else None + ) + result = int(track_result.result) if track_result.result is not None else None + wildcard_runs.append((last_result, result)) + + async_track_template_result( + hass, [TrackTemplate(template_condition, None)], wildcard_run_callback + ) + + async def wildercard_run_callback(event, updates): + track_result = updates.pop() + last_result = ( + int(track_result.last_result) + if track_result.last_result is not None + else None + ) + result = int(track_result.result) if track_result.result is not None else None + wildercard_runs.append((last_result, result)) + + async_track_template_result( + hass, + [TrackTemplate(template_condition_var, {"test": 5})], + wildercard_run_callback, + ) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test", "-") + await hass.async_block_till_done() + + assert specific_runs == [None] + assert wildcard_runs == [(None, None)] + assert wildercard_runs == [(None, 5)] + + hass.states.async_set("sensor.test", "-", {"battery": 5}) + await hass.async_block_till_done() + + assert specific_runs == [None, 5] + assert wildcard_runs == [(None, None), (None, 5)] + assert wildercard_runs == [(None, 5), (5, 10)] + + async def test_track_template_result_super_template(hass): """Test tracking template with super template listening to same entity.""" specific_runs = [] From 1b5d32514f067af4ef2c8424d7e3cc91a49b422b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Kert=C3=A9sz?= Date: Mon, 15 Nov 2021 13:14:22 +0100 Subject: [PATCH 0498/1452] Fix telnet fast state update (#59681) --- homeassistant/components/telnet/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index e4d93d9f685..5298713d5a1 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -157,9 +157,11 @@ class TelnetSwitch(SwitchEntity): self._telnet_command(self._command_on) if self.assumed_state: self._state = True + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" self._telnet_command(self._command_off) if self.assumed_state: self._state = False + self.schedule_update_ha_state() From a3885f4fda1294ee04f925a8907fe4d012292ca2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:33:50 +0100 Subject: [PATCH 0499/1452] Add frequency device class for sensor (#59700) Co-authored-by: Hedda Co-authored-by: epenet --- homeassistant/components/sensor/__init__.py | 2 ++ homeassistant/components/sensor/device_condition.py | 4 ++++ homeassistant/components/sensor/device_trigger.py | 4 ++++ homeassistant/components/sensor/strings.json | 2 ++ homeassistant/components/sensor/translations/en.json | 2 ++ homeassistant/const.py | 1 + tests/components/sensor/test_device_trigger.py | 2 +- tests/testing_config/custom_components/test/sensor.py | 2 ++ 8 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 91bff740ffd..4ae6e2129ed 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATE, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -72,6 +73,7 @@ DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_CURRENT, # current (A) DEVICE_CLASS_DATE, # date (ISO8601) DEVICE_CLASS_ENERGY, # energy (kWh, Wh) + DEVICE_CLASS_FREQUENCY, # frequency (Hz, kHz, MHz, GHz) DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_MONETARY, # Amount of money (currency) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index ffa59271d79..224ca26d28d 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -16,6 +16,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -55,6 +56,7 @@ CONF_IS_CO = "is_carbon_monoxide" CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CURRENT = "is_current" CONF_IS_ENERGY = "is_energy" +CONF_IS_FREQUENCY = "is_frequency" CONF_IS_HUMIDITY = "is_humidity" CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" @@ -81,6 +83,7 @@ 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_FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], @@ -115,6 +118,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_CO2, CONF_IS_CURRENT, CONF_IS_ENERGY, + CONF_IS_FREQUENCY, CONF_IS_GAS, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 189b098bea0..6f82ba1a34c 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -54,6 +55,7 @@ CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" CONF_CURRENT = "current" CONF_ENERGY = "energy" +CONF_FREQUENCY = "frequency" CONF_GAS = "gas" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" @@ -80,6 +82,7 @@ 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_FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], @@ -115,6 +118,7 @@ TRIGGER_SCHEMA = vol.All( CONF_CO2, CONF_CURRENT, CONF_ENERGY, + CONF_FREQUENCY, CONF_GAS, CONF_HUMIDITY, CONF_ILLUMINANCE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 1dec2b60e20..df2dc3560af 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -22,6 +22,7 @@ "is_temperature": "Current {entity_name} temperature", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", + "is_frequency": "Current {entity_name} frequency", "is_power_factor": "Current {entity_name} power factor", "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level", "is_voltage": "Current {entity_name} voltage", @@ -48,6 +49,7 @@ "temperature": "{entity_name} temperature changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", + "frequency": "{entity_name} frequency changes", "power_factor": "{entity_name} power factor changes", "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes", "voltage": "{entity_name} voltage changes", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index b5cb2f5a27f..531016b2007 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", + "is_frequency": "Current {entity_name} frequency", "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", + "frequency": "{entity_name} frequency changes", "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", diff --git a/homeassistant/const.py b/homeassistant/const.py index 4c8adb3426b..6c808ffd3b3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -240,6 +240,7 @@ DEVICE_CLASS_CO2: Final = "carbon_dioxide" DEVICE_CLASS_CURRENT: Final = "current" DEVICE_CLASS_DATE: Final = "date" DEVICE_CLASS_ENERGY: Final = "energy" +DEVICE_CLASS_FREQUENCY: Final = "frequency" DEVICE_CLASS_HUMIDITY: Final = "humidity" DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" DEVICE_CLASS_MONETARY: Final = "monetary" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 5ef99b6c669..21a52f691e9 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) == 23 + assert len(triggers) == 24 assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index fd35d1006a0..ea3b0fe7080 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -7,6 +7,7 @@ import homeassistant.components.sensor as sensor from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + FREQUENCY_GIGAHERTZ, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, @@ -38,6 +39,7 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) sensor.DEVICE_CLASS_CURRENT: "A", # current (A) sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) + sensor.DEVICE_CLASS_FREQUENCY: FREQUENCY_GIGAHERTZ, # energy (Hz/kHz/MHz/GHz) sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of vocs sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) From 81d1899094f5d82873524d76c7a630a35c506085 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Nov 2021 14:50:40 +0100 Subject: [PATCH 0500/1452] Bump pychromecast to 9.4.0 (#59716) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_home_assistant_cast.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index e74f0840a6c..dbbb7aa1417 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.3.1"], + "requirements": ["pychromecast==9.4.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 5f47b64934e..025c34b1767 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1390,7 +1390,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.3.1 +pychromecast==9.4.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62a484980f9..3c7854a817f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==9.3.1 +pychromecast==9.4.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 1e0618b066e..67b5454b6e1 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -29,7 +29,7 @@ async def test_service_show_view(hass, mock_zeroconf): assert controller.hass_url == "https://example.com" assert controller.client_id is None # Verify user did not accidentally submit their dev app id - assert controller.supporting_app_id == "B12CE3CA" + assert controller.supporting_app_id == "A078F6B0" assert entity_id == "media_player.kitchen" assert view_path == "mock_path" assert url_path is None From 1e5c76715863a40ae74eba9238dd18595c126f04 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 15:50:43 +0100 Subject: [PATCH 0501/1452] Add button entities to Renault (#59383) Co-authored-by: epenet --- .../components/renault/binary_sensor.py | 4 +- homeassistant/components/renault/button.py | 83 ++++++++ homeassistant/components/renault/const.py | 2 + .../components/renault/device_tracker.py | 6 +- .../components/renault/renault_entities.py | 38 ++-- homeassistant/components/renault/select.py | 6 +- homeassistant/components/renault/sensor.py | 6 +- homeassistant/components/renault/services.py | 8 + tests/components/renault/const.py | 51 +++++ tests/components/renault/test_button.py | 185 ++++++++++++++++++ tests/components/renault/test_services.py | 5 +- 11 files changed, 371 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/renault/button.py create mode 100644 tests/components/renault/test_button.py diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 2799289fc1d..a054cba2f12 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import DOMAIN -from .renault_entities import RenaultDataEntity, RenaultEntityDescription +from .renault_entities import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub @@ -33,7 +33,7 @@ class RenaultBinarySensorRequiredKeysMixin: @dataclass class RenaultBinarySensorEntityDescription( BinarySensorEntityDescription, - RenaultEntityDescription, + RenaultDataEntityDescription, RenaultBinarySensorRequiredKeysMixin, ): """Class describing Renault binary sensor entities.""" diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py new file mode 100644 index 00000000000..e62bdf083ae --- /dev/null +++ b/homeassistant/components/renault/button.py @@ -0,0 +1,83 @@ +"""Support for Renault button entities.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +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 RenaultEntity +from .renault_hub import RenaultHub + + +@dataclass +class RenaultButtonRequiredKeysMixin: + """Mixin for required keys.""" + + async_press: Callable[[RenaultButtonEntity], Awaitable] + + +@dataclass +class RenaultButtonEntityDescription( + ButtonEntityDescription, RenaultButtonRequiredKeysMixin +): + """Class describing Renault button entities.""" + + requires_electricity: bool = False + + +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[RenaultButtonEntity] = [ + RenaultButtonEntity(vehicle, description) + for vehicle in proxy.vehicles.values() + for description in BUTTON_TYPES + if not description.requires_electricity or vehicle.details.uses_electricity() + ] + async_add_entities(entities) + + +class RenaultButtonEntity(RenaultEntity, ButtonEntity): + """Mixin for button specific attributes.""" + + entity_description: RenaultButtonEntityDescription + + async def async_press(self) -> None: + """Process the button press.""" + await self.entity_description.async_press(self) + + +async def _start_charge(entity: RenaultButtonEntity) -> None: + """Start charge on the vehicle.""" + await entity.vehicle.vehicle.set_charge_start() + + +async def _start_air_conditioner(entity: RenaultButtonEntity) -> None: + """Start air conditioner on the vehicle.""" + await entity.vehicle.vehicle.set_ac_start(21, None) + + +BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( + RenaultButtonEntityDescription( + async_press=_start_air_conditioner, + key="start_air_conditioner", + icon="mdi:air-conditioner", + name="Start Air Conditioner", + ), + RenaultButtonEntityDescription( + async_press=_start_charge, + key="start_charge", + icon="mdi:ev-station", + name="Start Charge", + requires_electricity=True, + ), +) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 4c1376288f0..2a0ea3a0d49 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -1,5 +1,6 @@ """Constants for the Renault component.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -13,6 +14,7 @@ DEFAULT_SCAN_INTERVAL = 300 # 5 minutes PLATFORMS = [ BINARY_SENSOR_DOMAIN, + BUTTON_DOMAIN, DEVICE_TRACKER_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py index 466a1f9e4a6..3e9a2608f80 100644 --- a/homeassistant/components/renault/device_tracker.py +++ b/homeassistant/components/renault/device_tracker.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .renault_entities import RenaultDataEntity, RenaultEntityDescription +from .renault_entities import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub @@ -51,8 +51,8 @@ class RenaultDeviceTracker( return SOURCE_TYPE_GPS -DEVICE_TRACKER_TYPES: tuple[RenaultEntityDescription, ...] = ( - RenaultEntityDescription( +DEVICE_TRACKER_TYPES: tuple[RenaultDataEntityDescription, ...] = ( + RenaultDataEntityDescription( key="location", coordinator="location", icon="mdi:car", diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index b963edbc81f..14ebcf2c2e4 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -14,40 +14,33 @@ from .renault_vehicle import RenaultVehicleProxy @dataclass -class RenaultRequiredKeysMixin: +class RenaultDataRequiredKeysMixin: """Mixin for required keys.""" coordinator: str @dataclass -class RenaultEntityDescription(EntityDescription, RenaultRequiredKeysMixin): - """Class describing Renault entities.""" +class RenaultDataEntityDescription(EntityDescription, RenaultDataRequiredKeysMixin): + """Class describing Renault data entities.""" -class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): +class RenaultEntity(Entity): """Implementation of a Renault entity with a data coordinator.""" - entity_description: RenaultEntityDescription + entity_description: EntityDescription def __init__( self, vehicle: RenaultVehicleProxy, - description: RenaultEntityDescription, + description: EntityDescription, ) -> None: """Initialise entity.""" - super().__init__(vehicle.coordinators[description.coordinator]) self.vehicle = vehicle self.entity_description = description self._attr_device_info = self.vehicle.device_info self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower() - def _get_data_attr(self, key: str) -> StateType: - """Return the attribute value from the coordinator data.""" - if self.coordinator.data is None: - return None - return cast(StateType, getattr(self.coordinator.data, key)) - @property def name(self) -> str: """Return the name of the entity. @@ -55,3 +48,22 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): Overridden to include the device name. """ return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" + + +class RenaultDataEntity(CoordinatorEntity[Optional[T]], RenaultEntity): + """Implementation of a Renault entity with a data coordinator.""" + + def __init__( + self, + vehicle: RenaultVehicleProxy, + description: RenaultDataEntityDescription, + ) -> None: + """Initialise entity.""" + super().__init__(vehicle.coordinators[description.coordinator]) + RenaultEntity.__init__(self, vehicle, description) + + def _get_data_attr(self, key: str) -> StateType: + """Return the attribute value from the coordinator data.""" + if self.coordinator.data is None: + return None + return cast(StateType, getattr(self.coordinator.data, key)) diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index a8f4a15dc21..e7ec97b3927 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import DEVICE_CLASS_CHARGE_MODE, DOMAIN -from .renault_entities import RenaultDataEntity, RenaultEntityDescription +from .renault_entities import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub @@ -29,7 +29,9 @@ class RenaultSelectRequiredKeysMixin: @dataclass class RenaultSelectEntityDescription( - SelectEntityDescription, RenaultEntityDescription, RenaultSelectRequiredKeysMixin + SelectEntityDescription, + RenaultDataEntityDescription, + RenaultSelectRequiredKeysMixin, ): """Class describing Renault select entities.""" diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index e8e26e06d6c..d06ae497cf1 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -43,7 +43,7 @@ from homeassistant.util.dt import as_utc, parse_datetime from .const import DEVICE_CLASS_CHARGE_STATE, DEVICE_CLASS_PLUG_STATE, DOMAIN from .renault_coordinator import T -from .renault_entities import RenaultDataEntity, RenaultEntityDescription +from .renault_entities import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub from .renault_vehicle import RenaultVehicleProxy @@ -58,7 +58,9 @@ class RenaultSensorRequiredKeysMixin: @dataclass class RenaultSensorEntityDescription( - SensorEntityDescription, RenaultEntityDescription, RenaultSensorRequiredKeysMixin + SensorEntityDescription, + RenaultDataEntityDescription, + RenaultSensorRequiredKeysMixin, ): """Class describing Renault sensor entities.""" diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index 972befcec6d..de69daefef6 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -112,6 +112,14 @@ def setup_services(hass: HomeAssistant) -> None: async def charge_start(service_call: ServiceCall) -> None: """Start charge.""" + # The Renault start charge service has been replaced by a + # dedicated button entity and marked as deprecated + LOGGER.warning( + "The 'renault.charge_start' service is deprecated and " + "replaced by a dedicated start charge button entity; please " + "use that entity to start the charge instead" + ) + proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("Charge start attempt") diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index e3703173ad0..e0283867132 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PLUG, DOMAIN as BINARY_SENSOR_DOMAIN, ) +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, @@ -117,6 +118,20 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], + BUTTON_DOMAIN: [ + { + ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", + ATTR_ICON: "mdi:air-conditioner", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_air_conditioner", + }, + { + ATTR_ENTITY_ID: "button.reg_number_start_charge", + ATTR_ICON: "mdi:ev-station", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_charge", + }, + ], DEVICE_TRACKER_DOMAIN: [], SELECT_DOMAIN: [ { @@ -251,6 +266,20 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], + BUTTON_DOMAIN: [ + { + ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", + ATTR_ICON: "mdi:air-conditioner", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_air_conditioner", + }, + { + ATTR_ENTITY_ID: "button.reg_number_start_charge", + ATTR_ICON: "mdi:ev-station", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_charge", + }, + ], DEVICE_TRACKER_DOMAIN: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", @@ -391,6 +420,20 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging", }, ], + BUTTON_DOMAIN: [ + { + ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", + ATTR_ICON: "mdi:air-conditioner", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_start_air_conditioner", + }, + { + ATTR_ENTITY_ID: "button.reg_number_start_charge", + ATTR_ICON: "mdi:ev-station", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_start_charge", + }, + ], DEVICE_TRACKER_DOMAIN: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", @@ -532,6 +575,14 @@ MOCK_VEHICLES = { "location": "location.json", }, BINARY_SENSOR_DOMAIN: [], + BUTTON_DOMAIN: [ + { + ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", + ATTR_ICON: "mdi:air-conditioner", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_start_air_conditioner", + }, + ], DEVICE_TRACKER_DOMAIN: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py new file mode 100644 index 00000000000..729eb89d74c --- /dev/null +++ b/tests/components/renault/test_button.py @@ -0,0 +1,185 @@ +"""Tests for Renault sensors.""" +from unittest.mock import patch + +import pytest +from renault_api.kamereon import schemas + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import check_device_registry, check_entities_no_data +from .const import ATTR_ENTITY_ID, MOCK_VEHICLES + +from tests.common import load_fixture, mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") + + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", [BUTTON_DOMAIN]): + yield + + +@pytest.mark.usefixtures("fixtures_with_data") +async def test_buttons( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): + """Test for Renault device trackers.""" + + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + 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[BUTTON_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) + + +@pytest.mark.usefixtures("fixtures_with_no_data") +async def test_button_empty( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): + """Test for Renault device trackers with empty data from Renault.""" + + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + 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[BUTTON_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) + + +@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") +async def test_button_errors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): + """Test for Renault device trackers with temporary failure.""" + + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + 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[BUTTON_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) + + +@pytest.mark.usefixtures("fixtures_with_access_denied_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_button_access_denied( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): + """Test for Renault device trackers with access denied failure.""" + + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + 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[BUTTON_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) + + +@pytest.mark.usefixtures("fixtures_with_not_supported_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_button_not_supported( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): + """Test for Renault device trackers with not supported failure.""" + + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + 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[BUTTON_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) + + +@pytest.mark.usefixtures("fixtures_with_data") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntry): + """Test that button invokes renault_api with correct data.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + data = { + ATTR_ENTITY_ID: "button.reg_number_start_charge", + } + + with patch( + "renault_api.renault_vehicle.RenaultVehicle.set_charge_start", + return_value=( + schemas.KamereonVehicleHvacStartActionDataSchema.loads( + load_fixture("renault/action.set_charge_start.json") + ) + ), + ) as mock_action: + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True + ) + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == () + + +@pytest.mark.usefixtures("fixtures_with_data") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_button_start_air_conditioner( + hass: HomeAssistant, config_entry: ConfigEntry +): + """Test that button invokes renault_api with correct data.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + data = { + ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", + } + + with patch( + "renault_api.renault_vehicle.RenaultVehicle.set_ac_start", + return_value=( + schemas.KamereonVehicleHvacStartActionDataSchema.loads( + load_fixture("renault/action.set_ac_start.json") + ) + ), + ) as mock_action: + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True + ) + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == (21, None) diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 5a02fd814b9..b7748cafb5d 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -236,7 +236,9 @@ async def test_service_set_charge_schedule_multi( assert mock_action.mock_calls[0][1] == (mock_call_data,) -async def test_service_set_charge_start(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_service_set_charge_start( + hass: HomeAssistant, config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture +): """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -258,6 +260,7 @@ async def test_service_set_charge_start(hass: HomeAssistant, config_entry: Confi ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () + assert f"'{DOMAIN}.{SERVICE_CHARGE_START}' service is deprecated" in caplog.text async def test_service_invalid_device_id( From de16ce18576af99e28f857ddbf564a8698ab22cc Mon Sep 17 00:00:00 2001 From: hypnosiss <11396064+hypnosiss@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:14:49 +0100 Subject: [PATCH 0502/1452] Fix relative import in MySensors (#59710) Co-authored-by: Franck Nijhof --- homeassistant/components/mysensors/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 8f8c759c364..7c6602b7373 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -7,13 +7,13 @@ import voluptuous as vol from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from ...config_entries import ConfigEntry -from ...helpers.dispatcher import async_dispatcher_connect from .const import ( DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, From a4208c092680740864b15bef923add5b54de2da6 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Mon, 15 Nov 2021 17:25:19 +0100 Subject: [PATCH 0503/1452] Add Reauth flow to Wallbox integration (#58743) * Add Reauth flow to Wallbox integration * Review comments processed * Fixed tests * Added test for reauth invalid * Commit to compensate for timedrift, show changes Compensating for timedrift in my devcontainer, making a new commit with the right date/time. Requested changes were done in a previous commit. * remove reauth schema * Update homeassistant/components/wallbox/__init__.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/wallbox/__init__.py | 29 +++---- .../components/wallbox/config_flow.py | 33 +++++++- homeassistant/components/wallbox/number.py | 10 +-- homeassistant/components/wallbox/sensor.py | 3 +- homeassistant/components/wallbox/strings.json | 12 ++- .../components/wallbox/translations/en.json | 13 ++- .../components/wallbox/translations/nl.json | 3 +- tests/components/wallbox/test_config_flow.py | 81 +++++++++++++++++++ tests/components/wallbox/test_init.py | 9 +-- 9 files changed, 149 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index aade0c430f6..b1604a37c6f 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -9,16 +9,10 @@ from wallbox import Wallbox from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ( - CONF_CONNECTIONS, - CONF_DATA_KEY, - CONF_MAX_CHARGING_CURRENT_KEY, - CONF_STATION, - DOMAIN, -) +from .const import CONF_DATA_KEY, CONF_MAX_CHARGING_CURRENT_KEY, CONF_STATION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -48,7 +42,7 @@ class WallboxCoordinator(DataUpdateCoordinator): return True except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: - raise InvalidAuth from wallbox_connection_error + raise ConfigEntryAuthFailed from wallbox_connection_error raise ConnectionError from wallbox_connection_error def _validate(self): @@ -112,18 +106,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, ) - await wallbox_coordinator.async_validate_input() + try: + await wallbox_coordinator.async_validate_input() + + except InvalidAuth as ex: + raise ConfigEntryAuthFailed from ex await wallbox_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}}) - hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = wallbox_coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator - for platform in PLATFORMS: - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -132,7 +125,7 @@ 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][CONF_CONNECTIONS].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index f123ad0cd2d..8559c29c9aa 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -36,6 +36,18 @@ async def validate_input(hass: core.HomeAssistant, data): class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): """Handle a config flow for Wallbox.""" + def __init__(self): + """Start the Wallbox config flow.""" + self._reauth_entry = None + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is None: @@ -47,14 +59,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): errors = {} try: - info = await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input["station"]) + if not self._reauth_entry: + self._abort_if_unique_id_configured() + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], data=user_input) + if user_input["station"] == self._reauth_entry.data[CONF_STATION]: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=user_input["station"] + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + errors["base"] = "reauth_invalid" except ConnectionError: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - else: - 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=STEP_USER_DATA_SCHEMA, + errors=errors, ) diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 1bc561cb7c5..b94351f4353 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -8,12 +8,7 @@ from homeassistant.const import DEVICE_CLASS_CURRENT from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import InvalidAuth -from .const import ( - CONF_CONNECTIONS, - CONF_MAX_AVAILABLE_POWER_KEY, - CONF_MAX_CHARGING_CURRENT_KEY, - DOMAIN, -) +from .const import CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, DOMAIN @dataclass @@ -35,8 +30,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { async def async_setup_entry(hass, config, async_add_entities): """Create wallbox sensor entities in HASS.""" - coordinator = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] - + coordinator = hass.data[DOMAIN][config.entry_id] # Check if the user is authorized to change current, if so, add number component: try: await coordinator.async_set_charging_current( diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index affc78d6210..4571ed08725 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -28,7 +28,6 @@ from .const import ( CONF_ADDED_RANGE_KEY, CONF_CHARGING_POWER_KEY, CONF_CHARGING_SPEED_KEY, - CONF_CONNECTIONS, CONF_COST_KEY, CONF_CURRENT_MODE_KEY, CONF_DEPOT_PRICE_KEY, @@ -133,7 +132,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { async def async_setup_entry(hass, config, async_add_entities): """Create wallbox sensor entities in HASS.""" - coordinator = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] + coordinator = hass.data[DOMAIN][config.entry_id] async_add_entities( [ diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index 6824a1343fc..4cde9c6d255 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -7,15 +7,23 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_invalid": "Re-authentication failed; Serial Number does not match original" }, "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%]" } } } diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json index 52dcf8530d4..f32c7b7b481 100644 --- a/homeassistant/components/wallbox/translations/en.json +++ b/homeassistant/components/wallbox/translations/en.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "reauth_invalid": "Re-authentication failed; Serial Number does not match original", "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", @@ -17,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index 6ba03e7ee99..a152eb89362 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index ca55c076fea..01993d88968 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.core import HomeAssistant +from tests.components.wallbox import entry, setup_integration from tests.components.wallbox.const import ( CONF_ERROR, CONF_JWT, @@ -162,3 +163,83 @@ async def test_form_validate_input(hass): assert result2["title"] == "Wallbox Portal" assert result2["data"]["station"] == "12345" + + +async def test_form_reauth(hass): + """Test we handle reauth flow.""" + await setup_integration(hass) + assert entry.state == config_entries.ConfigEntryState.LOADED + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response, + status_code=200, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_form_reauth_invalid(hass): + """Test we handle reauth invalid flow.""" + await setup_integration(hass) + assert entry.state == config_entries.ConfigEntryState.LOADED + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response, + status_code=200, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345678", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "reauth_invalid"} + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 10e6cab99fc..66f0701e42e 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -3,10 +3,7 @@ import json import requests_mock -from homeassistant.components.wallbox import ( - CONF_CONNECTIONS, - CONF_MAX_CHARGING_CURRENT_KEY, -) +from homeassistant.components.wallbox import CONF_MAX_CHARGING_CURRENT_KEY from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -78,7 +75,7 @@ async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant): status_code=403, ) - wallbox = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] + wallbox = hass.data[DOMAIN][entry.entry_id] await wallbox.async_refresh() @@ -104,7 +101,7 @@ async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant): status_code=403, ) - wallbox = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] + wallbox = hass.data[DOMAIN][entry.entry_id] await wallbox.async_refresh() From 5cc594682f216250adb635f478886c2058bef1be Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Mon, 15 Nov 2021 18:28:19 +0200 Subject: [PATCH 0504/1452] Add unique id's to Vallox entities (#58459) * Add unique id's to Vallox entities * Cache uuid properties Requested in code review. Caching None isn't a problem as the underlying implementation of get_uuid in the vallox_websocket_api library can never return None. * Simplify get_uuid type check Based on review comments. * Set _attr_unique_id in init * Import the library get_uuid under a different name There are a few options here: 1. Rename the get_uuid method with a synonym 2. Import get_uuid under a different name 3. Convert get_uuid into a property 4. Rename get_uuid in the Vallox library None of these options is that appealing. I'll start with option two, anyways. --- homeassistant/components/vallox/__init__.py | 9 +++++++++ homeassistant/components/vallox/fan.py | 2 ++ homeassistant/components/vallox/sensor.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 63b594a5bf2..73dc633834e 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -5,9 +5,11 @@ from dataclasses import dataclass, field import ipaddress import logging from typing import Any, NamedTuple +from uuid import UUID from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api.exceptions import ValloxApiException +from vallox_websocket_api.vallox import get_uuid as calculate_uuid import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED @@ -114,6 +116,13 @@ class ValloxState: return value + def get_uuid(self) -> UUID | None: + """Return cached UUID value.""" + uuid = calculate_uuid(self.metric_cache) + if not isinstance(uuid, UUID): + raise ValueError + return uuid + class ValloxDataUpdateCoordinator(DataUpdateCoordinator): """The DataUpdateCoordinator for Vallox.""" diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 4d621615aef..6de30302838 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -99,6 +99,8 @@ class ValloxFan(CoordinatorEntity, FanEntity): self._attr_name = name + self._attr_unique_id = str(self.coordinator.data.get_uuid()) + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 0b96316b766..17bcf0e4499 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -52,6 +52,9 @@ class ValloxSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{name} {description.name}" + uuid = self.coordinator.data.get_uuid() + self._attr_unique_id = f"{uuid}-{description.key}" + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" From b3ffc1e183538655d49bb36ab16165c01b38a7ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:05:45 +0100 Subject: [PATCH 0505/1452] Adjust async_step_zeroconf signature for strict typing (#59503) Co-authored-by: epenet --- homeassistant/components/apple_tv/config_flow.py | 3 +-- homeassistant/components/axis/config_flow.py | 3 +-- homeassistant/components/bond/config_flow.py | 3 +-- homeassistant/components/brother/config_flow.py | 5 ++--- homeassistant/components/cast/config_flow.py | 4 ++-- homeassistant/components/daikin/config_flow.py | 3 +-- .../components/devolo_home_control/config_flow.py | 8 ++------ .../components/devolo_home_network/config_flow.py | 4 ++-- homeassistant/components/elgato/config_flow.py | 7 +++++-- homeassistant/components/esphome/config_flow.py | 3 +-- homeassistant/components/guardian/config_flow.py | 3 ++- homeassistant/components/hue/config_flow.py | 3 +-- homeassistant/components/lookin/config_flow.py | 4 ++-- .../components/modern_forms/config_flow.py | 8 ++++---- homeassistant/components/nam/config_flow.py | 6 +++--- homeassistant/components/nanoleaf/config_flow.py | 7 +++++-- homeassistant/components/rainmachine/config_flow.py | 11 +++++++++-- homeassistant/components/samsungtv/config_flow.py | 8 ++++---- homeassistant/components/shelly/config_flow.py | 4 ++-- homeassistant/components/sonos/config_flow.py | 8 +++++--- .../components/system_bridge/config_flow.py | 4 ++-- homeassistant/components/volumio/config_flow.py | 6 +++--- homeassistant/components/wled/config_flow.py | 8 ++++---- homeassistant/config_entries.py | 9 ++++++--- homeassistant/helpers/config_entry_flow.py | 13 ++++++++++++- tests/components/brother/test_config_flow.py | 6 +++--- 26 files changed, 85 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index f1a89688ad4..306a1d9f793 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -15,7 +15,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN @@ -145,7 +144,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle device found via zeroconf.""" service_type = discovery_info[zeroconf.ATTR_TYPE] diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index e3994cedd39..d1e834e7bb6 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -20,7 +20,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.network import is_link_local from .const import ( @@ -178,7 +177,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a Zeroconf discovered Axis device.""" return await self._process_discovered_device( diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 5996cd03bae..d9398edf2c9 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN from .utils import BondHub @@ -92,7 +91,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[zeroconf.ATTR_NAME] diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index fa743e68d56..7a814e2e77c 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -12,7 +12,6 @@ from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN, PRINTER_TYPES from .utils import get_snmp_engine @@ -81,7 +80,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: brother.local. @@ -91,7 +90,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: self.host}) snmp_engine = get_snmp_engine(self.hass) - model = discovery_info.get(zeroconf.ATTR_PROPERTIES, {}).get("product") + model = discovery_info[zeroconf.ATTR_PROPERTIES].get("product") try: self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index dbf52a8f238..aaf8d5b9c6c 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -2,9 +2,9 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, CONF_UUID, DOMAIN @@ -52,7 +52,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_config() async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 89b27b68c81..e907aaa4d74 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -13,7 +13,6 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -127,7 +126,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 0cff72a4321..fab5c2b5008 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from . import configure_mydevolo from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES @@ -46,14 +45,11 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._show_form(step_id="user", errors={"base": "invalid_auth"}) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" # Check if it is a gateway - if ( - discovery_info.get(zeroconf.ATTR_PROPERTIES, {}).get("MT") - in SUPPORTED_MODEL_TYPES - ): + if discovery_info[zeroconf.ATTR_PROPERTIES].get("MT") in SUPPORTED_MODEL_TYPES: await self._async_handle_discovery_without_unique_id() return await self.async_step_zeroconf_confirm() return self.async_abort(reason="Not a devolo Home Control gateway.") diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 20efe388407..c3e91a6ec65 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE @@ -74,7 +74,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zerooconf discovery.""" if discovery_info[zeroconf.ATTR_PROPERTIES]["MT"] in ["2600", "2601"]: diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 6008ccbee77..7a4eb7a7519 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -6,6 +6,7 @@ from typing import Any from elgato import Elgato, ElgatoError import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback @@ -41,10 +42,12 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_zeroconf(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] - self.port = discovery_info[CONF_PORT] + self.port = discovery_info[CONF_PORT] or 9123 try: await self._get_elgato_serial_number() diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index a794404b685..fa72cbd995e 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -20,7 +20,6 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from . import CONF_NOISE_PSK, DOMAIN, DomainData @@ -139,7 +138,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index ccebeb99675..452574d4eed 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -8,6 +8,7 @@ from aioguardian.errors import GuardianError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback @@ -107,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_handle_discovery() async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle the configuration via zeroconf.""" self.discovery_info = { diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 409f88cbe04..0ffa7e358f0 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import DiscoveryInfoType from .bridge import authenticate_bridge from .const import ( @@ -209,7 +208,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_link() async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a discovered Hue bridge. diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index 14e4b517b5b..e41aad0406b 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -9,10 +9,10 @@ from aiolookin import Device, LookInHttpProtocol, NoUsableService import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -28,7 +28,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._name: str | None = None async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Start a discovery flow from zeroconf.""" uid: str = discovery_info["hostname"][: -len(".local.")] diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index e8b557f7bc5..147976ee2b0 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -1,16 +1,16 @@ """Config flow for Modern Forms.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -27,7 +27,7 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" host = discovery_info["hostname"].rstrip(".") @@ -43,7 +43,7 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): ) # Prepare configuration flow - return await self._handle_config_flow(discovery_info, True) + return await self._handle_config_flow(cast(dict, discovery_info), True) async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 0fe2c8f9c65..86a30f95af6 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -11,11 +11,11 @@ from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import ATTR_NAME, CONF_HOST 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 DOMAIN @@ -69,7 +69,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] @@ -78,7 +78,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: self.host}) try: - mac = await self._async_get_mac(cast(str, self.host)) + mac = await self._async_get_mac(self.host) except (ApiError, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="cannot_connect") except CannotGetMac: diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 0f4f8ff75bd..17269f2e07a 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -9,6 +9,7 @@ from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -88,11 +89,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_link() async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle Nanoleaf Zeroconf discovery.""" _LOGGER.debug("Zeroconf discovered: %s", discovery_info) - return await self._async_homekit_zeroconf_discovery_handler(discovery_info) + return await self._async_homekit_zeroconf_discovery_handler( + cast(dict, discovery_info) + ) async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle Nanoleaf Homekit discovery.""" diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index c392ad1f8ce..47896cc6080 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the RainMachine component.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from regenmaschine import Client from regenmaschine.controller import Controller @@ -9,6 +9,7 @@ from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant, callback @@ -56,9 +57,15 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by homekit discovery.""" - return await self.async_step_zeroconf(discovery_info) + return await self.async_step_homekit_zeroconf(discovery_info) async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle discovery via zeroconf.""" + return await self.async_step_homekit_zeroconf(cast(dict, discovery_info)) + + async def async_step_homekit_zeroconf( self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle discovery via zeroconf.""" diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index c75086322da..1525d037cd9 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -10,6 +10,7 @@ import getmac import voluptuous as vol from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, @@ -37,7 +38,6 @@ from .bridge import ( mac_from_device_info, ) from .const import ( - ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, DEFAULT_MANUFACTURER, @@ -297,12 +297,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) - self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) - self._host = discovery_info[CONF_HOST] + self._mac = format_mac(discovery_info[zeroconf.ATTR_PROPERTIES]["deviceid"]) + self._host = discovery_info[zeroconf.ATTR_HOST] await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 580221d376f..b7b9ab72976 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -14,11 +14,11 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import DiscoveryInfoType from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN from .utils import ( @@ -186,7 +186,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" try: diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 98e1194ebd0..b26f1bdf40b 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,12 +1,14 @@ """Config flow for SONOS.""" +from typing import cast + import soco from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN from .helpers import hostname_to_uid @@ -26,7 +28,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): super().__init__(DOMAIN, "Sonos", _async_has_devices) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf.""" hostname = discovery_info["hostname"] @@ -43,7 +45,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): discovery_manager.async_discovered_player( "Zeroconf", properties, host, uid, boot_seqnum, model, mdns_name ) - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) config_entries.HANDLERS.register(DOMAIN)(SonosDiscoveryFlowHandler) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 4ff887c6389..8d0b7a5dfff 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -11,11 +11,11 @@ from systembridge.exceptions import BridgeAuthenticationException import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN @@ -148,7 +148,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" host = discovery_info["properties"].get("ip") diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index 45c424b356e..a499b7827b5 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -7,10 +7,10 @@ from pyvolumio import CannotConnectError, Volumio import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -93,10 +93,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf(self, discovery_info: zeroconf.ZeroconfServiceInfo): """Handle zeroconf discovery.""" self._host = discovery_info["host"] - self._port = int(discovery_info["port"]) + self._port = discovery_info["port"] self._name = discovery_info["properties"]["volumioName"] self._uuid = discovery_info["properties"]["UUID"] diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7f4d006d122..828dfc3368a 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the WLED integration.""" from __future__ import annotations -from typing import Any +from typing import Any, cast import voluptuous as vol from wled import WLED, WLEDConnectionError +from homeassistant.components import zeroconf from homeassistant.config_entries import ( SOURCE_ZEROCONF, ConfigEntry, @@ -16,7 +17,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT, DOMAIN @@ -39,7 +39,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" @@ -57,7 +57,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): ) # Prepare configuration flow - return await self._handle_config_flow(discovery_info, True) + return await self._handle_config_flow(cast(dict, discovery_info), True) async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 17f8b1396ed..521fb5d444c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -8,7 +8,7 @@ from enum import Enum import functools import logging from types import MappingProxyType, MethodType -from typing import Any, Callable, Optional, cast +from typing import TYPE_CHECKING, Any, Callable, Optional, cast import weakref from homeassistant import data_entry_flow, loader @@ -31,6 +31,9 @@ from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util +if TYPE_CHECKING: + from homeassistant.components.zeroconf import ZeroconfServiceInfo + _LOGGER = logging.getLogger(__name__) SOURCE_DISCOVERY = "discovery" @@ -1369,10 +1372,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(discovery_info) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_dhcp( self, discovery_info: DiscoveryInfoType diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 2f9f0b52839..1e87d0042d7 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,6 +5,7 @@ import logging from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType @@ -81,7 +82,17 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() - async_step_zeroconf = async_step_discovery + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle a flow initialized by Zeroconf discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async_step_ssdp = async_step_discovery async_step_mqtt = async_step_discovery async_step_homekit = async_step_discovery diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 314748e5cde..82757fa425a 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -145,7 +145,7 @@ async def test_zeroconf_snmp_error(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer" + hostname="example.local.", name="Brother Printer", properties={} ), ) @@ -185,7 +185,7 @@ async def test_zeroconf_device_exists_abort(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer" + hostname="example.local.", name="Brother Printer", properties={} ), ) @@ -223,7 +223,7 @@ async def test_zeroconf_confirm_create_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer" + hostname="example.local.", name="Brother Printer", properties={} ), ) From 5fc51130ea6fb6db92c50d0256671adc45a76888 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 15 Nov 2021 18:18:57 +0100 Subject: [PATCH 0506/1452] Replace util.get_local_ip in favor of components.network.async_get_source_ip() - part 4 (#58669) Co-authored-by: J. Nick Koston --- .../components/ambiclimate/config_flow.py | 2 +- .../components/emulated_hue/__init__.py | 3 +- .../components/emulated_roku/__init__.py | 5 +-- homeassistant/components/homekit/__init__.py | 3 +- homeassistant/components/http/__init__.py | 4 +- homeassistant/components/local_ip/sensor.py | 5 +-- homeassistant/components/network/__init__.py | 24 +++--------- homeassistant/components/network/const.py | 2 + homeassistant/components/network/network.py | 39 +++++++++---------- homeassistant/components/network/util.py | 9 ----- homeassistant/components/network/websocket.py | 13 ++----- homeassistant/components/zeroconf/__init__.py | 3 +- homeassistant/util/__init__.py | 23 +---------- requirements.txt | 1 + script/hassfest/dependencies.py | 4 +- setup.py | 1 + tests/components/motioneye/__init__.py | 5 ++- tests/components/motioneye/test_web_hooks.py | 16 ++++---- tests/components/network/conftest.py | 9 +++++ tests/components/network/test_init.py | 35 +++++++++-------- tests/conftest.py | 2 +- 21 files changed, 83 insertions(+), 125 deletions(-) create mode 100644 tests/components/network/conftest.py diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index b022b54c4fd..00fa339b0d8 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -124,7 +124,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) def _cb_url(self): - return f"{get_url(self.hass)}{AUTH_CALLBACK_PATH}" + return f"{get_url(self.hass, prefer_external=True)}{AUTH_CALLBACK_PATH}" async def _get_authorize_url(self): oauth = self._generate_oauth() diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 3cfa710703c..4dec35a80e8 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,7 +5,6 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.network import async_get_source_ip -from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.const import ( CONF_ENTITIES, CONF_TYPE, @@ -106,7 +105,7 @@ ATTR_EMULATED_HUE_NAME = "emulated_hue_name" async def async_setup(hass, yaml_config): """Activate the emulated_hue component.""" - local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP) + local_ip = await async_get_source_ip(hass) config = Config(hass, yaml_config.get(DOMAIN, {}), local_ip) await config.async_setup() diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 32e08342191..3d84bf20f44 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -3,7 +3,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.network import async_get_source_ip -from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -71,9 +70,7 @@ async def async_setup_entry(hass, config_entry): name = config[CONF_NAME] listen_port = config[CONF_LISTEN_PORT] - host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip( - hass, PUBLIC_TARGET_IP - ) + host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip(hass) advertise_ip = config.get(CONF_ADVERTISE_IP) advertise_port = config.get(CONF_ADVERTISE_PORT) upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 3e2a2a65d18..3c45ba8de39 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -18,6 +18,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN +from homeassistant.components.network.const import MDNS_TARGET_IP from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -119,8 +120,6 @@ STATUS_WAIT = 3 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 -MDNS_TARGET_IP = "224.0.0.251" - _HOMEKIT_CONFIG_UPDATE_TIME = ( 5 # number of seconds to wait for homekit to see the c# change ) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 15766acdd4c..7c30acfc118 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -13,6 +13,7 @@ from aiohttp.typedefs import StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection import voluptuous as vol +from homeassistant.components.network import async_get_source_ip from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import storage @@ -20,7 +21,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import async_start_setup, async_when_setup_or_start -import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util from .auth import setup_auth @@ -190,7 +190,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http = server - local_ip = await hass.async_add_executor_job(hass_util.get_local_ip) + local_ip = await async_get_source_ip(hass) host = local_ip if server_host is not None: diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index bd1b3d54fac..56c1fac7c8f 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -1,7 +1,6 @@ """Sensor platform for local_ip.""" from homeassistant.components.network import async_get_source_ip -from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME @@ -33,6 +32,4 @@ class IPSensor(SensorEntity): async def async_update(self) -> None: """Fetch new state data for the sensor.""" - self._attr_native_value = await async_get_source_ip( - self.hass, target_ip=PUBLIC_TARGET_IP - ) + self._attr_native_value = await async_get_source_ip(self.hass) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index e8a2c4c80fd..7cc864727d7 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -2,30 +2,28 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_interface -import logging from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import util -from .const import DOMAIN, IPV4_BROADCAST_ADDR +from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP from .models import Adapter -from .network import Network - -ZEROCONF_DOMAIN = "zeroconf" # cannot import from zeroconf due to circular dep -_LOGGER = logging.getLogger(__name__) +from .network import Network, async_get_network @bind_hass async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: """Get the network adapter configuration.""" - network: Network = hass.data[DOMAIN] + network: Network = await async_get_network(hass) return network.adapters @bind_hass -async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str: +async def async_get_source_ip( + hass: HomeAssistant, target_ip: str = PUBLIC_TARGET_IP +) -> str: """Get the source ip for a target ip.""" adapters = await async_get_adapters(hass) all_ipv4s = [] @@ -88,20 +86,10 @@ async def async_get_ipv4_broadcast_addresses(hass: HomeAssistant) -> set[IPv4Add async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" - - hass.data[DOMAIN] = network = Network(hass) - await network.async_setup() - if ZEROCONF_DOMAIN in config: - await network.async_migrate_from_zeroconf(config[ZEROCONF_DOMAIN]) - network.async_configure() - - _LOGGER.debug("Adapters: %s", network.adapters) - # Avoid circular issue: http->network->websocket_api->http from .websocket import ( # pylint: disable=import-outside-toplevel async_register_websocket_commands, ) async_register_websocket_commands(hass) - return True diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py index 7e7401251fc..07c12e63a10 100644 --- a/homeassistant/components/network/const.py +++ b/homeassistant/components/network/const.py @@ -11,6 +11,8 @@ DOMAIN: Final = "network" STORAGE_KEY: Final = "core.network" STORAGE_VERSION: Final = 1 +DATA_NETWORK: Final = "network" + ATTR_ADAPTERS: Final = "adapters" ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters" DEFAULT_CONFIGURED_ADAPTERS: list[str] = [] diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index 5e2e7b8ad6b..6ec9941da3c 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -1,23 +1,35 @@ """Network helper class for the network integration.""" from __future__ import annotations +import logging from typing import Any, cast from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton from .const import ( ATTR_CONFIGURED_ADAPTERS, + DATA_NETWORK, DEFAULT_CONFIGURED_ADAPTERS, STORAGE_KEY, STORAGE_VERSION, ) from .models import Adapter -from .util import ( - adapters_with_exernal_addresses, - async_load_adapters, - enable_adapters, - enable_auto_detected_adapters, -) +from .util import async_load_adapters, enable_adapters, enable_auto_detected_adapters + +_LOGGER = logging.getLogger(__name__) + + +@singleton(DATA_NETWORK) +@callback +async def async_get_network(hass: HomeAssistant) -> Network: + """Get network singleton.""" + network = Network(hass) + await network.async_setup() + network.async_configure() + + _LOGGER.debug("Adapters: %s", network.adapters) + return network class Network: @@ -41,21 +53,6 @@ class Network: await self.async_load() self.adapters = await async_load_adapters() - async def async_migrate_from_zeroconf(self, zc_config: dict[str, Any]) -> None: - """Migrate configuration from zeroconf.""" - if self._data or not zc_config: - return - - from homeassistant.components.zeroconf import ( # pylint: disable=import-outside-toplevel - CONF_DEFAULT_INTERFACE, - ) - - if zc_config.get(CONF_DEFAULT_INTERFACE) is False: - self._data[ATTR_CONFIGURED_ADAPTERS] = adapters_with_exernal_addresses( - self.adapters - ) - await self._async_save() - @callback def async_configure(self) -> None: """Configure from storage.""" diff --git a/homeassistant/components/network/util.py b/homeassistant/components/network/util.py index f8b33b3df90..4b920e5d927 100644 --- a/homeassistant/components/network/util.py +++ b/homeassistant/components/network/util.py @@ -57,15 +57,6 @@ def enable_auto_detected_adapters(adapters: list[Adapter]) -> None: ) -def adapters_with_exernal_addresses(adapters: list[Adapter]) -> list[str]: - """Enable all interfaces with an external address.""" - return [ - adapter["name"] - for adapter in adapters - if _adapter_has_external_address(adapter) - ] - - def _adapter_has_external_address(adapter: Adapter) -> bool: """Adapter has a non-loopback and non-link-local address.""" return any( diff --git a/homeassistant/components/network/websocket.py b/homeassistant/components/network/websocket.py index 77e01375b75..c19ed5e8fd7 100644 --- a/homeassistant/components/network/websocket.py +++ b/homeassistant/components/network/websocket.py @@ -7,13 +7,8 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.core import HomeAssistant, callback -from .const import ( - ATTR_ADAPTERS, - ATTR_CONFIGURED_ADAPTERS, - DOMAIN, - NETWORK_CONFIG_SCHEMA, -) -from .network import Network +from .const import ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, NETWORK_CONFIG_SCHEMA +from .network import async_get_network @callback @@ -32,7 +27,7 @@ async def websocket_network_adapters( msg: dict, ) -> None: """Return network preferences.""" - network: Network = hass.data[DOMAIN] + network = await async_get_network(hass) connection.send_result( msg["id"], { @@ -56,7 +51,7 @@ async def websocket_network_adapters_configure( msg: dict, ) -> None: """Update network config.""" - network: Network = hass.data[DOMAIN] + network = await async_get_network(hass) await network.async_reconfig(msg["config"]) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 81601702656..01b289f6d18 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -17,6 +17,7 @@ from zeroconf.asyncio import AsyncServiceInfo from homeassistant import config_entries from homeassistant.components import network from homeassistant.components.network import async_get_source_ip +from homeassistant.components.network.const import MDNS_TARGET_IP from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, @@ -52,8 +53,6 @@ DEFAULT_IPV6 = True HOMEKIT_PAIRED_STATUS_FLAG = "sf" HOMEKIT_MODEL = "md" -MDNS_TARGET_IP = "224.0.0.251" - # Property key=value has a max length of 255 # so we use 230 to leave space for key= MAX_PROPERTY_VALUE_LEN = 230 diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 4f7f1af2e7d..3840958d580 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -5,10 +5,9 @@ import asyncio from collections.abc import Callable, Coroutine, Iterable, KeysView from datetime import datetime, timedelta import enum -from functools import lru_cache, wraps +from functools import wraps import random import re -import socket import string import threading from types import MappingProxyType @@ -129,26 +128,6 @@ def ensure_unique_string( return test_string -# Taken from: http://stackoverflow.com/a/11735897 -@lru_cache(maxsize=None) -def get_local_ip() -> str: - """Try to determine the local IP address of the machine.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - # Use Google Public DNS server to determine own IP - sock.connect(("8.8.8.8", 80)) - - return sock.getsockname()[0] # type: ignore - except OSError: - try: - return socket.gethostbyname(socket.gethostname()) - except socket.gaierror: - return "127.0.0.1" - finally: - sock.close() - - # Taken from http://stackoverflow.com/a/23728630 def get_random_string(length: int = 10) -> str: """Return a random string with letters and digits.""" diff --git a/requirements.txt b/requirements.txt index fe35d852aef..0049df1cf7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.19.0 +ifaddr==0.1.7 jinja2==3.0.3 PyJWT==2.1.0 cryptography==35.0.0 diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index b650d3232ac..41510a46cb4 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -134,8 +134,8 @@ IGNORE_VIOLATIONS = { # Demo ("demo", "manual"), ("demo", "openalpr_local"), - # Migration of settings from zeroconf to network - ("network", "zeroconf"), + # This would be a circular dep + ("http", "network"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), diff --git a/setup.py b/setup.py index 38a48fb5f0f..e08fbdf43e5 100755 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ REQUIRES = [ "certifi>=2021.5.30", "ciso8601==2.2.0", "httpx==0.19.0", + "ifaddr==0.1.7", "jinja2==3.0.3", "PyJWT==2.1.0", # PyJWT has loose dependency. We want the latest one. diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index c695313c084..7558e6fbcc4 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -179,7 +179,10 @@ async def setup_mock_motioneye_config_entry( await async_process_ha_core_config( hass, - {"external_url": "https://example.com"}, + { + "internal_url": "https://internal.url", + "external_url": "https://external.url", + }, ) config_entry = config_entry or create_mock_motioneye_config_entry(hass) diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index 6a51ea871c7..c7aaa4a8638 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -77,7 +77,7 @@ async def test_setup_camera_without_webhook(hass: HomeAssistant) -> None: expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" ) @@ -85,7 +85,7 @@ async def test_setup_camera_without_webhook(hass: HomeAssistant) -> None: expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" ) @@ -132,7 +132,7 @@ async def test_setup_camera_with_wrong_webhook( expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" ) @@ -140,7 +140,7 @@ async def test_setup_camera_with_wrong_webhook( expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" ) @@ -185,7 +185,7 @@ async def test_setup_camera_with_old_webhook( expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" ) @@ -193,7 +193,7 @@ async def test_setup_camera_with_old_webhook( expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" ) @@ -223,7 +223,7 @@ async def test_setup_camera_with_correct_webhook( KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD ] = KEY_HTTP_METHOD_POST_JSON cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" ) @@ -232,7 +232,7 @@ async def test_setup_camera_with_correct_webhook( KEY_WEB_HOOK_STORAGE_HTTP_METHOD ] = KEY_HTTP_METHOD_POST_JSON cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_URL] = ( - "https://example.com" + "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" ) diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py new file mode 100644 index 00000000000..9c1bf232d7b --- /dev/null +++ b/tests/components/network/conftest.py @@ -0,0 +1,9 @@ +"""Tests for the Network Configuration integration.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_get_source_ip(): + """Override mock of network util's async_get_source_ip.""" + return diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 12d317e826a..5a6802a14fb 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components import network from homeassistant.components.network.const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, + DOMAIN, MDNS_TARGET_IP, STORAGE_KEY, STORAGE_VERSION, @@ -59,10 +60,10 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ @@ -121,10 +122,10 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { @@ -182,10 +183,10 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { @@ -243,10 +244,10 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { @@ -309,10 +310,10 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] assert network_obj.adapters == [ @@ -378,10 +379,10 @@ async def test_interfaces_configured_from_storage_websocket_update( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - network_obj = hass.data[network.DOMAIN] + network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] ws_client = await hass_ws_client(hass) await ws_client.send_json({"id": 1, "type": "network"}) @@ -507,7 +508,7 @@ async def test_async_get_source_ip_matching_interface(hass, hass_storage): "homeassistant.components.network.util.socket.socket", return_value=_mock_socket(["192.168.1.5"]), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" @@ -528,7 +529,7 @@ async def test_async_get_source_ip_interface_not_match(hass, hass_storage): "homeassistant.components.network.util.socket.socket", return_value=_mock_socket(["192.168.1.5"]), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "169.254.3.2" @@ -549,7 +550,7 @@ async def test_async_get_source_ip_cannot_determine_target(hass, hass_storage): "homeassistant.components.network.util.socket.socket", return_value=_mock_socket([None]), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" @@ -570,7 +571,7 @@ async def test_async_get_ipv4_broadcast_addresses_default(hass, hass_storage): "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert await network.async_get_ipv4_broadcast_addresses(hass) == { @@ -593,7 +594,7 @@ async def test_async_get_ipv4_broadcast_addresses_multiple(hass, hass_storage): "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): - assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert await network.async_get_ipv4_broadcast_addresses(hass) == { diff --git a/tests/conftest.py b/tests/conftest.py index 61dcd40e53f..f6713596e56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -598,7 +598,7 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): return component -@pytest.fixture +@pytest.fixture(autouse=True) def mock_get_source_ip(): """Mock network util's async_get_source_ip.""" with patch( From 49c4886f40743c939d4468e11685056dc9c8445d Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 15 Nov 2021 09:32:35 -0800 Subject: [PATCH 0507/1452] Fix totalconnect config flow (#59461) * update total_connect_client to 2021.10 * update for total_connect_client changes * remove unused return value * bump total_connect_client to 2021.11.1 * bump total_connect_client to 2021.11.2 * Move to public ResultCode * load locations to prevent 'unknown error occurred' * add test for zero locations * put error message in strings * test for abort and message from strings * handle AuthenticationError in step_user * update tests with exceptions * update reauth with exceptions * use try except else per suggestion * only create schema if necessary * catch auth error in async_setup_entry * one more fix in test_init --- .../components/totalconnect/__init__.py | 11 ++--- .../components/totalconnect/config_flow.py | 37 ++++++++++------- .../components/totalconnect/strings.json | 3 +- .../totalconnect/test_config_flow.py | 41 +++++++++++++++---- tests/components/totalconnect/test_init.py | 5 ++- 5 files changed, 67 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 8acc7801de8..dcbc1592814 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -34,12 +34,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: temp_codes = conf[CONF_USERCODES] usercodes = {int(code): temp_codes[code] for code in temp_codes} - client = await hass.async_add_executor_job( - TotalConnectClient, username, password, usercodes - ) - if not client.is_logged_in(): - raise ConfigEntryAuthFailed("TotalConnect authentication failed") + try: + client = await hass.async_add_executor_job( + TotalConnectClient, username, password, usercodes + ) + except AuthenticationError as exception: + raise ConfigEntryAuthFailed("TotalConnect authentication failed") from exception coordinator = TotalConnectDataUpdateCoordinator(hass, client) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index f3550722de5..b529bdd80fd 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -1,5 +1,6 @@ """Config flow for the Total Connect component.""" from total_connect_client.client import TotalConnectClient +from total_connect_client.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries @@ -36,18 +37,18 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(username) self._abort_if_unique_id_configured() - client = await self.hass.async_add_executor_job( - TotalConnectClient, username, password, None - ) - - if client.is_logged_in(): + try: + client = await self.hass.async_add_executor_job( + TotalConnectClient, username, password, None + ) + except AuthenticationError: + errors["base"] = "invalid_auth" + else: # username/password valid so show user locations self.username = username self.password = password self.client = client return await self.async_step_locations() - # authentication failed / invalid - errors["base"] = "invalid_auth" data_schema = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -88,6 +89,12 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) else: + # Force the loading of locations using I/O + number_locations = await self.hass.async_add_executor_job( + self.client.get_number_locations, + ) + if number_locations < 1: + return self.async_abort(reason="no_locations") for location_id in self.client.locations: self.usercodes[location_id] = None @@ -129,14 +136,14 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=PASSWORD_DATA_SCHEMA, ) - client = await self.hass.async_add_executor_job( - TotalConnectClient, - self.username, - user_input[CONF_PASSWORD], - self.usercodes, - ) - - if not client.is_logged_in(): + try: + await self.hass.async_add_executor_job( + TotalConnectClient, + self.username, + user_input[CONF_PASSWORD], + self.usercodes, + ) + except AuthenticationError: errors["base"] = "invalid_auth" return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 5c32d19b348..63505c2446c 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -26,7 +26,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "no_locations": "No locations are available for this user, check TotalConnect settings" } } } diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index a9debb26dd4..631553a4af4 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,6 +1,8 @@ """Tests for the TotalConnect config flow.""" from unittest.mock import patch +from total_connect_client.exceptions import AuthenticationError + from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER @@ -92,10 +94,7 @@ async def test_abort_if_already_setup(hass): ).add_to_hass(hass) # Should fail, same USERNAME (flow) - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient" - ) as client_mock: - client_mock.return_value.is_logged_in.return_value = True + with patch("homeassistant.components.totalconnect.config_flow.TotalConnectClient"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -111,7 +110,7 @@ async def test_login_failed(hass): with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient" ) as client_mock: - client_mock.return_value.is_logged_in.return_value = False + client_mock.side_effect = AuthenticationError() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -143,7 +142,7 @@ async def test_reauth(hass): "homeassistant.components.totalconnect.async_setup_entry", return_value=True ): # first test with an invalid password - client_mock.return_value.is_logged_in.return_value = False + client_mock.side_effect = AuthenticationError() result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -153,7 +152,7 @@ async def test_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} # now test with the password valid - client_mock.return_value.is_logged_in.return_value = True + client_mock.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -163,3 +162,31 @@ async def test_reauth(hass): await hass.async_block_till_done() assert len(hass.config_entries.async_entries()) == 1 + + +async def test_no_locations(hass): + """Test with no user locations.""" + responses = [ + RESPONSE_AUTHENTICATE, + RESPONSE_PARTITION_DETAILS, + RESPONSE_GET_ZONE_DETAILS_SUCCESS, + RESPONSE_DISARMED, + ] + + with patch(TOTALCONNECT_REQUEST, side_effect=responses,) as mock_request, patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.totalconnect.TotalConnectClient.get_number_locations", + return_value=0, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_DATA_NO_USERCODES, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_locations" + await hass.async_block_till_done() + + assert mock_request.call_count == 1 diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index f1797f840ab..4c8c61f7d99 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -1,6 +1,8 @@ """Tests for the TotalConnect init process.""" from unittest.mock import patch +from total_connect_client.exceptions import AuthenticationError + from homeassistant.components.totalconnect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component @@ -20,9 +22,8 @@ async def test_reauth_started(hass): with patch( "homeassistant.components.totalconnect.TotalConnectClient", - autospec=True, ) as mock_client: - mock_client.return_value.is_logged_in.return_value = False + mock_client.side_effect = AuthenticationError() assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 811b33092be73853c935312c002e35205ec79299 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Mon, 15 Nov 2021 13:32:50 -0500 Subject: [PATCH 0508/1452] Bump greeclimate to 0.12.5 (#59730) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index e87a7c6a0bb..f4f8cf153a3 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.4"], + "requirements": ["greeclimate==0.12.5"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 025c34b1767..553d92dd8a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -753,7 +753,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.4 +greeclimate==0.12.5 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c7854a817f..dbad0e20ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-nest-sdm==0.3.9 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.4 +greeclimate==0.12.5 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 From 0040606c651b488aacc31f26bf19cb0e242eb49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 15 Nov 2021 20:03:39 +0100 Subject: [PATCH 0509/1452] Bump Tibber library to 0.21.0 (#59732) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index bbc90f7218c..a653e91b991 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.19.1"], + "requirements": ["pyTibber==0.21.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 553d92dd8a0..9cd4bc46afc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1324,7 +1324,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.19.1 +pyTibber==0.21.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dbad0e20ea9..63785722bf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -799,7 +799,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.19.1 +pyTibber==0.21.0 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From ce9385d4421613e7f6d9d24946163724153122ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 20:04:21 +0100 Subject: [PATCH 0510/1452] Use zeroconf attributes in elgato (#58958) Co-authored-by: epenet --- .../components/elgato/config_flow.py | 4 +-- tests/components/elgato/test_config_flow.py | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 7a4eb7a7519..327aeead4dc 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -46,8 +46,8 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self.host = discovery_info[CONF_HOST] - self.port = discovery_info[CONF_PORT] or 9123 + self.host = discovery_info[zeroconf.ATTR_HOST] + self.port = discovery_info[zeroconf.ATTR_PORT] or 9123 try: await self._get_elgato_serial_number() diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 49c85c5f2a2..5a7487b5995 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -2,6 +2,7 @@ import aiohttp from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONTENT_TYPE_JSON @@ -27,12 +28,12 @@ async def test_full_user_flow_implementation( await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data={ - "host": "127.0.0.1", - "hostname": "example.local.", - "port": 9123, - "properties": {}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="example.local.", + port=9123, + properties={}, + ), ) result = await hass.config_entries.flow.async_init( @@ -70,12 +71,12 @@ async def test_full_zeroconf_flow_implementation( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data={ - "host": "127.0.0.1", - "hostname": "example.local.", - "port": 9123, - "properties": {}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="example.local.", + port=9123, + properties={}, + ), ) assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"} @@ -127,7 +128,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "127.0.0.1", "port": 9123}, + data=zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=9123), ) assert result["reason"] == "cannot_connect" @@ -158,7 +159,7 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data={"host": "127.0.0.1", "port": 9123}, + data=zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=9123), ) assert result["reason"] == "already_configured" @@ -167,7 +168,7 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data={"host": "127.0.0.2", "port": 9123}, + data=zeroconf.ZeroconfServiceInfo(host="127.0.0.2", port=9123), ) assert result["reason"] == "already_configured" From eaaa53d8d5565cf491381b6564b0d7c972d9e787 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 15 Nov 2021 12:09:22 -0700 Subject: [PATCH 0511/1452] Add button to litterrobot (#59734) --- .../components/litterrobot/__init__.py | 7 +- .../components/litterrobot/button.py | 43 +++++++++++++ .../components/litterrobot/vacuum.py | 17 +++++ tests/components/litterrobot/test_button.py | 48 ++++++++++++++ tests/components/litterrobot/test_vacuum.py | 64 ++++++++++--------- 5 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/litterrobot/button.py create mode 100644 tests/components/litterrobot/test_button.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 17bef9a23a8..d972ecc79d9 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -2,6 +2,11 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -9,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = ["select", "sensor", "switch", "vacuum"] +PLATFORMS = [BUTTON_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, VACUUM_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py new file mode 100644 index 00000000000..3b8be295731 --- /dev/null +++ b/homeassistant/components/litterrobot/button.py @@ -0,0 +1,43 @@ +"""Support for Litter-Robot button.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import LitterRobotEntity +from .hub import LitterRobotHub + +TYPE_RESET_WASTE_DRAWER = "Reset Waste Drawer" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Litter-Robot cleaner using config entry.""" + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + LitterRobotResetWasteDrawerButton( + robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub + ) + for robot in hub.account.robots + ] + ) + + +class LitterRobotResetWasteDrawerButton(LitterRobotEntity, ButtonEntity): + """Litter-Robot reset waste drawer button.""" + + _attr_icon = "mdi:delete-variant" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + async def async_press(self) -> None: + """Press the button.""" + await self.robot.reset_waste_drawer() + self.coordinator.async_set_updated_data(True) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 81f9e631beb..5af6c5b5ef3 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,6 +1,7 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations +import logging from typing import Any from pylitterbot.enums import LitterBoxStatus @@ -29,6 +30,8 @@ from .const import DOMAIN from .entity import LitterRobotControlEntity from .hub import LitterRobotHub +_LOGGER = logging.getLogger(__name__) + SUPPORT_LITTERROBOT = ( SUPPORT_START | SUPPORT_STATE | SUPPORT_STATUS | SUPPORT_TURN_OFF | SUPPORT_TURN_ON ) @@ -121,6 +124,13 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): async def async_reset_waste_drawer(self) -> None: """Reset the waste drawer level.""" + # The Litter-Robot reset waste drawer service has been replaced by a + # dedicated button entity and marked as deprecated + _LOGGER.warning( + "The 'litterrobot.reset_waste_drawer' service is deprecated and " + "replaced by a dedicated reset waste drawer button entity; Please " + "use that entity to reset the waste drawer instead" + ) await self.robot.reset_waste_drawer() self.coordinator.async_set_updated_data(True) @@ -136,6 +146,13 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): async def async_set_wait_time(self, minutes: int) -> None: """Set the wait time.""" + # The Litter-Robot set wait time service has been replaced by a + # dedicated select entity and marked as deprecated + _LOGGER.warning( + "The 'litterrobot.set_wait_time' service is deprecated and " + "replaced by a dedicated set wait time select entity; Please " + "use that entity to set the wait time instead" + ) await self.perform_action_and_refresh(self.robot.set_wait_time, minutes) @property diff --git a/tests/components/litterrobot/test_button.py b/tests/components/litterrobot/test_button.py new file mode 100644 index 00000000000..0ca74da5d02 --- /dev/null +++ b/tests/components/litterrobot/test_button.py @@ -0,0 +1,48 @@ +"""Test the Litter-Robot button entity.""" +from unittest.mock import MagicMock + +from freezegun import freeze_time + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + ENTITY_CATEGORY_CONFIG, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_integration + +BUTTON_ENTITY = "button.test_reset_waste_drawer" + + +@freeze_time("2021-11-15 17:37:00", tz_offset=-7) +async def test_button(hass: HomeAssistant, mock_account: MagicMock) -> None: + """Test the creation and values of the Litter-Robot button.""" + await setup_integration(hass, mock_account, BUTTON_DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(BUTTON_ENTITY) + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:delete-variant" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get(BUTTON_ENTITY) + assert entry + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: BUTTON_ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_account.robots[0].reset_waste_drawer.call_count == 1 + mock_account.robots[0].reset_waste_drawer.assert_called_with() + + state = hass.states.get(BUTTON_ENTITY) + assert state + assert state.state == "2021-11-15T10:37:00+00:00" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index aa0d38583e2..89f8f077b55 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,5 +1,9 @@ """Test the Litter-Robot vacuum entity.""" +from __future__ import annotations + from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock import pytest from voluptuous.error import MultipleInvalid @@ -36,7 +40,7 @@ COMPONENT_SERVICE_DOMAIN = { } -async def test_vacuum(hass: HomeAssistant, mock_account): +async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) @@ -48,8 +52,8 @@ async def test_vacuum(hass: HomeAssistant, mock_account): async def test_vacuum_status_when_sleeping( - hass: HomeAssistant, mock_account_with_sleeping_robot -): + hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock +) -> None: """Tests the vacuum status when sleeping.""" await setup_integration(hass, mock_account_with_sleeping_robot, PLATFORM_DOMAIN) @@ -58,14 +62,18 @@ async def test_vacuum_status_when_sleeping( assert vacuum.attributes.get(ATTR_STATUS) == "Ready (Sleeping)" -async def test_no_robots(hass: HomeAssistant, mock_account_with_no_robots): +async def test_no_robots( + hass: HomeAssistant, mock_account_with_no_robots: MagicMock +) -> None: """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) assert not hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) -async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): +async def test_vacuum_with_error( + hass: HomeAssistant, mock_account_with_error: MagicMock +) -> None: """Tests a vacuum entity with an error.""" await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN) @@ -80,39 +88,34 @@ async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): (SERVICE_START, "start_cleaning", None), (SERVICE_TURN_OFF, "set_power_status", None), (SERVICE_TURN_ON, "set_power_status", None), - ( - SERVICE_RESET_WASTE_DRAWER, - "reset_waste_drawer", - None, - ), + (SERVICE_RESET_WASTE_DRAWER, "reset_waste_drawer", {"deprecated": True}), ( SERVICE_SET_SLEEP_MODE, "set_sleep_mode", - {"enabled": True, "start_time": "22:30"}, + {"data": {"enabled": True, "start_time": "22:30"}}, ), + (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": True}}), + (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": False}}), ( - SERVICE_SET_SLEEP_MODE, - "set_sleep_mode", - {"enabled": True}, - ), - ( - SERVICE_SET_SLEEP_MODE, - "set_sleep_mode", - {"enabled": False}, + SERVICE_SET_WAIT_TIME, + "set_wait_time", + {"data": {"minutes": 3}, "deprecated": True}, ), ( SERVICE_SET_WAIT_TIME, "set_wait_time", - {"minutes": 3}, - ), - ( - SERVICE_SET_WAIT_TIME, - "set_wait_time", - {"minutes": "15"}, + {"data": {"minutes": "15"}, "deprecated": True}, ), ], ) -async def test_commands(hass: HomeAssistant, mock_account, service, command, extra): +async def test_commands( + hass: HomeAssistant, + mock_account: MagicMock, + caplog: pytest.LogCaptureFixture, + service: str, + command: str, + extra: dict[str, Any], +) -> None: """Test sending commands to the vacuum.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) @@ -120,9 +123,9 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext assert vacuum assert vacuum.state == STATE_DOCKED - data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID} - if extra: - data.update(extra) + extra = extra or {} + data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})} + deprecated = extra.get("deprecated", False) await hass.services.async_call( COMPONENT_SERVICE_DOMAIN.get(service, PLATFORM_DOMAIN), @@ -133,9 +136,10 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) async_fire_time_changed(hass, future) getattr(mock_account.robots[0], command).assert_called_once() + assert (f"'{DOMAIN}.{service}' service is deprecated" in caplog.text) is deprecated -async def test_invalid_wait_time(hass: HomeAssistant, mock_account): +async def test_invalid_wait_time(hass: HomeAssistant, mock_account: MagicMock) -> None: """Test an attempt to send an invalid wait time to the vacuum.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) From e88ea2d48ca0556d20e7b61ee98f36668910ea40 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Nov 2021 20:13:43 +0100 Subject: [PATCH 0512/1452] Show cast as playing when an app without media support is active (#59714) --- homeassistant/components/cast/media_player.py | 13 ++-- tests/components/cast/test_media_player.py | 62 +++++++++++++------ 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 310431bb488..cfaf8843865 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -79,11 +79,7 @@ _LOGGER = logging.getLogger(__name__) CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" -SUPPORT_CAST = ( - SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_TURN_OFF -) - -STATE_CASTING = "casting" +SUPPORT_CAST = SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ENTITY_SCHEMA = vol.All( vol.Schema( @@ -567,7 +563,7 @@ class CastDevice(MediaPlayerEntity): """Return the state of the player.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: - return STATE_CASTING + return STATE_PLAYING if (media_status := self._media_status()[0]) is not None: if media_status.player_is_playing: return STATE_PLAYING @@ -576,7 +572,7 @@ class CastDevice(MediaPlayerEntity): if media_status.player_is_idle: return STATE_IDLE if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: - return STATE_CASTING + return STATE_PLAYING if self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF return None @@ -701,7 +697,8 @@ class CastDevice(MediaPlayerEntity): ): support |= SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET - if media_status: + if media_status and self.app_id != CAST_APP_ID_HOMEASSISTANT_LOVELACE: + support |= SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP if media_status.supports_queue_next: support |= SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK if media_status.supports_seek: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 3bb9cbf2316..a038cb34aea 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -605,11 +605,9 @@ async def test_entity_cast_status(hass: HomeAssistant): assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + # No media status, pause, play, stop not supported assert state.attributes.get("supported_features") == ( - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP + SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE @@ -652,17 +650,12 @@ async def test_entity_cast_status(hass: HomeAssistant): await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.attributes.get("supported_features") == ( - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON + SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_TURN_ON ) @pytest.mark.parametrize( - "cast_type,supported_features", + "cast_type,supported_features,supported_features_no_media", [ ( pychromecast.const.CAST_TYPE_AUDIO, @@ -673,6 +666,10 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, + SUPPORT_PLAY_MEDIA + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, ), ( pychromecast.const.CAST_TYPE_CHROMECAST, @@ -684,6 +681,11 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, + SUPPORT_PLAY_MEDIA + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, ), ( pychromecast.const.CAST_TYPE_GROUP, @@ -694,10 +696,16 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, + SUPPORT_PLAY_MEDIA + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, ), ], ) -async def test_supported_features(hass: HomeAssistant, cast_type, supported_features): +async def test_supported_features( + hass: HomeAssistant, cast_type, supported_features, supported_features_no_media +): """Test supported features.""" entity_id = "media_player.speaker" @@ -705,7 +713,7 @@ async def test_supported_features(hass: HomeAssistant, cast_type, supported_feat chromecast, _ = await async_setup_media_player_cast(hass, info) chromecast.cast_type = cast_type - _, conn_status_cb, _ = get_status_callbacks(chromecast) + _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() connection_status.status = "CONNECTED" @@ -716,7 +724,14 @@ async def test_supported_features(hass: HomeAssistant, cast_type, supported_feat assert state is not None assert state.name == "Speaker" assert state.state == "off" + assert state.attributes.get("supported_features") == supported_features_no_media + media_status = MagicMock(images=None) + media_status.supports_queue_next = False + media_status.supports_seek = False + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.attributes.get("supported_features") == supported_features @@ -935,15 +950,23 @@ async def test_entity_control(hass: HomeAssistant): chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) + # Fake connection status connection_status = MagicMock() connection_status.status = "CONNECTED" conn_status_cb(connection_status) await hass.async_block_till_done() + # Fake media status + media_status = MagicMock(images=None) + media_status.supports_queue_next = False + media_status.supports_seek = False + media_status_cb(media_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "off" + assert state.state == "playing" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) assert state.attributes.get("supported_features") == ( @@ -1119,28 +1142,31 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): cast_status_cb(cast_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "casting" + assert state.state == "playing" + assert state.attributes.get("supported_features") == ( + SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) media_status = MagicMock(images=None) media_status.player_is_playing = True media_status_cb(media_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "casting" + assert state.state == "playing" media_status.player_is_playing = False media_status.player_is_paused = True media_status_cb(media_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "casting" + assert state.state == "playing" media_status.player_is_paused = False media_status.player_is_idle = True media_status_cb(media_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "casting" + assert state.state == "playing" chromecast.app_id = pychromecast.IDLE_APP_ID media_status.player_is_idle = False From 3c42ea1a2612aab26cd425d03d3d727640ee4d86 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 20:16:30 +0100 Subject: [PATCH 0513/1452] Use zeroconf attributes in forked-daapd (#58966) Co-authored-by: epenet --- .../components/forked_daapd/config_flow.py | 30 +++++++---- .../forked_daapd/test_config_flow.py | 53 ++++++++++--------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 16ebc1f82f7..86bc39c05ed 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -6,8 +6,10 @@ from pyforked_daapd import ForkedDaapdAPI import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -153,35 +155,41 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema(DATA_SCHEMA_DICT), errors={} ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Prepare configuration for a discovered forked-daapd device.""" version_num = 0 - if discovery_info.get("properties") and discovery_info["properties"].get( - "Machine Name" - ): + if discovery_info.get(zeroconf.ATTR_PROPERTIES) and discovery_info[ + zeroconf.ATTR_PROPERTIES + ].get("Machine Name"): with suppress(ValueError): version_num = int( - discovery_info["properties"].get("mtd-version", "0").split(".")[0] + discovery_info[zeroconf.ATTR_PROPERTIES] + .get("mtd-version", "0") + .split(".")[0] ) if version_num < 27: return self.async_abort(reason="not_forked_daapd") - await self.async_set_unique_id(discovery_info["properties"]["Machine Name"]) + await self.async_set_unique_id( + discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"] + ) self._abort_if_unique_id_configured() # Update title and abort if we already have an entry for this host for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) != discovery_info["host"]: + if entry.data.get(CONF_HOST) != discovery_info[zeroconf.ATTR_HOST]: continue self.hass.config_entries.async_update_entry( entry, - title=discovery_info["properties"]["Machine Name"], + title=discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"], ) return self.async_abort(reason="already_configured") zeroconf_data = { - CONF_HOST: discovery_info["host"], - CONF_PORT: int(discovery_info["port"]), - CONF_NAME: discovery_info["properties"]["Machine Name"], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_NAME: discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"], } self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data)) self.context.update({"title_placeholders": zeroconf_data}) diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 668f1be0a4f..c8b82c7a2e9 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.forked_daapd.const import ( CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, @@ -98,11 +99,11 @@ async def test_zeroconf_updates_title(hass, config_entry): MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "different host"}).add_to_hass(hass) config_entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - discovery_info = { - "host": "192.168.1.1", - "port": 23, - "properties": {"mtd-version": "27.0", "Machine Name": "zeroconf_test"}, - } + discovery_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.1", + port=23, + properties={"mtd-version": "27.0", "Machine Name": "zeroconf_test"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -129,40 +130,40 @@ async def test_config_flow_no_websocket(hass, config_entry): async def test_config_flow_zeroconf_invalid(hass): """Test that an invalid zeroconf entry doesn't work.""" # test with no discovery properties - discovery_info = {"host": "127.0.0.1", "port": 23} + discovery_info = zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=23) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 - discovery_info = { - "host": "127.0.0.1", - "port": 23, - "properties": {"mtd-version": "26.3", "Machine Name": "forked-daapd"}, - } + discovery_info = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + port=23, + properties={"mtd-version": "26.3", "Machine Name": "forked-daapd"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly - discovery_info = { - "host": "127.0.0.1", - "port": 23, - "properties": {"mtd-version": "0.2.4.1", "Machine Name": "firefly"}, - } + discovery_info = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + port=23, + properties={"mtd-version": "0.2.4.1", "Machine Name": "firefly"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly - discovery_info = { - "host": "127.0.0.1", - "port": 23, - "properties": {"mtd-version": "svn-1676", "Machine Name": "firefly"}, - } + discovery_info = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + port=23, + properties={"mtd-version": "svn-1676", "Machine Name": "firefly"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort @@ -172,15 +173,15 @@ async def test_config_flow_zeroconf_invalid(hass): async def test_config_flow_zeroconf_valid(hass): """Test that a valid zeroconf entry works.""" - discovery_info = { - "host": "192.168.1.1", - "port": 23, - "properties": { + discovery_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.1", + port=23, + properties={ "mtd-version": "27.0", "Machine Name": "zeroconf_test", "Machine ID": "5E55EEFF", }, - } + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) From 4f7e405a2c692f1f4a0397b0ec53d58b6c26cdab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 20:26:50 +0100 Subject: [PATCH 0514/1452] Use zeroconf attributes in esphome (#58963) Co-authored-by: epenet --- .../components/esphome/config_flow.py | 17 +++-- tests/components/esphome/test_config_flow.py | 73 ++++++++++--------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index fa72cbd995e..fab4b045d0c 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -142,14 +142,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = discovery_info["hostname"][:-1] + local_name = discovery_info[zeroconf.ATTR_HOSTNAME][:-1] node_name = local_name[: -len(".local")] - address = discovery_info["properties"].get("address", local_name) + address = discovery_info[zeroconf.ATTR_PROPERTIES].get("address", local_name) # Check if already configured await self.async_set_unique_id(node_name) self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[CONF_HOST]} + updates={CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} ) for entry in self._async_current_entries(): @@ -157,7 +157,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( address, - discovery_info[CONF_HOST], + discovery_info[zeroconf.ATTR_HOST], ): # Is this address or IP address already configured? already_configured = True @@ -174,14 +174,17 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): if not entry.unique_id: self.hass.config_entries.async_update_entry( entry, - data={**entry.data, CONF_HOST: discovery_info[CONF_HOST]}, + data={ + **entry.data, + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + }, unique_id=node_name, ) return self.async_abort(reason="already_configured") - self._host = discovery_info[CONF_HOST] - self._port = discovery_info[CONF_PORT] + self._host = discovery_info[zeroconf.ATTR_HOST] + self._port = discovery_info[zeroconf.ATTR_PORT] self._name = node_name return await self.async_step_discovery_confirm() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 6a96e88cab0..1ae332142a0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -12,6 +12,7 @@ from aioesphomeapi import ( import pytest from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -215,12 +216,12 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): """Test discovery importing works.""" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) - service_info = { - "host": "192.168.43.183", - "port": 6053, - "hostname": "test8266.local.", - "properties": {}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + port=6053, + hostname="test8266.local.", + properties={}, + ) flow = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) @@ -247,12 +248,12 @@ async def test_discovery_already_configured_hostname(hass, mock_client): entry.add_to_hass(hass) - service_info = { - "host": "192.168.43.183", - "port": 6053, - "hostname": "test8266.local.", - "properties": {}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + port=6053, + hostname="test8266.local.", + properties={}, + ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) @@ -272,12 +273,12 @@ async def test_discovery_already_configured_ip(hass, mock_client): entry.add_to_hass(hass) - service_info = { - "host": "192.168.43.183", - "port": 6053, - "hostname": "test8266.local.", - "properties": {"address": "192.168.43.183"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + port=6053, + hostname="test8266.local.", + properties={"address": "192.168.43.183"}, + ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) @@ -301,12 +302,12 @@ async def test_discovery_already_configured_name(hass, mock_client): domain_data = DomainData.get(hass) domain_data.set_entry_data(entry, mock_entry_data) - service_info = { - "host": "192.168.43.184", - "port": 6053, - "hostname": "test8266.local.", - "properties": {"address": "test8266.local"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.184", + port=6053, + hostname="test8266.local.", + properties={"address": "test8266.local"}, + ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) @@ -320,12 +321,12 @@ async def test_discovery_already_configured_name(hass, mock_client): async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" - service_info = { - "host": "192.168.43.183", - "port": 6053, - "hostname": "test8266.local.", - "properties": {"address": "test8266.local"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + port=6053, + hostname="test8266.local.", + properties={"address": "test8266.local"}, + ) mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) @@ -351,12 +352,12 @@ async def test_discovery_updates_unique_id(hass, mock_client): entry.add_to_hass(hass) - service_info = { - "host": "192.168.43.183", - "port": 6053, - "hostname": "test8266.local.", - "properties": {"address": "test8266.local"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + port=6053, + hostname="test8266.local.", + properties={"address": "test8266.local"}, + ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) From ca3e672b1d392e8eec503dd41bf0f5341a6acde2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 15 Nov 2021 20:42:59 +0100 Subject: [PATCH 0515/1452] Fix KNX rgb(w) color (#51060) * calculate brightness from color; scale color * fix merge * fix sending color only for brightness independent rgb color * fix tests for rgb and rgbw color * use public match_max_scale --- homeassistant/components/knx/light.py | 69 +++++---- tests/components/knx/test_light.py | 192 ++++++++++++++++++-------- 2 files changed, 179 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index bee96270c36..4841b056ebc 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -268,19 +268,28 @@ class KNXLight(KnxEntity, LightEntity): if self._device.current_xyy_color is not None: _, brightness = self._device.current_xyy_color return brightness - if (rgb := self.rgb_color) is not None: - return max(rgb) + if self._device.supports_color or self._device.supports_rgbw: + rgb, white = self._device.current_color + if rgb is None: + return white + if white is None: + return max(rgb) + return max(*rgb, white) return None @property def rgb_color(self) -> tuple[int, int, int] | None: """Return the rgb color value [int, int, int].""" - if (rgbw := self.rgbw_color) is not None: - # used in brightness calculation when no address is given - return color_util.color_rgbw_to_rgb(*rgbw) if self._device.supports_color: rgb, _ = self._device.current_color - return rgb + if rgb is not None: + if not self._device.supports_brightness: + # brightness will be calculated from color so color must not hold brightness again + # pylint: disable=protected-access + return cast( + Tuple[int, int, int], color_util.match_max_scale((255,), rgb) + ) + return rgb return None @property @@ -289,6 +298,13 @@ class KNXLight(KnxEntity, LightEntity): if self._device.supports_rgbw: rgb, white = self._device.current_color if rgb is not None and white is not None: + if not self._device.supports_brightness: + # brightness will be calculated from color so color must not hold brightness again + # pylint: disable=protected-access + return cast( + Tuple[int, int, int, int], + color_util.match_max_scale((255,), (*rgb, white)), + ) return (*rgb, white) return None @@ -376,16 +392,21 @@ class KNXLight(KnxEntity, LightEntity): rgb: tuple[int, int, int], white: int | None, brightness: int | None ) -> None: """Set color of light. Normalize colors for brightness when not writable.""" - if brightness: - if self._device.brightness.writable: - await self._device.set_color(rgb, white) + if self._device.brightness.writable: + # let the KNX light controller handle brightness + await self._device.set_color(rgb, white) + if brightness: await self._device.set_brightness(brightness) - return - rgb = cast( - Tuple[int, int, int], - tuple(color * brightness // 255 for color in rgb), - ) - white = white * brightness // 255 if white is not None else None + return + + if brightness is None: + # normalize for brightness if brightness is derived from color + brightness = self.brightness or 255 + rgb = cast( + Tuple[int, int, int], + tuple(color * brightness // 255 for color in rgb), + ) + white = white * brightness // 255 if white is not None else None await self._device.set_color(rgb, white) # return after RGB(W) color has changed as it implicitly sets the brightness @@ -433,18 +454,16 @@ class KNXLight(KnxEntity, LightEntity): return # default to white if color not known for RGB(W) if self.color_mode == COLOR_MODE_RGBW: - rgbw = self.rgbw_color - if not rgbw or not any(rgbw): - await self._device.set_color((0, 0, 0), brightness) - return - await set_color(rgbw[:3], rgbw[3], brightness) + _rgbw = self.rgbw_color + if not _rgbw or not any(_rgbw): + _rgbw = (0, 0, 0, 255) + await set_color(_rgbw[:3], _rgbw[3], brightness) return if self.color_mode == COLOR_MODE_RGB: - rgb = self.rgb_color - if not rgb or not any(rgb): - await self._device.set_color((brightness, brightness, brightness)) - return - await set_color(rgb, None, brightness) + _rgb = self.rgb_color + if not _rgb or not any(_rgb): + _rgb = (255, 255, 255) + await set_color(_rgb, None, brightness) return async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index bb7a67d6bb4..aa9a373e5ef 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.light import ( ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_RGBW_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, @@ -539,20 +540,31 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_write(test_red, (200,)) await knx.assert_write(test_green, (0,)) await knx.assert_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 0, 0)) - # change color and brightness from HA + # change only color, keep brightness from HA await hass.services.async_call( "light", "turn_on", {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, blocking=True, ) - # - await knx.assert_write(test_red, (255,)) - await knx.assert_write(test_green, (105,)) - await knx.assert_write(test_blue, (180,)) - knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 105, 180)) + await knx.assert_write(test_red, (200,)) + await knx.assert_write(test_green, (82,)) + await knx.assert_write(test_blue, (141,)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 105, 180)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, + blocking=True, + ) + await knx.assert_write(test_red, (100,)) + await knx.assert_write(test_green, (100,)) + await knx.assert_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=100, rgb_color=(255, 255, 0)) # turn OFF from KNX await knx.receive_write(test_red, (0,)) @@ -563,7 +575,7 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (180,)) await knx.receive_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) # turn OFF from HA await hass.services.async_call( @@ -684,40 +696,52 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_write(test_green, (0,)) await knx.assert_write(test_blue, (0,)) await knx.assert_write(test_white, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) - # change color and brightness from HA + # change only color, keep brightness from HA await hass.services.async_call( "light", "turn_on", {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, blocking=True, ) - await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_red, (200,)) await knx.assert_write(test_green, (0,)) - await knx.assert_write(test_blue, (128,)) - await knx.assert_write(test_white, (178,)) + await knx.assert_write(test_blue, (100,)) + await knx.assert_write(test_white, (139,)) knx.assert_state( "light.test", STATE_ON, - brightness=255, - rgb_color=(255, 105, 180), - rgbw_color=(255, 0, 128, 178), + brightness=200, + rgb_color=(255, 104, 179), # minor rounding error - expected (255, 105, 180) + rgbw_color=(255, 0, 127, 177), # expected (255, 0, 128, 178) + ) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, + blocking=True, + ) + await knx.assert_write(test_red, (100,)) + await knx.assert_write(test_green, (100,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (0,)) + knx.assert_state( + "light.test", STATE_ON, brightness=100, rgbw_color=(255, 255, 0, 0) ) # turn OFF from KNX await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (0,)) - await knx.receive_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON) - await knx.receive_write(test_white, (0,)) knx.assert_state("light.test", STATE_OFF) # turn ON from KNX await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (180,)) await knx.receive_write(test_blue, (0,)) await knx.receive_write(test_white, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) # turn OFF from HA await hass.services.async_call( @@ -815,18 +839,31 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgb, (200, 0, 0)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 0, 0)) + # change color, keep brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgb, (200, 82, 141)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=200, + rgb_color=(255, 105, 180), + ) # change color and brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, blocking=True, ) - # - await knx.assert_write(test_rgb, (128, 52, 90)) - knx.assert_state("light.test", STATE_ON, brightness=128, rgb_color=(128, 52, 90)) + await knx.assert_write(test_rgb, (100, 100, 0)) + knx.assert_state("light.test", STATE_ON, brightness=100, rgb_color=(255, 255, 0)) # turn OFF from KNX await knx.receive_write(test_address_state, False) @@ -836,7 +873,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): knx.assert_state("light.test", STATE_OFF) # turn ON from KNX - include color update await knx.receive_write(test_address_state, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) # turn OFF from HA await hass.services.async_call( @@ -857,7 +894,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): ) # color will be restored in no other state was received await knx.assert_write(test_address, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): @@ -883,14 +920,14 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_read(test_address_state) await knx.assert_read(test_rgbw_state) await knx.receive_response(test_address_state, True) - await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + await knx.receive_response(test_rgbw_state, (0xFF, 0x65, 0x66, 0x67, 0x00, 0x0F)) knx.assert_state( "light.test", STATE_ON, - brightness=103, + brightness=255, color_mode=COLOR_MODE_RGBW, - rgbw_color=(100, 101, 102, 103), + rgbw_color=(255, 101, 102, 103), ) # change color from HA await hass.services.async_call( @@ -910,22 +947,33 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgbw, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) + # change color, keep brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (200, 0, 100, 139, 0x00, 0x0F)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=200, + rgb_color=(255, 104, 179), # minor rounding error - expected (255, 105, 180) + rgbw_color=(255, 0, 127, 177), # expected (255, 0, 128, 178) + ) # change color and brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, blocking=True, ) - await knx.assert_write(test_rgbw, (128, 0, 64, 89, 0x00, 0x0F)) + await knx.assert_write(test_rgbw, (100, 100, 0, 0, 0x00, 0x0F)) knx.assert_state( - "light.test", - STATE_ON, - brightness=128, - rgb_color=(128, 52, 90), - rgbw_color=(128, 0, 64, 89), + "light.test", STATE_ON, brightness=100, rgbw_color=(255, 255, 0, 0) ) # turn OFF from KNX @@ -936,7 +984,7 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): knx.assert_state("light.test", STATE_OFF) # turn ON from KNX - include color update await knx.receive_write(test_address_state, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) # turn OFF from HA await hass.services.async_call( @@ -955,9 +1003,9 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): {"entity_id": "light.test"}, blocking=True, ) - # color will be restored in no other state was received + # color will be restored if no other state was received await knx.assert_write(test_address, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): @@ -987,16 +1035,16 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_read(test_address_state) await knx.assert_read(test_brightness_state) await knx.receive_response(test_address_state, True) - await knx.receive_response(test_brightness_state, (0x67,)) + await knx.receive_response(test_brightness_state, (0xFF,)) await knx.assert_read(test_rgbw_state) - await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + await knx.receive_response(test_rgbw_state, (0xFF, 0x65, 0x66, 0x67, 0x00, 0x0F)) knx.assert_state( "light.test", STATE_ON, - brightness=103, + brightness=255, color_mode=COLOR_MODE_RGBW, - rgbw_color=(100, 101, 102, 103), + rgbw_color=(255, 101, 102, 103), ) # change color from HA await hass.services.async_call( @@ -1006,25 +1054,22 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgbw, (0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=103, rgbw_color=(255, 0, 0, 0)) - # # relies on dedicated brightness state - await knx.receive_write(test_brightness_state, (0xFF,)) knx.assert_state("light.test", STATE_ON, brightness=255, rgbw_color=(255, 0, 0, 0)) + # # update from dedicated brightness state + await knx.receive_write(test_brightness_state, (0xF0,)) + knx.assert_state("light.test", STATE_ON, brightness=240, rgbw_color=(255, 0, 0, 0)) - # change brightness from HA + # single encoded brightness - at least one primary color = 255 + # # change brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 128}, blocking=True, ) - await knx.assert_write(test_brightness, (200,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) - # # relies on dedicated rgbw state - await knx.receive_write(test_rgbw_state, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) - - # change color and brightness from HA + await knx.assert_write(test_brightness, (128,)) + knx.assert_state("light.test", STATE_ON, brightness=128, rgbw_color=(255, 0, 0, 0)) + # # change color and brightness from HA await hass.services.async_call( "light", "turn_on", @@ -1040,3 +1085,36 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): rgb_color=(255, 105, 180), rgbw_color=(255, 0, 128, 178), ) + + # doubly encoded brightness + # brightness is handled by dedicated brightness address only + # # from dedicated rgbw state + await knx.receive_write(test_rgbw_state, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=128, rgbw_color=(200, 0, 0, 0)) + # # from HA - only color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_RGBW_COLOR: (20, 30, 40, 50)}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (20, 30, 40, 50, 0x00, 0x0F)) + knx.assert_state( + "light.test", STATE_ON, brightness=128, rgbw_color=(20, 30, 40, 50) + ) + # # from HA - brightness and color + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.test", + ATTR_BRIGHTNESS: 50, + ATTR_RGBW_COLOR: (100, 200, 55, 12), + }, + blocking=True, + ) + await knx.assert_write(test_rgbw, (100, 200, 55, 12, 0x00, 0x0F)) + await knx.assert_write(test_brightness, (50,)) + knx.assert_state( + "light.test", STATE_ON, brightness=50, rgbw_color=(100, 200, 55, 12) + ) From 032718abb1ef5bcd9df512bd3398bdfa75464970 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 21:43:52 +0100 Subject: [PATCH 0516/1452] Use ZeroconfServiceInfo in doorbird (#59737) Co-authored-by: epenet --- homeassistant/components/doorbird/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index a2c8496277d..c1345b16f91 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.network import is_link_local from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI @@ -94,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=data, errors=errors) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info[zeroconf.ATTR_PROPERTIES]["macaddress"] From 0b43cff37724eaa329bc140b566f1ddfdce371c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 23:13:17 +0100 Subject: [PATCH 0517/1452] Use ZeroconfServiceInfo in guardian (#59741) Co-authored-by: epenet --- .../components/guardian/config_flow.py | 8 +++-- tests/components/guardian/test_config_flow.py | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 452574d4eed..2aa00f56d99 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -112,10 +112,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the configuration via zeroconf.""" self.discovery_info = { - CONF_IP_ADDRESS: discovery_info["host"], - CONF_PORT: discovery_info["port"], + CONF_IP_ADDRESS: discovery_info[zeroconf.ATTR_HOST], + CONF_PORT: discovery_info[zeroconf.ATTR_PORT], } - pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"]) + pin = async_get_pin_from_discovery_hostname( + discovery_info[zeroconf.ATTR_HOSTNAME] + ) await self._async_set_unique_id(pin) return await self._async_handle_discovery() diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 4fbff7d7e48..b8d23508c77 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from aioguardian.errors import GuardianError from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( @@ -83,14 +84,14 @@ async def test_step_user(hass, ping_client): async def test_step_zeroconf(hass, ping_client): """Test the zeroconf step.""" - zeroconf_data = { - "host": "192.168.1.100", - "port": 7777, - "hostname": "GVC1-ABCD.local.", - "type": "_api._udp.local.", - "name": "Guardian Valve Controller API._api._udp.local.", - "properties": {"_raw": {}}, - } + zeroconf_data = zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + port=7777, + hostname="GVC1-ABCD.local.", + type="_api._udp.local.", + name="Guardian Valve Controller API._api._udp.local.", + properties={"_raw": {}}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data @@ -112,14 +113,14 @@ async def test_step_zeroconf(hass, ping_client): async def test_step_zeroconf_already_in_progress(hass): """Test the zeroconf step aborting because it's already in progress.""" - zeroconf_data = { - "host": "192.168.1.100", - "port": 7777, - "hostname": "GVC1-ABCD.local.", - "type": "_api._udp.local.", - "name": "Guardian Valve Controller API._api._udp.local.", - "properties": {"_raw": {}}, - } + zeroconf_data = zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + port=7777, + hostname="GVC1-ABCD.local.", + type="_api._udp.local.", + name="Guardian Valve Controller API._api._udp.local.", + properties={"_raw": {}}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data From 7122998307cb763529c539ca6c2cc032ddfa27ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Nov 2021 16:13:37 -0600 Subject: [PATCH 0518/1452] Bump flux_led to 0.24.24 (#59740) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 203acb77385..cd37897af67 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.21"], + "requirements": ["flux_led==0.24.24"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9cd4bc46afc..fd852a308bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.21 +flux_led==0.24.24 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63785722bf6..57056e3f709 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.21 +flux_led==0.24.24 # homeassistant.components.homekit fnvhash==0.1.0 From 1bd2d3c69e66a9934cf3cb6a0506a82e1652d890 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Nov 2021 23:13:48 +0100 Subject: [PATCH 0519/1452] Use ZeroconfServiceInfo in freebox (#59739) Co-authored-by: epenet --- homeassistant/components/freebox/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 58c106ab467..77041d803ee 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -8,7 +8,6 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN from .router import get_api @@ -109,7 +108,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Initialize flow from zeroconf.""" host = discovery_info[zeroconf.ATTR_PROPERTIES]["api_domain"] From ce3f918c2c6f5321f2401158afcb6ed5d4637f89 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Nov 2021 15:30:26 -0700 Subject: [PATCH 0520/1452] Fix bug in AirVisual re-auth (#59685) --- homeassistant/components/airvisual/config_flow.py | 5 ++++- tests/components/airvisual/test_config_flow.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index ebd5373f1b9..85ee4bf6ae5 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -135,6 +135,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._geo_id) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry( @@ -231,7 +234,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA ) - conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth} + conf = {**self._entry_data_for_reauth, CONF_API_KEY: user_input[CONF_API_KEY]} return await self._async_finish_geography( conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 6125b71e303..65534f1f16c 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -355,16 +355,19 @@ async def test_step_reauth(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" + new_api_key = "defgh67890" + with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True ), patch("pyairvisual.air_quality.AirQuality.nearest_city", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_API_KEY: "defgh67890"} + result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key async def test_step_user(hass): From 646c996b42d02bd72fb922f65f03867e604d1403 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 15 Nov 2021 23:30:48 +0100 Subject: [PATCH 0521/1452] Fix invalid string syntax in French OwnTracks config flow (#59752) --- homeassistant/components/owntracks/translations/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 0e753a455e0..9120cdb8637 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "\n\n Sous Android, ouvrez [l'application OwnTracks] ( {android_url} ), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: ` ` \n - ID de p\u00e9riph\u00e9rique: ` ` \n\n Sur iOS, ouvrez [l'application OwnTracks] ( {ios_url} ), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: ` ` \n\n {secret} \n \n Voir [la documentation] ( {docs_url} ) pour plus d'informations." + "default": "\n\n Sous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." }, "step": { "user": { From c0a8cea6fbcb533079ed429647a650ea30a7a769 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 16 Nov 2021 00:02:39 +0100 Subject: [PATCH 0522/1452] Fix KNX individual light tests (#59749) --- tests/components/knx/test_light.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index aa9a373e5ef..2988bf189d3 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -1,6 +1,10 @@ """Test KNX light.""" from __future__ import annotations +from datetime import timedelta + +from xknx.devices.light import Light as XknxLight + from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS from homeassistant.components.knx.schema import LightSchema from homeassistant.components.light import ( @@ -19,9 +23,12 @@ from homeassistant.components.light import ( ) 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_fire_time_changed + async def test_light_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX light.""" @@ -735,6 +742,12 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit): # turn OFF from KNX await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (0,)) + # # individual color debounce takes 0.2 seconds if not all 4 addresses received + knx.assert_state("light.test", STATE_ON) + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT) + ) + await knx.xknx.task_registry.block_till_done() knx.assert_state("light.test", STATE_OFF) # turn ON from KNX await knx.receive_write(test_red, (0,)) From 4d96ca3ddb3df29d131394c0a96a54b4f9c7d0bb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Nov 2021 00:27:04 +0100 Subject: [PATCH 0523/1452] Adjust async_step_homekit signature for strict typing (#59745) * Use ZeroconfServiceInfo in async_step_homekit * Update DiscoveryFlowHandler * Update components --- homeassistant/components/nanoleaf/config_flow.py | 10 +++++----- homeassistant/components/rainmachine/config_flow.py | 11 ++++++----- homeassistant/components/tradfri/config_flow.py | 8 +++++--- homeassistant/config_entries.py | 4 ++-- homeassistant/helpers/config_entry_flow.py | 12 +++++++++++- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 17269f2e07a..1d0b31549df 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -93,17 +93,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle Nanoleaf Zeroconf discovery.""" _LOGGER.debug("Zeroconf discovered: %s", discovery_info) - return await self._async_homekit_zeroconf_discovery_handler( - cast(dict, discovery_info) - ) + return await self._async_homekit_zeroconf_discovery_handler(discovery_info) - async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle Nanoleaf Homekit discovery.""" _LOGGER.debug("Homekit discovered: %s", discovery_info) return await self._async_homekit_zeroconf_discovery_handler(discovery_info) async def _async_homekit_zeroconf_discovery_handler( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle Nanoleaf Homekit and Zeroconf discovery.""" return await self._async_discovery_handler( diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 47896cc6080..4dcbbe0423f 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the RainMachine component.""" from __future__ import annotations -from typing import Any, cast +from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller @@ -15,7 +15,6 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN @@ -55,7 +54,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return RainMachineOptionsFlowHandler(config_entry) - async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a flow initialized by homekit discovery.""" return await self.async_step_homekit_zeroconf(discovery_info) @@ -63,10 +64,10 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery via zeroconf.""" - return await self.async_step_homekit_zeroconf(cast(dict, discovery_info)) + return await self.async_step_homekit_zeroconf(discovery_info) async def async_step_homekit_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery via zeroconf.""" ip_address = discovery_info["host"] diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 4de2aa302f0..f0f4016ba9b 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -11,9 +11,9 @@ from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_GATEWAY_ID, @@ -42,7 +42,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._host = None + self._host: str | None = None self._import_groups = False async def async_step_user( @@ -92,7 +92,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="auth", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle homekit discovery.""" await self.async_set_unique_id(discovery_info["properties"]["id"]) self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 521fb5d444c..d9c0faf2f71 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1354,10 +1354,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(discovery_info) async def async_step_homekit( - self, discovery_info: DiscoveryInfoType + self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_mqtt( self, discovery_info: DiscoveryInfoType diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1e87d0042d7..4a312b5e01f 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -82,6 +82,17 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle a flow initialized by Homekit discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: @@ -95,7 +106,6 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_ssdp = async_step_discovery async_step_mqtt = async_step_discovery - async_step_homekit = async_step_discovery async_step_dhcp = async_step_discovery async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: From 0228d11546d6f21ed9a26de78fcf1b29722ae811 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 16 Nov 2021 00:18:33 +0000 Subject: [PATCH 0524/1452] [ci skip] Translation update --- .../components/airthings/translations/id.json | 12 ++++++++ .../aurora_abb_powerone/translations/id.json | 10 +++++++ .../binary_sensor/translations/hu.json | 2 +- .../binary_sensor/translations/id.json | 30 +++++++++++++++++++ .../crownstone/translations/fr.json | 21 +++++++++++++ .../crownstone/translations/id.json | 15 ++++++++++ .../devolo_home_network/translations/fr.json | 19 ++++++++++++ .../devolo_home_network/translations/id.json | 19 ++++++++++++ .../components/dlna_dmr/translations/fr.json | 26 ++++++++++++++++ .../components/dlna_dmr/translations/id.json | 24 +++++++++++++++ .../components/efergy/translations/fr.json | 12 ++++++++ .../environment_canada/translations/fr.json | 16 ++++++++++ .../environment_canada/translations/id.json | 18 +++++++++++ .../components/esphome/translations/id.json | 5 ++++ .../evil_genius_labs/translations/fr.json | 15 ++++++++++ .../evil_genius_labs/translations/id.json | 14 +++++++++ .../components/iotawatt/translations/fr.json | 11 +++++++ .../components/iotawatt/translations/id.json | 5 ++++ .../components/jellyfin/translations/fr.json | 21 +++++++++++++ .../components/jellyfin/translations/id.json | 21 +++++++++++++ .../components/jellyfin/translations/nl.json | 21 +++++++++++++ .../components/jellyfin/translations/no.json | 21 +++++++++++++ .../components/light/translations/hu.json | 4 +-- .../components/lookin/translations/id.json | 28 +++++++++++++++++ .../modem_callerid/translations/fr.json | 19 ++++++++++++ .../components/nanoleaf/translations/id.json | 1 + .../components/nest/translations/id.json | 5 ++++ .../components/netatmo/translations/id.json | 4 +++ .../nfandroidtv/translations/id.json | 11 ++++++- .../components/notion/translations/id.json | 12 ++++++-- .../components/octoprint/translations/id.json | 18 +++++++++++ .../opengarage/translations/fr.json | 21 +++++++++++++ .../opengarage/translations/id.json | 21 +++++++++++++ .../components/owntracks/translations/de.json | 2 +- .../components/owntracks/translations/fr.json | 2 +- .../components/prosegur/translations/id.json | 3 +- .../components/rdw/translations/fr.json | 7 +++++ .../components/rdw/translations/id.json | 7 +++++ .../components/renault/translations/fr.json | 3 ++ .../components/ridwell/translations/fr.json | 26 ++++++++++++++++ .../components/ridwell/translations/id.json | 22 ++++++++++++++ .../components/sensor/translations/ca.json | 2 ++ .../components/sensor/translations/de.json | 2 ++ .../components/sensor/translations/et.json | 2 ++ .../components/sensor/translations/hu.json | 2 ++ .../components/sensor/translations/ru.json | 2 ++ .../components/shelly/translations/fr.json | 2 +- .../components/soma/translations/id.json | 6 ++-- .../stookalert/translations/fr.json | 7 +++++ .../stookalert/translations/id.json | 7 +++++ .../components/switchbot/translations/fr.json | 7 +++++ .../components/switchbot/translations/id.json | 1 + .../synology_dsm/translations/id.json | 1 + .../totalconnect/translations/ca.json | 1 + .../totalconnect/translations/de.json | 1 + .../totalconnect/translations/en.json | 1 + .../totalconnect/translations/et.json | 1 + .../totalconnect/translations/hu.json | 1 + .../totalconnect/translations/ru.json | 1 + .../components/tplink/translations/fr.json | 9 ++++++ .../components/tplink/translations/id.json | 10 +++++++ .../components/tractive/translations/id.json | 20 +++++++++++++ .../components/tuya/translations/id.json | 6 ++-- .../tuya/translations/select.fr.json | 17 +++++++++++ .../tuya/translations/select.id.json | 13 ++++++++ .../tuya/translations/select.nl.json | 1 + .../uptimerobot/translations/id.json | 1 + .../components/venstar/translations/fr.json | 18 +++++++++++ .../vlc_telnet/translations/fr.json | 24 +++++++++++++++ .../vlc_telnet/translations/id.json | 27 +++++++++++++++++ .../components/wallbox/translations/ca.json | 10 ++++++- .../components/wallbox/translations/de.json | 10 ++++++- .../components/wallbox/translations/en.json | 3 +- .../components/wallbox/translations/et.json | 10 ++++++- .../components/wallbox/translations/fr.json | 9 +++++- .../components/wallbox/translations/hu.json | 10 ++++++- .../components/wallbox/translations/nl.json | 3 +- .../components/wallbox/translations/ru.json | 10 ++++++- .../components/watttime/translations/fr.json | 25 +++++++++++++++- .../components/watttime/translations/id.json | 14 +++++++++ .../components/whirlpool/translations/fr.json | 11 +++++++ .../components/yeelight/translations/id.json | 2 +- .../components/zwave_js/translations/id.json | 15 ++++++++++ 83 files changed, 872 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/airthings/translations/id.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/id.json create mode 100644 homeassistant/components/crownstone/translations/fr.json create mode 100644 homeassistant/components/devolo_home_network/translations/fr.json create mode 100644 homeassistant/components/devolo_home_network/translations/id.json create mode 100644 homeassistant/components/dlna_dmr/translations/fr.json create mode 100644 homeassistant/components/dlna_dmr/translations/id.json create mode 100644 homeassistant/components/efergy/translations/fr.json create mode 100644 homeassistant/components/environment_canada/translations/fr.json create mode 100644 homeassistant/components/environment_canada/translations/id.json create mode 100644 homeassistant/components/evil_genius_labs/translations/fr.json create mode 100644 homeassistant/components/evil_genius_labs/translations/id.json create mode 100644 homeassistant/components/iotawatt/translations/fr.json create mode 100644 homeassistant/components/jellyfin/translations/fr.json create mode 100644 homeassistant/components/jellyfin/translations/id.json create mode 100644 homeassistant/components/jellyfin/translations/nl.json create mode 100644 homeassistant/components/jellyfin/translations/no.json create mode 100644 homeassistant/components/lookin/translations/id.json create mode 100644 homeassistant/components/modem_callerid/translations/fr.json create mode 100644 homeassistant/components/octoprint/translations/id.json create mode 100644 homeassistant/components/opengarage/translations/fr.json create mode 100644 homeassistant/components/opengarage/translations/id.json create mode 100644 homeassistant/components/rdw/translations/fr.json create mode 100644 homeassistant/components/rdw/translations/id.json create mode 100644 homeassistant/components/ridwell/translations/fr.json create mode 100644 homeassistant/components/ridwell/translations/id.json create mode 100644 homeassistant/components/stookalert/translations/fr.json create mode 100644 homeassistant/components/stookalert/translations/id.json create mode 100644 homeassistant/components/switchbot/translations/fr.json create mode 100644 homeassistant/components/tractive/translations/id.json create mode 100644 homeassistant/components/tuya/translations/select.fr.json create mode 100644 homeassistant/components/tuya/translations/select.id.json create mode 100644 homeassistant/components/venstar/translations/fr.json create mode 100644 homeassistant/components/vlc_telnet/translations/fr.json create mode 100644 homeassistant/components/vlc_telnet/translations/id.json create mode 100644 homeassistant/components/whirlpool/translations/fr.json diff --git a/homeassistant/components/airthings/translations/id.json b/homeassistant/components/airthings/translations/id.json new file mode 100644 index 00000000000..44648dab3d2 --- /dev/null +++ b/homeassistant/components/airthings/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/id.json b/homeassistant/components/aurora_abb_powerone/translations/id.json new file mode 100644 index 00000000000..ddfbf74ffeb --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/id.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 876cdb8b2e4..90fdcce7575 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -132,7 +132,7 @@ "on": "Hideg" }, "connectivity": { - "off": "Lekapcsol\u00f3dva", + "off": "Lev\u00e1lasztva", "on": "Kapcsol\u00f3dva" }, "door": { diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index 54dcb66dd7a..b9688d494db 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} tidak mendeteksi masalah", "is_no_smoke": "{entity_name} tidak mendeteksi asap", "is_no_sound": "{entity_name} tidak mendeteksi suara", + "is_no_update": "{entity_name} sudah yang terbaru", "is_no_vibration": "{entity_name} tidak mendeteksi getaran", "is_not_bat_low": "Baterai {entity_name} normal", "is_not_cold": "{entity_name} tidak dingin", @@ -30,6 +31,8 @@ "is_not_plugged_in": "{entity_name} dicabut", "is_not_powered": "{entity_name} tidak ditenagai", "is_not_present": "{entity_name} tidak ada", + "is_not_running": "{entity_name} tidak berjalan", + "is_not_tampered": "{entity_name} tidak mendeteksi gangguan", "is_not_unsafe": "{entity_name} aman", "is_occupied": "{entity_name} ditempati", "is_off": "{entity_name} mati", @@ -39,9 +42,12 @@ "is_powered": "{entity_name} ditenagai", "is_present": "{entity_name} ada", "is_problem": "{entity_name} mendeteksi masalah", + "is_running": "{entity_name} sedang berjalan", "is_smoke": "{entity_name} mendeteksi asap", "is_sound": "{entity_name} mendeteksi suara", + "is_tampered": "{entity_name} mendeteksi gangguan", "is_unsafe": "{entity_name} tidak aman", + "is_update": "{entity_name} memiliki pembaruan yang tersedia", "is_vibration": "{entity_name} mendeteksi getaran" }, "trigger_type": { @@ -50,6 +56,8 @@ "connected": "{entity_name} terhubung", "gas": "{entity_name} mulai mendeteksi gas", "hot": "{entity_name} menjadi panas", + "is_not_tampered": "{entity_name} berhenti mendeteksi gangguan", + "is_tampered": "{entity_name} mulai mendeteksi gangguan", "light": "{entity_name} mulai mendeteksi cahaya", "locked": "{entity_name} terkunci", "moist": "{entity_name} menjadi lembab", @@ -61,6 +69,7 @@ "no_problem": "{entity_name} berhenti mendeteksi masalah", "no_smoke": "{entity_name} berhenti mendeteksi asap", "no_sound": "{entity_name} berhenti mendeteksi suara", + "no_update": "{entity_name} menjadi yang terbaru", "no_vibration": "{entity_name} berhenti mendeteksi getaran", "not_bat_low": "Baterai {entity_name} normal", "not_cold": "{entity_name} menjadi tidak dingin", @@ -74,6 +83,7 @@ "not_plugged_in": "{entity_name} dicabut", "not_powered": "{entity_name} tidak ditenagai", "not_present": "{entity_name} tidak ada", + "not_running": "{entity_name} tidak lagi berjalan", "not_unsafe": "{entity_name} menjadi aman", "occupied": "{entity_name} menjadi ditempati", "opened": "{entity_name} terbuka", @@ -81,14 +91,29 @@ "powered": "{entity_name} ditenagai", "present": "{entity_name} ada", "problem": "{entity_name} mulai mendeteksi masalah", + "running": "{entity_name} mulai berjalan", "smoke": "{entity_name} mulai mendeteksi asap", "sound": "{entity_name} mulai mendeteksi suara", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan", "unsafe": "{entity_name} menjadi tidak aman", + "update": "{entity_name} mendapat pembaruan yang tersedia", "vibration": "{entity_name} mulai mendeteksi getaran" } }, + "device_class": { + "cold": "dingin", + "gas": "gas", + "heat": "panas", + "moisture": "kelembaban", + "motion": "gerakan", + "occupancy": "okupansi", + "power": "daya", + "problem": "masalah", + "smoke": "asap", + "sound": "suara", + "vibration": "vibrasi" + }, "state": { "_": { "off": "Mati", @@ -166,6 +191,10 @@ "off": "Oke", "on": "Bermasalah" }, + "running": { + "off": "Tidak berjalan", + "on": "Berjalan" + }, "safety": { "off": "Aman", "on": "Tidak aman" @@ -179,6 +208,7 @@ "on": "Terdeteksi" }, "update": { + "off": "Diperbarui", "on": "Pembaruan tersedia" }, "vibration": { diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json new file mode 100644 index 00000000000..55336a6a87f --- /dev/null +++ b/homeassistant/components/crownstone/translations/fr.json @@ -0,0 +1,21 @@ +{ + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + } + }, + "usb_config_option": { + "data": { + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/id.json b/homeassistant/components/crownstone/translations/id.json index 5bd28168d9a..b7fbfdc8c7d 100644 --- a/homeassistant/components/crownstone/translations/id.json +++ b/homeassistant/components/crownstone/translations/id.json @@ -28,6 +28,21 @@ }, "options": { "step": { + "usb_config": { + "data": { + "usb_path": "Jalur Perangkat USB" + } + }, + "usb_config_option": { + "data": { + "usb_path": "Jalur Perangkat USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Jalur Perangkat USB" + } + }, "usb_manual_config_option": { "data": { "usb_manual_path": "Jalur Perangkat USB" diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json new file mode 100644 index 00000000000..1c0fc4827c2 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP" + }, + "description": "Voulez-vous commencer la configuration ?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/id.json b/homeassistant/components/devolo_home_network/translations/id.json new file mode 100644 index 00000000000..187ccebf754 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP" + }, + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json new file mode 100644 index 00000000000..4db8bec9bd6 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration ?" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "host": "H\u00f4te", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/id.json b/homeassistant/components/dlna_dmr/translations/id.json new file mode 100644 index 00000000000..483d422d040 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/fr.json b/homeassistant/components/efergy/translations/fr.json new file mode 100644 index 00000000000..1e0299533ea --- /dev/null +++ b/homeassistant/components/efergy/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/fr.json b/homeassistant/components/environment_canada/translations/fr.json new file mode 100644 index 00000000000..84333da4f4a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/id.json b/homeassistant/components/environment_canada/translations/id.json new file mode 100644 index 00000000000..8334056d960 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "language": "Bahasa informasi cuaca", + "latitude": "Lintang", + "longitude": "Bujur", + "station": "ID stasiun cuaca" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index 530d86e2f56..0258df3feef 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -22,6 +22,11 @@ "description": "Ingin menambahkan node ESPHome `{name}` ke Home Assistant?", "title": "Perangkat node ESPHome yang ditemukan" }, + "encryption_key": { + "data": { + "noise_psk": "Kunci enkripsi" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/evil_genius_labs/translations/fr.json b/homeassistant/components/evil_genius_labs/translations/fr.json new file mode 100644 index 00000000000..bd75678406e --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/id.json b/homeassistant/components/evil_genius_labs/translations/id.json new file mode 100644 index 00000000000..859922f9fde --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/fr.json b/homeassistant/components/iotawatt/translations/fr.json new file mode 100644 index 00000000000..9cb1d7dfd16 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/id.json b/homeassistant/components/iotawatt/translations/id.json index a48af7cd34d..8461b692aaa 100644 --- a/homeassistant/components/iotawatt/translations/id.json +++ b/homeassistant/components/iotawatt/translations/id.json @@ -11,6 +11,11 @@ "password": "Kata Sandi", "username": "Nama Pengguna" } + }, + "user": { + "data": { + "host": "Host" + } } } } diff --git a/homeassistant/components/jellyfin/translations/fr.json b/homeassistant/components/jellyfin/translations/fr.json new file mode 100644 index 00000000000..c797b7e93a3 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/id.json b/homeassistant/components/jellyfin/translations/id.json new file mode 100644 index 00000000000..687cd917130 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/nl.json b/homeassistant/components/jellyfin/translations/nl.json new file mode 100644 index 00000000000..1072cfff418 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/no.json b/homeassistant/components/jellyfin/translations/no.json new file mode 100644 index 00000000000..c1351b9a97f --- /dev/null +++ b/homeassistant/components/jellyfin/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/hu.json b/homeassistant/components/light/translations/hu.json index 1ac835fd1af..7a82050c5a1 100644 --- a/homeassistant/components/light/translations/hu.json +++ b/homeassistant/components/light/translations/hu.json @@ -4,8 +4,8 @@ "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", + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", "turn_on": "{entity_name} felkapcsol\u00e1sa" }, "condition_type": { diff --git a/homeassistant/components/lookin/translations/id.json b/homeassistant/components/lookin/translations/id.json new file mode 100644 index 00000000000..99873637964 --- /dev/null +++ b/homeassistant/components/lookin/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nama" + } + }, + "user": { + "data": { + "ip_address": "Alamat IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/fr.json b/homeassistant/components/modem_callerid/translations/fr.json new file mode 100644 index 00000000000..5847c82af6a --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "name": "Nom", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json index b0e3328df0b..aaded77c720 100644 --- a/homeassistant/components/nanoleaf/translations/id.json +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -11,6 +11,7 @@ "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index d035433361f..948e3a894f7 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -18,6 +18,11 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "auth": { + "data": { + "code": "Token Akses" + } + }, "init": { "data": { "flow_impl": "Penyedia" diff --git a/homeassistant/components/netatmo/translations/id.json b/homeassistant/components/netatmo/translations/id.json index 6812d45816b..dedcb5532e1 100644 --- a/homeassistant/components/netatmo/translations/id.json +++ b/homeassistant/components/netatmo/translations/id.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "reauth_successful": "Autentikasi ulang berhasil", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "create_entry": { @@ -12,6 +13,9 @@ "step": { "pick_implementation": { "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" } } }, diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json index 087e25a22ae..131bfcb21ce 100644 --- a/homeassistant/components/nfandroidtv/translations/id.json +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -4,7 +4,16 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/id.json b/homeassistant/components/notion/translations/id.json index 35ee7a29544..23a1a6eddeb 100644 --- a/homeassistant/components/notion/translations/id.json +++ b/homeassistant/components/notion/translations/id.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid", - "no_devices": "Tidak ada perangkat yang ditemukan di akun" + "no_devices": "Tidak ada perangkat yang ditemukan di akun", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/octoprint/translations/id.json b/homeassistant/components/octoprint/translations/id.json new file mode 100644 index 00000000000..2f6f333f799 --- /dev/null +++ b/homeassistant/components/octoprint/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/fr.json b/homeassistant/components/opengarage/translations/fr.json new file mode 100644 index 00000000000..909f8bd9eec --- /dev/null +++ b/homeassistant/components/opengarage/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/id.json b/homeassistant/components/opengarage/translations/id.json new file mode 100644 index 00000000000..49f5e6b2a75 --- /dev/null +++ b/homeassistant/components/opengarage/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 891f914f8a9..737b92c642a 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 9120cdb8637..35530bd2c86 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "\n\n Sous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." + "default": "\n\nSous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." }, "step": { "user": { diff --git a/homeassistant/components/prosegur/translations/id.json b/homeassistant/components/prosegur/translations/id.json index 9616471c03a..7b4c569c516 100644 --- a/homeassistant/components/prosegur/translations/id.json +++ b/homeassistant/components/prosegur/translations/id.json @@ -19,7 +19,8 @@ "user": { "data": { "country": "Negara", - "password": "Kata Sandi" + "password": "Kata Sandi", + "username": "Nama Pengguna" } } } diff --git a/homeassistant/components/rdw/translations/fr.json b/homeassistant/components/rdw/translations/fr.json new file mode 100644 index 00000000000..4da885d870f --- /dev/null +++ b/homeassistant/components/rdw/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/id.json b/homeassistant/components/rdw/translations/id.json new file mode 100644 index 00000000000..a8a19d90504 --- /dev/null +++ b/homeassistant/components/rdw/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index d0ea9d0284c..8cfc294cf4a 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -15,6 +15,9 @@ "title": "S\u00e9lectionner l'identifiant du compte Kamereon" }, "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { diff --git a/homeassistant/components/ridwell/translations/fr.json b/homeassistant/components/ridwell/translations/fr.json new file mode 100644 index 00000000000..3324689e3d3 --- /dev/null +++ b/homeassistant/components/ridwell/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/id.json b/homeassistant/components/ridwell/translations/id.json new file mode 100644 index 00000000000..8cfa56b690a --- /dev/null +++ b/homeassistant/components/ridwell/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index c90ed273a67..3e914526e12 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -6,6 +6,7 @@ "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_frequency": "Freq\u00fc\u00e8ncia 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}", @@ -32,6 +33,7 @@ "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}", + "frequency": "Canvia la freq\u00fc\u00e8ncia 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}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index a042e3102f9..237259ffc11 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", "is_current": "Aktueller Strom von {entity_name}", "is_energy": "Aktuelle Energie von {entity_name}", + "is_frequency": "Aktuelle {entity_name} Frequenz", "is_gas": "Aktuelles {entity_name} Gas", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", "energy": "{entity_name} Energie\u00e4nderungen", + "frequency": "{entity_name} Frequenz\u00e4nderungen", "gas": "{entity_name} Gas\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 5cfa6a94852..06fbe321091 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", "is_current": "Praegune {entity_name} voolutugevus", "is_energy": "Praegune {entity_name} v\u00f5imsus", + "is_frequency": "Praegune {entity_name} sagedus", "is_gas": "Praegune {entity_name} gaas", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "energy": "{entity_name} v\u00f5imsus muutub", + "frequency": "{entity_name} sagedus muutub", "gas": "{entity_name} gaasivahetus", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 1e33cc18355..c8650cd1579 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -6,6 +6,7 @@ "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_frequency": "Aktu\u00e1lis {entity_name} gyakoris\u00e1g", "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", @@ -32,6 +33,7 @@ "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", + "frequency": "{entity_name} gyakoris\u00e1gi v\u00e1ltoz\u00e1sok", "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 821622ae20c..b7e8a912a11 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -6,6 +6,7 @@ "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_frequency": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "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", @@ -32,6 +33,7 @@ "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", + "frequency": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "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", diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index d458d0212e6..68dc5de667a 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -38,7 +38,7 @@ }, "trigger_type": { "double": "{subtype} double-cliqu\u00e9", - "long": " {sous-type} long cliqu\u00e9", + "long": "{subtype} long cliqu\u00e9", "long_push": "{subtype} appui long", "long_single": "{subtype} clic long et simple clic", "single": "{subtype} simple clic", diff --git a/homeassistant/components/soma/translations/id.json b/homeassistant/components/soma/translations/id.json index d512bd46797..367edae4d35 100644 --- a/homeassistant/components/soma/translations/id.json +++ b/homeassistant/components/soma/translations/id.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Anda hanya dapat mengonfigurasi satu akun Soma.", + "already_setup": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", - "connection_error": "Gagal menyambungkan ke SOMA Connect.", + "connection_error": "Gagal terhubung", "missing_configuration": "Komponen Soma tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "result_error": "SOMA Connect merespons dengan status kesalahan." }, "create_entry": { - "default": "Berhasil mengautentikasi dengan Soma." + "default": "Berhasil diautentikasi" }, "step": { "user": { diff --git a/homeassistant/components/stookalert/translations/fr.json b/homeassistant/components/stookalert/translations/fr.json new file mode 100644 index 00000000000..9c74e1b5026 --- /dev/null +++ b/homeassistant/components/stookalert/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/id.json b/homeassistant/components/stookalert/translations/id.json new file mode 100644 index 00000000000..fa8cd415378 --- /dev/null +++ b/homeassistant/components/stookalert/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json new file mode 100644 index 00000000000..d155457d4b6 --- /dev/null +++ b/homeassistant/components/switchbot/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u00c9chec de connexion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index af61966afa5..02f90ba89c3 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index ca322fc518e..89b29b9ae1f 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -32,6 +32,7 @@ }, "reauth": { "data": { + "password": "Kata Sandi", "username": "Nama Pengguna" }, "title": "Autentikasi Ulang Integrasi Synology DSM" diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index fa42c81e1be..0edad920cdf 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El compte ja est\u00e0 configurat", + "no_locations": "No hi ha ubicacions disponibles per a aquest usuari, comprova la configuraci\u00f3 de TotalConnect", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index c435169c804..b89b99fc2d4 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", + "no_locations": "F\u00fcr diesen Benutzer sind keine Standorte verf\u00fcgbar, \u00fcberpr\u00fcfe die TotalConnect-Einstellungen", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index 05f394fbb31..f3a96550cba 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Account is already configured", + "no_locations": "No locations are available for this user, check TotalConnect settings", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json index a4110f9bf0f..e2df8dd51e8 100644 --- a/homeassistant/components/totalconnect/translations/et.json +++ b/homeassistant/components/totalconnect/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto on juba seadistatud", + "no_locations": "Kasutaja jaoks ei ole asukohti saadaval, kontrolli TotalConnecti seadeid.", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 18cbad6bc50..3d40f84d262 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_locations": "Nincs el\u00e9rhet\u0151 helyzet a felhaszn\u00e1l\u00f3 sz\u00e1m\u00e1ra, ellen\u0151rizze a TotalConnect be\u00e1ll\u00edt\u00e1sait.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index 268f620c238..a46c37032a1 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -2,6 +2,7 @@ "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.", + "no_locations": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 TotalConnect.", "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": { diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index f36b3865e55..7d1efcafeb8 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -1,12 +1,21 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, "step": { "confirm": { "description": "Voulez-vous configurer TP-Link smart devices?" + }, + "user": { + "data": { + "host": "H\u00f4te" + } } } } diff --git a/homeassistant/components/tplink/translations/id.json b/homeassistant/components/tplink/translations/id.json index 66d510de4ed..8a04fd63fed 100644 --- a/homeassistant/components/tplink/translations/id.json +++ b/homeassistant/components/tplink/translations/id.json @@ -1,12 +1,22 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Ingin menyiapkan perangkat cerdas TP-Link?" + }, + "user": { + "data": { + "host": "Host" + } } } } diff --git a/homeassistant/components/tractive/translations/id.json b/homeassistant/components/tractive/translations/id.json new file mode 100644 index 00000000000..80f6595e215 --- /dev/null +++ b/homeassistant/components/tractive/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json index 8b7f196b5a2..06f99cc3a8e 100644 --- a/homeassistant/components/tuya/translations/id.json +++ b/homeassistant/components/tuya/translations/id.json @@ -12,13 +12,13 @@ "step": { "user": { "data": { - "country_code": "Kode negara akun Anda (mis., 1 untuk AS atau 86 untuk China)", + "country_code": "Negara", "password": "Kata Sandi", "platform": "Aplikasi tempat akun Anda terdaftar", - "username": "Nama Pengguna" + "username": "Akun" }, "description": "Masukkan kredensial Tuya Anda.", - "title": "Tuya" + "title": "Integrasi Tuya" } } }, diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json new file mode 100644 index 00000000000..6ca8d863030 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -0,0 +1,17 @@ +{ + "state": { + "tuya__basic_nightvision": { + "1": "Inactif", + "2": "Actif" + }, + "tuya__light_mode": { + "none": "Inactif" + }, + "tuya__relay_status": { + "off": "Inactif", + "on": "Actif", + "power_off": "Inactif", + "power_on": "Actif" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json new file mode 100644 index 00000000000..caa94b14c89 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.id.json @@ -0,0 +1,13 @@ +{ + "state": { + "tuya__light_mode": { + "none": "Mati" + }, + "tuya__relay_status": { + "off": "Mati", + "on": "Nyala", + "power_off": "Mati", + "power_on": "Nyala" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index 5efdae61193..169d81d0be4 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -20,6 +20,7 @@ }, "tuya__led_type": { "halogen": "Halogeen", + "incandescent": "Witgloeiend", "led": "LED" }, "tuya__light_mode": { diff --git a/homeassistant/components/uptimerobot/translations/id.json b/homeassistant/components/uptimerobot/translations/id.json index e107b1fcac6..763ed330bbe 100644 --- a/homeassistant/components/uptimerobot/translations/id.json +++ b/homeassistant/components/uptimerobot/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Akun sudah dikonfigurasi", "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/venstar/translations/fr.json b/homeassistant/components/venstar/translations/fr.json new file mode 100644 index 00000000000..2be362d5603 --- /dev/null +++ b/homeassistant/components/venstar/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "pin": "Code PIN", + "ssl": "Utilise un certificat SSL", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/fr.json b/homeassistant/components/vlc_telnet/translations/fr.json new file mode 100644 index 00000000000..9ed67c043de --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + } + }, + "user": { + "data": { + "password": "Mot de passe", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/id.json b/homeassistant/components/vlc_telnet/translations/id.json new file mode 100644 index 00000000000..d0b5d58f167 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{host}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ca.json b/homeassistant/components/wallbox/translations/ca.json index 55240065548..b6a10e16e2a 100644 --- a/homeassistant/components/wallbox/translations/ca.json +++ b/homeassistant/components/wallbox/translations/ca.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_invalid": "Ha fallat la re-autenticaci\u00f3; el n\u00famero de s\u00e8rie no coincideix amb l'original", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/wallbox/translations/de.json b/homeassistant/components/wallbox/translations/de.json index 89362597b85..d415b4f9e7a 100644 --- a/homeassistant/components/wallbox/translations/de.json +++ b/homeassistant/components/wallbox/translations/de.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_invalid": "Die erneute Authentifizierung ist fehlgeschlagen; Seriennummer stimmt nicht mit Original \u00fcberein", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json index f32c7b7b481..28ec5d08235 100644 --- a/homeassistant/components/wallbox/translations/en.json +++ b/homeassistant/components/wallbox/translations/en.json @@ -25,5 +25,6 @@ } } } - } + }, + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/et.json b/homeassistant/components/wallbox/translations/et.json index 12e24fd83ba..f5d3b3aac73 100644 --- a/homeassistant/components/wallbox/translations/et.json +++ b/homeassistant/components/wallbox/translations/et.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", + "reauth_invalid": "Taastuvastamine nurjus, seerianumber ei vasta originaalile", "unknown": "Tundmatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json index 05e57f9adc4..4b2eddb6ef9 100644 --- a/homeassistant/components/wallbox/translations/fr.json +++ b/homeassistant/components/wallbox/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,12 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/wallbox/translations/hu.json b/homeassistant/components/wallbox/translations/hu.json index 097ba53f02e..5579da0fe88 100644 --- a/homeassistant/components/wallbox/translations/hu.json +++ b/homeassistant/components/wallbox/translations/hu.json @@ -1,14 +1,22 @@ { "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 \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_invalid": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt; A sorozatsz\u00e1m nem egyezik az eredetivel", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index a152eb89362..6ba03e7ee99 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/wallbox/translations/ru.json b/homeassistant/components/wallbox/translations/ru.json index b6b33a6eb48..426c07c9423 100644 --- a/homeassistant/components/wallbox/translations/ru.json +++ b/homeassistant/components/wallbox/translations/ru.json @@ -1,14 +1,22 @@ { "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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "reauth_invalid": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u043c\u0443.", "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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/watttime/translations/fr.json b/homeassistant/components/watttime/translations/fr.json index c4bc0d48b1a..fb916a3f333 100644 --- a/homeassistant/components/watttime/translations/fr.json +++ b/homeassistant/components/watttime/translations/fr.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "coordinates": { + "data": { + "longitude": "Longitude" + } + }, + "location": { + "data": { + "location_type": "Emplacement" + } + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + } } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/id.json b/homeassistant/components/watttime/translations/id.json index 2549bd6f4ff..afce5debec3 100644 --- a/homeassistant/components/watttime/translations/id.json +++ b/homeassistant/components/watttime/translations/id.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, "step": { "coordinates": { "data": { @@ -14,6 +22,12 @@ }, "description": "Pilih lokasi untuk dipantau:" }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/whirlpool/translations/fr.json b/homeassistant/components/whirlpool/translations/fr.json new file mode 100644 index 00000000000..0cfccfa88ad --- /dev/null +++ b/homeassistant/components/whirlpool/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json index d9795662689..19537d658a1 100644 --- a/homeassistant/components/yeelight/translations/id.json +++ b/homeassistant/components/yeelight/translations/id.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (opsional)", + "model": "Model", "nightlight_switch": "Gunakan Sakelar Lampu Malam", "save_on_change": "Simpan Status Saat Berubah", "transition": "Waktu Transisi (milidetik)", diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 2004be7238f..0d796cc6491 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Kunci Jaringan", + "s0_legacy_key": "Kunci S0 (Warisan)", + "s2_access_control_key": "Kunci Kontrol Akses S2", + "s2_authenticated_key": "Kunci Autentikasi S2", + "s2_unauthenticated_key": "Kunci S2 Tidak Diautentikasi", "usb_path": "Jalur Perangkat USB" }, "title": "Masukkan konfigurasi add-on Z-Wave JS" @@ -51,10 +55,21 @@ }, "start_addon": { "title": "Add-on Z-Wave JS sedang dimulai." + }, + "usb_confirm": { + "description": "Ingin menyiapkan {name} dengan add-on Z-Wave JS?" } } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Hapus usercode pada {entity_name}", + "ping": "Ping perangkat", + "refresh_value": "Segarkan nilai untuk {entity_name}", + "reset_meter": "Setel ulang pengukur di {subtype}", + "set_config_parameter": "Tetapkan nilai parameter konfigurasi {subtype}", + "set_lock_usercode": "Setel usercode pada {entity_name}" + }, "condition_type": { "config_parameter": "Nilai parameter konfigurasi {subtype}", "node_status": "Status node", From 771922b8716739fabc92affc1e7e2400de790022 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 16 Nov 2021 01:25:22 +0100 Subject: [PATCH 0525/1452] Add support for property attribute shorthand in Fan entity (#59649) --- homeassistant/components/fan/__init__.py | 27 ++++++++++++++++++++---- tests/components/fan/test_init.py | 19 +++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 3a5b9bcda67..9289c899b57 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -235,6 +235,13 @@ class FanEntity(ToggleEntity): """Base class for fan entities.""" entity_description: FanEntityDescription + _attr_current_direction: str | None = None + _attr_oscillating: bool | None = None + _attr_percentage: int | None + _attr_preset_mode: str | None + _attr_preset_modes: list[str] | None + _attr_speed_count: int + _attr_supported_features: int = 0 @_fan_native def set_speed(self, speed: str) -> None: @@ -469,6 +476,9 @@ class FanEntity(ToggleEntity): @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" + if hasattr(self, "_attr_percentage"): + return self._attr_percentage + if ( not self._implemented_preset_mode and self.preset_modes @@ -482,6 +492,9 @@ class FanEntity(ToggleEntity): @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" + if hasattr(self, "_attr_speed_count"): + return self._attr_speed_count + speed_list = speed_list_without_preset_modes(self.speed_list) if speed_list: return len(speed_list) @@ -505,12 +518,12 @@ class FanEntity(ToggleEntity): @property def current_direction(self) -> str | None: """Return the current direction of the fan.""" - return None + return self._attr_current_direction @property - def oscillating(self): + def oscillating(self) -> bool | None: """Return whether or not the fan is currently oscillating.""" - return None + return self._attr_oscillating @property def capability_attributes(self): @@ -629,7 +642,7 @@ class FanEntity(ToggleEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return 0 + return self._attr_supported_features @property def preset_mode(self) -> str | None: @@ -637,6 +650,9 @@ class FanEntity(ToggleEntity): Requires SUPPORT_SET_SPEED. """ + if hasattr(self, "_attr_preset_mode"): + return self._attr_preset_mode + speed = self.speed if self.preset_modes and speed in self.preset_modes: return speed @@ -648,6 +664,9 @@ class FanEntity(ToggleEntity): Requires SUPPORT_SET_SPEED. """ + if hasattr(self, "_attr_preset_modes"): + return self._attr_preset_modes + return preset_modes_from_speed_list(self.speed_list) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 05ced3b8be7..3167cb16e67 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -65,3 +65,22 @@ async def test_async_fanentity(hass): await fan.async_increase_speed() with pytest.raises(NotImplementedError): await fan.async_decrease_speed() + + +@pytest.mark.parametrize( + "attribute_name, attribute_value", + [ + ("current_direction", "forward"), + ("oscillating", True), + ("percentage", 50), + ("preset_mode", "medium"), + ("preset_modes", ["low", "medium", "high"]), + ("speed_count", 50), + ("supported_features", 1), + ], +) +def test_fanentity_attributes(attribute_name, attribute_value): + """Test fan entity attribute shorthand.""" + fan = BaseFan() + setattr(fan, f"_attr_{attribute_name}", attribute_value) + assert getattr(fan, attribute_name) == attribute_value From 9256a033a6f4349bef4bf7863c6638c37dbf3def Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Nov 2021 02:23:59 +0100 Subject: [PATCH 0526/1452] Upgrade spotipy to 2.19.0 (#59728) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 4a4a904fe9e..402083aa25d 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.18.0"], + "requirements": ["spotipy==2.19.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index fd852a308bf..7b80cc3150e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2211,7 +2211,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.18.0 +spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57056e3f709..4336c51cc2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1299,7 +1299,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.18.0 +spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql From 4f01631bd65a00ad1c4a1c5fe086038a13a97239 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Nov 2021 08:35:52 +0100 Subject: [PATCH 0527/1452] Allow triggering on all state changes, ignoring attributes (#59713) * Allow triggering on all state changes, ignoring attributes * Add comment * Apply suggestions from code review Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .../homeassistant/triggers/state.py | 14 +- .../homeassistant/triggers/test_state.py | 138 +++++++++++++++++- 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index f60071d633c..f1e2bbf2c09 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -39,8 +39,8 @@ BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( TRIGGER_STATE_SCHEMA = BASE_SCHEMA.extend( { # These are str on purpose. Want to catch YAML conversions - vol.Optional(CONF_FROM): vol.Any(str, [str]), - vol.Optional(CONF_TO): vol.Any(str, [str]), + vol.Optional(CONF_FROM): vol.Any(str, [str], None), + vol.Optional(CONF_TO): vol.Any(str, [str], None), } ) @@ -75,11 +75,15 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) - from_state = config.get(CONF_FROM, MATCH_ALL) - to_state = config.get(CONF_TO, MATCH_ALL) + if (from_state := config.get(CONF_FROM)) is None: + from_state = MATCH_ALL + if (to_state := config.get(CONF_TO)) is None: + to_state = MATCH_ALL time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) - match_all = from_state == MATCH_ALL and to_state == MATCH_ALL + # If neither CONF_FROM or CONF_TO are specified, + # fire on all changes to the state or an attribute + match_all = CONF_FROM not in config and CONF_TO not in config unsub_track_same = {} period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 8671e40d293..c86bb0cc879 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -106,7 +106,7 @@ async def test_if_fires_on_entity_change_with_from_filter(hass, calls): async def test_if_fires_on_entity_change_with_to_filter(hass, calls): - """Test for firing on entity change with no filter.""" + """Test for firing on entity change with to filter.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -128,6 +128,54 @@ async def test_if_fires_on_entity_change_with_to_filter(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_entity_change_with_from_filter_all(hass, calls): + """Test for firing on entity change with filter.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "from": None, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "world") + hass.states.async_set("test.entity", "world", {"attribute": 5}) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_if_fires_on_entity_change_with_to_filter_all(hass, calls): + """Test for firing on entity change with to filter.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "to": None, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "world") + hass.states.async_set("test.entity", "world", {"attribute": 5}) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_attribute_change_with_to_filter(hass, calls): """Test for not firing on attribute change.""" assert await async_setup_component( @@ -1217,6 +1265,94 @@ async def test_attribute_if_fires_on_entity_where_attr_stays_constant(hass, call assert len(calls) == 1 +async def test_attribute_if_fires_on_entity_where_attr_stays_constant_filter( + hass, calls +): + """Test for firing if attribute stays the same.""" + hass.states.async_set("test.entity", "bla", {"name": "other_name"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "attribute": "name", + "to": "best_name", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + # Leave all attributes the same + hass.states.async_set( + "test.entity", "bla", {"name": "best_name", "other": "old_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Change the untracked attribute + hass.states.async_set( + "test.entity", "bla", {"name": "best_name", "other": "new_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Change the tracked attribute + hass.states.async_set( + "test.entity", "bla", {"name": "other_name", "other": "old_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_attribute_if_fires_on_entity_where_attr_stays_constant_all(hass, calls): + """Test for firing if attribute stays the same.""" + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "attribute": "name", + "to": None, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + # Leave all attributes the same + hass.states.async_set( + "test.entity", "bla", {"name": "name_1", "other": "old_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Change the untracked attribute + hass.states.async_set( + "test.entity", "bla", {"name": "name_1", "other": "new_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Change the tracked attribute + hass.states.async_set( + "test.entity", "bla", {"name": "name_2", "other": "old_value"} + ) + await hass.async_block_till_done() + assert len(calls) == 2 + + async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( hass, calls ): From cca3cdb096cd020015f051b6dd924c1754539986 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Nov 2021 11:40:49 +0100 Subject: [PATCH 0528/1452] Use ZeroconfServiceInfo in bosch_shc (#58957) Co-authored-by: epenet --- homeassistant/components/bosch_shc/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index c86569d6a4d..4c6070b41eb 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_HOSTNAME, @@ -181,7 +182,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="credentials", data_schema=schema, errors=errors ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" if not discovery_info.get(zeroconf.ATTR_NAME, "").startswith("Bosch SHC"): return self.async_abort(reason="not_bosch_shc") @@ -197,7 +200,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): continue self.info = await self._get_info(host) self.host = host - if self.host is None: + if self.info is None or self.host is None: return self.async_abort(reason="cannot_connect") except SHCConnectionError: return self.async_abort(reason="cannot_connect") From a78176e192cb427c7ddef0da32d40db50a4bff24 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 16 Nov 2021 03:56:17 -0700 Subject: [PATCH 0529/1452] Perform some Ambient PWS code cleanup (#58859) Co-authored-by: Paulus Schoutsen --- .../components/ambient_station/__init__.py | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 3b318ba5a81..385722bf597 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription import homeassistant.helpers.entity_registry as er -from homeassistant.helpers.event import async_call_later from .const import ( ATTR_LAST_DATA, @@ -61,26 +60,25 @@ def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Ambient PWS as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - if not entry.unique_id: hass.config_entries.async_update_entry( entry, unique_id=entry.data[CONF_APP_KEY] ) + ambient = AmbientStation( + hass, + entry, + Websocket(entry.data[CONF_APP_KEY], entry.data[CONF_API_KEY]), + ) + try: - ambient = AmbientStation( - hass, - entry, - Websocket(entry.data[CONF_APP_KEY], entry.data[CONF_API_KEY]), - ) - hass.loop.create_task(ambient.ws_connect()) - hass.data[DOMAIN][entry.entry_id] = ambient + await ambient.ws_connect() except WebsocketError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ambient + async def _async_disconnect_websocket(_: Event) -> None: await ambient.websocket.disconnect() @@ -139,20 +137,6 @@ class AmbientStation: self.stations: dict[str, dict] = {} self.websocket = websocket - async def _attempt_connect(self) -> None: - """Attempt to connect to the socket (retrying later on fail).""" - - async def connect(timestamp: int | None = None) -> None: - """Connect.""" - await self.websocket.connect() - - try: - await connect() - except WebsocketError as err: - LOGGER.error("Error with the websocket connection: %s", err) - self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) - async_call_later(self._hass, self._ws_reconnect_delay, connect) - async def ws_connect(self) -> None: """Register handlers and connect to the websocket.""" @@ -203,7 +187,7 @@ class AmbientStation: self.websocket.on_disconnect(on_disconnect) self.websocket.on_subscribed(on_subscribed) - await self._attempt_connect() + await self.websocket.connect() async def ws_disconnect(self) -> None: """Disconnect from the websocket.""" From 476a59d248df5f2f933affd70d42af4c8bf1175e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:19:50 +0100 Subject: [PATCH 0530/1452] Adjust async_step_dhcp signature for strict typing (#59751) Co-authored-by: epenet --- homeassistant/components/axis/config_flow.py | 11 ++++---- homeassistant/components/dhcp/__init__.py | 25 +++++++++++++------ .../components/flux_led/config_flow.py | 10 ++++---- .../components/goalzero/config_flow.py | 11 ++++---- .../components/guardian/config_flow.py | 8 +++--- .../components/samsungtv/config_flow.py | 9 +++---- .../components/tplink/config_flow.py | 6 ++--- homeassistant/config_entries.py | 5 ++-- homeassistant/helpers/config_entry_flow.py | 12 +++++++-- 9 files changed, 55 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index d1e834e7bb6..eb4fd9296fc 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -6,8 +6,7 @@ from urllib.parse import urlsplit import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, @@ -153,13 +152,13 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): return await self.async_step_user() - async def async_step_dhcp(self, discovery_info: dict): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Axis device.""" return await self._process_discovered_device( { - CONF_HOST: discovery_info[IP_ADDRESS], - CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS, "")), - CONF_NAME: discovery_info.get(HOSTNAME), + CONF_HOST: discovery_info[dhcp.IP_ADDRESS], + CONF_MAC: format_mac(discovery_info[dhcp.MAC_ADDRESS]), + CONF_NAME: discovery_info[dhcp.HOSTNAME], CONF_PORT: DEFAULT_PORT, } ) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index d52b30ccfb2..be6aa5072d8 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -6,6 +6,7 @@ from ipaddress import ip_address as make_ip_address import logging import os import threading +from typing import Final, TypedDict from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( @@ -45,15 +46,23 @@ from homeassistant.util.network import is_invalid, is_link_local, is_loopback FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" -HOSTNAME = "hostname" -MAC_ADDRESS = "macaddress" -IP_ADDRESS = "ip" +HOSTNAME: Final = "hostname" +MAC_ADDRESS: Final = "macaddress" +IP_ADDRESS: Final = "ip" DHCP_REQUEST = 3 SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) +class DhcpServiceInfo(TypedDict): + """Prepared info from dhcp entries.""" + + ip: str + hostname: str + macaddress: str + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" @@ -150,11 +159,11 @@ class WatcherBase: self.hass, entry["domain"], {"source": config_entries.SOURCE_DHCP}, - { - IP_ADDRESS: ip_address, - HOSTNAME: lowercase_hostname, - MAC_ADDRESS: data[MAC_ADDRESS], - }, + DhcpServiceInfo( + ip=ip_address, + hostname=lowercase_hostname, + macaddress=data[MAC_ADDRESS], + ), ) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 306dbc2c25e..0a059abaf34 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -7,7 +7,7 @@ from typing import Any, Final import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_PROTOCOL from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -82,12 +82,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" self._discovered_device = { - FLUX_HOST: discovery_info[IP_ADDRESS], - FLUX_MODEL: discovery_info[HOSTNAME], - FLUX_MAC: discovery_info[MAC_ADDRESS].replace(":", ""), + FLUX_HOST: discovery_info[dhcp.IP_ADDRESS], + FLUX_MODEL: discovery_info[dhcp.HOSTNAME], + FLUX_MAC: discovery_info[dhcp.MAC_ADDRESS].replace(":", ""), } return await self._async_handle_discovery() diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index f192c71cbf8..4b02fe1e83a 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -8,12 +8,11 @@ from goalzero import Yeti, exceptions import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp 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 @@ -27,13 +26,13 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize a Goal Zero Yeti flow.""" - self.ip_address = None + self.ip_address: str | None = None - async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.ip_address = discovery_info[IP_ADDRESS] + self.ip_address = discovery_info[dhcp.IP_ADDRESS] - await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) + await self.async_set_unique_id(discovery_info[dhcp.MAC_ADDRESS]) self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) self._async_abort_entries_match({CONF_HOST: self.ip_address}) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 2aa00f56d99..78a4cbd90ea 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -8,12 +8,10 @@ from aioguardian.errors import GuardianError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_UID, DOMAIN, LOGGER @@ -99,10 +97,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} ) - async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle the configuration via dhcp.""" self.discovery_info = { - CONF_IP_ADDRESS: discovery_info[IP_ADDRESS], + CONF_IP_ADDRESS: discovery_info[dhcp.IP_ADDRESS], CONF_PORT: DEFAULT_PORT, } return await self._async_handle_discovery() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 1525d037cd9..f476d1dc87d 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -10,8 +10,7 @@ import getmac import voluptuous as vol from homeassistant import config_entries, data_entry_flow -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -285,12 +284,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() async def async_step_dhcp( - self, discovery_info: DiscoveryInfoType + self, discovery_info: dhcp.DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) - self._mac = discovery_info[MAC_ADDRESS] - self._host = discovery_info[IP_ADDRESS] + self._mac = discovery_info[dhcp.MAC_ADDRESS] + self._host = discovery_info[dhcp.IP_ADDRESS] await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 8abc00d7fdb..f07d8887b85 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -9,7 +9,7 @@ from kasa.discover import Discover import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -32,10 +32,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_devices: dict[str, SmartDevice] = {} self._discovered_device: SmartDevice | None = None - async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" return await self._async_handle_discovery( - discovery_info[IP_ADDRESS], discovery_info[MAC_ADDRESS] + discovery_info[dhcp.IP_ADDRESS], discovery_info[dhcp.MAC_ADDRESS] ) async def async_step_discovery( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d9c0faf2f71..85215a5962f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -32,6 +32,7 @@ from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: + from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1378,10 +1379,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_dhcp( - self, discovery_info: DiscoveryInfoType + self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_usb( self, discovery_info: DiscoveryInfoType diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 4a312b5e01f..75e3d128435 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType @@ -82,6 +82,15 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle a flow initialized by dhcp discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: @@ -106,7 +115,6 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_ssdp = async_step_discovery async_step_mqtt = async_step_discovery - async_step_dhcp = async_step_discovery async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: """Handle a flow initialized by import.""" From 41e341028ea88803e4f0f00c77d8022db9473b50 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 16 Nov 2021 12:39:51 +0100 Subject: [PATCH 0531/1452] Add typing to deCONZ Fan and Light platforms (#59607) --- homeassistant/components/deconz/fan.py | 43 ++++++----- homeassistant/components/deconz/light.py | 95 ++++++++++++++---------- 2 files changed, 81 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 40862bfcde1..d1ff85f9d65 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,6 +1,9 @@ """Support for deCONZ fans.""" from __future__ import annotations +from collections.abc import ValuesView +from typing import Any + from pydeconz.light import ( FAN_SPEED_25_PERCENT, FAN_SPEED_50_PERCENT, @@ -19,15 +22,17 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.core import callback +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 homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry ORDERED_NAMED_FAN_SPEEDS = [ FAN_SPEED_25_PERCENT, @@ -50,13 +55,19 @@ LEGACY_DECONZ_TO_SPEED = { } -async def async_setup_entry(hass, config_entry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up fans for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_fan(lights=gateway.api.lights.values()) -> None: + def async_add_fan( + lights: list[Fan] | ValuesView[Fan] = gateway.api.lights.values(), + ) -> None: """Add fan from deCONZ.""" entities = [] @@ -86,8 +97,11 @@ class DeconzFan(DeconzDevice, FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN + _device: Fan - def __init__(self, device, gateway) -> None: + _attr_supported_features = SUPPORT_SET_SPEED + + def __init__(self, device: Fan, gateway: DeconzGateway) -> None: """Set up fan.""" super().__init__(device, gateway) @@ -95,12 +109,10 @@ class DeconzFan(DeconzDevice, FanEntity): if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed - self._attr_supported_features = SUPPORT_SET_SPEED - @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != FAN_SPEED_OFF + return self._device.speed != FAN_SPEED_OFF # type: ignore[no-any-return] @property def percentage(self) -> int | None: @@ -153,11 +165,6 @@ class DeconzFan(DeconzDevice, FanEntity): SPEED_MEDIUM, ) - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._attr_supported_features - @callback def async_update_callback(self) -> None: """Store latest configured speed from the device.""" @@ -183,10 +190,10 @@ class DeconzFan(DeconzDevice, FanEntity): async def async_turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on fan.""" new_speed = self._default_on_speed @@ -198,6 +205,6 @@ class DeconzFan(DeconzDevice, FanEntity): await self._device.set_speed(new_speed) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" await self._device.set_speed(FAN_SPEED_OFF) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 6bb4f5c5b00..e287d574633 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections.abc import ValuesView +from typing import Any + from pydeconz.group import DeconzGroup as Group from pydeconz.light import ( ALERT_LONG, @@ -33,27 +36,35 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import color_hs_to_xy from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_GROUP = "is_deconz_group" EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: EFFECT_COLOR_LOOP, "None": EFFECT_NONE} FLASH_TO_DECONZ = {FLASH_SHORT: ALERT_SHORT, FLASH_LONG: ALERT_LONG} -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_light(lights=gateway.api.lights.values()): + def async_add_light( + lights: list[Light] | ValuesView[Light] = gateway.api.lights.values(), + ) -> None: """Add light from deCONZ.""" entities = [] @@ -77,7 +88,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) @callback - def async_add_group(groups=gateway.api.groups.values()): + def async_add_group( + groups: list[Group] | ValuesView[Group] = gateway.api.groups.values(), + ) -> None: """Add group from deCONZ.""" if not gateway.option_allow_deconz_groups: return @@ -113,11 +126,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): TYPE = DOMAIN - def __init__(self, device, gateway): + def __init__(self, device: Group | Light, gateway: DeconzGateway) -> None: """Set up light.""" super().__init__(device, gateway) - self._attr_supported_color_modes = set() + self._attr_supported_color_modes: set[str] = set() if device.color_temp is not None: self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -158,83 +171,83 @@ class DeconzBaseLight(DeconzDevice, LightEntity): return color_mode @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" - return self._device.brightness + return self._device.brightness # type: ignore[no-any-return] @property - def color_temp(self): + def color_temp(self) -> int: """Return the CT color value.""" - return self._device.color_temp + return self._device.color_temp # type: ignore[no-any-return] @property - def hs_color(self) -> tuple: + def hs_color(self) -> tuple[float, float]: """Return the hs color value.""" return (self._device.hue / 65535 * 360, self._device.saturation / 255 * 100) @property - def xy_color(self) -> tuple | None: + def xy_color(self) -> tuple[float, float] | None: """Return the XY color value.""" - return self._device.xy + return self._device.xy # type: ignore[no-any-return] @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self._device.state + return self._device.state # type: ignore[no-any-return] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" - data = {"on": True} + data: dict[str, bool | float | int | str | tuple[float, float]] = {"on": True} - if ATTR_BRIGHTNESS in kwargs: - data["brightness"] = kwargs[ATTR_BRIGHTNESS] + if attr_brightness := kwargs.get(ATTR_BRIGHTNESS): + data["brightness"] = attr_brightness - if ATTR_COLOR_TEMP in kwargs: - data["color_temperature"] = kwargs[ATTR_COLOR_TEMP] + if attr_color_temp := kwargs.get(ATTR_COLOR_TEMP): + data["color_temperature"] = attr_color_temp - if ATTR_HS_COLOR in kwargs: + if attr_hs_color := kwargs.get(ATTR_HS_COLOR): if COLOR_MODE_XY in self._attr_supported_color_modes: - data["xy"] = color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + data["xy"] = color_hs_to_xy(*attr_hs_color) else: - data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - data["saturation"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + data["hue"] = int(attr_hs_color[0] / 360 * 65535) + data["saturation"] = int(attr_hs_color[1] / 100 * 255) if ATTR_XY_COLOR in kwargs: data["xy"] = kwargs[ATTR_XY_COLOR] - if ATTR_TRANSITION in kwargs: - data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) + if attr_transition := kwargs.get(ATTR_TRANSITION): + data["transition_time"] = int(attr_transition * 10) elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] - if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None: data["effect"] = effect await self._device.set_state(**data) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" if not self._device.state: return - data = {"on": False} + data: dict[str, bool | int | str] = {"on": False} if ATTR_TRANSITION in kwargs: data["brightness"] = 0 data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] await self._device.set_state(**data) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" return {DECONZ_GROUP: isinstance(self._device, Group)} @@ -242,13 +255,15 @@ class DeconzBaseLight(DeconzDevice, LightEntity): class DeconzLight(DeconzBaseLight): """Representation of a deCONZ light.""" + _device: Light + @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return self._device.max_color_temp or super().max_mireds @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return self._device.min_color_temp or super().min_mireds @@ -256,13 +271,15 @@ class DeconzLight(DeconzBaseLight): class DeconzGroup(DeconzBaseLight): """Representation of a deCONZ group.""" - def __init__(self, device, gateway): + _device: Group + + def __init__(self, device: Group, gateway: DeconzGateway) -> None: """Set up group and create an unique id.""" self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" super().__init__(device, gateway) @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device.""" return self._unique_id @@ -278,7 +295,7 @@ class DeconzGroup(DeconzBaseLight): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" attributes = dict(super().extra_state_attributes) attributes["all_on"] = self._device.all_on From f1d75f0dd7673df59f55118fba1a6462da45014e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 16 Nov 2021 12:40:54 +0100 Subject: [PATCH 0532/1452] Use source list property instead of the attribute in Denon AVR integration (#59768) --- homeassistant/components/denonavr/media_player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 39186680e09..c3106a98c72 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -155,7 +155,6 @@ class DenonDevice(MediaPlayerEntity): name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list - self._attr_source_list = receiver.input_func_list self._receiver = receiver self._update_audyssey = update_audyssey @@ -246,6 +245,11 @@ class DenonDevice(MediaPlayerEntity): """Return the state of the device.""" return self._receiver.state + @property + def source_list(self): + """Return a list of available input sources.""" + return self._receiver.input_func_list + @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" From 4387bbfb949fba2c7dead76ab2d824dbe36da085 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Nov 2021 13:30:38 +0100 Subject: [PATCH 0533/1452] Adjust async_step_mqtt signature for strict typing (#59761) * Add MqttServiceInfo * Adjust async_step_mqtt signature * Adjust async_step_mqtt signature * Adjust components Co-authored-by: epenet --- homeassistant/components/mqtt/discovery.py | 30 ++++++++++++++----- .../components/tasmota/config_flow.py | 5 ++-- homeassistant/config_entries.py | 5 ++-- homeassistant/helpers/config_entry_flow.py | 11 ++++++- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d490374ed53..7aec80b2e9c 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,11 +1,13 @@ """Support for MQTT discovery.""" import asyncio from collections import deque +import datetime as dt import functools import json import logging import re import time +from typing import TypedDict from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -27,6 +29,7 @@ from .const import ( CONF_TOPIC, DOMAIN, ) +from .models import ReceivePayloadType _LOGGER = logging.getLogger(__name__) @@ -86,6 +89,17 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" +class MqttServiceInfo(TypedDict): + """Prepared info from mqtt entries.""" + + topic: str + payload: ReceivePayloadType + qos: int + retain: bool + subscribed_topic: str + timestamp: dt.datetime + + async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None ) -> None: @@ -288,14 +302,14 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return - data = { - "topic": msg.topic, - "payload": msg.payload, - "qos": msg.qos, - "retain": msg.retain, - "subscribed_topic": msg.subscribed_topic, - "timestamp": msg.timestamp, - } + data = MqttServiceInfo( + topic=msg.topic, + payload=msg.payload, + qos=msg.qos, + retain=msg.retain, + subscribed_topic=msg.subscribed_topic, + timestamp=msg.timestamp, + ) result = await hass.config_entries.flow.async_init( integration, context={"source": DOMAIN}, data=data ) diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index 435604b4bdd..9c22934678e 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,9 +6,8 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import valid_subscribe_topic +from homeassistant.components.mqtt import discovery as mqtt, valid_subscribe_topic from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN @@ -22,7 +21,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize flow.""" self._prefix = DEFAULT_PREFIX - async def async_step_mqtt(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: """Handle a flow initialized by MQTT discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 85215a5962f..e1e4c103dc4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -33,6 +33,7 @@ import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: from homeassistant.components.dhcp import DhcpServiceInfo + from homeassistant.components.mqtt.discovery import MqttServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1361,10 +1362,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_mqtt( - self, discovery_info: DiscoveryInfoType + self, discovery_info: MqttServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_ssdp( self, discovery_info: DiscoveryInfoType diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 75e3d128435..939394a243a 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,6 +6,7 @@ from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf +from homeassistant.components.mqtt import discovery as mqtt from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType @@ -102,6 +103,15 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() + async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: + """Handle a flow initialized by mqtt discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: @@ -114,7 +124,6 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() async_step_ssdp = async_step_discovery - async_step_mqtt = async_step_discovery async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: """Handle a flow initialized by import.""" From 8a02d87a174d00364cf94c1288e68ac5e61c2909 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 16 Nov 2021 16:11:46 +0100 Subject: [PATCH 0534/1452] Removed deprecated "device_state_attributes" for Amberelectric (#59672) * Removed deprecated "device_state_attributes" * Cleanup * Postpone removal * log deprecation warning * Update homeassistant/helpers/entity.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/amberelectric/binary_sensor.py | 2 +- homeassistant/components/amberelectric/sensor.py | 4 ++-- homeassistant/helpers/entity.py | 10 +++++++++- tests/components/amberelectric/test_sensor.py | 1 - 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index fe6edea18f8..422ff66db59 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -61,7 +61,7 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor): return self.coordinator.data["grid"]["price_spike"] == "spike" @property - def device_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional pieces of information about the price spike.""" spike_status = self.coordinator.data["grid"]["price_spike"] diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index a1644fb7924..7cee95dcfcf 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -86,7 +86,7 @@ class AmberPriceSensor(AmberSensor): return format_cents_to_dollars(interval.per_kwh) @property - def device_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional pieces of information about the price.""" interval = self.coordinator.data[self.entity_description.key][self.channel_type] @@ -133,7 +133,7 @@ class AmberForecastSensor(AmberSensor): return format_cents_to_dollars(interval.per_kwh) @property - def device_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional pieces of information about the price.""" intervals = self.coordinator.data[self.entity_description.key].get( self.channel_type diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9ee7221ffa6..650ce0701a6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -515,7 +515,15 @@ class Entity(ABC): attr.update(self.state_attributes or {}) extra_state_attributes = self.extra_state_attributes # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 - # Add warning in 2021.6, remove in 2021.10 + # Warning added in 2021.12, will be removed in 2022.4 + if self.device_state_attributes is not None: + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) implements device_state_attributes. Please %s", + self.entity_id, + type(self), + report_issue, + ) if extra_state_attributes is None: extra_state_attributes = self.device_state_attributes attr.update(extra_state_attributes or {}) diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index ccfcd82b3bd..deafcb70fb7 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -164,7 +164,6 @@ async def test_general_and_feed_in_price_sensor( ) -> None: """Test the Feed In sensor.""" assert len(hass.states.async_all()) == 6 - print(hass.states) price = hass.states.get("sensor.mock_title_feed_in_price") assert price assert price.state == "-0.08" From 1bcd62cd32ff84d3a56d6bfd88589410e1eb9891 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Wed, 17 Nov 2021 01:13:54 +1000 Subject: [PATCH 0535/1452] Add topic_template for mqtt.publish (#53743) Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 50 +++++++++--- tests/components/mqtt/test_init.py | 97 +++++++++++++++++++++++ 2 files changed, 137 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f42663ac4a8..efa14388a58 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -106,6 +106,7 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_TLS_PROTOCOL = "auto" +ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" MAX_RECONNECT_WAIT = 300 # seconds @@ -220,15 +221,19 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( ) # Service call validation schema -MQTT_PUBLISH_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, - vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, +MQTT_PUBLISH_SCHEMA = vol.All( + vol.Schema( + { + vol.Exclusive(ATTR_TOPIC, CONF_TOPIC): valid_publish_topic, + vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string, + vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, + ), + cv.has_at_least_one_key(ATTR_TOPIC, ATTR_TOPIC_TEMPLATE), ) @@ -450,11 +455,36 @@ async def async_setup_entry(hass, entry): async def async_publish_service(call: ServiceCall): """Handle MQTT publish service calls.""" - msg_topic: str = call.data[ATTR_TOPIC] + msg_topic = call.data.get(ATTR_TOPIC) + msg_topic_template = call.data.get(ATTR_TOPIC_TEMPLATE) payload = call.data.get(ATTR_PAYLOAD) payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE) qos: int = call.data[ATTR_QOS] retain: bool = call.data[ATTR_RETAIN] + if msg_topic_template is not None: + try: + rendered_topic = template.Template( + msg_topic_template, hass + ).async_render(parse_result=False) + msg_topic = valid_publish_topic(rendered_topic) + except (template.jinja2.TemplateError, TemplateError) as exc: + _LOGGER.error( + "Unable to publish: rendering topic template of %s " + "failed because %s", + msg_topic_template, + exc, + ) + return + except vol.Invalid as err: + _LOGGER.error( + "Unable to publish: topic template '%s' produced an " + "invalid topic '%s' after rendering (%s)", + msg_topic_template, + rendered_topic, + err, + ) + return + if payload_template is not None: try: payload = template.Template(payload_template, hass).async_render( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 26ceb583818..9b862e38a7c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -150,6 +150,103 @@ async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): assert not mqtt_mock.async_publish.called +async def test_service_call_with_topic_and_topic_template_does_not_publish( + hass, mqtt_mock +): + """Test the service call with topic/topic template. + + If both 'topic' and 'topic_template' are provided then fail. + """ + topic = "test/topic" + topic_template = "test/{{ 'topic' }}" + with pytest.raises(vol.Invalid): + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: topic, + mqtt.ATTR_TOPIC_TEMPLATE: topic_template, + mqtt.ATTR_PAYLOAD: "payload", + }, + blocking=True, + ) + assert not mqtt_mock.async_publish.called + + +async def test_service_call_with_invalid_topic_template_does_not_publish( + hass, mqtt_mock +): + """Test the service call with a problematic topic template.""" + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1 | no_such_filter }}", + mqtt.ATTR_PAYLOAD: "payload", + }, + blocking=True, + ) + assert not mqtt_mock.async_publish.called + + +async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock): + """Test the service call with rendered topic template. + + If 'topic_template' is provided and 'topic' is not, then render it. + """ + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1+1 }}", + mqtt.ATTR_PAYLOAD: "payload", + }, + blocking=True, + ) + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][0] == "test/2" + + +async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt_mock): + """Test the service call with rendered, invalid topic template. + + If a wildcard topic is rendered, then fail. + """ + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ '+' if True else 'topic' }}/topic", + mqtt.ATTR_PAYLOAD: "payload", + }, + blocking=True, + ) + assert not mqtt_mock.async_publish.called + + +async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_template( + hass, mqtt_mock +): + """Test the service call with unrendered template. + + If both 'payload' and 'payload_template' are provided then fail. + """ + payload = "not a template" + payload_template = "a template" + with pytest.raises(vol.Invalid): + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_PAYLOAD: payload, + mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template, + }, + blocking=True, + ) + assert not mqtt_mock.async_publish.called + + async def test_service_call_with_template_payload_renders_template(hass, mqtt_mock): """Test the service call with rendered template. From e9c8de25df1a836b398d731809de7c6f157a9ce4 Mon Sep 17 00:00:00 2001 From: Khole Date: Tue, 16 Nov 2021 15:18:09 +0000 Subject: [PATCH 0536/1452] Add Hive Alarm Support (#59670) * Add alarm support * Update code coverage * Update homeassistant/components/hive/alarm_control_panel.py Co-authored-by: Allen Porter * Add alarm support * Update code coverage * Update homeassistant/components/hive/alarm_control_panel.py Co-authored-by: Allen Porter * Update icon and device info Co-authored-by: Allen Porter --- .coveragerc | 3 +- .../components/hive/alarm_control_panel.py | 100 ++++++++++++++++++ homeassistant/components/hive/const.py | 11 +- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/hive/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index 872e707a864..55a435a7e50 100644 --- a/.coveragerc +++ b/.coveragerc @@ -431,8 +431,9 @@ omit = homeassistant/components/hisense_aehw4a1/climate.py homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/__init__.py - homeassistant/components/hive/climate.py + homeassistant/components/hive/alarm_control_panel.py homeassistant/components/hive/binary_sensor.py + homeassistant/components/hive/climate.py homeassistant/components/hive/light.py homeassistant/components/hive/sensor.py homeassistant/components/hive/switch.py diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py new file mode 100644 index 00000000000..a17d71f51ab --- /dev/null +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -0,0 +1,100 @@ +"""Support for the Hive alarm.""" +from datetime import timedelta + +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.helpers.entity import DeviceInfo + +from . import HiveEntity +from .const import DOMAIN + +ICON = "mdi:security" +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) +HIVETOHA = { + "home": STATE_ALARM_DISARMED, + "asleep": STATE_ALARM_ARMED_NIGHT, + "away": STATE_ALARM_ARMED_AWAY, +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" + + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("alarm_control_panel") + if devices: + async_add_entities( + [HiveAlarmControlPanelEntity(hive, dev) for dev in devices], True + ) + + +class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): + """Representation of a Hive alarm.""" + + _attr_icon = ICON + + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this AdGuard Home instance.""" + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) + + @property + def name(self): + """Return the name of the alarm.""" + return self.device["haName"] + + @property + def available(self): + """Return if the device is available.""" + return self.device["deviceData"]["online"] + + @property + def state(self): + """Return state of alarm.""" + if self.device["status"]["state"]: + return STATE_ALARM_TRIGGERED + return HIVETOHA[self.device["status"]["mode"]] + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_AWAY + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self.hive.alarm.setMode(self.device, "home") + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + await self.hive.alarm.setMode(self.device, "asleep") + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + await self.hive.alarm.setMode(self.device, "away") + + async def async_update(self): + """Update all Node data from Hive.""" + await self.hive.session.updateData(self.device) + self.device = await self.hive.alarm.getAlarm(self.device) diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index 9e1d7fc1f80..f24ed0f7b24 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -6,8 +6,17 @@ CONF_CODE = "2fa" CONFIG_ENTRY_VERSION = 1 DEFAULT_NAME = "Hive" DOMAIN = "hive" -PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch", "water_heater"] +PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "climate", + "light", + "sensor", + "switch", + "water_heater", +] PLATFORM_LOOKUP = { + "alarm_control_panel": "alarm_control_panel", "binary_sensor": "binary_sensor", "climate": "climate", "light": "light", From 14adcbc07c7cb0ffe26942d56d937e34c0870db4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 16 Nov 2021 10:18:30 -0500 Subject: [PATCH 0537/1452] Add tests for goalzero (#59446) * Add tests for goalzero * tweak * tweak --- .coveragerc | 4 - homeassistant/components/goalzero/__init__.py | 5 +- .../components/goalzero/config_flow.py | 4 +- homeassistant/components/goalzero/const.py | 2 +- tests/components/goalzero/__init__.py | 89 +++++++++--- .../goalzero/fixtures/info_data.json | 7 + .../goalzero/fixtures/state_change.json | 38 +++++ .../goalzero/fixtures/state_data.json | 38 +++++ .../components/goalzero/test_binary_sensor.py | 36 +++++ tests/components/goalzero/test_config_flow.py | 133 +++++++----------- tests/components/goalzero/test_init.py | 80 +++++++++++ tests/components/goalzero/test_sensor.py | 112 +++++++++++++++ tests/components/goalzero/test_switch.py | 51 +++++++ 13 files changed, 491 insertions(+), 108 deletions(-) create mode 100644 tests/components/goalzero/fixtures/info_data.json create mode 100644 tests/components/goalzero/fixtures/state_change.json create mode 100644 tests/components/goalzero/fixtures/state_data.json create mode 100644 tests/components/goalzero/test_binary_sensor.py create mode 100644 tests/components/goalzero/test_init.py create mode 100644 tests/components/goalzero/test_sensor.py create mode 100644 tests/components/goalzero/test_switch.py diff --git a/.coveragerc b/.coveragerc index 55a435a7e50..6750675da26 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,10 +386,6 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* - homeassistant/components/goalzero/__init__.py - homeassistant/components/goalzero/binary_sensor.py - homeassistant/components/goalzero/sensor.py - homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 774f1fd0e21..e03aa25f8f9 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -25,6 +26,7 @@ from .const import ( DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, + MANUFACTURER, MIN_TIME_BETWEEN_UPDATES, ) @@ -101,8 +103,9 @@ class YetiEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.api.sysdata["macAddress"])}, identifiers={(DOMAIN, self._server_unique_id)}, - manufacturer="Goal Zero", + manufacturer=MANUFACTURER, model=self.api.sysdata[ATTR_MODEL], name=self._name, sw_version=self.api.data["firmwareVersion"], diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 4b02fe1e83a..50927dd7729 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -14,7 +14,7 @@ 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 .const import DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Allow the user to confirm adding the device.""" if user_input is not None: return self.async_create_entry( - title="Goal Zero", + title=MANUFACTURER, data={ CONF_HOST: self.ip_address, CONF_NAME: DEFAULT_NAME, diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index d99cacb253e..fef1636005d 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -8,5 +8,5 @@ DATA_KEY_COORDINATOR = "coordinator" DOMAIN = "goalzero" DEFAULT_NAME = "Yeti" DATA_KEY_API = "api" - +MANUFACTURER = "Goal Zero" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 1b5302dbc1b..890cd75fcd4 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,42 +1,95 @@ """Tests for the Goal Zero Yeti integration.""" - from unittest.mock import AsyncMock, patch from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.goalzero import DOMAIN +from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker HOST = "1.2.3.4" -NAME = "Yeti" +MAC = "aa:bb:cc:dd:ee:ff" CONF_DATA = { CONF_HOST: HOST, - CONF_NAME: NAME, -} - -CONF_CONFIG_FLOW = { - CONF_HOST: HOST, - CONF_NAME: NAME, + CONF_NAME: DEFAULT_NAME, } CONF_DHCP_FLOW = { - IP_ADDRESS: "1.1.1.1", - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "any", + IP_ADDRESS: HOST, + MAC_ADDRESS: format_mac("AA:BB:CC:DD:EE:FF"), + HOSTNAME: "yeti", } -async def _create_mocked_yeti(raise_exception=False): +def create_entry(hass: HomeAssistant): + """Add config entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + unique_id=MAC, + ) + entry.add_to_hass(hass) + return entry + + +async def create_mocked_yeti(): + """Create mocked yeti device.""" mocked_yeti = AsyncMock() - mocked_yeti.get_state = AsyncMock() + mocked_yeti.data = {} + mocked_yeti.data["firmwareVersion"] = "1.0.0" + mocked_yeti.sysdata = {} + mocked_yeti.sysdata["model"] = "test_model" + mocked_yeti.sysdata["macAddress"] = MAC return mocked_yeti -def _patch_init_yeti(mocked_yeti): - return patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti) - - -def _patch_config_flow_yeti(mocked_yeti): +def patch_config_flow_yeti(mocked_yeti): + """Patch Goal Zero config flow.""" return patch( "homeassistant.components.goalzero.config_flow.Yeti", return_value=mocked_yeti, ) + + +async def async_init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Goal Zero integration in Home Assistant.""" + entry = create_entry(hass) + base_url = f"http://{HOST}/" + aioclient_mock.get( + f"{base_url}state", + text=load_fixture("goalzero/state_data.json"), + ) + aioclient_mock.get( + f"{base_url}sysinfo", + text=load_fixture("goalzero/info_data.json"), + ) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def async_setup_platform( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + platform: str, +): + """Set up the platform.""" + entry = await async_init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.goalzero.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + + return entry diff --git a/tests/components/goalzero/fixtures/info_data.json b/tests/components/goalzero/fixtures/info_data.json new file mode 100644 index 00000000000..6be95e6c482 --- /dev/null +++ b/tests/components/goalzero/fixtures/info_data.json @@ -0,0 +1,7 @@ +{ + "name":"yeti123456789012", + "model":"Yeti 1400", + "firmwareVersion":"1.5.7", + "macAddress":"123456789012", + "platform":"esp32" +} \ No newline at end of file diff --git a/tests/components/goalzero/fixtures/state_change.json b/tests/components/goalzero/fixtures/state_change.json new file mode 100644 index 00000000000..301a27da954 --- /dev/null +++ b/tests/components/goalzero/fixtures/state_change.json @@ -0,0 +1,38 @@ +{ + "thingName":"yeti123456789012", + "v12PortStatus":1, + "usbPortStatus":0, + "acPortStatus":1, + "backlight":1, + "app_online":0, + "wattsIn":0.0, + "ampsIn":0.0, + "wattsOut":50.5, + "ampsOut":2.1, + "whOut":5.23, + "whStored":1330, + "volts":12.0, + "socPercent":95, + "isCharging":0, + "inputDetected":0, + "timeToEmptyFull":-1, + "temperature":25, + "wifiStrength":-62, + "ssid":"wifi", + "ipAddr":"1.2.3.4", + "timestamp":1720984, + "firmwareVersion":"1.5.7", + "version":3, + "ota":{ + "delay":0, + "status":"000-000-100_001-000-100_002-000-100_003-000-100" + }, + "notify":{ + "enabled":1048575, + "trigger":0 + }, + "foreignAcsry":{ + "model":"Yeti MPPT", + "firmwareVersion":"1.1.2" + } +} \ No newline at end of file diff --git a/tests/components/goalzero/fixtures/state_data.json b/tests/components/goalzero/fixtures/state_data.json new file mode 100644 index 00000000000..455524584f7 --- /dev/null +++ b/tests/components/goalzero/fixtures/state_data.json @@ -0,0 +1,38 @@ +{ + "thingName":"yeti123456789012", + "v12PortStatus":0, + "usbPortStatus":0, + "acPortStatus":1, + "backlight":1, + "app_online":0, + "wattsIn":0.0, + "ampsIn":0.0, + "wattsOut":50.5, + "ampsOut":2.1, + "whOut":5.23, + "whStored":1330, + "volts":12.0, + "socPercent":95, + "isCharging":0, + "inputDetected":0, + "timeToEmptyFull":-1, + "temperature":25, + "wifiStrength":-62, + "ssid":"wifi", + "ipAddr":"1.2.3.4", + "timestamp":1720984, + "firmwareVersion":"1.5.7", + "version":3, + "ota":{ + "delay":0, + "status":"000-000-100_001-000-100_002-000-100_003-000-100" + }, + "notify":{ + "enabled":1048575, + "trigger":0 + }, + "foreignAcsry":{ + "model":"Yeti MPPT", + "firmwareVersion":"1.1.2" + } +} \ No newline at end of file diff --git a/tests/components/goalzero/test_binary_sensor.py b/tests/components/goalzero/test_binary_sensor.py new file mode 100644 index 00000000000..1c130013283 --- /dev/null +++ b/tests/components/goalzero/test_binary_sensor.py @@ -0,0 +1,36 @@ +"""Binary sensor tests for the Goalzero integration.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CONNECTIVITY, + DOMAIN, +) +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEVICE_CLASS_POWER, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from . import async_setup_platform + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test we get sensor data.""" + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_backlight") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_app_online") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CONNECTIVITY + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_charging") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY_CHARGING + state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_input_detected") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 838a0f8a124..669cace729b 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -3,181 +3,150 @@ from unittest.mock import patch from goalzero import exceptions -from homeassistant.components.goalzero.const import DOMAIN +from homeassistant import data_entry_flow +from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.core import HomeAssistant from . import ( - CONF_CONFIG_FLOW, CONF_DATA, CONF_DHCP_FLOW, - CONF_HOST, - CONF_NAME, - NAME, - _create_mocked_yeti, - _patch_config_flow_yeti, + MAC, + create_entry, + create_mocked_yeti, + patch_config_flow_yeti, ) -from tests.common import MockConfigEntry - - -def _flow_next(hass, flow_id): - return next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == flow_id - ) - def _patch_setup(): - return patch( - "homeassistant.components.goalzero.async_setup_entry", - return_value=True, - ) + return patch("homeassistant.components.goalzero.async_setup_entry") -async def test_flow_user(hass): +async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" - mocked_yeti = await _create_mocked_yeti() - with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): + mocked_yeti = await create_mocked_yeti() + with patch_config_flow_yeti(mocked_yeti), _patch_setup(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW, + user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA + assert result["result"].unique_id == MAC -async def test_flow_user_already_configured(hass): +async def test_flow_user_already_configured(hass: HomeAssistant): """Test user initialized flow with duplicate server.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.2.3.4", CONF_NAME: "Yeti"}, - ) - - entry.add_to_hass(hass) - - service_info = { - "host": "1.2.3.4", - "name": "Yeti", - } + create_entry(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=service_info + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_flow_user_cannot_connect(hass): +async def test_flow_user_cannot_connect(hass: HomeAssistant): """Test user initialized flow with unreachable server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with patch_config_flow_yeti(await create_mocked_yeti()) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"]["base"] == "cannot_connect" -async def test_flow_user_invalid_host(hass): +async def test_flow_user_invalid_host(hass: HomeAssistant): """Test user initialized flow with invalid server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with patch_config_flow_yeti(await create_mocked_yeti()) as yetimock: yetimock.side_effect = exceptions.InvalidHost result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_host"} + assert result["errors"]["base"] == "invalid_host" -async def test_flow_user_unknown_error(hass): +async def test_flow_user_unknown_error(hass: HomeAssistant): """Test user initialized flow with unreachable server.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with patch_config_flow_yeti(await create_mocked_yeti()) as yetimock: yetimock.side_effect = Exception result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "unknown"} + assert result["errors"]["base"] == "unknown" -async def test_dhcp_discovery(hass): +async def test_dhcp_discovery(hass: HomeAssistant): """Test we can process the discovery from dhcp.""" - mocked_yeti = await _create_mocked_yeti() - with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): + mocked_yeti = await create_mocked_yeti() + with patch_config_flow_yeti(mocked_yeti), _patch_setup(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_NAME: "Yeti", - } + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MANUFACTURER + assert result["data"] == CONF_DATA + assert result["result"].unique_id == MAC result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_dhcp_discovery_failed(hass): +async def test_dhcp_discovery_failed(hass: HomeAssistant): """Test failed setup from dhcp.""" - mocked_yeti = await _create_mocked_yeti(True) - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + mocked_yeti = await create_mocked_yeti() + with patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.ConnectError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = exceptions.InvalidHost result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "invalid_host" - with _patch_config_flow_yeti(mocked_yeti) as yetimock: + with patch_config_flow_yeti(mocked_yeti) as yetimock: yetimock.side_effect = Exception result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unknown" diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py new file mode 100644 index 00000000000..a436b491d48 --- /dev/null +++ b/tests/components/goalzero/test_init.py @@ -0,0 +1,80 @@ +"""Test Goal Zero integration.""" +from datetime import timedelta +from unittest.mock import patch + +from goalzero import exceptions + +from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +import homeassistant.util.dt as dt_util + +from . import CONF_DATA, async_init_integration, create_entry, create_mocked_yeti + +from tests.common import async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup_config_and_unload(hass: HomeAssistant): + """Test Goal Zero setup and unload.""" + entry = create_entry(hass) + mocked_yeti = await create_mocked_yeti() + with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.data == CONF_DATA + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = create_entry(hass) + with patch( + "homeassistant.components.goalzero.Yeti.init_connect", + side_effect=exceptions.ConnectError, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_update_failed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test data update failure.""" + await async_init_integration(hass, aioclient_mock) + assert hass.states.get(f"switch.{DEFAULT_NAME}_ac_port_status").state == STATE_ON + with patch( + "homeassistant.components.goalzero.Yeti.get_state", + side_effect=exceptions.ConnectError, + ) as updater: + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + updater.assert_called_once() + state = hass.states.get(f"switch.{DEFAULT_NAME}_ac_port_status") + assert state.state == STATE_UNAVAILABLE + + +async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test device info.""" + entry = await async_init_integration(hass, aioclient_mock) + device_registry = await dr.async_get_registry(hass) + + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.connections == {("mac", "12:34:56:78:90:12")} + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == MANUFACTURER + assert device.model == "Yeti 1400" + assert device.name == DEFAULT_NAME + assert device.sw_version == "1.5.7" diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py new file mode 100644 index 00000000000..592c43b5d43 --- /dev/null +++ b/tests/components/goalzero/test_sensor.py @@ -0,0 +1,112 @@ +"""Sensor tests for the Goalzero integration.""" +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.components.goalzero.sensor import SENSOR_TYPES +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + 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 . import async_setup_platform + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test we get sensor data.""" + for description in SENSOR_TYPES: + description.entity_registry_enabled_default = True + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") + assert state.state == "0.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") + assert state.state == "0.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") + assert state.state == "50.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") + assert state.state == "2.1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") + assert state.state == "5.23" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") + assert state.state == "1330" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") + assert state.state == "12.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") + assert state.state == "95" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") + assert state.state == "-1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") + assert state.state == "25" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") + assert state.state == "-62" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") + assert state.state == "1720984" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid") + assert state.state == "wifi" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get(f"sensor.{DEFAULT_NAME}_ip_address") + assert state.state == "1.2.3.4" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None diff --git a/tests/components/goalzero/test_switch.py b/tests/components/goalzero/test_switch.py new file mode 100644 index 00000000000..4c625653033 --- /dev/null +++ b/tests/components/goalzero/test_switch.py @@ -0,0 +1,51 @@ +"""Switch tests for the Goalzero integration.""" +from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.components.switch import DOMAIN as DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from . import async_setup_platform + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switches_states( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test we get sensor data.""" + await async_setup_platform(hass, aioclient_mock, DOMAIN) + + assert hass.states.get(f"switch.{DEFAULT_NAME}_usb_port_status").state == STATE_OFF + assert hass.states.get(f"switch.{DEFAULT_NAME}_ac_port_status").state == STATE_ON + entity_id = f"switch.{DEFAULT_NAME}_12v_port_status" + assert hass.states.get(entity_id).state == STATE_OFF + aioclient_mock.post( + "http://1.2.3.4/state", + text=load_fixture("goalzero/state_change.json"), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_ON + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://1.2.3.4/state", + text=load_fixture("goalzero/state_data.json"), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_OFF From fa952364cc30465315b13512f105a22d8b5d1bfb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Nov 2021 16:54:08 +0100 Subject: [PATCH 0538/1452] Remove test_check_package_version_does_not_match (#59785) --- tests/util/test_package.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 3006cb17c37..d6b7402a5b6 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -251,13 +251,6 @@ def test_check_package_global(): assert not package.is_installed(f"{installed_package}<{installed_version}") -def test_check_package_version_does_not_match(): - """Test for version mismatch.""" - installed_package = list(pkg_resources.working_set)[0].project_name - assert not package.is_installed(f"{installed_package}==999.999.999") - assert not package.is_installed(f"{installed_package}>=999.999.999") - - def test_check_package_zip(): """Test for an installed zip package.""" assert not package.is_installed(TEST_ZIP_REQ) From 16027b9f43d4f383b4d9c3724103045ac8ef7f87 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 16 Nov 2021 11:16:24 -0500 Subject: [PATCH 0539/1452] Fix mqtt undefined error (#59787) --- homeassistant/components/mqtt/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index efa14388a58..b99036efbe3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -60,6 +60,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_TOPIC, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, DEFAULT_BIRTH, From c7c1d6000f2205710887de95cc41605c3e2a3096 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 16 Nov 2021 17:25:56 +0100 Subject: [PATCH 0540/1452] Add type hints to Siren and Switch deCONZ platforms (#59602) * Add typing to Siren and Switch deCONZ platforms * Update homeassistant/components/deconz/switch.py Co-authored-by: Matthias Alphart * Update homeassistant/components/deconz/switch.py Co-authored-by: Matthias Alphart * Update homeassistant/components/deconz/switch.py Co-authored-by: Matthias Alphart * Update homeassistant/components/deconz/siren.py Co-authored-by: Matthias Alphart * Update homeassistant/components/deconz/siren.py Co-authored-by: Matthias Alphart * Add Any import * Update homeassistant/components/deconz/siren.py Co-authored-by: Matthias Alphart Co-authored-by: Matthias Alphart --- homeassistant/components/deconz/siren.py | 32 ++++++++++++++++------- homeassistant/components/deconz/switch.py | 30 +++++++++++++++------ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index c3679b6ad89..7f0f6cc39ed 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -1,5 +1,10 @@ """Support for deCONZ siren.""" +from __future__ import annotations + +from collections.abc import ValuesView +from typing import Any + from pydeconz.light import Siren from homeassistant.components.siren import ( @@ -10,20 +15,28 @@ from homeassistant.components.siren import ( SUPPORT_TURN_ON, SirenEntity, ) -from homeassistant.core import callback +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 .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up sirens for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_siren(lights=gateway.api.lights.values()): + def async_add_siren( + lights: list[Siren] | ValuesView[Siren] = gateway.api.lights.values(), + ) -> None: """Add siren from deCONZ.""" entities = [] @@ -53,8 +66,9 @@ class DeconzSiren(DeconzDevice, SirenEntity): """Representation of a deCONZ siren.""" TYPE = DOMAIN + _device: Siren - def __init__(self, device, gateway) -> None: + def __init__(self, device: Siren, gateway: DeconzGateway) -> None: """Set up siren.""" super().__init__(device, gateway) @@ -63,17 +77,17 @@ class DeconzSiren(DeconzDevice, SirenEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if siren is on.""" - return self._device.is_on + return self._device.is_on # type: ignore[no-any-return] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on siren.""" data = {} if (duration := kwargs.get(ATTR_DURATION)) is not None: data["duration"] = duration * 10 await self._device.turn_on(**data) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off siren.""" await self._device.turn_off() diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 39489fe1fc3..ab4577a427c 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -1,18 +1,29 @@ """Support for deCONZ switches.""" -from pydeconz.light import Siren +from __future__ import annotations + +from collections.abc import ValuesView +from typing import Any + +from pydeconz.light import Light, Siren from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up switches for deCONZ component. Switches are based on the same device class as lights in deCONZ. @@ -32,7 +43,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_registry.async_remove(entity_id) @callback - def async_add_switch(lights=gateway.api.lights.values()): + def async_add_switch( + lights: list[Light] | ValuesView[Light] = gateway.api.lights.values(), + ) -> None: """Add switch from deCONZ.""" entities = [] @@ -62,16 +75,17 @@ class DeconzPowerPlug(DeconzDevice, SwitchEntity): """Representation of a deCONZ power plug.""" TYPE = DOMAIN + _device: Light @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - return self._device.state + return self._device.state # type: ignore[no-any-return] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self._device.set_state(on=True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self._device.set_state(on=False) From f0b3fbc5a798f0a22de3b198b68a97c1179e3e8d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 16 Nov 2021 19:03:18 +0100 Subject: [PATCH 0541/1452] Always fire event for known devices in rfxtrx (#58845) --- homeassistant/components/rfxtrx/__init__.py | 5 +---- homeassistant/components/rfxtrx/config_flow.py | 8 +------- homeassistant/components/rfxtrx/const.py | 1 - homeassistant/components/rfxtrx/strings.json | 1 - tests/components/rfxtrx/test_config_flow.py | 13 ++----------- tests/components/rfxtrx/test_init.py | 4 ++-- 6 files changed, 6 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 88bddc6afbe..d7de0f5b7be 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -30,7 +30,6 @@ from .const import ( COMMAND_GROUP_LIST, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, - CONF_FIRE_EVENT, CONF_REMOVE_DEVICE, DATA_CLEANUP_CALLBACKS, DATA_LISTENER, @@ -186,9 +185,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_EVENT, event, device_id) # Signal event to any other listeners - fire_event = devices.get(device_id, {}).get(CONF_FIRE_EVENT) - if fire_event: - hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data) + hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data) @callback def _add_device(event, device_id): diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index be4ec111390..cf1069a7907 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -35,7 +35,6 @@ from .binary_sensor import supported as binary_supported from .const import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, - CONF_FIRE_EVENT, CONF_OFF_DELAY, CONF_REMOVE_DEVICE, CONF_REPLACE_DEVICE, @@ -208,7 +207,6 @@ class OptionsFlow(config_entries.OptionsFlow): devices = {} device = { CONF_DEVICE_ID: device_id, - CONF_FIRE_EVENT: user_input.get(CONF_FIRE_EVENT, False), CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1), } @@ -235,11 +233,7 @@ class OptionsFlow(config_entries.OptionsFlow): device_data = self._selected_device - data_schema = { - vol.Optional( - CONF_FIRE_EVENT, default=device_data.get(CONF_FIRE_EVENT, False) - ): bool, - } + data_schema = {} if binary_supported(self._selected_device_object): if device_data.get(CONF_OFF_DELAY): diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 17f54ef24c9..20f6fd75dc2 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -1,6 +1,5 @@ """Constants for RFXtrx integration.""" -CONF_FIRE_EVENT = "fire_event" CONF_DATA_BITS = "data_bits" CONF_AUTOMATIC_ADD = "automatic_add" CONF_SIGNAL_REPETITIONS = "signal_repetitions" diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 75c0de88f13..eb3a9ba699c 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -49,7 +49,6 @@ }, "set_device_options": { "data": { - "fire_event": "Enable device event", "off_delay": "Off delay", "off_delay_enabled": "Enable off delay", "data_bit": "Number of data bits", diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 4aa275fa3b4..10c9ca12022 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -352,7 +352,7 @@ async def test_options_add_device(hass): assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"fire_event": True, "signal_repetitions": 5} + result["flow_id"], user_input={"signal_repetitions": 5} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -362,7 +362,6 @@ async def test_options_add_device(hass): assert entry.data["automatic_add"] assert entry.data["devices"]["0b1100cd0213c7f230010f71"] - assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"] assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5 assert "delay_off" not in entry.data["devices"]["0b1100cd0213c7f230010f71"] @@ -442,7 +441,7 @@ async def test_options_add_remove_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"fire_event": True, "signal_repetitions": 5, "off_delay": "4"}, + user_input={"signal_repetitions": 5, "off_delay": "4"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -452,7 +451,6 @@ async def test_options_add_remove_device(hass): assert entry.data["automatic_add"] assert entry.data["devices"]["0b1100cd0213c7f230010f71"] - assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"] assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5 assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["off_delay"] == 4 @@ -864,7 +862,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "fire_event": False, "signal_repetitions": 5, "data_bits": 4, "off_delay": "abcdef", @@ -883,7 +880,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "fire_event": False, "signal_repetitions": 5, "data_bits": 4, "command_on": "0xE", @@ -899,7 +895,6 @@ async def test_options_add_and_configure_device(hass): assert entry.data["automatic_add"] assert entry.data["devices"]["0913000022670e013970"] - assert not entry.data["devices"]["0913000022670e013970"]["fire_event"] assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9 @@ -932,7 +927,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "fire_event": True, "signal_repetitions": 5, "data_bits": 4, "command_on": "0xE", @@ -945,7 +939,6 @@ async def test_options_add_and_configure_device(hass): await hass.async_block_till_done() assert entry.data["devices"]["0913000022670e013970"] - assert entry.data["devices"]["0913000022670e013970"]["fire_event"] assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 assert "delay_off" not in entry.data["devices"]["0913000022670e013970"] @@ -988,7 +981,6 @@ async def test_options_configure_rfy_cover_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "fire_event": False, "venetian_blind_mode": "EU", }, ) @@ -1021,7 +1013,6 @@ async def test_options_configure_rfy_cover_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "fire_event": False, "venetian_blind_mode": "EU", }, ) diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 0c904896090..4ad5f9a342a 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -17,8 +17,8 @@ async def test_fire_event(hass, rfxtrx): device="/dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0", automatic_add=True, devices={ - "0b1100cd0213c7f210010f51": {"fire_event": True}, - "0716000100900970": {"fire_event": True}, + "0b1100cd0213c7f210010f51": {}, + "0716000100900970": {}, }, ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) From 0dcfd55c84ad020e905cf5359ba1c035c3dbd27f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:03:50 +0100 Subject: [PATCH 0542/1452] Adjust async_step_usb signature for strict typing (#59773) Co-authored-by: epenet --- .../components/modem_callerid/config_flow.py | 2 +- homeassistant/components/usb/__init__.py | 21 ++++++++++++++++++- .../components/zwave_js/config_flow.py | 4 ++-- homeassistant/config_entries.py | 5 +++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index 51671dcbe16..70fd3969e02 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -30,7 +30,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Set up flow instance.""" self._device: str | None = None - async def async_step_usb(self, discovery_info: dict[str, str]) -> FlowResult: + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB Discovery.""" device = discovery_info["device"] diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 80d01417ea7..355b60906b3 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,6 +6,7 @@ import fnmatch import logging import os import sys +from typing import TypedDict from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -30,6 +31,17 @@ _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown +class UsbServiceInfo(TypedDict): + """Prepared info from usb entries.""" + + device: str + vid: str + pid: str + serial_number: str | None + manufacturer: str | None + description: str | None + + def human_readable_device_name( device: str, serial_number: str | None, @@ -193,7 +205,14 @@ class USBDiscovery: self.hass, matcher["domain"], {"source": config_entries.SOURCE_USB}, - dataclasses.asdict(device), + UsbServiceInfo( + device=device.device, + vid=device.vid, + pid=device.pid, + serial_number=device.serial_number, + manufacturer=device.manufacturer, + description=device.description, + ), ) @callback diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 37e6b7c9320..86107cd52f8 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -336,7 +336,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual() - async def async_step_usb(self, discovery_info: dict[str, str]) -> FlowResult: + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB Discovery.""" if not is_hassio(self.hass): return self.async_abort(reason="discovery_requires_supervisor") @@ -352,7 +352,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): manufacturer = discovery_info["manufacturer"] description = discovery_info["description"] # Zooz uses this vid/pid, but so do 2652 sticks - if vid == "10C4" and pid == "EA60" and "2652" in description: + if vid == "10C4" and pid == "EA60" and description and "2652" in description: return self.async_abort(reason="not_zwave_device") addon_info = await self._async_get_addon_info() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e1e4c103dc4..9a6709820ff 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -34,6 +34,7 @@ import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.mqtt.discovery import MqttServiceInfo + from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1386,10 +1387,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(cast(dict, discovery_info)) async def async_step_usb( - self, discovery_info: DiscoveryInfoType + self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(cast(dict, discovery_info)) @callback def async_create_entry( # pylint: disable=arguments-differ From 06d35853b6b6fb33d17b38319be88caffb82d462 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 16 Nov 2021 19:30:50 +0100 Subject: [PATCH 0543/1452] Fix typo in attribute for Fritz (#59791) --- homeassistant/components/fritz/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 994c7ff656e..594c1721be4 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -92,5 +92,5 @@ class FritzBoxBinarySensor(FritzBoxBaseEntity, BinarySensorEntity): self._attr_is_on = self._fritzbox_tools.update_available self._attr_extra_state_attributes = { "installed_version": self._fritzbox_tools.current_firmware, - "latest_available_version:": self._fritzbox_tools.latest_firmware, + "latest_available_version": self._fritzbox_tools.latest_firmware, } From 9faf3996db2ccf999a983cdf0fa1795a89ed1453 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Nov 2021 19:58:04 +0100 Subject: [PATCH 0544/1452] Add WLED firmware upgrade button (#59793) --- homeassistant/components/wled/button.py | 65 +++++++++++++- tests/components/wled/fixtures/rgb.json | 2 + .../wled/fixtures/rgb_single_segment.json | 4 +- .../wled/fixtures/rgb_websocket.json | 2 + tests/components/wled/fixtures/rgbw.json | 4 +- tests/components/wled/test_button.py | 86 ++++++++++++++++++- 6 files changed, 157 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 7278495b3fa..3e10ccb902f 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -20,11 +20,16 @@ async def async_setup_entry( ) -> None: """Set up WLED button based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([WLEDRestartButton(coordinator)]) + async_add_entities( + [ + WLEDRestartButton(coordinator), + WLEDUpgradeButton(coordinator), + ] + ) class WLEDRestartButton(WLEDEntity, ButtonEntity): - """Defines a WLED restart switch.""" + """Defines a WLED restart button.""" _attr_icon = "mdi:restart" _attr_entity_category = ENTITY_CATEGORY_CONFIG @@ -39,3 +44,59 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): async def async_press(self) -> None: """Send out a restart command.""" await self.coordinator.wled.reset() + + +class WLEDUpgradeButton(WLEDEntity, ButtonEntity): + """Defines a WLED upgrade button.""" + + _attr_icon = "mdi:cellphone-arrow-down" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize the button entity.""" + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Upgrade" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_upgrade" + + @property + def available(self) -> bool: + """Return if the entity and an upgrade is available.""" + current = self.coordinator.data.info.version + beta = self.coordinator.data.info.version_latest_beta + stable = self.coordinator.data.info.version_latest_stable + + # If we already run a pre-release, allow upgrading to a newer + # pre-release offer a normal upgrade otherwise. + return ( + super().available + and current is not None + and ( + (stable is not None and stable > current) + or ( + beta is not None + and (current.alpha or current.beta or current.release_candidate) + and beta > current + ) + ) + ) + + @wled_exception_handler + async def async_press(self) -> None: + """Send out a restart command.""" + current = self.coordinator.data.info.version + beta = self.coordinator.data.info.version_latest_beta + stable = self.coordinator.data.info.version_latest_stable + + # If we already run a pre-release, allow upgrading to a newer + # pre-release or newer stable, otherwise, offer a normal stable upgrades. + version = stable + if ( + current is not None + and beta is not None + and (current.alpha or current.beta or current.release_candidate) + and beta > current + and beta > stable + ): + version = beta + + await self.coordinator.wled.upgrade(version=str(version)) diff --git a/tests/components/wled/fixtures/rgb.json b/tests/components/wled/fixtures/rgb.json index 41d2c69d63c..20647c0f946 100644 --- a/tests/components/wled/fixtures/rgb.json +++ b/tests/components/wled/fixtures/rgb.json @@ -48,6 +48,8 @@ }, "info": { "ver": "0.8.5", + "version_latest_stable": "0.12.0", + "version_latest_beta": "0.13.0b1", "vid": 1909122, "leds": { "count": 30, diff --git a/tests/components/wled/fixtures/rgb_single_segment.json b/tests/components/wled/fixtures/rgb_single_segment.json index e53ce680ece..f82ef498fb6 100644 --- a/tests/components/wled/fixtures/rgb_single_segment.json +++ b/tests/components/wled/fixtures/rgb_single_segment.json @@ -33,7 +33,9 @@ ] }, "info": { - "ver": "0.8.5", + "ver": "0.8.6b1", + "version_latest_stable": "0.8.5", + "version_latest_beta": "0.8.6b2", "vid": 1909122, "leds": { "count": 30, diff --git a/tests/components/wled/fixtures/rgb_websocket.json b/tests/components/wled/fixtures/rgb_websocket.json index 7e37b489549..eea1733ee83 100644 --- a/tests/components/wled/fixtures/rgb_websocket.json +++ b/tests/components/wled/fixtures/rgb_websocket.json @@ -63,6 +63,8 @@ }, "info": { "ver": "0.12.0-b2", + "version_latest_stable": "0.11.0", + "version_latest_beta": "0.12.0-b2", "vid": 2103220, "leds": { "count": 13, diff --git a/tests/components/wled/fixtures/rgbw.json b/tests/components/wled/fixtures/rgbw.json index 824612613b1..6d9796c0fb9 100644 --- a/tests/components/wled/fixtures/rgbw.json +++ b/tests/components/wled/fixtures/rgbw.json @@ -33,7 +33,9 @@ ] }, "info": { - "ver": "0.8.6", + "ver": "0.8.6b4", + "version_latest_stable": "0.8.6", + "version_latest_beta": "0.8.6b5", "vid": 1910255, "leds": { "count": 13, diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index 9034632ae93..a4ca9a86506 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -19,7 +19,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -async def test_button( +async def test_button_restart( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: """Test the creation and values of the WLED button.""" @@ -35,7 +35,6 @@ async def test_button( assert entry.unique_id == "aabbccddeeff_restart" assert entry.entity_category == ENTITY_CATEGORY_CONFIG - # Restart await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, @@ -92,3 +91,86 @@ async def test_button_connection_error( assert state assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text + + +async def test_button_upgrade_stay_stable( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the upgrade button. + + There is both an upgrade for beta and stable available, however, the device + is currently running a stable version. Therefore, the upgrade button should + upgrade the the next stable (even though beta is newer). + """ + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.wled_rgb_light_upgrade") + assert entry + assert entry.unique_id == "aabbccddeeff_upgrade" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + state = hass.states.get("button.wled_rgb_light_upgrade") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:cellphone-arrow-down" + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_upgrade"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.12.0") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_button_upgrade_beta_to_stable( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the upgrade button. + + There is both an upgrade for beta and stable available the device + is currently a beta, however, a newer stable is available. Therefore, the + upgrade button should upgrade to the next stable. + """ + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgbw_light_upgrade"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.8.6") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_button_upgrade_stay_beta( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the upgrade button. + + There is an upgrade for beta and the device is currently a beta. Therefore, + the upgrade button should upgrade to the next beta. + """ + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_upgrade"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.8.6b2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_button_no_upgrade_available( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the upgrade button. There is no update available.""" + state = hass.states.get("button.wled_websocket_upgrade") + assert state + assert state.state == STATE_UNAVAILABLE From 4642a70651308d89a4a15b326195c8b133266f42 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 16 Nov 2021 20:01:10 +0100 Subject: [PATCH 0545/1452] Add typing to deCONZ Alarm Control Panel and Binary Sensor platforms (#59611) * Add typing to deCONZ Alarm Control Panel and Binary Sensor platforms * Address review comments * Don't use asserts, use # type: ignore[no-any-return] * Improve lazy typing of dict --- .../components/deconz/alarm_control_panel.py | 54 ++++++++++++++----- .../components/deconz/binary_sensor.py | 43 ++++++++++----- 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 823c9c67654..e16e4bcc327 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -1,6 +1,9 @@ """Support for deCONZ alarm control panel devices.""" from __future__ import annotations +from collections.abc import ValuesView + +from pydeconz.alarm_system import AlarmSystem from pydeconz.sensor import ( ANCILLARY_CONTROL_ARMED_AWAY, ANCILLARY_CONTROL_ARMED_NIGHT, @@ -23,6 +26,7 @@ from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_NIGHT, AlarmControlPanelEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -32,11 +36,12 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_TO_ALARM_STATE = { ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, @@ -52,14 +57,21 @@ DECONZ_TO_ALARM_STATE = { } -def get_alarm_system_for_unique_id(gateway, unique_id: str): +def get_alarm_system_for_unique_id( + gateway: DeconzGateway, unique_id: str +) -> AlarmSystem | None: """Retrieve alarm system unique ID is registered to.""" for alarm_system in gateway.api.alarmsystems.values(): if unique_id in alarm_system.devices: return alarm_system + return None -async def async_setup_entry(hass, config_entry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ alarm control panel devices. Alarm control panels are based on the same device class as sensors in deCONZ. @@ -68,7 +80,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: gateway.entities[DOMAIN] = set() @callback - def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: + def async_add_alarm_control_panel( + sensors: list[AncillaryControl] + | ValuesView[AncillaryControl] = gateway.api.sensors.values(), + ) -> None: """Add alarm control panel devices from deCONZ.""" entities = [] @@ -77,10 +92,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: if ( isinstance(sensor, AncillaryControl) and sensor.unique_id not in gateway.entities[DOMAIN] - and get_alarm_system_for_unique_id(gateway, sensor.unique_id) + and ( + alarm_system := get_alarm_system_for_unique_id( + gateway, sensor.unique_id + ) + ) + is not None ): - entities.append(DeconzAlarmControlPanel(sensor, gateway)) + entities.append(DeconzAlarmControlPanel(sensor, gateway, alarm_system)) if entities: async_add_entities(entities) @@ -100,16 +120,22 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): """Representation of a deCONZ alarm control panel.""" TYPE = DOMAIN + _device: AncillaryControl _attr_code_format = FORMAT_NUMBER _attr_supported_features = ( SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT ) - def __init__(self, device, gateway) -> None: + def __init__( + self, + device: AncillaryControl, + gateway: DeconzGateway, + alarm_system: AlarmSystem, + ) -> None: """Set up alarm control panel device.""" super().__init__(device, gateway) - self.alarm_system = get_alarm_system_for_unique_id(gateway, device.unique_id) + self.alarm_system = alarm_system @callback def async_update_callback(self) -> None: @@ -124,20 +150,20 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): @property def state(self) -> str | None: """Return the state of the control panel.""" - return DECONZ_TO_ALARM_STATE.get(self._device.state) + return DECONZ_TO_ALARM_STATE.get(self._device.panel) - async def async_alarm_arm_away(self, code: None = None) -> None: + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self.alarm_system.arm_away(code) - async def async_alarm_arm_home(self, code: None = None) -> None: + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self.alarm_system.arm_stay(code) - async def async_alarm_arm_night(self, code: None = None) -> None: + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" await self.alarm_system.arm_night(code) - async def async_alarm_disarm(self, code: None = None) -> None: + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self.alarm_system.disarm(code) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 7f77bbb8809..475c631bdf8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,7 +1,13 @@ """Support for deCONZ binary sensors.""" +from __future__ import annotations + +from collections.abc import ValuesView + from pydeconz.sensor import ( Alarm, CarbonMonoxide, + DeconzBinarySensor as PydeconzBinarySensor, + DeconzSensor as PydeconzSensor, Fire, GenericFlag, OpenClose, @@ -22,13 +28,15 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_BINARY_SENSORS = ( Alarm, @@ -73,15 +81,22 @@ ENTITY_DESCRIPTIONS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors=gateway.api.sensors.values()): + def async_add_sensor( + sensors: list[PydeconzSensor] + | ValuesView[PydeconzSensor] = gateway.api.sensors.values(), + ) -> None: """Add binary sensor from deCONZ.""" - entities = [] + entities: list[DeconzBinarySensor | DeconzTampering] = [] for sensor in sensors: @@ -120,8 +135,9 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" TYPE = DOMAIN + _device: PydeconzBinarySensor - def __init__(self, device, gateway): + def __init__(self, device: PydeconzBinarySensor, gateway: DeconzGateway) -> None: """Initialize deCONZ binary sensor.""" super().__init__(device, gateway) @@ -129,21 +145,21 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the sensor's state.""" keys = {"on", "reachable", "state"} if self._device.changed_keys.intersection(keys): super().async_update_callback() @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" - return self._device.state + return self._device.state # type: ignore[no-any-return] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" - attr = {} + attr: dict[str, bool | float | int | list | None] = {} if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -168,11 +184,12 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ tampering sensor.""" TYPE = DOMAIN + _device: PydeconzSensor _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_device_class = DEVICE_CLASS_TAMPER - def __init__(self, device, gateway): + def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: """Initialize deCONZ binary sensor.""" super().__init__(device, gateway) @@ -193,4 +210,4 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): @property def is_on(self) -> bool: """Return the state of the sensor.""" - return self._device.tampered + return self._device.tampered # type: ignore[no-any-return] From e1e69250978de5f8e64c0a7228319746e31b09f8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 16 Nov 2021 20:59:17 +0100 Subject: [PATCH 0546/1452] Refactor of Hue integration with full V2 support (#58996) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 +- homeassistant/components/hue/__init__.py | 179 +- homeassistant/components/hue/binary_sensor.py | 72 +- homeassistant/components/hue/bridge.py | 295 +-- homeassistant/components/hue/config_flow.py | 107 +- homeassistant/components/hue/const.py | 34 +- .../components/hue/device_trigger.py | 252 +- homeassistant/components/hue/light.py | 577 +---- homeassistant/components/hue/manifest.json | 4 +- homeassistant/components/hue/migration.py | 174 ++ homeassistant/components/hue/scene.py | 117 + homeassistant/components/hue/sensor.py | 149 +- homeassistant/components/hue/services.py | 158 ++ homeassistant/components/hue/services.yaml | 5 + homeassistant/components/hue/strings.json | 15 +- homeassistant/components/hue/switch.py | 94 + .../components/hue/translations/en.json | 41 +- homeassistant/components/hue/v1/__init__.py | 1 + .../components/hue/v1/binary_sensor.py | 56 + .../components/hue/v1/device_trigger.py | 185 ++ .../components/hue/{ => v1}/helpers.py | 15 +- .../components/hue/{ => v1}/hue_event.py | 15 +- homeassistant/components/hue/v1/light.py | 557 +++++ homeassistant/components/hue/v1/sensor.py | 135 ++ .../components/hue/{ => v1}/sensor_base.py | 14 +- .../components/hue/{ => v1}/sensor_device.py | 18 +- homeassistant/components/hue/v2/__init__.py | 1 + .../components/hue/v2/binary_sensor.py | 110 + homeassistant/components/hue/v2/device.py | 86 + .../components/hue/v2/device_trigger.py | 115 + homeassistant/components/hue/v2/entity.py | 113 + homeassistant/components/hue/v2/group.py | 249 ++ homeassistant/components/hue/v2/hue_event.py | 57 + homeassistant/components/hue/v2/light.py | 187 ++ homeassistant/components/hue/v2/sensor.py | 174 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/conftest.py | 270 ++- tests/components/hue/const.py | 97 + .../components/hue/fixtures/v2_resources.json | 2107 +++++++++++++++++ tests/components/hue/test_binary_sensor.py | 61 + tests/components/hue/test_bridge.py | 357 +-- tests/components/hue/test_config_flow.py | 294 ++- ...e_trigger.py => test_device_trigger_v1.py} | 60 +- .../components/hue/test_device_trigger_v2.py | 82 + tests/components/hue/test_init.py | 28 +- .../hue/test_init_multiple_bridges.py | 143 -- .../hue/{test_light.py => test_light_v1.py} | 253 +- tests/components/hue/test_light_v2.py | 353 +++ tests/components/hue/test_migration.py | 179 ++ tests/components/hue/test_scene.py | 114 + ...{test_sensor_base.py => test_sensor_v1.py} | 141 +- tests/components/hue/test_sensor_v2.py | 123 + tests/components/hue/test_services.py | 265 +++ tests/components/hue/test_switch.py | 107 + 55 files changed, 7146 insertions(+), 2255 deletions(-) create mode 100644 homeassistant/components/hue/migration.py create mode 100644 homeassistant/components/hue/scene.py create mode 100644 homeassistant/components/hue/services.py create mode 100644 homeassistant/components/hue/switch.py create mode 100644 homeassistant/components/hue/v1/__init__.py create mode 100644 homeassistant/components/hue/v1/binary_sensor.py create mode 100644 homeassistant/components/hue/v1/device_trigger.py rename homeassistant/components/hue/{ => v1}/helpers.py (77%) rename homeassistant/components/hue/{ => v1}/hue_event.py (90%) create mode 100644 homeassistant/components/hue/v1/light.py create mode 100644 homeassistant/components/hue/v1/sensor.py rename homeassistant/components/hue/{ => v1}/sensor_base.py (95%) rename homeassistant/components/hue/{ => v1}/sensor_device.py (83%) create mode 100644 homeassistant/components/hue/v2/__init__.py create mode 100644 homeassistant/components/hue/v2/binary_sensor.py create mode 100644 homeassistant/components/hue/v2/device.py create mode 100644 homeassistant/components/hue/v2/device_trigger.py create mode 100644 homeassistant/components/hue/v2/entity.py create mode 100644 homeassistant/components/hue/v2/group.py create mode 100644 homeassistant/components/hue/v2/hue_event.py create mode 100644 homeassistant/components/hue/v2/light.py create mode 100644 homeassistant/components/hue/v2/sensor.py create mode 100644 tests/components/hue/const.py create mode 100644 tests/components/hue/fixtures/v2_resources.json create mode 100644 tests/components/hue/test_binary_sensor.py rename tests/components/hue/{test_device_trigger.py => test_device_trigger_v1.py} (71%) create mode 100644 tests/components/hue/test_device_trigger_v2.py delete mode 100644 tests/components/hue/test_init_multiple_bridges.py rename tests/components/hue/{test_light.py => test_light_v1.py} (77%) create mode 100644 tests/components/hue/test_light_v2.py create mode 100644 tests/components/hue/test_migration.py create mode 100644 tests/components/hue/test_scene.py rename tests/components/hue/{test_sensor_base.py => test_sensor_v1.py} (81%) create mode 100644 tests/components/hue/test_sensor_v2.py create mode 100644 tests/components/hue/test_services.py create mode 100644 tests/components/hue/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index a43b51d8b5b..c143826fe14 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -231,7 +231,7 @@ homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle -homeassistant/components/hue/* @balloob @frenck +homeassistant/components/hue/* @balloob @frenck @marcelveldt homeassistant/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 8e0f194e904..794283e09f5 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,79 +1,41 @@ """Support for the Philips Hue system.""" -import asyncio -import logging from aiohue.util import normalize_bridge_id -import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import persistent_notification -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.service import verify_domain_control +from homeassistant.helpers import device_registry as dr from .bridge import HueBridge -from .const import ( - ATTR_GROUP_NAME, - ATTR_SCENE_NAME, - ATTR_TRANSITION, - CONF_ALLOW_HUE_GROUPS, - CONF_ALLOW_UNREACHABLE, - DEFAULT_ALLOW_HUE_GROUPS, - DEFAULT_ALLOW_UNREACHABLE, - DOMAIN, -) - -_LOGGER = logging.getLogger(__name__) -SERVICE_HUE_SCENE = "hue_activate_scene" +from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE +from .migration import check_migration +from .services import async_register_services async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry -): +) -> bool: """Set up a bridge from a config entry.""" + # check (and run) migrations if needed + await check_migration(hass, entry) - # Migrate allow_unreachable from config entry data to config entry options - if ( - CONF_ALLOW_UNREACHABLE not in entry.options - and CONF_ALLOW_UNREACHABLE in entry.data - and entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE - ): - options = { - **entry.options, - CONF_ALLOW_UNREACHABLE: entry.data[CONF_ALLOW_UNREACHABLE], - } - data = entry.data.copy() - data.pop(CONF_ALLOW_UNREACHABLE) - hass.config_entries.async_update_entry(entry, data=data, options=options) - - # Migrate allow_hue_groups from config entry data to config entry options - if ( - CONF_ALLOW_HUE_GROUPS not in entry.options - and CONF_ALLOW_HUE_GROUPS in entry.data - and entry.data[CONF_ALLOW_HUE_GROUPS] != DEFAULT_ALLOW_HUE_GROUPS - ): - options = { - **entry.options, - CONF_ALLOW_HUE_GROUPS: entry.data[CONF_ALLOW_HUE_GROUPS], - } - data = entry.data.copy() - data.pop(CONF_ALLOW_HUE_GROUPS) - hass.config_entries.async_update_entry(entry, data=data, options=options) - + # setup the bridge instance bridge = HueBridge(hass, entry) - - if not await bridge.async_setup(): + if not await bridge.async_initialize_bridge(): return False - _register_services(hass) + # register Hue domain services + async_register_services(hass) - config = bridge.api.config + api = bridge.api # For backwards compat - unique_id = normalize_bridge_id(config.bridgeid) + unique_id = normalize_bridge_id(api.config.bridge_id) if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=unique_id) # For recovering from bug where we incorrectly assumed homekit ID = bridge ID + # Remove this logic after Home Assistant 2022.4 elif entry.unique_id != unique_id: # Find entries with this unique ID other_entry = next( @@ -84,7 +46,6 @@ async def async_setup_entry( ), None, ) - if other_entry is None: # If no other entry, update unique ID of this entry ID. hass.config_entries.async_update_entry(entry, unique_id=unique_id) @@ -100,88 +61,54 @@ async def async_setup_entry( hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) return False + # add bridge device to device registry device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, config.mac)}, - identifiers={(DOMAIN, config.bridgeid)}, - manufacturer="Signify", - name=config.name, - model=config.modelid, - sw_version=config.swversion, - ) - - if config.modelid == "BSB002" and config.swversion < "1935144040": - persistent_notification.async_create( - hass, - "Your Hue hub has a known security vulnerability ([CVE-2020-6007](https://cve.circl.lu/cve/CVE-2020-6007)). Go to the Hue app and check for software updates.", - "Signify Hue", - "hue_hub_firmware", + if bridge.api_version == 1: + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, api.config.mac_address)}, + identifiers={(DOMAIN, api.config.bridge_id)}, + manufacturer="Signify", + name=api.config.name, + model=api.config.model_id, + sw_version=api.config.software_version, ) - - elif config.swupdate2_bridge_state == "readytoinstall": - err = ( - "Please check for software updates of the bridge in the Philips Hue App.", - "Signify Hue", - "hue_hub_firmware", + # create persistent notification if we found a bridge version with security vulnerability + if ( + api.config.model_id == "BSB002" + and api.config.software_version < "1935144040" + ): + persistent_notification.async_create( + hass, + "Your Hue hub has a known security vulnerability ([CVE-2020-6007] " + "(https://cve.circl.lu/cve/CVE-2020-6007)). " + "Go to the Hue app and check for software updates.", + "Signify Hue", + "hue_hub_firmware", + ) + else: + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, api.config.mac_address)}, + identifiers={ + (DOMAIN, api.config.bridge_id), + (DOMAIN, api.config.bridge_device.id), + }, + manufacturer=api.config.bridge_device.product_data.manufacturer_name, + name=api.config.name, + model=api.config.model_id, + sw_version=api.config.software_version, ) - _LOGGER.warning(err) return True -async def async_unload_entry(hass, entry): +async def async_unload_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): """Unload a config entry.""" unload_success = await hass.data[DOMAIN][entry.entry_id].async_reset() if len(hass.data[DOMAIN]) == 0: hass.data.pop(DOMAIN) - hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE) return unload_success - - -@core.callback -def _register_services(hass): - """Register Hue services.""" - - async def hue_activate_scene(call, skip_reload=True): - """Handle activation of Hue scene.""" - # Get parameters - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - - # Call the set scene function on each bridge - tasks = [ - bridge.hue_activate_scene( - call.data, skip_reload=skip_reload, hide_warnings=skip_reload - ) - for bridge in hass.data[DOMAIN].values() - if isinstance(bridge, HueBridge) - ] - results = await asyncio.gather(*tasks) - - # Did *any* bridge succeed? If not, refresh / retry - # Note that we'll get a "None" value for a successful call - if None not in results: - if skip_reload: - await hue_activate_scene(call, skip_reload=False) - return - _LOGGER.warning( - "No bridge was able to activate " "scene %s in group %s", - scene_name, - group_name, - ) - - if not hass.services.has_service(DOMAIN, SERVICE_HUE_SCENE): - # Register a local handler for scene activation - hass.services.async_register( - DOMAIN, - SERVICE_HUE_SCENE, - verify_domain_control(hass, DOMAIN)(hue_activate_scene), - schema=vol.Schema( - { - vol.Required(ATTR_GROUP_NAME): cv.string, - vol.Required(ATTR_SCENE_NAME): cv.string, - vol.Optional(ATTR_TRANSITION): cv.positive_int, - } - ), - ) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index c675544503c..b66b85a4844 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,56 +1,24 @@ -"""Hue binary sensor entities.""" -from aiohue.sensors import TYPE_ZLL_PRESENCE +"""Support for Hue binary sensors.""" +from __future__ import annotations -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - BinarySensorEntity, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as HUE_DOMAIN -from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor - -PRESENCE_NAME_FORMAT = "{} motion" +from .bridge import HueBridge +from .const import DOMAIN +from .v1.binary_sensor import async_setup_entry as setup_entry_v1 +from .v2.binary_sensor import async_setup_entry as setup_entry_v2 -async def async_setup_entry(hass, config_entry, async_add_entities): - """Defer binary sensor setup to the shared sensor module.""" - bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] - - if not bridge.sensor_manager: - return - - await bridge.sensor_manager.async_register_component( - "binary_sensor", async_add_entities - ) - - -class HuePresence(GenericZLLSensor, BinarySensorEntity): - """The presence sensor entity for a Hue motion sensor device.""" - - _attr_device_class = DEVICE_CLASS_MOTION - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.sensor.presence - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = super().extra_state_attributes - if "sensitivity" in self.sensor.config: - attributes["sensitivity"] = self.sensor.config["sensitivity"] - if "sensitivitymax" in self.sensor.config: - attributes["sensitivity_max"] = self.sensor.config["sensitivitymax"] - return attributes - - -SENSOR_CONFIG_MAP.update( - { - TYPE_ZLL_PRESENCE: { - "platform": "binary_sensor", - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - } - } -) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensor entities.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + if bridge.api_version == 1: + await setup_entry_v1(hass, config_entry, async_add_entities) + else: + await setup_entry_v2(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index e669cf7b031..5005f858a58 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -2,126 +2,119 @@ from __future__ import annotations import asyncio -from functools import partial +from collections.abc import Callable from http import HTTPStatus import logging +from typing import Any from aiohttp import client_exceptions -import aiohue +from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized +from aiohue.errors import AiohueException import async_timeout -import slugify as unicode_slug from homeassistant import core +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from .const import ( - ATTR_GROUP_NAME, - ATTR_SCENE_NAME, - ATTR_TRANSITION, - CONF_ALLOW_HUE_GROUPS, - CONF_ALLOW_UNREACHABLE, - DEFAULT_ALLOW_HUE_GROUPS, - DEFAULT_ALLOW_UNREACHABLE, - DOMAIN, - LOGGER, -) -from .errors import AuthenticationRequired, CannotConnect -from .helpers import create_config_flow -from .sensor_base import SensorManager +from .const import CONF_API_VERSION, DOMAIN +from .v1.sensor_base import SensorManager +from .v2.device import async_setup_devices +from .v2.hue_event import async_setup_hue_events # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 -PLATFORMS = ["light", "binary_sensor", "sensor"] - -_LOGGER = logging.getLogger(__name__) +PLATFORMS_v1 = ["light", "binary_sensor", "sensor"] +PLATFORMS_v2 = ["light", "binary_sensor", "sensor", "scene", "switch"] class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: core.HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the system.""" self.config_entry = config_entry self.hass = hass - self.available = True self.authorized = False - self.api = None - self.parallel_updates_semaphore = None + self.parallel_updates_semaphore = asyncio.Semaphore( + 3 if self.api_version == 1 else 10 + ) # Jobs to be executed when API is reset. - self.reset_jobs = [] - self.sensor_manager = None - self._update_callbacks = {} + self.reset_jobs: list[core.CALLBACK_TYPE] = [] + self.sensor_manager: SensorManager | None = None + self.logger = logging.getLogger(__name__) + # store actual api connection to bridge as api + app_key: str = self.config_entry.data[CONF_API_KEY] + websession = aiohttp_client.async_get_clientsession(hass) + if self.api_version == 1: + self.api = HueBridgeV1(self.host, app_key, websession) + else: + self.api = HueBridgeV2(self.host, app_key, websession) + # store (this) bridge object in hass data + hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self @property - def host(self): + def host(self) -> str: """Return the host of this bridge.""" - return self.config_entry.data["host"] + return self.config_entry.data[CONF_HOST] @property - def allow_unreachable(self): - """Allow unreachable light bulbs.""" - return self.config_entry.options.get( - CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE - ) - - @property - def allow_groups(self): - """Allow groups defined in the Hue bridge.""" - return self.config_entry.options.get( - CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS - ) - - async def async_setup(self, tries=0): - """Set up a phue bridge based on host parameter.""" - host = self.host - hass = self.hass - - bridge = aiohue.Bridge( - host, - username=self.config_entry.data["username"], - websession=aiohttp_client.async_get_clientsession(hass), - ) + def api_version(self) -> int: + """Return api version we're set-up for.""" + return self.config_entry.data[CONF_API_VERSION] + async def async_initialize_bridge(self) -> bool: + """Initialize Connection with the Hue API.""" try: - await authenticate_bridge(hass, bridge) + with async_timeout.timeout(10): + await self.api.initialize() - except AuthenticationRequired: + except (LinkButtonNotPressed, Unauthorized): # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - create_config_flow(hass, host) + create_config_flow(self.hass, self.host) return False - - except CannotConnect as err: + except ( + asyncio.TimeoutError, + client_exceptions.ClientOSError, + client_exceptions.ServerDisconnectedError, + client_exceptions.ContentTypeError, + ) as err: raise ConfigEntryNotReady( - f"Error connecting to the Hue bridge at {host}" + f"Error connecting to the Hue bridge at {self.host}" ) from err - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) + self.logger.exception("Unknown error connecting to Hue bridge") return False - self.api = bridge - if bridge.sensors is not None: - self.sensor_manager = SensorManager(self) + # v1 specific initialization/setup code here + if self.api_version == 1: + if self.api.sensors is not None: + self.sensor_manager = SensorManager(self) + self.hass.config_entries.async_setup_platforms( + self.config_entry, PLATFORMS_v1 + ) - hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self - hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - - self.parallel_updates_semaphore = asyncio.Semaphore( - 3 if self.api.config.modelid == "BSB001" else 10 - ) + # v2 specific initialization/setup code here + else: + await async_setup_devices(self) + await async_setup_hue_events(self) + self.hass.config_entries.async_setup_platforms( + self.config_entry, PLATFORMS_v2 + ) + # add listener for config entry updates. self.reset_jobs.append(self.config_entry.add_update_listener(_update_listener)) - self.reset_jobs.append(asyncio.create_task(self._subscribe_events()).cancel) - self.authorized = True return True - async def async_request_call(self, task): + async def async_request_call( + self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs + ) -> Any: """Limit parallel requests to Hue hub. The Hue hub can only handle a certain amount of parallel requests, total. @@ -132,17 +125,30 @@ class HueBridge: ContentResponseError means hub raised an error. Since we don't make bad requests, this is on them. """ + max_tries = 5 async with self.parallel_updates_semaphore: - for tries in range(4): + for tries in range(max_tries): try: - return await task() + return await task(*args, **kwargs) + except AiohueException as err: + # The new V2 api is a bit more fanatic with throwing errors + # some of which we accept in certain conditions + # handle that here. Note that these errors are strings and do not have + # an identifier or something. + if allowed_errors is not None and str(err) in allowed_errors: + # log only + self.logger.debug( + "Ignored error/warning from Hue API: %s", str(err) + ) + return None + raise err except ( client_exceptions.ClientOSError, client_exceptions.ClientResponseError, client_exceptions.ServerDisconnectedError, ) as err: - if tries == 3: - _LOGGER.error("Request failed %s times, giving up", tries) + if tries == max_tries: + self.logger.error("Request failed %s times, giving up", tries) raise # We only retry if it's a server error. So raise on all 4XX errors. @@ -154,7 +160,7 @@ class HueBridge: await asyncio.sleep(HUB_BUSY_SLEEP * tries) - async def async_reset(self): + async def async_reset(self) -> bool: """Reset this bridge to default state. Will cancel any scheduled setup retry and will unload @@ -171,12 +177,9 @@ class HueBridge: while self.reset_jobs: self.reset_jobs.pop()() - self._update_callbacks = {} - - # If setup was successful, we set api variable, forwarded entry and - # register service + # Unload platforms unload_success = await self.hass.config_entries.async_unload_platforms( - self.config_entry, PLATFORMS + self.config_entry, PLATFORMS_v1 if self.api_version == 1 else PLATFORMS_v2 ) if unload_success: @@ -184,127 +187,29 @@ class HueBridge: return unload_success - async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): - """Service to call directly into bridge to set scenes.""" - if self.api.scenes is None: - _LOGGER.warning("Hub %s does not support scenes", self.api.host) - return - - group_name = data[ATTR_GROUP_NAME] - scene_name = data[ATTR_SCENE_NAME] - transition = data.get(ATTR_TRANSITION) - - group = next( - (group for group in self.api.groups.values() if group.name == group_name), - None, - ) - - # Additional scene logic to handle duplicate scene names across groups - scene = next( - ( - scene - for scene in self.api.scenes.values() - if scene.name == scene_name - and group is not None - and sorted(scene.lights) == sorted(group.lights) - ), - None, - ) - - # If we can't find it, fetch latest info. - if not skip_reload and (group is None or scene is None): - await self.async_request_call(self.api.groups.update) - await self.async_request_call(self.api.scenes.update) - return await self.hue_activate_scene(data, skip_reload=True) - - if group is None: - if not hide_warnings: - LOGGER.warning( - "Unable to find group %s" " on bridge %s", group_name, self.host - ) - return False - - if scene is None: - LOGGER.warning("Unable to find scene %s", scene_name) - return False - - return await self.async_request_call( - partial(group.set_action, scene=scene.id, transitiontime=transition) - ) - - async def handle_unauthorized_error(self): + async def handle_unauthorized_error(self) -> None: """Create a new config flow when the authorization is no longer valid.""" if not self.authorized: # we already created a new config flow, no need to do it again return - LOGGER.error( + self.logger.error( "Unable to authorize to bridge %s, setup the linking again", self.host ) self.authorized = False create_config_flow(self.hass, self.host) - async def _subscribe_events(self): - """Subscribe to Hue events.""" - try: - async for updated_object in self.api.listen_events(): - key = (updated_object.ITEM_TYPE, updated_object.id) - if key in self._update_callbacks: - for callback in self._update_callbacks[key]: - callback() - - except GeneratorExit: - pass - - @core.callback - def listen_updates(self, item_type, item_id, update_callback): - """Listen to updates.""" - key = (item_type, item_id) - callbacks: list[core.CALLBACK_TYPE] | None = self._update_callbacks.get(key) - - if callbacks is None: - callbacks = self._update_callbacks[key] = [] - - callbacks.append(update_callback) - - @core.callback - def unsub(): - try: - callbacks.remove(update_callback) - except ValueError: - pass - - return unsub - - -async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): - """Create a bridge object and verify authentication.""" - try: - async with async_timeout.timeout(10): - # Create username if we don't have one - if not bridge.username: - device_name = unicode_slug.slugify( - hass.config.location_name, max_length=19 - ) - await bridge.create_user(f"home-assistant#{device_name}") - - # Initialize bridge (and validate our username) - await bridge.initialize() - - except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized) as err: - raise AuthenticationRequired from err - except ( - asyncio.TimeoutError, - client_exceptions.ClientOSError, - client_exceptions.ServerDisconnectedError, - client_exceptions.ContentTypeError, - ) as err: - raise CannotConnect from err - except aiohue.AiohueException as err: - LOGGER.exception("Unknown Hue linking error occurred") - raise AuthenticationRequired from err - - -async def _update_listener(hass, entry): - """Handle options update.""" +async def _update_listener(hass: core.HomeAssistant, entry: ConfigEntry) -> None: + """Handle ConfigEntry options update.""" await hass.config_entries.async_reload(entry.entry_id) + + +def create_config_flow(hass: core.HomeAssistant, host: str) -> None: + """Start a config flow.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={"host": host}, + ) + ) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0ffa7e358f0..0499031c4f2 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -2,31 +2,35 @@ from __future__ import annotations import asyncio -from typing import Any +import logging from urllib.parse import urlparse -import aiohue -from aiohue.discovery import discover_nupnp, normalize_bridge_id +from aiohue import LinkButtonNotPressed, create_app_key +from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp +from aiohue.util import normalize_bridge_id import async_timeout +import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf -from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .bridge import authenticate_bridge from .const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, + CONF_API_VERSION, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, DOMAIN, - LOGGER, ) -from .errors import AuthenticationRequired, CannotConnect +from .errors import CannotConnect + +LOGGER = logging.getLogger(__name__) HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com") HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] @@ -40,33 +44,35 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" return HueOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize the Hue flow.""" - self.bridge: aiohue.Bridge | None = None - self.discovered_bridges: dict[str, aiohue.Bridge] | None = None + self.bridge: DiscoveredHueBridge | None = None + self.discovered_bridges: dict[str, DiscoveredHueBridge] | None = None - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user.""" # This is for backwards compatibility. return await self.async_step_init(user_input) - @core.callback - def _async_get_bridge(self, host: str, bridge_id: str | None = None): - """Return a bridge object.""" + async def _get_bridge( + self, host: str, bridge_id: str | None = None + ) -> DiscoveredHueBridge: + """Return a DiscoveredHueBridge object.""" + bridge = await discover_bridge( + host, websession=aiohttp_client.async_get_clientsession(self.hass) + ) if bridge_id is not None: bridge_id = normalize_bridge_id(bridge_id) + assert bridge_id == bridge.id + return bridge - return aiohue.Bridge( - host, - websession=aiohttp_client.async_get_clientsession(self.hass), - bridge_id=bridge_id, - ) - - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID: @@ -116,7 +122,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_manual( - self, user_input: dict[str, Any] | None = None + self, user_input: ConfigType | None = None ) -> FlowResult: """Handle manual bridge setup.""" if user_input is None: @@ -126,10 +132,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._async_abort_entries_match({"host": user_input["host"]}) - self.bridge = self._async_get_bridge(user_input[CONF_HOST]) + self.bridge = await self._get_bridge(user_input[CONF_HOST]) return await self.async_step_link() - async def async_step_link(self, user_input=None): + async def async_step_link(self, user_input: ConfigType | None = None) -> FlowResult: """Attempt to link with the Hue bridge. Given a configured host, will ask the user to press the link button @@ -141,10 +147,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): bridge = self.bridge assert bridge is not None errors = {} + device_name = unicode_slug.slugify( + self.hass.config.location_name, max_length=19 + ) try: - await authenticate_bridge(self.hass, bridge) - except AuthenticationRequired: + app_key = await create_app_key( + bridge.host, + f"home-assistant#{device_name}", + websession=aiohttp_client.async_get_clientsession(self.hass), + ) + except LinkButtonNotPressed: errors["base"] = "register_failed" except CannotConnect: LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) @@ -165,11 +178,15 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=bridge.config.name, - data={CONF_HOST: bridge.host, CONF_USERNAME: bridge.username}, + title=f"Hue Bridge {bridge.id}", + data={ + CONF_HOST: bridge.host, + CONF_API_KEY: app_key, + CONF_API_VERSION: 2 if bridge.supports_v2 else 1, + }, ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a discovered Hue bridge. This flow is triggered by the SSDP component. It will check if the @@ -196,8 +213,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_hue_bridge") host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - - bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) + bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) # type: ignore[arg-type] await self.async_set_unique_id(bridge.id) self._abort_if_unique_id_configured( @@ -215,9 +231,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the Zeroconf component. It will check if the host is already configured and delegate to the import step if not. """ - bridge = self._async_get_bridge( - discovery_info[zeroconf.ATTR_HOST], - discovery_info[zeroconf.ATTR_PROPERTIES]["bridgeid"], + bridge = await self._get_bridge( + discovery_info["host"], discovery_info["properties"]["bridgeid"] ) await self.async_set_unique_id(bridge.id) @@ -228,18 +243,20 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = bridge return await self.async_step_link() - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a discovered Hue bridge on HomeKit. The bridge ID communicated over HomeKit differs, so we cannot use that as the unique identifier. Therefore, this method uses discovery without a unique ID. """ - self.bridge = self._async_get_bridge(discovery_info[CONF_HOST]) + self.bridge = await self._get_bridge(discovery_info[CONF_HOST]) await self._async_handle_discovery_without_unique_id() return await self.async_step_link() - async def async_step_import(self, import_info): + async def async_step_import(self, import_info: ConfigType) -> FlowResult: """Import a new bridge as a config entry. This flow is triggered by `async_setup` for both configured and @@ -251,24 +268,26 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check if host exists, abort if so. self._async_abort_entries_match({"host": import_info["host"]}) - self.bridge = self._async_get_bridge(import_info["host"]) + self.bridge = await self._get_bridge(import_info["host"]) return await self.async_step_link() class HueOptionsFlowHandler(config_entries.OptionsFlow): """Handle Hue options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Hue options flow.""" self.config_entry = config_entry - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) + if self.config_entry.data.get(CONF_API_VERSION, 1) > 1: + # Options for Hue are only applicable to V1 bridges. + return self.async_show_form(step_id="init") + return self.async_show_form( step_id="init", data_schema=vol.Schema( diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 5313584659d..eef453fb83d 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -1,24 +1,34 @@ """Constants for the Hue component.""" -import logging -LOGGER = logging.getLogger(__package__) DOMAIN = "hue" -# How long to wait to actually do the refresh after requesting it. -# We wait some time so if we control multiple lights, we batch requests. -REQUEST_REFRESH_DELAY = 0.3 +CONF_API_VERSION = "api_version" -CONF_ALLOW_UNREACHABLE = "allow_unreachable" -DEFAULT_ALLOW_UNREACHABLE = False +CONF_SUBTYPE = "subtype" -CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" -DEFAULT_ALLOW_HUE_GROUPS = False +ATTR_HUE_EVENT = "hue_event" +SERVICE_HUE_ACTIVATE_SCENE = "hue_activate_scene" +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +ATTR_TRANSITION = "transition" +ATTR_DYNAMIC = "dynamic" + + +# V1 API SPECIFIC CONSTANTS ################## GROUP_TYPE_LIGHT_GROUP = "LightGroup" GROUP_TYPE_ROOM = "Room" GROUP_TYPE_LUMINAIRE = "Luminaire" GROUP_TYPE_LIGHT_SOURCE = "LightSource" +GROUP_TYPE_ZONE = "Zone" +GROUP_TYPE_ENTERTAINMENT = "Entertainment" -ATTR_GROUP_NAME = "group_name" -ATTR_SCENE_NAME = "scene_name" -ATTR_TRANSITION = "transition" +CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" +DEFAULT_ALLOW_HUE_GROUPS = False + +CONF_ALLOW_UNREACHABLE = "allow_unreachable" +DEFAULT_ALLOW_UNREACHABLE = False + +# How long to wait to actually do the refresh after requesting it. +# We wait some time so if we control multiple lights, we batch requests. +REQUEST_REFRESH_DELAY = 0.3 diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index 5af68b9d769..76fb8cd6c96 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -1,189 +1,105 @@ """Provides device automations for Philips Hue events.""" -import voluptuous as vol -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from typing import TYPE_CHECKING + from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.homeassistant.triggers import event as event_trigger -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_EVENT, - CONF_PLATFORM, - CONF_TYPE, - CONF_UNIQUE_ID, +from homeassistant.const import CONF_DEVICE_ID +from homeassistant.core import CALLBACK_TYPE +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN +from .v1.device_trigger import ( + async_attach_trigger as async_attach_trigger_v1, + async_get_triggers as async_get_triggers_v1, + async_validate_trigger_config as async_validate_trigger_config_v1, +) +from .v2.device_trigger import ( + async_attach_trigger as async_attach_trigger_v2, + async_get_triggers as async_get_triggers_v2, + async_validate_trigger_config as async_validate_trigger_config_v2, ) -from . import DOMAIN -from .hue_event import CONF_HUE_EVENT +if TYPE_CHECKING: + from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, + ) + from homeassistant.core import HomeAssistant -CONF_SUBTYPE = "subtype" - -CONF_SHORT_PRESS = "remote_button_short_press" -CONF_SHORT_RELEASE = "remote_button_short_release" -CONF_LONG_RELEASE = "remote_button_long_release" -CONF_DOUBLE_SHORT_RELEASE = "remote_double_button_short_press" -CONF_DOUBLE_LONG_RELEASE = "remote_double_button_long_press" - -CONF_TURN_ON = "turn_on" -CONF_TURN_OFF = "turn_off" -CONF_DIM_UP = "dim_up" -CONF_DIM_DOWN = "dim_down" -CONF_BUTTON_1 = "button_1" -CONF_BUTTON_2 = "button_2" -CONF_BUTTON_3 = "button_3" -CONF_BUTTON_4 = "button_4" -CONF_DOUBLE_BUTTON_1 = "double_buttons_1_3" -CONF_DOUBLE_BUTTON_2 = "double_buttons_2_4" - -HUE_DIMMER_REMOTE_MODEL = "Hue dimmer switch" # RWL020/021 -HUE_DIMMER_REMOTE = { - (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, - (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, - (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, - (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, - (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, - (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, - (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, - (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, -} - -HUE_BUTTON_REMOTE_MODEL = "Hue Smart button" # ZLLSWITCH/ROM001 -HUE_BUTTON_REMOTE = { - (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, - (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, -} - -HUE_WALL_REMOTE_MODEL = "Hue wall switch module" # ZLLSWITCH/RDM001 -HUE_WALL_REMOTE = { - (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, - (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, -} - -HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH -HUE_TAP_REMOTE = { - (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, - (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 16}, - (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 17}, - (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 18}, -} - -HUE_FOHSWITCH_REMOTE_MODEL = "Friends of Hue Switch" # ZGPSWITCH -HUE_FOHSWITCH_REMOTE = { - (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 20}, - (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 16}, - (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 21}, - (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 17}, - (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 23}, - (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 19}, - (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 22}, - (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 18}, - (CONF_DOUBLE_SHORT_RELEASE, CONF_DOUBLE_BUTTON_1): {CONF_EVENT: 101}, - (CONF_DOUBLE_LONG_RELEASE, CONF_DOUBLE_BUTTON_1): {CONF_EVENT: 100}, - (CONF_DOUBLE_SHORT_RELEASE, CONF_DOUBLE_BUTTON_2): {CONF_EVENT: 99}, - (CONF_DOUBLE_LONG_RELEASE, CONF_DOUBLE_BUTTON_2): {CONF_EVENT: 98}, -} + from .bridge import HueBridge -REMOTES = { - HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, - HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, - HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, - HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE, - HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE, -} - -TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} -) - - -def _get_hue_event_from_device_id(hass, device_id): - """Resolve hue event from device id.""" - for bridge in hass.data.get(DOMAIN, {}).values(): - for hue_event in bridge.sensor_manager.current_events.values(): - if device_id == hue_event.device_registry_id: - return hue_event - - return None - - -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config(hass: "HomeAssistant", config: ConfigType): """Validate config.""" - config = TRIGGER_SCHEMA(config) + if DOMAIN not in hass.data: + # happens at startup + return config + device_id = config[CONF_DEVICE_ID] + # lookup device in HASS DeviceRegistry + dev_reg: dr.DeviceRegistry = dr.async_get(hass) + device_entry = dev_reg.async_get(device_id) + if device_entry is None: + raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") - device_registry = await hass.helpers.device_registry.async_get_registry() - device = device_registry.async_get(config[CONF_DEVICE_ID]) - - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - - if not device: - raise InvalidDeviceAutomationConfig( - f"Device {config[CONF_DEVICE_ID]} not found" - ) - - if device.model not in REMOTES: - raise InvalidDeviceAutomationConfig( - f"Device model {device.model} is not a remote" - ) - - if trigger not in REMOTES[device.model]: - raise InvalidDeviceAutomationConfig( - f"Device does not support trigger {trigger}" - ) - - return config + for conf_entry_id in device_entry.config_entries: + if conf_entry_id not in hass.data[DOMAIN]: + continue + bridge: "HueBridge" = hass.data[DOMAIN][conf_entry_id] + if bridge.api_version == 1: + return await async_validate_trigger_config_v1(bridge, device_entry, config) + return await async_validate_trigger_config_v2(bridge, device_entry, config) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: "HomeAssistant", + config: ConfigType, + action: "AutomationActionType", + automation_info: "AutomationTriggerInfo", +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - device_registry = await hass.helpers.device_registry.async_get_registry() - device = device_registry.async_get(config[CONF_DEVICE_ID]) + device_id = config[CONF_DEVICE_ID] + # lookup device in HASS DeviceRegistry + dev_reg: dr.DeviceRegistry = dr.async_get(hass) + device_entry = dev_reg.async_get(device_id) + if device_entry is None: + raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") - hue_event = _get_hue_event_from_device_id(hass, device.id) - if hue_event is None: - raise InvalidDeviceAutomationConfig - - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - - trigger = REMOTES[device.model][trigger] - - event_config = { - event_trigger.CONF_PLATFORM: "event", - event_trigger.CONF_EVENT_TYPE: CONF_HUE_EVENT, - event_trigger.CONF_EVENT_DATA: {CONF_UNIQUE_ID: hue_event.unique_id, **trigger}, - } - - event_config = event_trigger.TRIGGER_SCHEMA(event_config) - return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + for conf_entry_id in device_entry.config_entries: + if conf_entry_id not in hass.data[DOMAIN]: + continue + bridge: "HueBridge" = hass.data[DOMAIN][conf_entry_id] + if bridge.api_version == 1: + return await async_attach_trigger_v1( + bridge, device_entry, config, action, automation_info + ) + return await async_attach_trigger_v2( + bridge, device_entry, config, action, automation_info + ) + raise InvalidDeviceAutomationConfig( + f"Device ID {device_id} is not found on any Hue bridge" ) -async def async_get_triggers(hass, device_id): - """List device triggers. +async def async_get_triggers(hass: "HomeAssistant", device_id: str): + """Get device triggers for given (hass) device id.""" + if DOMAIN not in hass.data: + return [] + # lookup device in HASS DeviceRegistry + dev_reg: dr.DeviceRegistry = dr.async_get(hass) + device_entry = dev_reg.async_get(device_id) + if device_entry is None: + raise ValueError(f"Device ID {device_id} is not valid") - Make sure device is a supported remote model. - Retrieve the hue event object matching device entry. - Generate device trigger list. - """ - device_registry = await hass.helpers.device_registry.async_get_registry() - device = device_registry.async_get(device_id) + # Iterate all config entries for this device + # and work out the bridge version + for conf_entry_id in device_entry.config_entries: + if conf_entry_id not in hass.data[DOMAIN]: + continue + bridge: "HueBridge" = hass.data[DOMAIN][conf_entry_id] - if device.model not in REMOTES: - return - - triggers = [] - for trigger, subtype in REMOTES[device.model]: - triggers.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: trigger, - CONF_SUBTYPE: subtype, - } - ) - - return triggers + if bridge.api_version == 1: + return await async_get_triggers_v1(bridge, device_entry) + return await async_get_triggers_v2(bridge, device_entry) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 12fbf77aa8b..2bd9652f9b0 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,563 +1,28 @@ -"""Support for the Philips Hue lights.""" +"""Support for Hue lights.""" from __future__ import annotations -from datetime import timedelta -from functools import partial -import logging -import random +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -import aiohue -import async_timeout - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_FLASH, - ATTR_HS_COLOR, - ATTR_TRANSITION, - EFFECT_COLORLOOP, - EFFECT_RANDOM, - FLASH_LONG, - FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, - SUPPORT_FLASH, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.core import callback -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -from homeassistant.util import color - -from .const import ( - DOMAIN as HUE_DOMAIN, - GROUP_TYPE_LIGHT_GROUP, - GROUP_TYPE_LIGHT_SOURCE, - GROUP_TYPE_LUMINAIRE, - GROUP_TYPE_ROOM, - REQUEST_REFRESH_DELAY, -) -from .helpers import remove_devices - -SCAN_INTERVAL = timedelta(seconds=5) - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_HUE_ON_OFF = SUPPORT_FLASH | SUPPORT_TRANSITION -SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS -SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP -SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR -SUPPORT_HUE_EXTENDED = SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR - -SUPPORT_HUE = { - "Extended color light": SUPPORT_HUE_EXTENDED, - "Color light": SUPPORT_HUE_COLOR, - "Dimmable light": SUPPORT_HUE_DIMMABLE, - "On/Off plug-in unit": SUPPORT_HUE_ON_OFF, - "Color temperature light": SUPPORT_HUE_COLOR_TEMP, -} - -ATTR_IS_HUE_GROUP = "is_hue_group" -GAMUT_TYPE_UNAVAILABLE = "None" -# Minimum Hue Bridge API version to support groups -# 1.4.0 introduced extended group info -# 1.12 introduced the state object for groups -# 1.13 introduced "any_on" to group state objects -GROUP_MIN_API_VERSION = (1, 13, 0) +from .bridge import HueBridge +from .const import DOMAIN +from .v1.light import async_setup_entry as setup_entry_v1 +from .v2.group import async_setup_entry as setup_groups_entry_v2 +from .v2.light import async_setup_entry as setup_entry_v2 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Hue lights. +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up light entities.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] - Can only be called when a user accidentally mentions hue platform in their - config. But even in that case it would have been ignored. - """ - - -def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id): - """Create the light.""" - api_item = api[item_id] - - if is_group: - supported_features = 0 - for light_id in api_item.lights: - if light_id not in bridge.api.lights: - continue - light = bridge.api.lights[light_id] - supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED) - supported_features = supported_features or SUPPORT_HUE_EXTENDED - else: - supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED) - return item_class( - coordinator, bridge, is_group, api_item, supported_features, rooms - ) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Hue lights from a config entry.""" - bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] - api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) - rooms = {} - - allow_groups = bridge.allow_groups - supports_groups = api_version >= GROUP_MIN_API_VERSION - if allow_groups and not supports_groups: - _LOGGER.warning("Please update your Hue bridge to support groups") - - light_coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="light", - update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), - update_interval=SCAN_INTERVAL, - request_refresh_debouncer=Debouncer( - bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True - ), - ) - - # First do a refresh to see if we can reach the hub. - # Otherwise we will declare not ready. - await light_coordinator.async_refresh() - - if not light_coordinator.last_update_success: - raise PlatformNotReady - - if not supports_groups: - update_lights_without_group_support = partial( - async_update_items, - bridge, - bridge.api.lights, - {}, - async_add_entities, - partial(create_light, HueLight, light_coordinator, bridge, False, rooms), - None, - ) - # We add a listener after fetching the data, so manually trigger listener - bridge.reset_jobs.append( - light_coordinator.async_add_listener(update_lights_without_group_support) - ) + if bridge.api_version == 1: + await setup_entry_v1(hass, config_entry, async_add_entities) return - - group_coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="group", - update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), - update_interval=SCAN_INTERVAL, - request_refresh_debouncer=Debouncer( - bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True - ), - ) - - if allow_groups: - update_groups = partial( - async_update_items, - bridge, - bridge.api.groups, - {}, - async_add_entities, - partial(create_light, HueLight, group_coordinator, bridge, True, None), - None, - ) - - bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups)) - - cancel_update_rooms_listener = None - - @callback - def _async_update_rooms(): - """Update rooms.""" - nonlocal cancel_update_rooms_listener - rooms.clear() - for item_id in bridge.api.groups: - group = bridge.api.groups[item_id] - if group.type != GROUP_TYPE_ROOM: - continue - for light_id in group.lights: - rooms[light_id] = group.name - - # Once we do a rooms update, we cancel the listener - # until the next time lights are added - bridge.reset_jobs.remove(cancel_update_rooms_listener) - cancel_update_rooms_listener() # pylint: disable=not-callable - cancel_update_rooms_listener = None - - @callback - def _setup_rooms_listener(): - nonlocal cancel_update_rooms_listener - if cancel_update_rooms_listener is not None: - # If there are new lights added before _async_update_rooms - # is called we should not add another listener - return - - cancel_update_rooms_listener = group_coordinator.async_add_listener( - _async_update_rooms - ) - bridge.reset_jobs.append(cancel_update_rooms_listener) - - _setup_rooms_listener() - await group_coordinator.async_refresh() - - update_lights_with_group_support = partial( - async_update_items, - bridge, - bridge.api.lights, - {}, - async_add_entities, - partial(create_light, HueLight, light_coordinator, bridge, False, rooms), - _setup_rooms_listener, - ) - # We add a listener after fetching the data, so manually trigger listener - bridge.reset_jobs.append( - light_coordinator.async_add_listener(update_lights_with_group_support) - ) - update_lights_with_group_support() - - -async def async_safe_fetch(bridge, fetch_method): - """Safely fetch data.""" - try: - async with async_timeout.timeout(4): - return await bridge.async_request_call(fetch_method) - except aiohue.Unauthorized as err: - await bridge.handle_unauthorized_error() - raise UpdateFailed("Unauthorized") from err - except aiohue.AiohueException as err: - raise UpdateFailed(f"Hue error: {err}") from err - - -@callback -def async_update_items( - bridge, api, current, async_add_entities, create_item, new_items_callback -): - """Update items.""" - new_items = [] - - for item_id in api: - if item_id in current: - continue - - current[item_id] = create_item(api, item_id) - new_items.append(current[item_id]) - - bridge.hass.async_create_task(remove_devices(bridge, api, current)) - - if new_items: - # This is currently used to setup the listener to update rooms - if new_items_callback: - new_items_callback() - async_add_entities(new_items) - - -def hue_brightness_to_hass(value): - """Convert hue brightness 1..254 to hass format 0..255.""" - return min(255, round((value / 254) * 255)) - - -def hass_to_hue_brightness(value): - """Convert hass brightness 0..255 to hue 1..254 scale.""" - return max(1, round((value / 255) * 254)) - - -class HueLight(CoordinatorEntity, LightEntity): - """Representation of a Hue light.""" - - def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms): - """Initialize the light.""" - super().__init__(coordinator) - self.light = light - self.bridge = bridge - self.is_group = is_group - self._supported_features = supported_features - self._rooms = rooms - - if is_group: - self.is_osram = False - self.is_philips = False - self.is_innr = False - self.is_ewelink = False - self.is_livarno = False - self.gamut_typ = GAMUT_TYPE_UNAVAILABLE - self.gamut = None - else: - self.is_osram = light.manufacturername == "OSRAM" - self.is_philips = light.manufacturername == "Philips" - self.is_innr = light.manufacturername == "innr" - self.is_ewelink = light.manufacturername == "eWeLink" - self.is_livarno = light.manufacturername.startswith("_TZ3000_") - self.gamut_typ = self.light.colorgamuttype - self.gamut = self.light.colorgamut - _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) - if self.light.swupdatestate == "readytoinstall": - err = ( - "Please check for software updates of the %s " - "bulb in the Philips Hue App." - ) - _LOGGER.warning(err, self.name) - if self.gamut and not color.check_valid_gamut(self.gamut): - err = "Color gamut of %s: %s, not valid, setting gamut to None." - _LOGGER.debug(err, self.name, str(self.gamut)) - self.gamut_typ = GAMUT_TYPE_UNAVAILABLE - self.gamut = None - - @property - def unique_id(self): - """Return the unique ID of this Hue light.""" - unique_id = self.light.uniqueid - if not unique_id and self.is_group and self.light.room: - unique_id = self.light.room["id"] - - return unique_id - - @property - def device_id(self): - """Return the ID of this Hue light.""" - return self.unique_id - - @property - def name(self): - """Return the name of the Hue light.""" - return self.light.name - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - if self.is_group: - bri = self.light.action.get("bri") - else: - bri = self.light.state.get("bri") - - if bri is None: - return bri - - return hue_brightness_to_hass(bri) - - @property - def _color_mode(self): - """Return the hue color mode.""" - if self.is_group: - return self.light.action.get("colormode") - return self.light.state.get("colormode") - - @property - def hs_color(self): - """Return the hs color value.""" - mode = self._color_mode - source = self.light.action if self.is_group else self.light.state - - if mode in ("xy", "hs") and "xy" in source: - return color.color_xy_to_hs(*source["xy"], self.gamut) - - return None - - @property - def color_temp(self): - """Return the CT color value.""" - # Don't return color temperature unless in color temperature mode - if self._color_mode != "ct": - return None - - if self.is_group: - return self.light.action.get("ct") - return self.light.state.get("ct") - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - if self.is_group: - return super().min_mireds - - min_mireds = self.light.controlcapabilities.get("ct", {}).get("min") - - # We filter out '0' too, which can be incorrectly reported by 3rd party buls - if not min_mireds: - return super().min_mireds - - return min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - if self.is_group: - return super().max_mireds - if self.is_livarno: - return 500 - - max_mireds = self.light.controlcapabilities.get("ct", {}).get("max") - - if not max_mireds: - return super().max_mireds - - return max_mireds - - @property - def is_on(self): - """Return true if device is on.""" - if self.is_group: - return self.light.state["any_on"] - return self.light.state["on"] - - @property - def available(self): - """Return if light is available.""" - return self.coordinator.last_update_success and ( - self.is_group - or self.bridge.allow_unreachable - or self.light.state["reachable"] - ) - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - @property - def effect(self): - """Return the current effect.""" - return self.light.state.get("effect", None) - - @property - def effect_list(self): - """Return the list of supported effects.""" - if self.is_osram: - return [EFFECT_RANDOM] - return [EFFECT_COLORLOOP, EFFECT_RANDOM] - - @property - def device_info(self) -> DeviceInfo | None: - """Return the device info.""" - if self.light.type in ( - GROUP_TYPE_LIGHT_GROUP, - GROUP_TYPE_ROOM, - GROUP_TYPE_LUMINAIRE, - GROUP_TYPE_LIGHT_SOURCE, - ): - return None - - suggested_area = None - if self.light.id in self._rooms: - suggested_area = self._rooms[self.light.id] - - return DeviceInfo( - identifiers={(HUE_DOMAIN, self.device_id)}, - manufacturer=self.light.manufacturername, - # productname added in Hue Bridge API 1.24 - # (published 03/05/2018) - model=self.light.productname or self.light.modelid, - name=self.name, - # Not yet exposed as properties in aiohue - suggested_area=suggested_area, - sw_version=self.light.raw["swversion"], - via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), - ) - - async def async_added_to_hass(self) -> None: - """Handle entity being added to Home Assistant.""" - self.async_on_remove( - self.bridge.listen_updates( - self.light.ITEM_TYPE, self.light.id, self.async_write_ha_state - ) - ) - await super().async_added_to_hass() - - async def async_turn_on(self, **kwargs): - """Turn the specified or all lights on.""" - command = {"on": True} - - if ATTR_TRANSITION in kwargs: - command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) - - if ATTR_HS_COLOR in kwargs: - if self.is_osram: - command["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - command["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) - else: - # Philips hue bulb models respond differently to hue/sat - # requests, so we convert to XY first to ensure a consistent - # color. - xy_color = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR], self.gamut) - command["xy"] = xy_color - elif ATTR_COLOR_TEMP in kwargs: - temp = kwargs[ATTR_COLOR_TEMP] - command["ct"] = max(self.min_mireds, min(temp, self.max_mireds)) - - if ATTR_BRIGHTNESS in kwargs: - command["bri"] = hass_to_hue_brightness(kwargs[ATTR_BRIGHTNESS]) - - flash = kwargs.get(ATTR_FLASH) - - if flash == FLASH_LONG: - command["alert"] = "lselect" - del command["on"] - elif flash == FLASH_SHORT: - command["alert"] = "select" - del command["on"] - elif not self.is_innr and not self.is_ewelink and not self.is_livarno: - command["alert"] = "none" - - if ATTR_EFFECT in kwargs: - effect = kwargs[ATTR_EFFECT] - if effect == EFFECT_COLORLOOP: - command["effect"] = "colorloop" - elif effect == EFFECT_RANDOM: - command["hue"] = random.randrange(0, 65535) - command["sat"] = random.randrange(150, 254) - else: - command["effect"] = "none" - - if self.is_group: - await self.bridge.async_request_call( - partial(self.light.set_action, **command) - ) - else: - await self.bridge.async_request_call( - partial(self.light.set_state, **command) - ) - - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **kwargs): - """Turn the specified or all lights off.""" - command = {"on": False} - - if ATTR_TRANSITION in kwargs: - command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) - - flash = kwargs.get(ATTR_FLASH) - - if flash == FLASH_LONG: - command["alert"] = "lselect" - del command["on"] - elif flash == FLASH_SHORT: - command["alert"] = "select" - del command["on"] - elif not self.is_innr and not self.is_livarno: - command["alert"] = "none" - - if self.is_group: - await self.bridge.async_request_call( - partial(self.light.set_action, **command) - ) - else: - await self.bridge.async_request_call( - partial(self.light.set_state, **command) - ) - - await self.coordinator.async_request_refresh() - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - if not self.is_group: - return {} - return {ATTR_IS_HUE_GROUP: self.is_group} + # v2 setup logic here + await setup_entry_v2(hass, config_entry, async_add_entities) + await setup_groups_entry_v2(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 6640ffc9fae..75cdea853e7 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.6.3"], + "requirements": ["aiohue==3.0.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", @@ -22,7 +22,7 @@ "models": ["BSB002"] }, "zeroconf": ["_hue._tcp.local."], - "codeowners": ["@balloob", "@frenck"], + "codeowners": ["@balloob", "@frenck", "@marcelveldt"], "quality_scale": "platinum", "iot_class": "local_push" } diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py new file mode 100644 index 00000000000..fb584a19100 --- /dev/null +++ b/homeassistant/components/hue/migration.py @@ -0,0 +1,174 @@ +"""Various helpers to handle config entry and api schema migrations.""" + +import logging + +from aiohue import HueBridgeV2 +from aiohue.discovery import is_v2_bridge +from aiohue.v2.models.resource import ResourceTypes + +from homeassistant import core +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_USERNAME, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry as entities_for_config_entry, + async_entries_for_device, + async_get as async_get_entity_registry, +) + +from .const import CONF_API_VERSION, DOMAIN + +LOGGER = logging.getLogger(__name__) + + +async def check_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: + """Check if config entry needs any migration actions.""" + host = entry.data[CONF_HOST] + + # migrate CONF_USERNAME --> CONF_API_KEY + if CONF_USERNAME in entry.data: + LOGGER.info("Migrate %s to %s in schema", CONF_USERNAME, CONF_API_KEY) + data = dict(entry.data) + data[CONF_API_KEY] = data.pop(CONF_USERNAME) + hass.config_entries.async_update_entry(entry, data=data) + + conf_api_version = entry.data.get(CONF_API_VERSION, 1) + if conf_api_version == 1: + # a bridge might have upgraded firmware since last run so + # we discover its capabilities at every startup + websession = aiohttp_client.async_get_clientsession(hass) + if await is_v2_bridge(host, websession): + supported_api_version = 2 + else: + supported_api_version = 1 + LOGGER.debug( + "Configured api version is %s and supported api version %s for bridge %s", + conf_api_version, + supported_api_version, + host, + ) + + # the call to `is_v2_bridge` returns (silently) False even on connection error + # so if a migration is needed it will be done on next startup + + if conf_api_version == 1 and supported_api_version == 2: + # run entity/device schema migration for v2 + await handle_v2_migration(hass, entry) + + # store api version in entry data + if ( + CONF_API_VERSION not in entry.data + or conf_api_version != supported_api_version + ): + data = dict(entry.data) + data[CONF_API_VERSION] = supported_api_version + hass.config_entries.async_update_entry(entry, data=data) + + +async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: + """Perform migration of devices and entities to V2 Id's.""" + host = entry.data[CONF_HOST] + api_key = entry.data[CONF_API_KEY] + websession = aiohttp_client.async_get_clientsession(hass) + dev_reg = async_get_device_registry(hass) + ent_reg = async_get_entity_registry(hass) + LOGGER.info("Start of migration of devices and entities to support API schema 2") + # initialize bridge connection just for the migration + async with HueBridgeV2(host, api_key, websession) as api: + + sensor_class_mapping = { + DEVICE_CLASS_BATTERY: ResourceTypes.DEVICE_POWER, + DEVICE_CLASS_MOTION: ResourceTypes.MOTION, + DEVICE_CLASS_ILLUMINANCE: ResourceTypes.LIGHT_LEVEL, + DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, + } + + # handle entities attached to device + for hue_dev in api.devices: + zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) + if not zigbee: + # not a zigbee device + continue + mac = zigbee.mac_address + # get/update existing device by V1 identifier (mac address) + # the device will now have both the old and the new identifier + identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, mac)} + hass_dev = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, identifiers=identifiers + ) + LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id) + # loop through al entities for device and find match + for ent in async_entries_for_device(ent_reg, hass_dev.id, True): + # migrate light + if ent.entity_id.startswith("light"): + # should always return one lightid here + new_unique_id = next(iter(hue_dev.lights)) + if ent.unique_id == new_unique_id: + continue # just in case + LOGGER.info( + "Migrating %s from unique id %s to %s", + ent.entity_id, + ent.unique_id, + new_unique_id, + ) + ent_reg.async_update_entity( + ent.entity_id, new_unique_id=new_unique_id + ) + continue + # migrate sensors + matched_dev_class = sensor_class_mapping.get( + ent.device_class or "unknown" + ) + if matched_dev_class is None: + # this may happen if we're looking at orphaned or unsupported entity + LOGGER.warning( + "Skip migration of %s because it no longer exists on the bridge", + ent.entity_id, + ) + continue + for sensor in api.devices.get_sensors(hue_dev.id): + if sensor.type != matched_dev_class: + continue + new_unique_id = sensor.id + if ent.unique_id == new_unique_id: + break # just in case + LOGGER.info( + "Migrating %s from unique id %s to %s", + ent.entity_id, + ent.unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(ent.entity_id, new_unique_id=sensor.id) + break + + # migrate entities that are not connected to a device (groups) + for ent in entities_for_config_entry(ent_reg, entry.entry_id): + if ent.device_id is not None: + continue + v1_id = f"/groups/{ent.unique_id}" + hue_group = api.groups.room.get_by_v1_id(v1_id) + if hue_group is None or hue_group.grouped_light is None: + # this may happen if we're looking at some orphaned entity + LOGGER.warning( + "Skip migration of %s because it no longer exist on the bridge", + ent.entity_id, + ) + continue + new_unique_id = hue_group.grouped_light + LOGGER.info( + "Migrating %s from unique id %s to %s ", + ent.entity_id, + ent.unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) + LOGGER.info("Migration of devices and entities to support API schema 2 finished") diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py new file mode 100644 index 00000000000..55807ad0f2f --- /dev/null +++ b/homeassistant/components/hue/scene.py @@ -0,0 +1,117 @@ +"""Support for scene platform for Hue scenes (V2 only).""" +from __future__ import annotations + +from typing import Any + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.scenes import ScenesController +from aiohue.v2.models.scene import Scene as HueScene + +from homeassistant.components.scene import Scene as SceneEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .bridge import HueBridge +from .const import DOMAIN +from .v2.entity import HueBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up scene platform from Hue group scenes.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + + if bridge.api_version == 1: + # should not happen, but just in case + raise NotImplementedError("Scene support is only available for V2 bridges") + + # add entities for all scenes + @callback + def async_add_entity(event_type: EventType, resource: HueScene) -> None: + """Add entity from Hue resource.""" + async_add_entities([HueSceneEntity(bridge, api.scenes, resource)]) + + # add all current items in controller + for item in api.scenes: + async_add_entity(EventType.RESOURCE_ADDED, item) + + # register listener for new items only + config_entry.async_on_unload( + api.scenes.subscribe(async_add_entity, event_filter=EventType.RESOURCE_ADDED) + ) + + +class HueSceneEntity(HueBaseEntity, SceneEntity): + """Representation of a Scene entity from Hue Scenes.""" + + def __init__( + self, + bridge: HueBridge, + controller: ScenesController, + resource: HueScene, + ) -> None: + """Initialize the entity.""" + super().__init__(bridge, controller, resource) + self.resource = resource + self.controller = controller + + @property + def name(self) -> str: + """Return default entity name.""" + group = self.controller.get_group(self.resource.id) + return f"{group.metadata.name} - {self.resource.metadata.name}" + + @property + def is_dynamic(self) -> bool: + """Return if this scene has a dynamic color palette.""" + if self.resource.palette.color and len(self.resource.palette.color) > 1: + return True + if ( + self.resource.palette.color_temperature + and len(self.resource.palette.color_temperature) > 1 + ): + return True + return False + + async def async_activate(self, **kwargs: Any) -> None: + """Activate Hue scene.""" + transition = kwargs.get("transition") + if transition is not None: + # hue transition duration is in steps of 100 ms + transition = int(transition * 100) + dynamic = kwargs.get("dynamic", self.is_dynamic) + await self.bridge.async_request_call( + self.controller.recall, + self.resource.id, + dynamic=dynamic, + duration=transition, + ) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the optional state attributes.""" + group = self.controller.get_group(self.resource.id) + brightness = None + if palette := self.resource.palette: + if palette.dimming: + brightness = palette.dimming[0].brightness + if brightness is None: + # get brightness from actions + for action in self.resource.actions: + if action.action.dimming: + brightness = action.action.dimming.brightness + break + return { + "group_name": group.metadata.name, + "group_type": group.type.value, + "name": self.resource.metadata.name, + "speed": self.resource.speed, + "brightness": brightness, + "is_dynamic": self.is_dynamic, + } diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 9bd701fe526..7218831abe2 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,135 +1,24 @@ -"""Hue sensor entities.""" -from aiohue.sensors import ( - TYPE_ZLL_LIGHTLEVEL, - TYPE_ZLL_ROTARY, - TYPE_ZLL_SWITCH, - TYPE_ZLL_TEMPERATURE, -) +"""Support for Hue sensors.""" +from __future__ import annotations -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as HUE_DOMAIN -from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor - -LIGHT_LEVEL_NAME_FORMAT = "{} light level" -REMOTE_NAME_FORMAT = "{} battery level" -TEMPERATURE_NAME_FORMAT = "{} temperature" +from .bridge import HueBridge +from .const import DOMAIN +from .v1.sensor import async_setup_entry as setup_entry_v1 +from .v2.sensor import async_setup_entry as setup_entry_v2 -async def async_setup_entry(hass, config_entry, async_add_entities): - """Defer sensor setup to the shared sensor module.""" - bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] - - if not bridge.sensor_manager: +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + if bridge.api_version == 1: + await setup_entry_v1(hass, config_entry, async_add_entities) return - - await bridge.sensor_manager.async_register_component("sensor", async_add_entities) - - -class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): - """Parent class for all 'gauge' Hue device sensors.""" - - -class HueLightLevel(GenericHueGaugeSensorEntity): - """The light level sensor entity for a Hue motion sensor device.""" - - _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_native_unit_of_measurement = LIGHT_LUX - - @property - def native_value(self): - """Return the state of the device.""" - if self.sensor.lightlevel is None: - return None - - # https://developers.meethue.com/develop/hue-api/supported-devices/#clip_zll_lightlevel - # Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm - # scale used because the human eye adjusts to light levels and small - # changes at low lux levels are more noticeable than at high lux - # levels. - return round(float(10 ** ((self.sensor.lightlevel - 1) / 10000)), 2) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = super().extra_state_attributes - attributes.update( - { - "lightlevel": self.sensor.lightlevel, - "daylight": self.sensor.daylight, - "dark": self.sensor.dark, - "threshold_dark": self.sensor.tholddark, - "threshold_offset": self.sensor.tholdoffset, - } - ) - return attributes - - -class HueTemperature(GenericHueGaugeSensorEntity): - """The temperature sensor entity for a Hue motion sensor device.""" - - _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def native_value(self): - """Return the state of the device.""" - if self.sensor.temperature is None: - return None - - return self.sensor.temperature / 100 - - -class HueBattery(GenericHueSensor, SensorEntity): - """Battery class for when a batt-powered device is only represented as an event.""" - - _attr_device_class = DEVICE_CLASS_BATTERY - _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_native_unit_of_measurement = PERCENTAGE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - - @property - def unique_id(self): - """Return a unique identifier for this device.""" - return f"{self.sensor.uniqueid}-battery" - - @property - def native_value(self): - """Return the state of the battery.""" - return self.sensor.battery - - -SENSOR_CONFIG_MAP.update( - { - TYPE_ZLL_LIGHTLEVEL: { - "platform": "sensor", - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - TYPE_ZLL_TEMPERATURE: { - "platform": "sensor", - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - TYPE_ZLL_SWITCH: { - "platform": "sensor", - "name_format": REMOTE_NAME_FORMAT, - "class": HueBattery, - }, - TYPE_ZLL_ROTARY: { - "platform": "sensor", - "name_format": REMOTE_NAME_FORMAT, - "class": HueBattery, - }, - } -) + await setup_entry_v2(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py new file mode 100644 index 00000000000..72e88f0d956 --- /dev/null +++ b/homeassistant/components/hue/services.py @@ -0,0 +1,158 @@ +"""Handle Hue Service calls.""" +from __future__ import annotations + +import asyncio +import logging + +from aiohue import HueBridgeV1, HueBridgeV2 +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import verify_domain_control + +from .bridge import HueBridge +from .const import ( + ATTR_DYNAMIC, + ATTR_GROUP_NAME, + ATTR_SCENE_NAME, + ATTR_TRANSITION, + DOMAIN, + SERVICE_HUE_ACTIVATE_SCENE, +) + +LOGGER = logging.getLogger(__name__) + + +def async_register_services(hass: HomeAssistant) -> None: + """Register services for Hue integration.""" + + async def hue_activate_scene(call: ServiceCall, skip_reload=True): + """Handle activation of Hue scene.""" + # Get parameters + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + transition = call.data.get(ATTR_TRANSITION) + dynamic = call.data.get(ATTR_DYNAMIC, False) + + # Call the set scene function on each bridge + tasks = [ + hue_activate_scene_v1(bridge, group_name, scene_name, transition) + if bridge.api_version == 1 + else hue_activate_scene_v2( + bridge, group_name, scene_name, transition, dynamic + ) + for bridge in hass.data[DOMAIN].values() + if isinstance(bridge, HueBridge) + ] + results = await asyncio.gather(*tasks) + + # Did *any* bridge succeed? + # Note that we'll get a "True" value for a successful call + if True not in results: + LOGGER.warning( + "No bridge was able to activate scene %s in group %s", + scene_name, + group_name, + ) + + if not hass.services.has_service(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE): + # Register a local handler for scene activation + hass.services.async_register( + DOMAIN, + SERVICE_HUE_ACTIVATE_SCENE, + verify_domain_control(hass, DOMAIN)(hue_activate_scene), + schema=vol.Schema( + { + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, + vol.Optional(ATTR_TRANSITION): cv.positive_int, + vol.Optional(ATTR_DYNAMIC): cv.boolean, + } + ), + ) + + +async def hue_activate_scene_v1( + bridge: HueBridge, + group_name: str, + scene_name: str, + transition: int | None = None, + is_retry: bool = False, +) -> bool: + """Service for V1 bridge to call directly into bridge to set scenes.""" + api: HueBridgeV1 = bridge.api + if api.scenes is None: + LOGGER.warning("Hub %s does not support scenes", api.host) + return False + + group = next( + (group for group in api.groups.values() if group.name == group_name), + None, + ) + # Additional scene logic to handle duplicate scene names across groups + scene = next( + ( + scene + for scene in api.scenes.values() + if scene.name == scene_name + and group is not None + and sorted(scene.lights) == sorted(group.lights) + ), + None, + ) + # If we can't find it, fetch latest info and try again + if not is_retry and (group is None or scene is None): + await bridge.async_request_call(api.groups.update) + await bridge.async_request_call(api.scenes.update) + return await hue_activate_scene_v1( + bridge, group_name, scene_name, transition, is_retry=True + ) + + if group is None or scene is None: + LOGGER.debug( + "Unable to find scene %s for group %s on bridge %s", + scene_name, + group_name, + bridge.host, + ) + return False + + await bridge.async_request_call( + group.set_action, scene=scene.id, transitiontime=transition + ) + return True + + +async def hue_activate_scene_v2( + bridge: HueBridge, + group_name: str, + scene_name: str, + transition: int | None = None, + dynamic: bool = True, +) -> bool: + """Service for V2 bridge to call scene by name.""" + LOGGER.warning( + "Use of service_call '%s' is deprecated and will be removed " + "in a future release. Please use scene entities instead", + SERVICE_HUE_ACTIVATE_SCENE, + ) + api: HueBridgeV2 = bridge.api + for scene in api.scenes: + if scene.metadata.name.lower() != scene_name.lower(): + continue + group = api.scenes.get_group(scene.id) + if group.metadata.name.lower() != group_name.lower(): + continue + # found match! + if transition: + transition = transition * 100 # in steps of 100ms + await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition) + return True + LOGGER.debug( + "Unable to find scene %s for group %s on bridge %s", + scene_name, + group_name, + bridge.host, + ) + return False diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index 07eeca6fa0f..4e6d1ad6998 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -16,3 +16,8 @@ hue_activate_scene: example: "Energize" selector: text: + dynamic: + name: Dynamic + description: Enable dynamic mode of the scene (V2 bridges and supported scenes only). + selector: + boolean: diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 678b7c2cad2..458e21419ab 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -44,14 +44,24 @@ "dim_down": "Dim down", "dim_up": "Dim up", "turn_off": "Turn off", - "turn_on": "Turn on" + "turn_on": "Turn on", + "1": "First button", + "2": "Second button", + "3": "Third button", + "4": "Fourth button" }, "trigger_type": { "remote_button_long_release": "\"{subtype}\" button released after long press", "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", - "remote_double_button_short_press": "Both \"{subtype}\" released" + "remote_double_button_short_press": "Both \"{subtype}\" released", + + "initial_press": "Button \"{subtype}\" pressed initially", + "repeat": "Button \"{subtype}\" held down", + "short_release": "Button \"{subtype}\" released after short press", + "long_release": "Button \"{subtype}\" released after long press", + "double_short_release": "Both \"{subtype}\" released" } }, "options": { @@ -59,6 +69,7 @@ "init": { "data": { "allow_hue_groups": "Allow Hue groups", + "allow_hue_scenes": "Allow Hue scenes", "allow_unreachable": "Allow unreachable bulbs to report their state correctly" } } diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py new file mode 100644 index 00000000000..3de96b45842 --- /dev/null +++ b/homeassistant/components/hue/switch.py @@ -0,0 +1,94 @@ +"""Support for switch platform for Hue resources (V2 only).""" +from __future__ import annotations + +from typing import Any, Union + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.sensors import LightLevelController, MotionController +from aiohue.v2.models.resource import SensingService + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .bridge import HueBridge +from .const import DOMAIN +from .v2.entity import HueBaseEntity + +ControllerType = Union[LightLevelController, MotionController] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Hue switch platform from Hue resources.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + + if bridge.api_version == 1: + # should not happen, but just in case + raise NotImplementedError("Switch support is only available for V2 bridges") + + @callback + def register_items(controller: ControllerType): + @callback + def async_add_entity(event_type: EventType, resource: SensingService) -> None: + """Add entity from Hue resource.""" + async_add_entities( + [HueSensingServiceEnabledEntity(bridge, controller, resource)] + ) + + # add all current items in controller + for item in controller: + async_add_entity(EventType.RESOURCE_ADDED, item) + + # register listener for new items only + config_entry.async_on_unload( + controller.subscribe( + async_add_entity, event_filter=EventType.RESOURCE_ADDED + ) + ) + + # setup for each switch-type hue resource + register_items(api.sensors.motion) + register_items(api.sensors.light_level) + + +class HueSensingServiceEnabledEntity(HueBaseEntity, SwitchEntity): + """Representation of a Switch entity from Hue SensingService.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_device_class = DEVICE_CLASS_SWITCH + + def __init__( + self, + bridge: HueBridge, + controller: LightLevelController | MotionController, + resource: SensingService, + ) -> None: + """Initialize the entity.""" + super().__init__(bridge, controller, resource) + self.resource = resource + self.controller = controller + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return self.resource.enabled + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.bridge.async_request_call( + self.controller.set_enabled, self.resource.id, enabled=True + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.bridge.async_request_call( + self.controller.set_enabled, self.resource.id, enabled=False + ) diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index e03eabd3d23..2f1010c88b7 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -35,23 +35,33 @@ }, "device_automation": { "trigger_subtype": { - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "dim_down": "Dim down", - "dim_up": "Dim up", - "double_buttons_1_3": "First and Third buttons", - "double_buttons_2_4": "Second and Fourth buttons", - "turn_off": "Turn off", - "turn_on": "Turn on" + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "double_buttons_1_3": "First and Third buttons", + "double_buttons_2_4": "Second and Fourth buttons", + "dim_down": "Dim down", + "dim_up": "Dim up", + "turn_off": "Turn off", + "turn_on": "Turn on", + "1": "First button", + "2": "Second button", + "3": "Third button", + "4": "Fourth button" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_double_button_long_press": "Both \"{subtype}\" released after long press", - "remote_double_button_short_press": "Both \"{subtype}\" released" + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_double_button_long_press": "Both \"{subtype}\" released after long press", + "remote_double_button_short_press": "Both \"{subtype}\" released", + + "initial_press": "Button \"{subtype}\" pressed initially", + "repeat": "Button \"{subtype}\" held down", + "short_release": "Button \"{subtype}\" released after short press", + "long_release": "Button \"{subtype}\" released after long press", + "double_short_release": "Both \"{subtype}\" released" } }, "options": { @@ -59,6 +69,7 @@ "init": { "data": { "allow_hue_groups": "Allow Hue groups", + "allow_hue_scenes": "Allow Hue scenes", "allow_unreachable": "Allow unreachable bulbs to report their state correctly" } } diff --git a/homeassistant/components/hue/v1/__init__.py b/homeassistant/components/hue/v1/__init__.py new file mode 100644 index 00000000000..fdca29a0d94 --- /dev/null +++ b/homeassistant/components/hue/v1/__init__.py @@ -0,0 +1 @@ +"""Hue V1 API specific platform implementation.""" diff --git a/homeassistant/components/hue/v1/binary_sensor.py b/homeassistant/components/hue/v1/binary_sensor.py new file mode 100644 index 00000000000..21650e52b9c --- /dev/null +++ b/homeassistant/components/hue/v1/binary_sensor.py @@ -0,0 +1,56 @@ +"""Hue binary sensor entities.""" +from aiohue.v1.sensors import TYPE_ZLL_PRESENCE + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) + +from ..const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor + +PRESENCE_NAME_FORMAT = "{} motion" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer binary sensor setup to the shared sensor module.""" + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component( + "binary_sensor", async_add_entities + ) + + +class HuePresence(GenericZLLSensor, BinarySensorEntity): + """The presence sensor entity for a Hue motion sensor device.""" + + _attr_device_class = DEVICE_CLASS_MOTION + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.sensor.presence + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + attributes = super().extra_state_attributes + if "sensitivity" in self.sensor.config: + attributes["sensitivity"] = self.sensor.config["sensitivity"] + if "sensitivitymax" in self.sensor.config: + attributes["sensitivity_max"] = self.sensor.config["sensitivitymax"] + return attributes + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_PRESENCE: { + "platform": "binary_sensor", + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } +) diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py new file mode 100644 index 00000000000..d6b471b7257 --- /dev/null +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -0,0 +1,185 @@ +"""Provides device automations for Philips Hue events in V1 bridge/api.""" +from typing import TYPE_CHECKING + +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, + CONF_UNIQUE_ID, +) +from homeassistant.helpers.device_registry import DeviceEntry + +from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN + +if TYPE_CHECKING: + from ..bridge import HueBridge + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} +) + + +CONF_SHORT_PRESS = "remote_button_short_press" +CONF_SHORT_RELEASE = "remote_button_short_release" +CONF_LONG_RELEASE = "remote_button_long_release" +CONF_DOUBLE_SHORT_RELEASE = "remote_double_button_short_press" +CONF_DOUBLE_LONG_RELEASE = "remote_double_button_long_press" + +CONF_TURN_ON = "turn_on" +CONF_TURN_OFF = "turn_off" +CONF_DIM_UP = "dim_up" +CONF_DIM_DOWN = "dim_down" +CONF_BUTTON_1 = "button_1" +CONF_BUTTON_2 = "button_2" +CONF_BUTTON_3 = "button_3" +CONF_BUTTON_4 = "button_4" +CONF_DOUBLE_BUTTON_1 = "double_buttons_1_3" +CONF_DOUBLE_BUTTON_2 = "double_buttons_2_4" + +HUE_DIMMER_REMOTE_MODEL = "Hue dimmer switch" # RWL020/021 +HUE_DIMMER_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +HUE_BUTTON_REMOTE_MODEL = "Hue Smart button" # ZLLSWITCH/ROM001 +HUE_BUTTON_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, +} + +HUE_WALL_REMOTE_MODEL = "Hue wall switch module" # ZLLSWITCH/RDM001 +HUE_WALL_REMOTE = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, +} + +HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH +HUE_TAP_REMOTE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 16}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 17}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 18}, +} + +HUE_FOHSWITCH_REMOTE_MODEL = "Friends of Hue Switch" # ZGPSWITCH +HUE_FOHSWITCH_REMOTE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 20}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 16}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 21}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 17}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 23}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 19}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 22}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 18}, + (CONF_DOUBLE_SHORT_RELEASE, CONF_DOUBLE_BUTTON_1): {CONF_EVENT: 101}, + (CONF_DOUBLE_LONG_RELEASE, CONF_DOUBLE_BUTTON_1): {CONF_EVENT: 100}, + (CONF_DOUBLE_SHORT_RELEASE, CONF_DOUBLE_BUTTON_2): {CONF_EVENT: 99}, + (CONF_DOUBLE_LONG_RELEASE, CONF_DOUBLE_BUTTON_2): {CONF_EVENT: 98}, +} + + +REMOTES = { + HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, + HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE, + HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE, +} + + +def _get_hue_event_from_device_id(hass, device_id): + """Resolve hue event from device id.""" + for bridge in hass.data.get(DOMAIN, {}).values(): + for hue_event in bridge.sensor_manager.current_events.values(): + if device_id == hue_event.device_registry_id: + return hue_event + + return None + + +async def async_validate_trigger_config(bridge, device_entry, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + if not device_entry: + raise InvalidDeviceAutomationConfig( + f"Device {config[CONF_DEVICE_ID]} not found" + ) + + if device_entry.model not in REMOTES: + raise InvalidDeviceAutomationConfig( + f"Device model {device_entry.model} is not a remote" + ) + + if trigger not in REMOTES[device_entry.model]: + raise InvalidDeviceAutomationConfig( + f"Device does not support trigger {trigger}" + ) + + return config + + +async def async_attach_trigger(bridge, device_entry, config, action, automation_info): + """Listen for state changes based on configuration.""" + hass = bridge.hass + + hue_event = _get_hue_event_from_device_id(hass, device_entry.id) + if hue_event is None: + raise InvalidDeviceAutomationConfig + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + trigger = REMOTES[device_entry.model][trigger] + + event_config = { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: ATTR_HUE_EVENT, + event_trigger.CONF_EVENT_DATA: {CONF_UNIQUE_ID: hue_event.unique_id, **trigger}, + } + + event_config = event_trigger.TRIGGER_SCHEMA(event_config) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(bridge: "HueBridge", device: DeviceEntry): + """Return device triggers for device on `v1` bridge. + + Make sure device is a supported remote model. + Retrieve the hue event object matching device entry. + Generate device trigger list. + """ + if device.model not in REMOTES: + return + + triggers = [] + for trigger, subtype in REMOTES[device.model]: + triggers.append( + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/v1/helpers.py similarity index 77% rename from homeassistant/components/hue/helpers.py rename to homeassistant/components/hue/v1/helpers.py index 739e27d3360..d4582f0bb52 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/v1/helpers.py @@ -1,9 +1,9 @@ """Helper functions for Philips Hue.""" -from homeassistant import config_entries + from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from .const import DOMAIN +from ..const import DOMAIN async def remove_devices(bridge, api_ids, current): @@ -30,14 +30,3 @@ async def remove_devices(bridge, api_ids, current): for item_id in removed_items: del current[item_id] - - -def create_config_flow(hass, host): - """Start a config flow.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": host}, - ) - ) diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/v1/hue_event.py similarity index 90% rename from homeassistant/components/hue/hue_event.py rename to homeassistant/components/hue/v1/hue_event.py index 00b6c3e44f2..9074baaaa88 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/v1/hue_event.py @@ -1,7 +1,7 @@ """Representation of a Hue remote firing events for button presses.""" import logging -from aiohue.sensors import ( +from aiohue.v1.sensors import ( EVENT_BUTTON, TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, @@ -12,11 +12,11 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE from homeassistant.core import callback from homeassistant.util import dt as dt_util, slugify +from ..const import ATTR_HUE_EVENT from .sensor_device import GenericHueDevice -_LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) -CONF_HUE_EVENT = "hue_event" CONF_LAST_UPDATED = "last_updated" EVENT_NAME_FORMAT = "{}" @@ -44,11 +44,6 @@ class HueEvent(GenericHueDevice): self.async_update_callback ) ) - self.bridge.reset_jobs.append( - self.bridge.listen_updates( - self.sensor.ITEM_TYPE, self.sensor.id, self.async_update_callback - ) - ) @callback def async_update_callback(self): @@ -90,7 +85,7 @@ class HueEvent(GenericHueDevice): CONF_EVENT: state, CONF_LAST_UPDATED: self.sensor.lastupdated, } - self.bridge.hass.bus.async_fire(CONF_HUE_EVENT, data) + self.bridge.hass.bus.async_fire(ATTR_HUE_EVENT, data) async def async_update_device_registry(self): """Update device registry.""" @@ -102,7 +97,7 @@ class HueEvent(GenericHueDevice): config_entry_id=self.bridge.config_entry.entry_id, **self.device_info ) self.device_registry_id = entry.id - _LOGGER.debug( + LOGGER.debug( "Event registry with entry_id: %s and device_id: %s", self.device_registry_id, self.device_id, diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py new file mode 100644 index 00000000000..9cddb665006 --- /dev/null +++ b/homeassistant/components/hue/v1/light.py @@ -0,0 +1,557 @@ +"""Support for the Philips Hue lights.""" +from __future__ import annotations + +from datetime import timedelta +from functools import partial +import logging +import random + +import aiohue +import async_timeout + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_TRANSITION, + EFFECT_COLORLOOP, + EFFECT_RANDOM, + FLASH_LONG, + FLASH_SHORT, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + LightEntity, +) +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +from homeassistant.util import color + +from ..bridge import HueBridge +from ..const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN as HUE_DOMAIN, + GROUP_TYPE_ENTERTAINMENT, + GROUP_TYPE_LIGHT_GROUP, + GROUP_TYPE_LIGHT_SOURCE, + GROUP_TYPE_LUMINAIRE, + GROUP_TYPE_ROOM, + GROUP_TYPE_ZONE, + REQUEST_REFRESH_DELAY, +) +from .helpers import remove_devices + +SCAN_INTERVAL = timedelta(seconds=5) + +LOGGER = logging.getLogger(__name__) + +SUPPORT_HUE_ON_OFF = SUPPORT_FLASH | SUPPORT_TRANSITION +SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS +SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP +SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR +SUPPORT_HUE_EXTENDED = SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR + +SUPPORT_HUE = { + "Extended color light": SUPPORT_HUE_EXTENDED, + "Color light": SUPPORT_HUE_COLOR, + "Dimmable light": SUPPORT_HUE_DIMMABLE, + "On/Off plug-in unit": SUPPORT_HUE_ON_OFF, + "Color temperature light": SUPPORT_HUE_COLOR_TEMP, +} + +ATTR_IS_HUE_GROUP = "is_hue_group" +GAMUT_TYPE_UNAVAILABLE = "None" +# Minimum Hue Bridge API version to support groups +# 1.4.0 introduced extended group info +# 1.12 introduced the state object for groups +# 1.13 introduced "any_on" to group state objects +GROUP_MIN_API_VERSION = (1, 13, 0) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up Hue lights. + + Can only be called when a user accidentally mentions hue platform in their + config. But even in that case it would have been ignored. + """ + + +def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id): + """Create the light.""" + api_item = api[item_id] + + if is_group: + supported_features = 0 + for light_id in api_item.lights: + if light_id not in bridge.api.lights: + continue + light = bridge.api.lights[light_id] + supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED) + supported_features = supported_features or SUPPORT_HUE_EXTENDED + else: + supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED) + return item_class( + coordinator, bridge, is_group, api_item, supported_features, rooms + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Hue lights from a config entry.""" + bridge: HueBridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) + rooms = {} + + allow_groups = config_entry.options.get( + CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS + ) + supports_groups = api_version >= GROUP_MIN_API_VERSION + if allow_groups and not supports_groups: + LOGGER.warning("Please update your Hue bridge to support groups") + + light_coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="light", + update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), + ) + + # First do a refresh to see if we can reach the hub. + # Otherwise we will declare not ready. + await light_coordinator.async_refresh() + + if not light_coordinator.last_update_success: + raise PlatformNotReady + + if not supports_groups: + update_lights_without_group_support = partial( + async_update_items, + bridge, + bridge.api.lights, + {}, + async_add_entities, + partial(create_light, HueLight, light_coordinator, bridge, False, rooms), + None, + ) + # We add a listener after fetching the data, so manually trigger listener + bridge.reset_jobs.append( + light_coordinator.async_add_listener(update_lights_without_group_support) + ) + return + + group_coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="group", + update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), + ) + + if allow_groups: + update_groups = partial( + async_update_items, + bridge, + bridge.api.groups, + {}, + async_add_entities, + partial(create_light, HueLight, group_coordinator, bridge, True, None), + None, + ) + + bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups)) + + cancel_update_rooms_listener = None + + @callback + def _async_update_rooms(): + """Update rooms.""" + nonlocal cancel_update_rooms_listener + rooms.clear() + for item_id in bridge.api.groups: + group = bridge.api.groups[item_id] + if group.type not in [GROUP_TYPE_ROOM, GROUP_TYPE_ZONE]: + continue + for light_id in group.lights: + rooms[light_id] = group.name + + # Once we do a rooms update, we cancel the listener + # until the next time lights are added + bridge.reset_jobs.remove(cancel_update_rooms_listener) + cancel_update_rooms_listener() # pylint: disable=not-callable + cancel_update_rooms_listener = None + + @callback + def _setup_rooms_listener(): + nonlocal cancel_update_rooms_listener + if cancel_update_rooms_listener is not None: + # If there are new lights added before _async_update_rooms + # is called we should not add another listener + return + + cancel_update_rooms_listener = group_coordinator.async_add_listener( + _async_update_rooms + ) + bridge.reset_jobs.append(cancel_update_rooms_listener) + + _setup_rooms_listener() + await group_coordinator.async_refresh() + + update_lights_with_group_support = partial( + async_update_items, + bridge, + bridge.api.lights, + {}, + async_add_entities, + partial(create_light, HueLight, light_coordinator, bridge, False, rooms), + _setup_rooms_listener, + ) + # We add a listener after fetching the data, so manually trigger listener + bridge.reset_jobs.append( + light_coordinator.async_add_listener(update_lights_with_group_support) + ) + update_lights_with_group_support() + + +async def async_safe_fetch(bridge, fetch_method): + """Safely fetch data.""" + try: + with async_timeout.timeout(4): + return await bridge.async_request_call(fetch_method) + except aiohue.Unauthorized as err: + await bridge.handle_unauthorized_error() + raise UpdateFailed("Unauthorized") from err + except aiohue.AiohueException as err: + raise UpdateFailed(f"Hue error: {err}") from err + + +@callback +def async_update_items( + bridge, api, current, async_add_entities, create_item, new_items_callback +): + """Update items.""" + new_items = [] + + for item_id in api: + if item_id in current: + continue + + current[item_id] = create_item(api, item_id) + new_items.append(current[item_id]) + + bridge.hass.async_create_task(remove_devices(bridge, api, current)) + + if new_items: + # This is currently used to setup the listener to update rooms + if new_items_callback: + new_items_callback() + async_add_entities(new_items) + + +def hue_brightness_to_hass(value): + """Convert hue brightness 1..254 to hass format 0..255.""" + return min(255, round((value / 254) * 255)) + + +def hass_to_hue_brightness(value): + """Convert hass brightness 0..255 to hue 1..254 scale.""" + return max(1, round((value / 255) * 254)) + + +class HueLight(CoordinatorEntity, LightEntity): + """Representation of a Hue light.""" + + def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms): + """Initialize the light.""" + super().__init__(coordinator) + self.light = light + self.bridge = bridge + self.is_group = is_group + self._supported_features = supported_features + self._rooms = rooms + self.allow_unreachable = self.bridge.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) + + if is_group: + self.is_osram = False + self.is_philips = False + self.is_innr = False + self.is_ewelink = False + self.is_livarno = False + self.gamut_typ = GAMUT_TYPE_UNAVAILABLE + self.gamut = None + else: + self.is_osram = light.manufacturername == "OSRAM" + self.is_philips = light.manufacturername == "Philips" + self.is_innr = light.manufacturername == "innr" + self.is_ewelink = light.manufacturername == "eWeLink" + self.is_livarno = light.manufacturername.startswith("_TZ3000_") + self.gamut_typ = self.light.colorgamuttype + self.gamut = self.light.colorgamut + LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) + if self.light.swupdatestate == "readytoinstall": + err = ( + "Please check for software updates of the %s " + "bulb in the Philips Hue App." + ) + LOGGER.warning(err, self.name) + if self.gamut and not color.check_valid_gamut(self.gamut): + err = "Color gamut of %s: %s, not valid, setting gamut to None." + LOGGER.debug(err, self.name, str(self.gamut)) + self.gamut_typ = GAMUT_TYPE_UNAVAILABLE + self.gamut = None + + @property + def unique_id(self): + """Return the unique ID of this Hue light.""" + unique_id = self.light.uniqueid + if not unique_id and self.is_group: + unique_id = self.light.id + + return unique_id + + @property + def device_id(self): + """Return the ID of this Hue light.""" + return self.unique_id + + @property + def name(self): + """Return the name of the Hue light.""" + return self.light.name + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if self.is_group: + bri = self.light.action.get("bri") + else: + bri = self.light.state.get("bri") + + if bri is None: + return bri + + return hue_brightness_to_hass(bri) + + @property + def _color_mode(self): + """Return the hue color mode.""" + if self.is_group: + return self.light.action.get("colormode") + return self.light.state.get("colormode") + + @property + def hs_color(self): + """Return the hs color value.""" + mode = self._color_mode + source = self.light.action if self.is_group else self.light.state + + if mode in ("xy", "hs") and "xy" in source: + return color.color_xy_to_hs(*source["xy"], self.gamut) + + return None + + @property + def color_temp(self): + """Return the CT color value.""" + # Don't return color temperature unless in color temperature mode + if self._color_mode != "ct": + return None + + if self.is_group: + return self.light.action.get("ct") + return self.light.state.get("ct") + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + if self.is_group: + return super().min_mireds + + min_mireds = self.light.controlcapabilities.get("ct", {}).get("min") + + # We filter out '0' too, which can be incorrectly reported by 3rd party buls + if not min_mireds: + return super().min_mireds + + return min_mireds + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + if self.is_group: + return super().max_mireds + if self.is_livarno: + return 500 + + max_mireds = self.light.controlcapabilities.get("ct", {}).get("max") + + if not max_mireds: + return super().max_mireds + + return max_mireds + + @property + def is_on(self): + """Return true if device is on.""" + if self.is_group: + return self.light.state["any_on"] + return self.light.state["on"] + + @property + def available(self): + """Return if light is available.""" + return self.coordinator.last_update_success and ( + self.is_group or self.allow_unreachable or self.light.state["reachable"] + ) + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + @property + def effect(self): + """Return the current effect.""" + return self.light.state.get("effect", None) + + @property + def effect_list(self): + """Return the list of supported effects.""" + if self.is_osram: + return [EFFECT_RANDOM] + return [EFFECT_COLORLOOP, EFFECT_RANDOM] + + @property + def device_info(self) -> DeviceInfo | None: + """Return the device info.""" + if self.light.type in ( + GROUP_TYPE_ENTERTAINMENT, + GROUP_TYPE_LIGHT_GROUP, + GROUP_TYPE_ROOM, + GROUP_TYPE_LUMINAIRE, + GROUP_TYPE_LIGHT_SOURCE, + GROUP_TYPE_ZONE, + ): + return None + + suggested_area = None + if self._rooms and self.light.id in self._rooms: + suggested_area = self._rooms[self.light.id] + + return DeviceInfo( + identifiers={(HUE_DOMAIN, self.device_id)}, + manufacturer=self.light.manufacturername, + # productname added in Hue Bridge API 1.24 + # (published 03/05/2018) + model=self.light.productname or self.light.modelid, + name=self.name, + sw_version=self.light.swversion, + suggested_area=suggested_area, + via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), + ) + + async def async_turn_on(self, **kwargs): + """Turn the specified or all lights on.""" + command = {"on": True} + + if ATTR_TRANSITION in kwargs: + command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) + + if ATTR_HS_COLOR in kwargs: + if self.is_osram: + command["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + else: + # Philips hue bulb models respond differently to hue/sat + # requests, so we convert to XY first to ensure a consistent + # color. + xy_color = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR], self.gamut) + command["xy"] = xy_color + elif ATTR_COLOR_TEMP in kwargs: + temp = kwargs[ATTR_COLOR_TEMP] + command["ct"] = max(self.min_mireds, min(temp, self.max_mireds)) + + if ATTR_BRIGHTNESS in kwargs: + command["bri"] = hass_to_hue_brightness(kwargs[ATTR_BRIGHTNESS]) + + flash = kwargs.get(ATTR_FLASH) + + if flash == FLASH_LONG: + command["alert"] = "lselect" + del command["on"] + elif flash == FLASH_SHORT: + command["alert"] = "select" + del command["on"] + elif not self.is_innr and not self.is_ewelink and not self.is_livarno: + command["alert"] = "none" + + if ATTR_EFFECT in kwargs: + effect = kwargs[ATTR_EFFECT] + if effect == EFFECT_COLORLOOP: + command["effect"] = "colorloop" + elif effect == EFFECT_RANDOM: + command["hue"] = random.randrange(0, 65535) + command["sat"] = random.randrange(150, 254) + else: + command["effect"] = "none" + + if self.is_group: + await self.bridge.async_request_call(self.light.set_action, **command) + else: + await self.bridge.async_request_call(self.light.set_state, **command) + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Turn the specified or all lights off.""" + command = {"on": False} + + if ATTR_TRANSITION in kwargs: + command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) + + flash = kwargs.get(ATTR_FLASH) + + if flash == FLASH_LONG: + command["alert"] = "lselect" + del command["on"] + elif flash == FLASH_SHORT: + command["alert"] = "select" + del command["on"] + elif not self.is_innr and not self.is_livarno: + command["alert"] = "none" + + if self.is_group: + await self.bridge.async_request_call(self.light.set_action, **command) + else: + await self.bridge.async_request_call(self.light.set_state, **command) + + await self.coordinator.async_request_refresh() + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + if not self.is_group: + return {} + return {ATTR_IS_HUE_GROUP: self.is_group} diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py new file mode 100644 index 00000000000..df12fe84274 --- /dev/null +++ b/homeassistant/components/hue/v1/sensor.py @@ -0,0 +1,135 @@ +"""Hue sensor entities.""" +from aiohue.v1.sensors import ( + TYPE_ZLL_LIGHTLEVEL, + TYPE_ZLL_ROTARY, + TYPE_ZLL_SWITCH, + TYPE_ZLL_TEMPERATURE, +) + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + LIGHT_LUX, + PERCENTAGE, + TEMP_CELSIUS, +) + +from ..const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor + +LIGHT_LEVEL_NAME_FORMAT = "{} light level" +REMOTE_NAME_FORMAT = "{} battery level" +TEMPERATURE_NAME_FORMAT = "{} temperature" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component("sensor", async_add_entities) + + +class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): + """Parent class for all 'gauge' Hue device sensors.""" + + +class HueLightLevel(GenericHueGaugeSensorEntity): + """The light level sensor entity for a Hue motion sensor device.""" + + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_native_unit_of_measurement = LIGHT_LUX + + @property + def native_value(self): + """Return the state of the device.""" + if self.sensor.lightlevel is None: + return None + + # https://developers.meethue.com/develop/hue-api/supported-devices/#clip_zll_lightlevel + # Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm + # scale used because the human eye adjusts to light levels and small + # changes at low lux levels are more noticeable than at high lux + # levels. + return round(float(10 ** ((self.sensor.lightlevel - 1) / 10000)), 2) + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + attributes = super().extra_state_attributes + attributes.update( + { + "lightlevel": self.sensor.lightlevel, + "daylight": self.sensor.daylight, + "dark": self.sensor.dark, + "threshold_dark": self.sensor.tholddark, + "threshold_offset": self.sensor.tholdoffset, + } + ) + return attributes + + +class HueTemperature(GenericHueGaugeSensorEntity): + """The temperature sensor entity for a Hue motion sensor device.""" + + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_native_unit_of_measurement = TEMP_CELSIUS + + @property + def native_value(self): + """Return the state of the device.""" + if self.sensor.temperature is None: + return None + + return self.sensor.temperature / 100 + + +class HueBattery(GenericHueSensor, SensorEntity): + """Battery class for when a batt-powered device is only represented as an event.""" + + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.sensor.uniqueid}-battery" + + @property + def native_value(self): + """Return the state of the battery.""" + return self.sensor.battery + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "platform": "sensor", + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "platform": "sensor", + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + TYPE_ZLL_SWITCH: { + "platform": "sensor", + "name_format": REMOTE_NAME_FORMAT, + "class": HueBattery, + }, + TYPE_ZLL_ROTARY: { + "platform": "sensor", + "name_format": REMOTE_NAME_FORMAT, + "class": HueBattery, + }, + } +) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py similarity index 95% rename from homeassistant/components/hue/sensor_base.py rename to homeassistant/components/hue/v1/sensor_base.py index a99d92c0393..142941a1859 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -6,7 +6,7 @@ import logging from typing import Any from aiohue import AiohueException, Unauthorized -from aiohue.sensors import TYPE_ZLL_PRESENCE +from aiohue.v1.sensors import TYPE_ZLL_PRESENCE import async_timeout from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT @@ -14,13 +14,13 @@ from homeassistant.core import callback from homeassistant.helpers import debounce, entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import REQUEST_REFRESH_DELAY +from ..const import REQUEST_REFRESH_DELAY from .helpers import remove_devices from .hue_event import EVENT_CONFIG_MAP from .sensor_device import GenericHueDevice SENSOR_CONFIG_MAP: dict[str, Any] = {} -_LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) def _device_id(aiohue_sensor): @@ -49,12 +49,12 @@ class SensorManager: self._enabled_platforms = ("binary_sensor", "sensor") self.coordinator = DataUpdateCoordinator( bridge.hass, - _LOGGER, + LOGGER, name="sensor", update_method=self.async_update_data, update_interval=self.SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( - bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), ) @@ -76,7 +76,7 @@ class SensorManager: self._component_add_entities[platform] = async_add_entities if len(self._component_add_entities) < len(self._enabled_platforms): - _LOGGER.debug("Aborting start with %s, waiting for the rest", platform) + LOGGER.debug("Aborting start with %s, waiting for the rest", platform) return # We have all components available, start the updating. @@ -173,7 +173,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): def available(self): """Return if sensor is available.""" return self.bridge.sensor_manager.coordinator.last_update_success and ( - self.bridge.allow_unreachable + self.allow_unreachable # remotes like Hue Tap (ZGPSwitchSensor) have no _reachability_ or self.sensor.config.get("reachable", True) ) diff --git a/homeassistant/components/hue/sensor_device.py b/homeassistant/components/hue/v1/sensor_device.py similarity index 83% rename from homeassistant/components/hue/sensor_device.py rename to homeassistant/components/hue/v1/sensor_device.py index 92c586ff8e0..176b5f118b2 100644 --- a/homeassistant/components/hue/sensor_device.py +++ b/homeassistant/components/hue/v1/sensor_device.py @@ -1,7 +1,11 @@ """Support for the Philips Hue sensor devices.""" from homeassistant.helpers import entity -from .const import DOMAIN as HUE_DOMAIN +from ..const import ( + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN as HUE_DOMAIN, +) class GenericHueDevice(entity.Entity): @@ -13,6 +17,9 @@ class GenericHueDevice(entity.Entity): self._name = name self._primary_sensor = primary_sensor self.bridge = bridge + self.allow_unreachable = bridge.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) @property def primary_sensor(self): @@ -53,12 +60,3 @@ class GenericHueDevice(entity.Entity): sw_version=self.primary_sensor.swversion, via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), ) - - async def async_added_to_hass(self) -> None: - """Handle entity being added to Home Assistant.""" - self.async_on_remove( - self.bridge.listen_updates( - self.sensor.ITEM_TYPE, self.sensor.id, self.async_write_ha_state - ) - ) - await super().async_added_to_hass() diff --git a/homeassistant/components/hue/v2/__init__.py b/homeassistant/components/hue/v2/__init__.py new file mode 100644 index 00000000000..ebcf4873dc7 --- /dev/null +++ b/homeassistant/components/hue/v2/__init__.py @@ -0,0 +1 @@ +"""Hue V2 API specific platform implementation.""" diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py new file mode 100644 index 00000000000..f655e203755 --- /dev/null +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -0,0 +1,110 @@ +"""Support for Hue binary sensors.""" +from __future__ import annotations + +from typing import Any, Union + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.config import EntertainmentConfigurationController +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.sensors import MotionController +from aiohue.v2.models.entertainment import ( + EntertainmentConfiguration, + EntertainmentStatus, +) +from aiohue.v2.models.motion import Motion + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_RUNNING, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from ..bridge import HueBridge +from ..const import DOMAIN +from .entity import HueBaseEntity + +SensorType = Union[Motion, EntertainmentConfiguration] +ControllerType = Union[MotionController, EntertainmentConfigurationController] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Hue Sensors from Config Entry.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + + @callback + def register_items(controller: ControllerType, sensor_class: SensorType): + @callback + def async_add_sensor(event_type: EventType, resource: SensorType) -> None: + """Add Hue Binary Sensor.""" + async_add_entities([sensor_class(bridge, controller, resource)]) + + # add all current items in controller + for sensor in controller: + async_add_sensor(EventType.RESOURCE_ADDED, sensor) + + # register listener for new sensors + config_entry.async_on_unload( + controller.subscribe( + async_add_sensor, event_filter=EventType.RESOURCE_ADDED + ) + ) + + # setup for each binary-sensor-type hue resource + register_items(api.sensors.motion, HueMotionSensor) + register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor) + + +class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity): + """Representation of a Hue binary_sensor.""" + + def __init__( + self, + bridge: HueBridge, + controller: ControllerType, + resource: SensorType, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(bridge, controller, resource) + self.resource = resource + self.controller = controller + + +class HueMotionSensor(HueBinarySensorBase): + """Representation of a Hue Motion sensor.""" + + _attr_device_class = DEVICE_CLASS_MOTION + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.resource.motion.motion + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the optional state attributes.""" + return {"motion_valid": self.resource.motion.motion_valid} + + +class HueEntertainmentActiveSensor(HueBinarySensorBase): + """Representation of a Hue Entertainment Configuration as binary sensor.""" + + _attr_device_class = DEVICE_CLASS_RUNNING + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.resource.status == EntertainmentStatus.ACTIVE + + @property + def name(self) -> str: + """Return sensor name.""" + type_title = self.resource.type.value.replace("_", " ").title() + return f"{self.resource.name}: {type_title}" diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py new file mode 100644 index 00000000000..1608743cc48 --- /dev/null +++ b/homeassistant/components/hue/v2/device.py @@ -0,0 +1,86 @@ +"""Handles Hue resource of type `device` mapping to Home Assistant device.""" +from typing import TYPE_CHECKING + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.models.device import Device, DeviceArchetypes + +from homeassistant.const import ( + ATTR_CONNECTIONS, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, + ATTR_VIA_DEVICE, +) +from homeassistant.core import callback +from homeassistant.helpers import device_registry + +from ..const import DOMAIN + +if TYPE_CHECKING: + from ..bridge import HueBridge + + +async def async_setup_devices(bridge: "HueBridge"): + """Manage setup of devices from Hue devices.""" + entry = bridge.config_entry + hass = bridge.hass + api: HueBridgeV2 = bridge.api # to satisfy typing + dev_reg = device_registry.async_get(hass) + dev_controller = api.devices + + @callback + def add_device(hue_device: Device) -> device_registry.DeviceEntry: + """Register a Hue device in device registry.""" + model = f"{hue_device.product_data.product_name} ({hue_device.product_data.model_id})" + params = { + ATTR_IDENTIFIERS: {(DOMAIN, hue_device.id)}, + ATTR_SW_VERSION: hue_device.product_data.software_version, + ATTR_NAME: hue_device.metadata.name, + ATTR_MODEL: model, + ATTR_MANUFACTURER: hue_device.product_data.manufacturer_name, + } + if room := dev_controller.get_room(hue_device.id): + params[ATTR_SUGGESTED_AREA] = room.metadata.name + if hue_device.metadata.archetype == DeviceArchetypes.BRIDGE_V2: + params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id)) + else: + params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id) + if zigbee := dev_controller.get_zigbee_connectivity(hue_device.id): + params[ATTR_CONNECTIONS] = { + (device_registry.CONNECTION_NETWORK_MAC, zigbee.mac_address) + } + + return dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) + + @callback + def remove_device(hue_device_id: str) -> None: + """Remove device from registry.""" + if device := dev_reg.async_get_device({(DOMAIN, hue_device_id)}): + # note: removal of any underlying entities is handled by core + dev_reg.async_remove_device(device.id) + + @callback + def handle_device_event(evt_type: EventType, hue_device: Device) -> None: + """Handle event from Hue devices controller.""" + if evt_type == EventType.RESOURCE_DELETED: + remove_device(hue_device.id) + else: + # updates to existing device will also be handled by this call + add_device(hue_device) + + # create/update all current devices found in controller + known_devices = [add_device(hue_device) for hue_device in dev_controller] + + # Check for nodes that no longer exist and remove them + for device in device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ): + if device not in known_devices: + dev_reg.async_remove_device(device.id) + + # add listener for updates on Hue devices controller + entry.async_on_unload(dev_controller.subscribe(handle_device_event)) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py new file mode 100644 index 00000000000..b33b7540cb8 --- /dev/null +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -0,0 +1,115 @@ +"""Provides device automations for Philips Hue events.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aiohue.v2.models.button import ButtonEvent +from aiohue.v2.models.resource import ResourceTypes +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, + CONF_UNIQUE_ID, +) +from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.typing import ConfigType + +from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN + +if TYPE_CHECKING: + from aiohue.v2 import HueBridgeV2 + + from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, + ) + + from ..bridge import HueBridge + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): str, + vol.Required(CONF_SUBTYPE): int, + vol.Optional(CONF_UNIQUE_ID): str, + } +) + + +async def async_validate_trigger_config( + bridge: "HueBridge", + device_entry: DeviceEntry, + config: ConfigType, +): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + return config + + +async def async_attach_trigger( + bridge: "HueBridge", + device_entry: DeviceEntry, + config: ConfigType, + action: "AutomationActionType", + automation_info: "AutomationTriggerInfo", +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + hass = bridge.hass + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: ATTR_HUE_EVENT, + event_trigger.CONF_EVENT_DATA: { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + CONF_TYPE: config[CONF_TYPE], + CONF_SUBTYPE: config[CONF_SUBTYPE], + }, + } + ) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): + """Return device triggers for device on `v2` bridge.""" + api: HueBridgeV2 = bridge.api + + # Get Hue device id from device identifier + hue_dev_id = get_hue_device_id(device_entry) + # extract triggers from all button resources of this Hue device + triggers = [] + for resource in api.devices.get_sensors(hue_dev_id): + if resource.type != ResourceTypes.BUTTON: + continue + for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN): + triggers.append( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type, + CONF_SUBTYPE: resource.metadata.control_id, + CONF_UNIQUE_ID: device_entry.id, + } + ) + return triggers + + +@callback +def get_hue_device_id(device_entry: DeviceEntry) -> str | None: + """Get Hue device id from device entry.""" + return next( + ( + identifier[1] + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + and ":" not in identifier[1] # filter out v1 mac id + ), + None, + ) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py new file mode 100644 index 00000000000..1b646857e44 --- /dev/null +++ b/homeassistant/components/hue/v2/entity.py @@ -0,0 +1,113 @@ +"""Generic Hue Entity Model.""" +from __future__ import annotations + +from aiohue.v2.controllers.base import BaseResourcesController +from aiohue.v2.controllers.events import EventType +from aiohue.v2.models.clip import CLIPResource +from aiohue.v2.models.connectivity import ConnectivityServiceStatus +from aiohue.v2.models.resource import ResourceTypes + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry + +from ..bridge import HueBridge +from ..const import DOMAIN + +RESOURCE_TYPE_NAMES = { + # a simple mapping of hue resource type to Hass name + ResourceTypes.LIGHT_LEVEL: "Illuminance", + ResourceTypes.DEVICE_POWER: "Battery", +} + + +class HueBaseEntity(Entity): + """Generic Entity Class for a Hue resource.""" + + _attr_should_poll = False + + def __init__( + self, + bridge: HueBridge, + controller: BaseResourcesController, + resource: CLIPResource, + ) -> None: + """Initialize a generic Hue resource entity.""" + self.bridge = bridge + self.controller = controller + self.resource = resource + self.device = ( + controller.get_device(resource.id) or bridge.api.config.bridge_device + ) + self.logger = bridge.logger.getChild(resource.type.value) + + # Entity class attributes + self._attr_unique_id = resource.id + # device is precreated in main handler + # this attaches the entity to the precreated device + if self.device is not None: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device.id)}, + ) + + @property + def name(self) -> str: + """Return name for the entity.""" + if self.device is None: + # this is just a guard + # creating a pretty name for device-less entities (e.g. groups/scenes) + # should be handled in the platform instead + return self.resource.type.value + dev_name = self.device.metadata.name + # if resource is a light, use the device name + if self.resource.type == ResourceTypes.LIGHT: + return dev_name + # for sensors etc, use devicename + pretty name of type + type_title = RESOURCE_TYPE_NAMES.get( + self.resource.type, self.resource.type.value.replace("_", " ").title() + ) + return f"{dev_name}: {type_title}" + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + # Add value_changed callbacks. + self.async_on_remove( + self.controller.subscribe( + self._handle_event, + self.resource.id, + (EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED), + ) + ) + + @property + def available(self) -> bool: + """Return entity availability.""" + if self.device is None: + # devices without a device attached should be always available + return True + if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: + # the zigbee connectivity sensor itself should be always available + return True + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + # all device-attached entities get availability from the zigbee connectivity + return zigbee.status == ConnectivityServiceStatus.CONNECTED + return True + + @callback + def on_update(self) -> None: + """Call on update event.""" + # used in subclasses + + @callback + def _handle_event(self, event_type: EventType, resource: CLIPResource) -> None: + """Handle status event for this resource.""" + if event_type == EventType.RESOURCE_DELETED and resource.id == self.resource.id: + self.logger.debug("Received delete for %s", self.entity_id) + # non-device bound entities like groups and scenes need to be removed here + # all others will be be removed by device setup in case of device removal + ent_reg = async_get_entity_registry(self.hass) + ent_reg.async_remove(self.entity_id) + else: + self.logger.debug("Received status update for %s", self.entity_id) + self.on_update() + self.async_write_ha_state() diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py new file mode 100644 index 00000000000..ae77ab38539 --- /dev/null +++ b/homeassistant/components/hue/v2/group.py @@ -0,0 +1,249 @@ +"""Support for Hue groups (room/zone).""" +from __future__ import annotations + +from typing import Any + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.groups import GroupedLight, Room, Zone + +from homeassistant.components.group.light import LightGroup +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, + SUPPORT_TRANSITION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from ..bridge import HueBridge +from ..const import DOMAIN +from .entity import HueBaseEntity + +ALLOWED_ERRORS = [ + "device (groupedLight) has communication issues, command (on) may not have effect", + 'device (groupedLight) is "soft off", command (on) may not have effect', + "device (light) has communication issues, command (on) may not have effect", + 'device (light) is "soft off", command (on) may not have effect', +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Hue groups on light platform.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + + # to prevent race conditions (groupedlight is created before zone/room) + # we create groupedlights from the room/zone and actually use the + # underlying grouped_light resource for control + + @callback + def async_add_light(event_type: EventType, resource: Room | Zone) -> None: + """Add Grouped Light for Hue Room/Zone.""" + if grouped_light_id := resource.grouped_light: + grouped_light = api.groups.grouped_light[grouped_light_id] + light = GroupedHueLight(bridge, grouped_light, resource) + async_add_entities([light]) + + # add current items + for item in api.groups.room.items + api.groups.zone.items: + async_add_light(EventType.RESOURCE_ADDED, item) + + # register listener for new zones/rooms + config_entry.async_on_unload( + api.groups.room.subscribe( + async_add_light, event_filter=EventType.RESOURCE_ADDED + ) + ) + config_entry.async_on_unload( + api.groups.zone.subscribe( + async_add_light, event_filter=EventType.RESOURCE_ADDED + ) + ) + + +class GroupedHueLight(HueBaseEntity, LightGroup): + """Representation of a Grouped Hue light.""" + + # Entities for Hue groups are disabled by default + _attr_entity_registry_enabled_default = False + + def __init__( + self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone + ) -> None: + """Initialize the light.""" + controller = bridge.api.groups.grouped_light + super().__init__(bridge, controller, resource) + self.resource = resource + self.group = group + self.controller = controller + self.api: HueBridgeV2 = bridge.api + self._attr_supported_features |= SUPPORT_TRANSITION + + self._update_values() + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + await super().async_added_to_hass() + + # subscribe to group updates + self.async_on_remove( + self.api.groups.subscribe(self._handle_event, self.group.id) + ) + # We need to watch the underlying lights too + # if we want feedback about color/brightness changes + if self._attr_supported_color_modes: + light_ids = tuple( + x.id for x in self.controller.get_lights(self.resource.id) + ) + self.async_on_remove( + self.api.lights.subscribe(self._handle_event, light_ids) + ) + + @property + def name(self) -> str: + """Return name of room/zone for this grouped light.""" + return self.group.metadata.name + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self.resource.on.on + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the optional state attributes.""" + scenes = { + x.metadata.name for x in self.api.scenes if x.group.rid == self.group.id + } + lights = {x.metadata.name for x in self.controller.get_lights(self.resource.id)} + return { + "is_hue_group": True, + "hue_scenes": scenes, + "hue_type": self.group.type.value, + "lights": lights, + } + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + transition = kwargs.get(ATTR_TRANSITION) + xy_color = kwargs.get(ATTR_XY_COLOR) + color_temp = kwargs.get(ATTR_COLOR_TEMP) + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is not None: + # Hue uses a range of [0, 100] to control brightness. + brightness = float((brightness / 255) * 100) + if transition is not None: + # hue transition duration is in steps of 100 ms + transition = int(transition * 100) + + # NOTE: a grouped_light can only handle turn on/off + # To set other features, you'll have to control the attached lights + if ( + brightness is None + and xy_color is None + and color_temp is None + and transition is None + ): + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=True, + allowed_errors=ALLOWED_ERRORS, + ) + return + + # redirect all other feature commands to underlying lights + # note that this silently ignores params sent to light that are not supported + for light in self.controller.get_lights(self.resource.id): + await self.bridge.async_request_call( + self.api.lights.set_state, + light.id, + on=True, + brightness=brightness if light.supports_dimming else None, + color_xy=xy_color if light.supports_color else None, + color_temp=color_temp if light.supports_color_temperature else None, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + allowed_errors=ALLOWED_ERRORS, + ) + + @callback + def on_update(self) -> None: + """Call on update event.""" + self._update_values() + + @callback + def _update_values(self) -> None: + """Set base values from underlying lights of a group.""" + supported_color_modes = set() + lights_with_color_support = 0 + lights_with_color_temp_support = 0 + lights_with_dimming_support = 0 + total_brightness = 0 + all_lights = self.controller.get_lights(self.resource.id) + lights_in_colortemp_mode = 0 + # loop through all lights to find capabilities + for light in all_lights: + if color_temp := light.color_temperature: + lights_with_color_temp_support += 1 + # we assume mired values from the first capable light + self._attr_color_temp = color_temp.mirek + self._attr_max_mireds = color_temp.mirek_schema.mirek_maximum + self._attr_min_mireds = color_temp.mirek_schema.mirek_minimum + if color_temp.mirek is not None and color_temp.mirek_valid: + lights_in_colortemp_mode += 1 + if color := light.color: + lights_with_color_support += 1 + # we assume xy values from the first capable light + self._attr_xy_color = (color.xy.x, color.xy.y) + if dimming := light.dimming: + lights_with_dimming_support += 1 + total_brightness += dimming.brightness + # this is a bit hacky because light groups may contain lights + # of different capabilities. We set a colormode as supported + # if any of the lights support it + # this means that the state is derived from only some of the lights + # and will never be 100% accurate but it will be close + if lights_with_color_support > 0: + supported_color_modes.add(COLOR_MODE_XY) + if lights_with_color_temp_support > 0: + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if lights_with_dimming_support > 0: + if len(supported_color_modes) == 0: + # only add color mode brightness if no color variants + supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + self._attr_brightness = round( + ((total_brightness / lights_with_dimming_support) / 100) * 255 + ) + else: + supported_color_modes.add(COLOR_MODE_ONOFF) + self._attr_supported_color_modes = supported_color_modes + # pick a winner for the current colormode + if lights_in_colortemp_mode == lights_with_color_temp_support: + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + elif lights_with_color_support > 0: + self._attr_color_mode = COLOR_MODE_XY + elif lights_with_dimming_support > 0: + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + else: + self._attr_color_mode = COLOR_MODE_ONOFF diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py new file mode 100644 index 00000000000..86dabc26660 --- /dev/null +++ b/homeassistant/components/hue/v2/hue_event.py @@ -0,0 +1,57 @@ +"""Handle forward of events transmitted by Hue devices to HASS.""" +import logging +from typing import TYPE_CHECKING + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.models.button import Button + +from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID +from homeassistant.core import callback +from homeassistant.helpers import device_registry +from homeassistant.util import slugify + +from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN as DOMAIN + +CONF_CONTROL_ID = "control_id" + +if TYPE_CHECKING: + from ..bridge import HueBridge + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_hue_events(bridge: "HueBridge"): + """Manage listeners for stateless Hue sensors that emit events.""" + hass = bridge.hass + api: HueBridgeV2 = bridge.api # to satisfy typing + conf_entry = bridge.config_entry + dev_reg = device_registry.async_get(hass) + + # at this time the `button` resource is the only source of hue events + btn_controller = api.sensors.button + + @callback + def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: + """Handle event from Hue devices controller.""" + LOGGER.debug("Received button event: %s", hue_resource) + hue_device = btn_controller.get_device(hue_resource.id) + device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) + + # Fire event + data = { + # send slugified entity name as id = backwards compatibility with previous version + CONF_ID: slugify(f"{hue_device.metadata.name}: Button"), + CONF_DEVICE_ID: device.id, # type: ignore + CONF_UNIQUE_ID: hue_resource.id, + CONF_TYPE: hue_resource.button.last_event.value, + CONF_SUBTYPE: hue_resource.metadata.control_id, + } + hass.bus.async_fire(ATTR_HUE_EVENT, data) + + # add listener for updates from `button` resource + conf_entry.async_on_unload( + btn_controller.subscribe( + handle_button_event, event_filter=EventType.RESOURCE_UPDATED + ) + ) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py new file mode 100644 index 00000000000..42972f2242c --- /dev/null +++ b/homeassistant/components/hue/v2/light.py @@ -0,0 +1,187 @@ +"""Support for Hue lights.""" +from __future__ import annotations + +from typing import Any + +from aiohue import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.lights import LightsController +from aiohue.v2.models.light import Light + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, + SUPPORT_TRANSITION, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from ..bridge import HueBridge +from ..const import DOMAIN +from .entity import HueBaseEntity + +ALLOWED_ERRORS = [ + "device (light) has communication issues, command (on) may not have effect", + 'device (light) is "soft off", command (on) may not have effect', +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Hue Light from Config Entry.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + controller: LightsController = api.lights + + @callback + def async_add_light(event_type: EventType, resource: Light) -> None: + """Add Hue Light.""" + light = HueLight(bridge, controller, resource) + async_add_entities([light]) + + # add all current items in controller + for light in controller: + async_add_light(EventType.RESOURCE_ADDED, resource=light) + + # register listener for new lights + config_entry.async_on_unload( + controller.subscribe(async_add_light, event_filter=EventType.RESOURCE_ADDED) + ) + + +class HueLight(HueBaseEntity, LightEntity): + """Representation of a Hue light.""" + + def __init__( + self, bridge: HueBridge, controller: LightsController, resource: Light + ) -> None: + """Initialize the light.""" + super().__init__(bridge, controller, resource) + self.resource = resource + self.controller = controller + self._supported_color_modes = set() + if self.resource.supports_color: + self._supported_color_modes.add(COLOR_MODE_XY) + if self.resource.supports_color_temperature: + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if self.resource.supports_dimming: + if len(self._supported_color_modes) == 0: + # only add color mode brightness if no color variants + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + # support transition if brightness control + self._attr_supported_features |= SUPPORT_TRANSITION + + @property + def brightness(self) -> int | None: + """Return the brightness of this light between 0..255.""" + if dimming := self.resource.dimming: + # Hue uses a range of [0, 100] to control brightness. + return round((dimming.brightness / 100) * 255) + return None + + @property + def color_mode(self) -> str: + """Return the current color mode of the light.""" + if color_temp := self.resource.color_temperature: + if color_temp.mirek_valid and color_temp.mirek is not None: + return COLOR_MODE_COLOR_TEMP + if self.resource.supports_color: + return COLOR_MODE_XY + if self.resource.supports_dimming: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + @property + def is_on(self) -> bool: + """Return true if device is on (brightness above 0).""" + return self.resource.on.on + + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color.""" + if color := self.resource.color: + return (color.xy.x, color.xy.y) + return None + + @property + def color_temp(self) -> int: + """Return the color temperature.""" + if color_temp := self.resource.color_temperature: + return color_temp.mirek + return 0 + + @property + def min_mireds(self) -> int: + """Return the coldest color_temp that this light supports.""" + if color_temp := self.resource.color_temperature: + return color_temp.mirek_schema.mirek_minimum + return 0 + + @property + def max_mireds(self) -> int: + """Return the warmest color_temp that this light supports.""" + if color_temp := self.resource.color_temperature: + return color_temp.mirek_schema.mirek_maximum + return 0 + + @property + def supported_color_modes(self) -> set | None: + """Flag supported features.""" + return self._supported_color_modes + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the optional state attributes.""" + return { + "mode": self.resource.mode.value, + "dynamics": self.resource.dynamics.status.value, + } + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + transition = kwargs.get(ATTR_TRANSITION) + xy_color = kwargs.get(ATTR_XY_COLOR) + color_temp = kwargs.get(ATTR_COLOR_TEMP) + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is not None: + # Hue uses a range of [0, 100] to control brightness. + brightness = float((brightness / 255) * 100) + if transition is not None: + # hue transition duration is in steps of 100 ms + transition = int(transition * 100) + + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=True, + brightness=brightness, + color_xy=xy_color, + color_temp=color_temp, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + transition = kwargs.get(ATTR_TRANSITION) + if transition is not None: + # hue transition duration is in steps of 100 ms + transition = int(transition * 100) + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py new file mode 100644 index 00000000000..bb801b7817d --- /dev/null +++ b/homeassistant/components/hue/v2/sensor.py @@ -0,0 +1,174 @@ +"""Support for Hue sensors.""" +from __future__ import annotations + +from typing import Any, Union + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.controllers.sensors import ( + DevicePowerController, + LightLevelController, + SensorsController, + TemperatureController, + ZigbeeConnectivityController, +) +from aiohue.v2.models.connectivity import ZigbeeConnectivity +from aiohue.v2.models.device_power import DevicePower +from aiohue.v2.models.light_level import LightLevel +from aiohue.v2.models.temperature import Temperature + +from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + LIGHT_LUX, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from ..bridge import HueBridge +from ..const import DOMAIN +from .entity import HueBaseEntity + +SensorType = Union[DevicePower, LightLevel, Temperature, ZigbeeConnectivity] +ControllerType = Union[ + DevicePowerController, + LightLevelController, + TemperatureController, + ZigbeeConnectivityController, +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Hue Sensors from Config Entry.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + ctrl_base: SensorsController = api.sensors + + @callback + def register_items(controller: ControllerType, sensor_class: SensorType): + @callback + def async_add_sensor(event_type: EventType, resource: SensorType) -> None: + """Add Hue Sensor.""" + async_add_entities([sensor_class(bridge, controller, resource)]) + + # add all current items in controller + for sensor in controller: + async_add_sensor(EventType.RESOURCE_ADDED, sensor) + + # register listener for new sensors + config_entry.async_on_unload( + controller.subscribe( + async_add_sensor, event_filter=EventType.RESOURCE_ADDED + ) + ) + + # setup for each sensor-type hue resource + register_items(ctrl_base.temperature, HueTemperatureSensor) + register_items(ctrl_base.light_level, HueLightLevelSensor) + register_items(ctrl_base.device_power, HueBatterySensor) + register_items(ctrl_base.zigbee_connectivity, HueZigbeeConnectivitySensor) + + +class HueSensorBase(HueBaseEntity, SensorEntity): + """Representation of a Hue sensor.""" + + _attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__( + self, + bridge: HueBridge, + controller: ControllerType, + resource: SensorType, + ) -> None: + """Initialize the light.""" + super().__init__(bridge, controller, resource) + self.resource = resource + self.controller = controller + + +class HueTemperatureSensor(HueSensorBase): + """Representation of a Hue Temperature sensor.""" + + _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_device_class = DEVICE_CLASS_TEMPERATURE + + @property + def native_value(self) -> float: + """Return the value reported by the sensor.""" + return round(self.resource.temperature.temperature, 1) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the optional state attributes.""" + return {"temperature_valid": self.resource.temperature.temperature_valid} + + +class HueLightLevelSensor(HueSensorBase): + """Representation of a Hue LightLevel (illuminance) sensor.""" + + _attr_native_unit_of_measurement = LIGHT_LUX + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + + @property + def native_value(self) -> int: + """Return the value reported by the sensor.""" + # Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm + # scale used because the human eye adjusts to light levels and small + # changes at low lux levels are more noticeable than at high lux + # levels. + return int(10 ** ((self.resource.light.light_level - 1) / 10000)) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the optional state attributes.""" + return { + "light_level": self.resource.light.light_level, + "light_level_valid": self.resource.light.light_level_valid, + } + + +class HueBatterySensor(HueSensorBase): + """Representation of a Hue Battery sensor.""" + + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + + @property + def native_value(self) -> int: + """Return the value reported by the sensor.""" + return self.resource.power_state.battery_level + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the optional state attributes.""" + return {"battery_state": self.resource.power_state.battery_state.value} + + +class HueZigbeeConnectivitySensor(HueSensorBase): + """Representation of a Hue ZigbeeConnectivity sensor.""" + + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_registry_enabled_default = False + + @property + def native_value(self) -> str: + """Return the value reported by the sensor.""" + return self.resource.status.value + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the optional state attributes.""" + return {"mac_address": self.resource.mac_address} diff --git a/requirements_all.txt b/requirements_all.txt index 7b80cc3150e..123a9c618c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.3 +aiohue==3.0.1 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4336c51cc2f..768c3c7854f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiohomekit==0.6.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.3 +aiohue==3.0.1 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 648337d7539..0ebeaca9b88 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,56 +1,70 @@ """Test helpers for Hue.""" +import asyncio from collections import deque +import json import logging from unittest.mock import AsyncMock, Mock, patch -from aiohue.groups import Groups -from aiohue.lights import Lights -from aiohue.scenes import Scenes -from aiohue.sensors import Sensors +import aiohue.v1 as aiohue_v1 +import aiohue.v2 as aiohue_v2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.models.clip import parse_clip_resource import pytest from homeassistant.components import hue -from homeassistant.components.hue import sensor_base as hue_sensor_base +from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.light.conftest import mock_light_profiles # noqa: F401 +from tests.common import ( + MockConfigEntry, + async_mock_service, + load_fixture, + mock_device_registry, +) + +# from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture(autouse=True) def no_request_delay(): """Make the request refresh delay 0 for instant tests.""" - with patch("homeassistant.components.hue.light.REQUEST_REFRESH_DELAY", 0): + with patch("homeassistant.components.hue.const.REQUEST_REFRESH_DELAY", 0): yield -def create_mock_bridge(hass): - """Create a mock Hue bridge.""" +def create_mock_bridge(hass, api_version=1): + """Create a mocked HueBridge instance.""" bridge = Mock( hass=hass, - available=True, authorized=True, - allow_unreachable=False, - allow_groups=False, - api=create_mock_api(hass), config_entry=None, reset_jobs=[], + api_version=api_version, spec=hue.HueBridge, ) - bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = bridge.api.mock_requests - bridge.mock_light_responses = bridge.api.mock_light_responses - bridge.mock_group_responses = bridge.api.mock_group_responses - bridge.mock_sensor_responses = bridge.api.mock_sensor_responses - async def async_setup(): + bridge.logger = logging.getLogger(__name__) + + if bridge.api_version == 2: + bridge.api = create_mock_api_v2(hass) + bridge.mock_requests = bridge.api.mock_requests + else: + bridge.api = create_mock_api_v1(hass) + bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) + bridge.mock_requests = bridge.api.mock_requests + bridge.mock_light_responses = bridge.api.mock_light_responses + bridge.mock_group_responses = bridge.api.mock_group_responses + bridge.mock_sensor_responses = bridge.api.mock_sensor_responses + + async def async_initialize_bridge(): if bridge.config_entry: hass.data.setdefault(hue.DOMAIN, {})[bridge.config_entry.entry_id] = bridge return True - bridge.async_setup = async_setup + bridge.async_initialize_bridge = async_initialize_bridge - async def async_request_call(task): - await task() + async def async_request_call(task, *args, allowed_errors=None, **kwargs): + await task(*args, **kwargs) bridge.async_request_call = async_request_call @@ -65,14 +79,21 @@ def create_mock_bridge(hass): @pytest.fixture -def mock_api(hass): - """Mock the Hue api.""" - return create_mock_api(hass) +def mock_api_v1(hass): + """Mock the Hue V1 api.""" + return create_mock_api_v1(hass) -def create_mock_api(hass): - """Create a mock API.""" - api = Mock(initialize=AsyncMock()) +@pytest.fixture +def mock_api_v2(hass): + """Mock the Hue V2 api.""" + return create_mock_api_v2(hass) + + +def create_mock_api_v1(hass): + """Create a mock V1 API.""" + api = Mock(spec=aiohue_v1.HueBridgeV1) + api.initialize = AsyncMock() api.mock_requests = [] api.mock_light_responses = deque() api.mock_group_responses = deque() @@ -97,43 +118,194 @@ def create_mock_api(hass): logger = logging.getLogger(__name__) api.config = Mock( - bridgeid="ff:ff:ff:ff:ff:ff", - mac="aa:bb:cc:dd:ee:ff", - modelid="BSB002", + bridge_id="ff:ff:ff:ff:ff:ff", + mac_address="aa:bb:cc:dd:ee:ff", + model_id="BSB002", apiversion="9.9.9", - swversion="1935144040", + software_version="1935144040", ) api.config.name = "Home" - api.lights = Lights(logger, {}, [], mock_request) - api.groups = Groups(logger, {}, [], mock_request) - api.sensors = Sensors(logger, {}, [], mock_request) - api.scenes = Scenes(logger, {}, [], mock_request) + api.lights = aiohue_v1.Lights(logger, {}, mock_request) + api.groups = aiohue_v1.Groups(logger, {}, mock_request) + api.sensors = aiohue_v1.Sensors(logger, {}, mock_request) + api.scenes = aiohue_v1.Scenes(logger, {}, mock_request) + return api + + +@pytest.fixture(scope="session") +def v2_resources_test_data(): + """Load V2 resources mock data.""" + return json.loads(load_fixture("hue/v2_resources.json")) + + +def create_mock_api_v2(hass): + """Create a mock V2 API.""" + api = Mock(spec=aiohue_v2.HueBridgeV2) + api.initialize = AsyncMock() + api.config = Mock( + bridge_id="aabbccddeeffggh", + mac_address="00:17:88:01:aa:bb:fd:c7", + model_id="BSB002", + api_version="9.9.9", + software_version="1935144040", + bridge_device=Mock( + id="4a507550-8742-4087-8bf5-c2334f29891c", + product_data=Mock(manufacturer_name="Mock"), + ), + spec=aiohue_v2.ConfigController, + ) + api.config.name = "Home" + api.mock_requests = [] + + api.logger = logging.getLogger(__name__) + api.events = aiohue_v2.EventStream(api) + api.devices = aiohue_v2.DevicesController(api) + api.lights = aiohue_v2.LightsController(api) + api.sensors = aiohue_v2.SensorsController(api) + api.groups = aiohue_v2.GroupsController(api) + api.scenes = aiohue_v2.ScenesController(api) + + async def mock_request(method, path, **kwargs): + kwargs["method"] = method + kwargs["path"] = path + api.mock_requests.append(kwargs) + return kwargs.get("json") + + api.request = mock_request + + async def load_test_data(data): + """Load test data into controllers.""" + api.config = aiohue_v2.ConfigController(api) + + await asyncio.gather( + api.config.initialize(data), + api.devices.initialize(data), + api.lights.initialize(data), + api.scenes.initialize(data), + api.sensors.initialize(data), + api.groups.initialize(data), + ) + + def emit_event(event_type, data): + """Emit an event from a (hue resource) dict.""" + api.events.emit(EventType(event_type), parse_clip_resource(data)) + + api.load_test_data = load_test_data + api.emit_event = emit_event + # mock context manager too + api.__aenter__ = AsyncMock(return_value=api) + api.__aexit__ = AsyncMock() return api @pytest.fixture -def mock_bridge(hass): - """Mock a Hue bridge.""" - return create_mock_bridge(hass) +def mock_bridge_v1(hass): + """Mock a Hue bridge with V1 api.""" + return create_mock_bridge(hass, api_version=1) -async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): - """Load the Hue platform with the provided bridge for sensor-related platforms.""" +@pytest.fixture +def mock_bridge_v2(hass): + """Mock a Hue bridge with V2 api.""" + return create_mock_bridge(hass, api_version=2) + + +@pytest.fixture +def mock_config_entry_v1(hass): + """Mock a config entry for a Hue V1 bridge.""" + return create_config_entry(api_version=1) + + +@pytest.fixture +def mock_config_entry_v2(hass): + """Mock a config entry.""" + return create_config_entry(api_version=2) + + +def create_config_entry(api_version=1, host="mock-host"): + """Mock a config entry for a Hue bridge.""" + return MockConfigEntry( + domain=hue.DOMAIN, + title=f"Mock bridge {api_version}", + data={"host": host, "api_version": api_version, "api_key": ""}, + ) + + +async def setup_component(hass): + """Mock setup Hue component.""" + with patch.object(hue, "async_setup_entry", return_value=True): + assert ( + await async_setup_component( + hass, + hue.DOMAIN, + {}, + ) + is True + ) + + +async def setup_bridge(hass, mock_bridge, config_entry): + """Load the Hue integration with the provided bridge.""" + mock_bridge.config_entry = config_entry + config_entry.add_to_hass(hass) + with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): + await hass.config_entries.async_setup(config_entry.entry_id) + + +async def setup_platform( + hass, + mock_bridge, + platforms, + hostname=None, +): + """Load the Hue integration with the provided bridge for given platform(s).""" + if not isinstance(platforms, (list, tuple)): + platforms = [platforms] if hostname is None: hostname = "mock-host" hass.config.components.add(hue.DOMAIN) - config_entry = MockConfigEntry( - domain=hue.DOMAIN, - title="Mock Title", - data={"host": hostname}, + config_entry = create_config_entry( + api_version=mock_bridge.api_version, host=hostname ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + # simulate a full setup by manually adding the bridge config entry - config_entry.add_to_hass(hass) + await setup_bridge(hass, mock_bridge, config_entry) + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + await hass.async_block_till_done() + + for platform in platforms: + await hass.config_entries.async_forward_entry_setup(config_entry, platform) # and make sure it completes before going further await hass.async_block_till_done() + + +@pytest.fixture +def mock_bridge_setup(): + """Mock bridge setup.""" + with patch.object(hue, "HueBridge") as mock_bridge: + mock_bridge.return_value.async_initialize_bridge = AsyncMock(return_value=True) + mock_bridge.return_value.api_version = 1 + mock_bridge.return_value.api.config = Mock( + bridge_id="mock-id", + mac_address="00:00:00:00:00:00", + software_version="1.0.0", + model_id="BSB002", + ) + mock_bridge.return_value.api.config.name = "Mock Hue bridge" + yield mock_bridge.return_value + + +@pytest.fixture(name="device_reg") +def get_device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="calls") +def track_calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py new file mode 100644 index 00000000000..84b342c73be --- /dev/null +++ b/tests/components/hue/const.py @@ -0,0 +1,97 @@ +"""Constants for Hue tests.""" + + +FAKE_DEVICE = { + "id": "fake_device_id_1", + "id_v1": "/lights/1", + "metadata": {"archetype": "unknown_archetype", "name": "Hue mocked device"}, + "product_data": { + "certified": True, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "abcdefg", + "product_archetype": "unknown_archetype", + "product_name": "Hue Mocked on/off light with a sensor", + "software_version": "1.88.1", + }, + "services": [ + {"rid": "fake_light_id_1", "rtype": "light"}, + {"rid": "fake_zigbee_connectivity_id_1", "rtype": "zigbee_connectivity"}, + {"rid": "fake_temperature_sensor_id_1", "rtype": "temperature"}, + {"rid": "fake_motion_sensor_id_1", "rtype": "motion"}, + ], + "type": "device", +} + +FAKE_LIGHT = { + "alert": {"action_values": ["breathe"]}, + "dynamics": { + "speed": 0.0, + "speed_valid": False, + "status": "none", + "status_values": ["none"], + }, + "id": "fake_light_id_1", + "id_v1": "/lights/1", + "metadata": {"archetype": "unknown", "name": "Hue fake light 1"}, + "mode": "normal", + "on": {"on": False}, + "owner": {"rid": "fake_device_id_1", "rtype": "device"}, + "type": "light", +} + +FAKE_ZIGBEE_CONNECTIVITY = { + "id": "fake_zigbee_connectivity_id_1", + "id_v1": "/lights/29", + "mac_address": "00:01:02:03:04:05:06:07", + "owner": {"rid": "fake_device_id_1", "rtype": "device"}, + "status": "connected", + "type": "zigbee_connectivity", +} + +FAKE_SENSOR = { + "enabled": True, + "id": "fake_temperature_sensor_id_1", + "id_v1": "/sensors/1", + "owner": {"rid": "fake_device_id_1", "rtype": "device"}, + "temperature": {"temperature": 18.0, "temperature_valid": True}, + "type": "temperature", +} + +FAKE_BINARY_SENSOR = { + "enabled": True, + "id": "fake_motion_sensor_id_1", + "id_v1": "/sensors/2", + "motion": {"motion": False, "motion_valid": True}, + "owner": {"rid": "fake_device_id_1", "rtype": "device"}, + "type": "motion", +} + +FAKE_SCENE = { + "actions": [ + { + "action": { + "color_temperature": {"mirek": 156}, + "dimming": {"brightness": 65.0}, + "on": {"on": True}, + }, + "target": {"rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", "rtype": "light"}, + }, + { + "action": {"on": {"on": True}}, + "target": {"rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", "rtype": "light"}, + }, + ], + "group": {"rid": "6ddc9066-7e7d-4a03-a773-c73937968296", "rtype": "room"}, + "id": "fake_scene_id_1", + "id_v1": "/scenes/test", + "metadata": { + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image", + }, + "name": "Mocked Scene", + }, + "palette": {"color": [], "color_temperature": [], "dimming": []}, + "speed": 0.5, + "type": "scene", +} diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json new file mode 100644 index 00000000000..806dcecfacf --- /dev/null +++ b/tests/components/hue/fixtures/v2_resources.json @@ -0,0 +1,2107 @@ +[ + { + "id": "9c489c26-9e34-4fcd-8324-a57e3a664cc0", + "status": "unpaired", + "status_values": ["pairing", "paired", "unpaired"], + "type": "homekit" + }, + { + "actions": [ + { + "action": { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 + } + }, + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true + } + }, + "target": { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + } + }, + { + "action": { + "dimming": { + "brightness": 46.85 + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 + } + } + }, + { + "color": { + "xy": { + "x": 0.4958, + "y": 0.443 + } + } + }, + { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 + } + } + }, + { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + } + }, + { + "color": { + "xy": { + "x": 0.569, + "y": 0.4003 + } + } + } + ] + }, + "on": { + "on": true + } + }, + "target": { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + }, + { + "action": { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + }, + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true + } + }, + "target": { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + } + } + ], + "group": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, + "id": "fce5eabb-2f51-461b-b112-5362da301236", + "id_v1": "/scenes/qYDehk7EfGoRvkj", + "metadata": { + "image": { + "rid": "93984a4f-2d1b-4554-b972-b60fa8e476c5", + "rtype": "public_image" + }, + "name": "Dynamic Test Scene" + }, + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 + } + }, + "dimming": { + "brightness": 74.02 + } + }, + { + "color": { + "xy": { + "x": 0.5023, + "y": 0.4467 + } + }, + "dimming": { + "brightness": 100.0 + } + }, + { + "color": { + "xy": { + "x": 0.5615, + "y": 0.4059 + } + }, + "dimming": { + "brightness": 100.0 + } + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 451 + }, + "dimming": { + "brightness": 31.1 + } + } + ], + "dimming": [] + }, + "speed": 0.6269841194152832, + "type": "scene" + }, + { + "actions": [ + { + "action": { + "color_temperature": { + "mirek": 156 + }, + "dimming": { + "brightness": 100.0 + }, + "on": { + "on": true + } + }, + "target": { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + } + }, + { + "action": { + "on": { + "on": true + } + }, + "target": { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + } + } + ], + "group": { + "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", + "rtype": "room" + }, + "id": "cdbf3740-7977-4a11-8275-8c78636ad4bd", + "id_v1": "/scenes/LwgmWgRnaRUxg6K", + "metadata": { + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + }, + "name": "Regular Test Scene" + }, + "palette": { + "color": [], + "color_temperature": [], + "dimming": [] + }, + "speed": 0.5, + "type": "scene" + }, + { + "id": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "id_v1": "/sensors/50", + "metadata": { + "archetype": "unknown_archetype", + "name": "Wall switch with 2 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RDM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue wall switch module", + "software_version": "1.0.3" + }, + "services": [ + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "rtype": "device_power" + }, + { + "rid": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "4080248P9", + "product_archetype": "floor_shade", + "product_name": "Hue color floor", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "rtype": "zigbee_connectivity" + }, + { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LTC001", + "product_archetype": "ceiling_round", + "product_name": "Hue ambiance ceiling", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + }, + { + "rid": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "id_v1": "/sensors/10", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Dimmer switch with 4 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RWL021", + "product_archetype": "unknown_archetype", + "product_name": "Hue dimmer switch", + "software_version": "1.1.28573" + }, + "services": [ + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "rtype": "device_power" + }, + { + "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "b9e76da7-ac22-476a-986d-e466e62e962f", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LST002", + "product_archetype": "hue_lightstrip", + "product_name": "Hue lightstrip plus", + "software_version": "67.88.1" + }, + "services": [ + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "product_data": { + "certified": false, + "manufacturer_name": "eWeLink", + "model_id": "SA-003-Zigbee", + "product_archetype": "classic_bulb", + "product_name": "On/Off light", + "software_version": "1.0.2" + }, + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "id_v1": "/sensors/5", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Smart button 1 control" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "ROM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue Smart button", + "software_version": "2.47.8" + }, + "services": [ + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "rtype": "device_power" + }, + { + "rid": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "4a507550-8742-4087-8bf5-c2334f29891c", + "id_v1": "", + "metadata": { + "archetype": "bridge_v2", + "name": "Philips hue" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "BSB002", + "product_archetype": "bridge_v2", + "product_name": "Philips hue", + "software_version": "1.48.1948086000" + }, + "services": [ + { + "rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "rtype": "bridge" + }, + { + "rid": "6c898412-ed25-4402-9807-a0c326616b0f", + "rtype": "zigbee_connectivity" + }, + { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LLC011", + "product_archetype": "hue_bloom", + "product_name": "Hue bloom", + "software_version": "67.91.1" + }, + "services": [ + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "rtype": "zigbee_connectivity" + }, + { + "rid": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LCX003", + "product_archetype": "hue_lightstrip_tv", + "product_name": "Hue play gradient lightstrip", + "software_version": "1.86.7" + }, + "services": [ + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "rtype": "zigbee_connectivity" + }, + { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "id_v1": "/sensors/66", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue motion sensor" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "SML001", + "product_archetype": "unknown_archetype", + "product_name": "Hue motion sensor", + "software_version": "1.1.27575" + }, + "services": [ + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "rtype": "device_power" + }, + { + "rid": "ec9b5ad7-2471-4356-b757-d00537828963", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + } + ], + "type": "device" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5614, + "y": 0.4058 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color_temperature": { + "mirek": 369, + "mirek_schema": { + "mirek_maximum": 454, + "mirek_minimum": 153 + }, + "mirek_valid": true + }, + "dimming": { + "brightness": 59.45, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.02500000037252903 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.138, + "y": 0.08 + }, + "green": { + "x": 0.2151, + "y": 0.7106 + }, + "red": { + "x": 0.704, + "y": 0.296 + } + }, + "gamut_type": "A", + "xy": { + "x": 0.4849, + "y": 0.3895 + } + }, + "dimming": { + "brightness": 50.0, + "min_dim_level": 10.0 + }, + "dynamics": { + "speed": 0.6389, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.4806, + "y": 0.4484 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.5614, + "y": 0.4058 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + } + ], + "points_capable": 5 + }, + "id": "8015b17f-8336-415b-966a-b364bd082397", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "type": "light" + }, + { + "id": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "id_v1": "/sensors/50", + "mac_address": "00:17:88:01:0b:aa:bb:99", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "id_v1": "/lights/29", + "mac_address": "00:17:88:01:09:aa:bb:65", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "id_v1": "/lights/4", + "mac_address": "00:17:88:01:06:aa:bb:58", + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "id_v1": "/sensors/10", + "mac_address": "00:17:88:01:08:aa:cc:60", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "id_v1": "/lights/16", + "mac_address": "00:17:88:aa:aa:bb:0d:ab", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "id_v1": "/lights/23", + "mac_address": "00:12:4b:00:1f:aa:bb:f3", + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "id_v1": "/sensors/5", + "mac_address": "00:17:88:01:aa:cc:87:b6", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6c898412-ed25-4402-9807-a0c326616b0f", + "id_v1": "", + "mac_address": "00:17:88:01:aa:bb:fd:c7", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "d2ae969a-add5-41b1-afbd-f2837b2eb551", + "id_v1": "/lights/34", + "mac_address": "00:17:88:01:aa:bb:cc:ed", + "owner": { + "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "id_v1": "/lights/11", + "mac_address": "00:17:88:aa:bb:1e:cc:b2", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "id_v1": "/lights/24", + "mac_address": "00:17:88:01:aa:bb:cc:3d", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ec9b5ad7-2471-4356-b757-d00537828963", + "id_v1": "/sensors/66", + "mac_address": "00:17:aa:bb:cc:09:ac:c3", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "5d7b3979-b936-47ff-8458-554f8a2921db", + "id_v1": "/lights/29", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "id_v1": "/lights/16", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "proxy": true, + "renderer": false, + "type": "entertainment" + }, + { + "id": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "id_v1": "/lights/11", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "proxy": false, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "id_v1": "/lights/24", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 10, + "segments": [ + { + "length": 2, + "start": 0 + }, + { + "length": 3, + "start": 2 + }, + { + "length": 5, + "start": 5 + }, + { + "length": 4, + "start": 10 + }, + { + "length": 5, + "start": 14 + }, + { + "length": 3, + "start": 19 + }, + { + "length": 2, + "start": 22 + } + ] + }, + "type": "entertainment" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 3 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 4 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "31cffcda-efc2-401f-a152-e10db3eed232", + "id_v1": "/sensors/5", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "id_v1": "/sensors/50", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "id_v1": "/sensors/10", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "power_state": { + "battery_level": 83, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "id_v1": "/sensors/5", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "power_state": { + "battery_level": 91, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "id_v1": "/sensors/66", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "children": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "grouped_services": [ + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "id": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "id_v1": "/groups/5", + "metadata": { + "archetype": "downstairs", + "name": "Test Zone" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "type": "zone" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "f2416154-9607-43ab-a684-4453108a200e", + "id_v1": "/groups/5", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "id_v1": "/groups/0", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "id_v1": "/groups/3", + "on": { + "on": false + }, + "type": "grouped_light" + }, + { + "children": [ + { + "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", + "rtype": "room" + }, + { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + { + "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", + "rtype": "device" + }, + { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "id": "a3fbc86a-bf4c-4c69-899d-d6eafc37e288", + "id_v1": "/groups/0", + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + }, + { + "rid": "d0df7249-02c1-4480-ba2c-d61b1e648a58", + "rtype": "light" + }, + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "d2d48fac-df99-4f8d-8bdc-bac82d2cfb24", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "1d1ac857-9b89-48aa-a4f3-68302e7d0998", + "rtype": "light" + }, + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "6a5d8ce8-c0a0-43bb-870e-d7e641cdb063", + "rtype": "button" + }, + { + "rid": "85fa4928-b061-4d19-8458-c5e30d375e39", + "rtype": "button" + }, + { + "rid": "a0640313-0a01-42b9-b236-c5e0a1568ef5", + "rtype": "button" + }, + { + "rid": "50fe978e-117c-4fc5-bb17-f707c1614a11", + "rtype": "button" + }, + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "487aa265-8ea1-4280-a663-cbf93a79ccd7", + "rtype": "button" + }, + { + "rid": "f6e137cf-8e93-4f6a-be9c-2f820bf6d893", + "rtype": "button" + }, + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + }, + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "type": "bridge_home" + }, + { + "children": [ + { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "id": "6ddc9066-7e7d-4a03-a773-c73937968296", + "id_v1": "/groups/3", + "metadata": { + "archetype": "bathroom", + "name": "Test Room" + }, + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + }, + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "type": "room" + }, + { + "channels": [ + { + "channel_id": 0, + "members": [ + { + "index": 0, + "service": { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + }, + { + "channel_id": 1, + "members": [ + { + "index": 0, + "service": { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + }, + { + "channel_id": 2, + "members": [ + { + "index": 0, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 3, + "members": [ + { + "index": 1, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 4, + "members": [ + { + "index": 2, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 5, + "members": [ + { + "index": 3, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.0, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 6, + "members": [ + { + "index": 4, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 7, + "members": [ + { + "index": 5, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 8, + "members": [ + { + "index": 6, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 9, + "members": [ + { + "index": 0, + "service": { + "rid": "be321947-0a48-4742-913d-073b3b540c97", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + } + } + ], + "configuration_type": "screen", + "id": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "id_v1": "/groups/2", + "light_services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "locations": { + "service_locations": [ + { + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + }, + "positions": [ + { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + ], + "service": { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + }, + "positions": [ + { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + ], + "service": { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + "positions": [ + { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + ], + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + }, + { + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + }, + "positions": [ + { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + } + ], + "service": { + "rid": "be321947-0a48-4742-913d-073b3b540c97", + "rtype": "entertainment" + } + } + ] + }, + "metadata": { + "name": "Entertainmentroom 1" + }, + "name": "Entertainmentroom 1", + "status": "inactive", + "stream_proxy": { + "mode": "auto", + "node": { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + }, + "type": "entertainment_configuration" + }, + { + "bridge_id": "aabbccddeeffggh", + "id": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "time_zone": { + "time_zone": "Europe/Amsterdam" + }, + "type": "bridge" + }, + { + "enabled": true, + "id": "b6896534-016d-4052-8cb4-ef04454df62c", + "id_v1": "/sensors/66", + "motion": { + "motion": false, + "motion_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "motion" + }, + { + "enabled": true, + "id": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "id_v1": "/sensors/67", + "light": { + "light_level": 18027, + "light_level_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "light_level" + }, + { + "enabled": true, + "id": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "id_v1": "/sensors/68", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "temperature": { + "temperature": 18.139999389648438, + "temperature_valid": true + }, + "type": "temperature" + }, + { + "configuration": { + "end_state": "last_state", + "where": [ + { + "group": { + "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "rtype": "entertainment_configuration" + } + } + ] + }, + "dependees": [ + { + "level": "critical", + "target": { + "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "rtype": "entertainment_configuration" + }, + "type": "ResourceDependee" + } + ], + "enabled": true, + "id": "0670cfb1-2bd7-4237-a0e3-1827a44d7231", + "last_error": "", + "metadata": { + "name": "state_after_streaming" + }, + "migrated_from": "/resourcelinks/47450", + "script_id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "status": "running", + "type": "behavior_instance" + }, + { + "configuration_schema": { + "$ref": "leaving_home_config.json#" + }, + "description": "Automatically turn off your lights when you leave", + "id": "0194752a-2d53-4f92-8209-dfdc52745af3", + "metadata": { + "name": "Leaving home" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "schedule_config.json#" + }, + "description": "Schedule turning on and off lights", + "id": "7238c707-8693-4f19-9095-ccdc1444d228", + "metadata": { + "name": "Schedule" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "lights_state_after_streaming_config.json#" + }, + "description": "State of lights in the entertainment group after streaming ends", + "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "metadata": { + "name": "Light state after streaming" + }, + "state_schema": {}, + "trigger_schema": {}, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_goto_sleep_config.json#" + }, + "description": "Get ready for nice sleep.", + "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", + "metadata": { + "name": "Basic go to sleep routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "coming_home_config.json#" + }, + "description": "Automatically turn your lights to choosen light states, when you arrive at home.", + "id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "metadata": { + "name": "Coming home" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_wake_up_config.json#" + }, + "description": "Get your body in the mood to wake up by fading on the lights in the morning.", + "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", + "metadata": { + "name": "Basic wake up routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "natural_light_config.json#" + }, + "description": "Natural light during the day", + "id": "a4260b49-0c69-4926-a29c-417f4a38a352", + "metadata": { + "name": "Natural Light" + }, + "state_schema": { + "$ref": "natural_light_state.json#" + }, + "trigger_schema": { + "$ref": "smart_scene_trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "timer_config.json#" + }, + "description": "Countdown Timer", + "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", + "metadata": { + "name": "Timers" + }, + "state_schema": { + "$ref": "timer_state.json#" + }, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "id": "c6e03a31-4c30-4cef-834f-26ffbb06a593", + "name": "Test geofence client", + "type": "geofence_client" + }, + { + "id": "52612630-841e-4d39-9763-60346a0da759", + "is_configured": true, + "type": "geolocation" + } + ] + \ No newline at end of file diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py new file mode 100644 index 00000000000..f1d6a1a8087 --- /dev/null +++ b/tests/components/hue/test_binary_sensor.py @@ -0,0 +1,61 @@ +"""Philips Hue binary_sensor platform tests for V2 bridge/api.""" + + +from .conftest import setup_platform +from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY + + +async def test_binary_sensors(hass, mock_bridge_v2, v2_resources_test_data): + """Test if all v2 binary_sensors get created with correct features.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "binary_sensor") + # there shouldn't have been any requests at this point + assert len(mock_bridge_v2.mock_requests) == 0 + # 2 binary_sensors should be created from test data + assert len(hass.states.async_all()) == 2 + + # test motion sensor + sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion") + assert sensor is not None + assert sensor.state == "off" + assert sensor.name == "Hue motion sensor: Motion" + assert sensor.attributes["device_class"] == "motion" + assert sensor.attributes["motion_valid"] is True + + # test entertainment room active sensor + sensor = hass.states.get( + "binary_sensor.entertainmentroom_1_entertainment_configuration" + ) + assert sensor is not None + assert sensor.state == "off" + assert sensor.name == "Entertainmentroom 1: Entertainment Configuration" + assert sensor.attributes["device_class"] == "running" + + +async def test_binary_sensor_add_update(hass, mock_bridge_v2): + """Test if binary_sensor get added/updated from events.""" + await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) + await setup_platform(hass, mock_bridge_v2, "binary_sensor") + + test_entity_id = "binary_sensor.hue_mocked_device_motion" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake sensor by emitting event + mock_bridge_v2.api.emit_event("add", FAKE_BINARY_SENSOR) + await hass.async_block_till_done() + + # the entity should now be available + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "off" + + # test update of entity works on incoming event + updated_sensor = {**FAKE_BINARY_SENSOR, "motion": {"motion": True}} + mock_bridge_v2.api.emit_event("update", updated_sensor) + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "on" diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 034acf88efa..bede2f75789 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -2,61 +2,70 @@ import asyncio from unittest.mock import AsyncMock, Mock, patch +from aiohttp import client_exceptions +from aiohue.errors import Unauthorized +from aiohue.v1 import HueBridgeV1 +from aiohue.v2 import HueBridgeV2 import pytest -from homeassistant import config_entries -from homeassistant.components import hue -from homeassistant.components.hue import bridge, errors +from homeassistant.components.hue import bridge from homeassistant.components.hue.const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, ) from homeassistant.exceptions import ConfigEntryNotReady -ORIG_SUBSCRIBE_EVENTS = bridge.HueBridge._subscribe_events +async def test_bridge_setup_v1(hass, mock_api_v1): + """Test a successful setup for V1 bridge.""" + config_entry = Mock() + config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} + config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} -@pytest.fixture(autouse=True) -def mock_subscribe_events(): - """Mock subscribe events method.""" - with patch( - "homeassistant.components.hue.bridge.HueBridge._subscribe_events" - ) as mock: - yield mock - - -async def test_bridge_setup(hass, mock_subscribe_events): - """Test a successful setup.""" - entry = Mock() - api = Mock(initialize=AsyncMock()) - entry.data = {"host": "1.2.3.4", "username": "mock-username"} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - hue_bridge = bridge.HueBridge(hass, entry) - - with patch("aiohue.Bridge", return_value=api), patch.object( + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward: - assert await hue_bridge.async_setup() is True + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True - assert hue_bridge.api is api + assert hue_bridge.api is mock_api_v1 + assert isinstance(hue_bridge.api, HueBridgeV1) + assert hue_bridge.api_version == 1 assert len(mock_forward.mock_calls) == 3 forward_entries = {c[1][1] for c in mock_forward.mock_calls} assert forward_entries == {"light", "binary_sensor", "sensor"} - assert len(mock_subscribe_events.mock_calls) == 1 + +async def test_bridge_setup_v2(hass, mock_api_v2): + """Test a successful setup for V2 bridge.""" + config_entry = Mock() + config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 2} + + with patch.object(bridge, "HueBridgeV2", return_value=mock_api_v2), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True + + assert hue_bridge.api is mock_api_v2 + assert isinstance(hue_bridge.api, HueBridgeV2) + assert hue_bridge.api_version == 2 + assert len(mock_forward.mock_calls) == 5 + forward_entries = {c[1][1] for c in mock_forward.mock_calls} + assert forward_entries == {"light", "binary_sensor", "sensor", "switch", "scene"} -async def test_bridge_setup_invalid_username(hass): +async def test_bridge_setup_invalid_api_key(hass): """Test we start config flow if username is no longer whitelisted.""" entry = Mock() - entry.data = {"host": "1.2.3.4", "username": "mock-username"} + entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) with patch.object( - bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired + hue_bridge.api, "initialize", side_effect=Unauthorized ), patch.object(hass.config_entries.flow, "async_init") as mock_init: - assert await hue_bridge.async_setup() is False + assert await hue_bridge.async_initialize_bridge() is False assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][2]["data"] == {"host": "1.2.3.4"} @@ -65,50 +74,34 @@ async def test_bridge_setup_invalid_username(hass): async def test_bridge_setup_timeout(hass): """Test we retry to connect if we cannot connect.""" entry = Mock() - entry.data = {"host": "1.2.3.4", "username": "mock-username"} + entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) with patch.object( - bridge, "authenticate_bridge", side_effect=errors.CannotConnect + hue_bridge.api, + "initialize", + side_effect=client_exceptions.ServerDisconnectedError, ), pytest.raises(ConfigEntryNotReady): - await hue_bridge.async_setup() + await hue_bridge.async_initialize_bridge() -async def test_reset_if_entry_had_wrong_auth(hass): - """Test calling reset when the entry contained wrong auth.""" - entry = Mock() - entry.data = {"host": "1.2.3.4", "username": "mock-username"} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - hue_bridge = bridge.HueBridge(hass, entry) - - with patch.object( - bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired - ), patch.object(bridge, "create_config_flow") as mock_create: - assert await hue_bridge.async_setup() is False - - assert len(mock_create.mock_calls) == 1 - - assert await hue_bridge.async_reset() - - -async def test_reset_unloads_entry_if_setup(hass, mock_subscribe_events): +async def test_reset_unloads_entry_if_setup(hass, mock_api_v1): """Test calling reset while the entry has been setup.""" - entry = Mock() - entry.data = {"host": "1.2.3.4", "username": "mock-username"} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - hue_bridge = bridge.HueBridge(hass, entry) + config_entry = Mock() + config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} + config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - with patch.object(bridge, "authenticate_bridge"), patch( - "aiohue.Bridge" - ), patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: - assert await hue_bridge.async_setup() is True + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True await asyncio.sleep(0) assert len(hass.services.async_services()) == 0 assert len(mock_forward.mock_calls) == 3 - assert len(mock_subscribe_events.mock_calls) == 1 with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -119,17 +112,15 @@ async def test_reset_unloads_entry_if_setup(hass, mock_subscribe_events): assert len(hass.services.async_services()) == 0 -async def test_handle_unauthorized(hass): +async def test_handle_unauthorized(hass, mock_api_v1): """Test handling an unauthorized error on update.""" - entry = Mock(async_setup=AsyncMock()) - entry.data = {"host": "1.2.3.4", "username": "mock-username"} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - hue_bridge = bridge.HueBridge(hass, entry) + config_entry = Mock(async_setup=AsyncMock()) + config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} + config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} - with patch.object(bridge, "authenticate_bridge"), patch("aiohue.Bridge"): - assert await hue_bridge.async_setup() is True - - assert hue_bridge.authorized is True + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True with patch.object(bridge, "create_config_flow") as mock_create: await hue_bridge.handle_unauthorized_error() @@ -137,233 +128,3 @@ async def test_handle_unauthorized(hass): assert hue_bridge.authorized is False assert len(mock_create.mock_calls) == 1 assert mock_create.mock_calls[0][1][1] == "1.2.3.4" - - -GROUP_RESPONSE = { - "group_1": { - "name": "Group 1", - "lights": ["1", "2"], - "type": "LightGroup", - "action": { - "on": True, - "bri": 254, - "hue": 10000, - "sat": 254, - "effect": "none", - "xy": [0.5, 0.5], - "ct": 250, - "alert": "select", - "colormode": "ct", - }, - "state": {"any_on": True, "all_on": False}, - } -} -SCENE_RESPONSE = { - "scene_1": { - "name": "Cozy dinner", - "lights": ["1", "2"], - "owner": "ffffffffe0341b1b376a2389376a2389", - "recycle": True, - "locked": False, - "appdata": {"version": 1, "data": "myAppData"}, - "picture": "", - "lastupdated": "2015-12-03T10:09:22", - "version": 2, - } -} - - -async def test_hue_activate_scene(hass, mock_api): - """Test successful hue_activate_scene.""" - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host", "username": "mock-username"}, - "test", - options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, - ) - hue_bridge = bridge.HueBridge(hass, config_entry) - - mock_api.mock_group_responses.append(GROUP_RESPONSE) - mock_api.mock_scene_responses.append(SCENE_RESPONSE) - - with patch("aiohue.Bridge", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - assert await hue_bridge.async_setup() is True - - assert hue_bridge.api is mock_api - - call = Mock() - call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} - with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call.data) is None - - assert len(mock_api.mock_requests) == 3 - assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1" - assert "transitiontime" not in mock_api.mock_requests[2]["json"] - assert mock_api.mock_requests[2]["path"] == "groups/group_1/action" - - -async def test_hue_activate_scene_transition(hass, mock_api): - """Test successful hue_activate_scene with transition.""" - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host", "username": "mock-username"}, - "test", - options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, - ) - hue_bridge = bridge.HueBridge(hass, config_entry) - - mock_api.mock_group_responses.append(GROUP_RESPONSE) - mock_api.mock_scene_responses.append(SCENE_RESPONSE) - - with patch("aiohue.Bridge", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - assert await hue_bridge.async_setup() is True - - assert hue_bridge.api is mock_api - - call = Mock() - call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner", "transition": 30} - with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call.data) is None - - assert len(mock_api.mock_requests) == 3 - assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1" - assert mock_api.mock_requests[2]["json"]["transitiontime"] == 30 - assert mock_api.mock_requests[2]["path"] == "groups/group_1/action" - - -async def test_hue_activate_scene_group_not_found(hass, mock_api): - """Test failed hue_activate_scene due to missing group.""" - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host", "username": "mock-username"}, - "test", - options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, - ) - hue_bridge = bridge.HueBridge(hass, config_entry) - - mock_api.mock_group_responses.append({}) - mock_api.mock_scene_responses.append(SCENE_RESPONSE) - - with patch("aiohue.Bridge", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - assert await hue_bridge.async_setup() is True - - assert hue_bridge.api is mock_api - - call = Mock() - call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} - with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call.data) is False - - -async def test_hue_activate_scene_scene_not_found(hass, mock_api): - """Test failed hue_activate_scene due to missing scene.""" - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host", "username": "mock-username"}, - "test", - options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, - ) - hue_bridge = bridge.HueBridge(hass, config_entry) - - mock_api.mock_group_responses.append(GROUP_RESPONSE) - mock_api.mock_scene_responses.append({}) - - with patch("aiohue.Bridge", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - assert await hue_bridge.async_setup() is True - - assert hue_bridge.api is mock_api - - call = Mock() - call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} - with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call.data) is False - - -async def test_event_updates(hass, caplog): - """Test calling reset while the entry has been setup.""" - events = asyncio.Queue() - - async def iterate_queue(): - while True: - event = await events.get() - if event is None: - return - yield event - - async def wait_empty_queue(): - count = 0 - while not events.empty() and count < 50: - await asyncio.sleep(0) - count += 1 - - hue_bridge = bridge.HueBridge(None, None) - hue_bridge.api = Mock(listen_events=iterate_queue) - subscription_task = asyncio.create_task(ORIG_SUBSCRIBE_EVENTS(hue_bridge)) - - calls = [] - - def obj_updated(): - calls.append(True) - - unsub = hue_bridge.listen_updates("lights", "2", obj_updated) - - events.put_nowait(Mock(ITEM_TYPE="lights", id="1")) - - await wait_empty_queue() - assert len(calls) == 0 - - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) - - await wait_empty_queue() - assert len(calls) == 1 - - unsub() - - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) - - await wait_empty_queue() - assert len(calls) == 1 - - # Test we can override update listener. - def obj_updated_false(): - calls.append(False) - - unsub = hue_bridge.listen_updates("lights", "2", obj_updated) - unsub_false = hue_bridge.listen_updates("lights", "2", obj_updated_false) - - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) - - await wait_empty_queue() - assert len(calls) == 3 - assert calls[-2] is True - assert calls[-1] is False - - # Also call multiple times to make sure that works. - unsub() - unsub() - unsub_false() - unsub_false() - - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) - - await wait_empty_queue() - assert len(calls) == 3 - - events.put_nowait(None) - await subscription_task diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 2c79795d48b..31468e198da 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,16 +1,16 @@ """Tests for Philips Hue config flow.""" import asyncio -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch -from aiohttp import client_exceptions -import aiohue from aiohue.discovery import URL_NUPNP +from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const +from homeassistant.components.hue.errors import CannotConnect from tests.common import MockConfigEntry @@ -22,36 +22,36 @@ def hue_setup_fixture(): yield -def get_mock_bridge( - bridge_id="aabbccddeeff", host="1.2.3.4", mock_create_user=None, username=None -): - """Return a mock bridge.""" - mock_bridge = Mock() - mock_bridge.host = host - mock_bridge.username = username - mock_bridge.config.name = "Mock Bridge" - mock_bridge.id = bridge_id +def get_discovered_bridge(bridge_id="aabbccddeeff", host="1.2.3.4", supports_v2=False): + """Return a mocked Discovered Bridge.""" + return Mock(host=host, id=bridge_id, supports_v2=supports_v2) - if not mock_create_user: - async def create_user(username): - mock_bridge.username = username - - mock_create_user = create_user - - mock_bridge.create_user = mock_create_user - mock_bridge.initialize = AsyncMock() - - return mock_bridge +def create_mock_api_discovery(aioclient_mock, bridges): + """Patch aiohttp responses with fake data for bridge discovery.""" + aioclient_mock.get( + URL_NUPNP, + json=[{"internalipaddress": host, "id": id} for (host, id) in bridges], + ) + for (host, bridge_id) in bridges: + aioclient_mock.get( + f"http://{host}/api/config", + json={"bridgeid": bridge_id}, + ) + # mock v2 support if v2 found in id + aioclient_mock.get( + f"https://{host}/clip/v2/resources", + status=403 if "v2" in bridge_id else 404, + ) async def test_flow_works(hass): """Test config flow .""" - mock_bridge = get_mock_bridge() + disc_bridge = get_discovered_bridge(supports_v2=True) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], + return_value=[disc_bridge], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -61,7 +61,7 @@ async def test_flow_works(hass): assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"id": mock_bridge.id} + result["flow_id"], user_input={"id": disc_bridge.id} ) assert result["type"] == "form" @@ -74,23 +74,23 @@ async def test_flow_works(hass): ) assert flow["context"]["unique_id"] == "aabbccddeeff" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + with patch.object(config_flow, "create_app_key", return_value="123456789"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "create_entry" - assert result["title"] == "Mock Bridge" + assert result["title"] == "Hue Bridge aabbccddeeff" assert result["data"] == { "host": "1.2.3.4", - "username": "home-assistant#test-home", + "api_key": "123456789", + "api_version": 2, } - assert len(mock_bridge.initialize.mock_calls) == 1 - -async def test_manual_flow_works(hass, aioclient_mock): +async def test_manual_flow_works(hass): """Test config flow discovers only already configured bridges.""" - mock_bridge = get_mock_bridge() + disc_bridge = get_discovered_bridge(bridge_id="id-1234", host="2.2.2.2") MockConfigEntry( domain="hue", source=config_entries.SOURCE_IGNORE, unique_id="bla" @@ -98,7 +98,7 @@ async def test_manual_flow_works(hass, aioclient_mock): with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], + return_value=[disc_bridge], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -114,14 +114,7 @@ async def test_manual_flow_works(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "manual" - bridge = get_mock_bridge( - bridge_id="id-1234", host="2.2.2.2", username="username-abc" - ) - - with patch( - "aiohue.Bridge", - return_value=bridge, - ): + with patch.object(config_flow, "discover_bridge", return_value=disc_bridge): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "2.2.2.2"} ) @@ -129,16 +122,17 @@ async def test_manual_flow_works(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "link" - with patch("homeassistant.components.hue.config_flow.authenticate_bridge"), patch( + with patch.object(config_flow, "create_app_key", return_value="123456789"), patch( "homeassistant.components.hue.async_unload_entry", return_value=True ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == "create_entry" - assert result["title"] == "Mock Bridge" + assert result["title"] == f"Hue Bridge {disc_bridge.id}" assert result["data"] == { "host": "2.2.2.2", - "username": "username-abc", + "api_key": "123456789", + "api_version": 1, } entries = hass.config_entries.async_entries("hue") assert len(entries) == 2 @@ -146,8 +140,8 @@ async def test_manual_flow_works(hass, aioclient_mock): assert entry.unique_id == "id-1234" -async def test_manual_flow_bridge_exist(hass, aioclient_mock): - """Test config flow discovers only already configured bridges.""" +async def test_manual_flow_bridge_exist(hass): + """Test config flow aborts on already configured bridges.""" MockConfigEntry( domain="hue", unique_id="id-1234", data={"host": "2.2.2.2"} ).add_to_hass(hass) @@ -163,25 +157,17 @@ async def test_manual_flow_bridge_exist(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "manual" - bridge = get_mock_bridge( - bridge_id="id-1234", host="2.2.2.2", username="username-abc" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "2.2.2.2"} ) - with patch( - "aiohue.Bridge", - return_value=bridge, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "2.2.2.2"} - ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" async def test_manual_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" - aioclient_mock.get(URL_NUPNP, json=[]) + create_mock_api_discovery(aioclient_mock, []) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -192,9 +178,12 @@ async def test_manual_flow_no_discovered_bridges(hass, aioclient_mock): async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]) + mock_host = "1.2.3.4" + mock_id = "bla" + create_mock_api_discovery(aioclient_mock, [(mock_host, mock_id)]) + MockConfigEntry( - domain="hue", unique_id="bla", data={"host": "1.2.3.4"} + domain="hue", unique_id=mock_id, data={"host": mock_host} ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -212,12 +201,8 @@ async def test_flow_bridges_discovered(hass, aioclient_mock): domain="hue", source=config_entries.SOURCE_IGNORE, unique_id="bla" ).add_to_hass(hass) - aioclient_mock.get( - URL_NUPNP, - json=[ - {"internalipaddress": "1.2.3.4", "id": "bla"}, - {"internalipaddress": "5.6.7.8", "id": "beer"}, - ], + create_mock_api_discovery( + aioclient_mock, [("1.2.3.4", "bla"), ("5.6.7.8", "beer_v2")] ) result = await hass.config_entries.flow.async_init( @@ -230,19 +215,13 @@ async def test_flow_bridges_discovered(hass, aioclient_mock): assert result["data_schema"]({"id": "not-discovered"}) result["data_schema"]({"id": "bla"}) - result["data_schema"]({"id": "beer"}) + result["data_schema"]({"id": "beer_v2"}) result["data_schema"]({"id": "manual"}) async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): """Test config flow discovers two bridges.""" - aioclient_mock.get( - URL_NUPNP, - json=[ - {"internalipaddress": "1.2.3.4", "id": "bla"}, - {"internalipaddress": "5.6.7.8", "id": "beer"}, - ], - ) + create_mock_api_discovery(aioclient_mock, [("1.2.3.4", "bla"), ("5.6.7.8", "beer")]) MockConfigEntry( domain="hue", unique_id="bla", data={"host": "1.2.3.4"} ).add_to_hass(hass) @@ -273,51 +252,25 @@ async def test_flow_timeout_discovery(hass): assert result["reason"] == "discover_timeout" -async def test_flow_link_timeout(hass): - """Test config flow.""" - mock_bridge = get_mock_bridge( - mock_create_user=AsyncMock(side_effect=asyncio.TimeoutError), - ) - with patch( - "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], - ): - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"id": mock_bridge.id} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - async def test_flow_link_unknown_error(hass): """Test if a unknown error happened during the linking processes.""" - mock_bridge = get_mock_bridge( - mock_create_user=AsyncMock(side_effect=OSError), - ) + disc_bridge = get_discovered_bridge() with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], + return_value=[disc_bridge], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"id": mock_bridge.id} - ) + with patch.object(config_flow, "create_app_key", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": disc_bridge.id} + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -326,58 +279,57 @@ async def test_flow_link_unknown_error(hass): async def test_flow_link_button_not_pressed(hass): """Test config flow .""" - mock_bridge = get_mock_bridge( - mock_create_user=AsyncMock(side_effect=aiohue.LinkButtonNotPressed), - ) + disc_bridge = get_discovered_bridge() with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], + return_value=[disc_bridge], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"id": mock_bridge.id} - ) + with patch.object(config_flow, "create_app_key", side_effect=LinkButtonNotPressed): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": disc_bridge.id} + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "form" assert result["step_id"] == "link" assert result["errors"] == {"base": "register_failed"} -async def test_flow_link_unknown_host(hass): +async def test_flow_link_cannot_connect(hass): """Test config flow .""" - mock_bridge = get_mock_bridge( - mock_create_user=AsyncMock(side_effect=client_exceptions.ClientOSError), - ) + disc_bridge = get_discovered_bridge() with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=[mock_bridge], + return_value=[disc_bridge], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"id": mock_bridge.id} - ) + with patch.object(config_flow, "create_app_key", side_effect=CannotConnect): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": disc_bridge.id} + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" @pytest.mark.parametrize("mf_url", config_flow.HUE_MANUFACTURERURL) -async def test_bridge_ssdp(hass, mf_url): +async def test_bridge_ssdp(hass, mf_url, aioclient_mock): """Test a bridge being discovered.""" + create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -468,8 +420,9 @@ async def test_bridge_ssdp_espalexa(hass): assert result["reason"] == "not_hue_bridge" -async def test_bridge_ssdp_already_configured(hass): +async def test_bridge_ssdp_already_configured(hass, aioclient_mock): """Test if a discovered bridge has already been configured.""" + create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) MockConfigEntry( domain="hue", unique_id="1234", data={"host": "0.0.0.0"} ).add_to_hass(hass) @@ -488,8 +441,9 @@ async def test_bridge_ssdp_already_configured(hass): assert result["reason"] == "already_configured" -async def test_import_with_no_config(hass): +async def test_import_with_no_config(hass, aioclient_mock): """Test importing a host without an existing config file.""" + create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -500,55 +454,52 @@ async def test_import_with_no_config(hass): assert result["step_id"] == "link" -async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): +async def test_creating_entry_removes_entries_for_same_host_or_bridge( + hass, aioclient_mock +): """Test that we clean up entries for same host and bridge. An IP can only hold a single bridge and a single bridge can only be accessible via a single IP. So when we create a new entry, we'll remove all existing entries that either have same IP or same bridge_id. """ + create_mock_api_discovery(aioclient_mock, [("2.2.2.2", "id-1234")]) orig_entry = MockConfigEntry( domain="hue", - data={"host": "0.0.0.0", "username": "aaaa"}, + data={"host": "0.0.0.0", "api_key": "123456789"}, unique_id="id-1234", ) orig_entry.add_to_hass(hass) MockConfigEntry( domain="hue", - data={"host": "1.2.3.4", "username": "bbbb"}, + data={"host": "1.2.3.4", "api_key": "123456789"}, unique_id="id-5678", ).add_to_hass(hass) assert len(hass.config_entries.async_entries("hue")) == 2 - bridge = get_mock_bridge( - bridge_id="id-1234", host="2.2.2.2", username="username-abc" + result = await hass.config_entries.flow.async_init( + "hue", + data={"host": "2.2.2.2"}, + context={"source": config_entries.SOURCE_IMPORT}, ) - with patch( - "aiohue.Bridge", - return_value=bridge, - ): - result = await hass.config_entries.flow.async_init( - "hue", - data={"host": "2.2.2.2"}, - context={"source": config_entries.SOURCE_IMPORT}, - ) - assert result["type"] == "form" assert result["step_id"] == "link" - with patch("homeassistant.components.hue.config_flow.authenticate_bridge"), patch( - "homeassistant.components.hue.async_unload_entry", return_value=True - ): + with patch( + "homeassistant.components.hue.config_flow.create_app_key", + return_value="123456789", + ), patch("homeassistant.components.hue.async_unload_entry", return_value=True): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == "create_entry" - assert result["title"] == "Mock Bridge" + assert result["title"] == "Hue Bridge id-1234" assert result["data"] == { "host": "2.2.2.2", - "username": "username-abc", + "api_key": "123456789", + "api_version": 1, } entries = hass.config_entries.async_entries("hue") assert len(entries) == 2 @@ -559,7 +510,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): async def test_bridge_homekit(hass, aioclient_mock): """Test a bridge being discovered via HomeKit.""" - aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]) + create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "bla")]) result = await hass.config_entries.flow.async_init( const.DOMAIN, @@ -599,8 +550,9 @@ async def test_bridge_import_already_configured(hass): assert result["reason"] == "already_configured" -async def test_bridge_homekit_already_configured(hass): +async def test_bridge_homekit_already_configured(hass, aioclient_mock): """Test if a HomeKit discovered bridge has already been configured.""" + create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "aabbccddeeff")]) MockConfigEntry( domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} ).add_to_hass(hass) @@ -615,8 +567,9 @@ async def test_bridge_homekit_already_configured(hass): assert result["reason"] == "already_configured" -async def test_ssdp_discovery_update_configuration(hass): +async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): """Test if a discovered bridge is configured and updated with new host.""" + create_mock_api_discovery(aioclient_mock, [("1.1.1.1", "aabbccddeeff")]) entry = MockConfigEntry( domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} ) @@ -637,8 +590,8 @@ async def test_ssdp_discovery_update_configuration(hass): assert entry.data["host"] == "1.1.1.1" -async def test_options_flow(hass): - """Test options config flow.""" +async def test_options_flow_v1(hass): + """Test options config flow for a V1 bridge.""" entry = MockConfigEntry( domain="hue", unique_id="aabbccddeeff", @@ -683,8 +636,26 @@ def _get_schema_default(schema, key_name): raise KeyError(f"{key_name} not found in schema") -async def test_bridge_zeroconf(hass): +async def test_options_flow_v2(hass): + """Test options config flow for a V2 bridge.""" + entry = MockConfigEntry( + domain="hue", + unique_id="v2bridge", + data={"host": "0.0.0.0", "api_version": 2}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + # V2 bridge does not have config options + assert result["data_schema"] is None + + +async def test_bridge_zeroconf(hass, aioclient_mock): """Test a bridge being discovered.""" + create_mock_api_discovery(aioclient_mock, [("192.168.1.217", "ecb5fafffeabcabc")]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -706,8 +677,9 @@ async def test_bridge_zeroconf(hass): assert result["step_id"] == "link" -async def test_bridge_zeroconf_already_exists(hass): +async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): """Test a bridge being discovered by zeroconf already exists.""" + create_mock_api_discovery(aioclient_mock, [("192.168.1.217", "ecb5faabcabc")]) entry = MockConfigEntry( domain="hue", source=config_entries.SOURCE_SSDP, diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger_v1.py similarity index 71% rename from tests/components/hue/test_device_trigger.py rename to tests/components/hue/test_device_trigger_v1.py index d0c20018c30..fcb6ca5668e 100644 --- a/tests/components/hue/test_device_trigger.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -1,43 +1,23 @@ -"""The tests for Philips Hue device triggers.""" -import pytest +"""The tests for Philips Hue device triggers for V1 bridge.""" -from homeassistant.components import hue -import homeassistant.components.automation as automation -from homeassistant.components.hue import device_trigger +from homeassistant.components import automation, hue +from homeassistant.components.hue.v1 import device_trigger from homeassistant.setup import async_setup_component -from .conftest import setup_bridge_for_sensors as setup_bridge -from .test_sensor_base import HUE_DIMMER_REMOTE_1, HUE_TAP_REMOTE_1 +from .conftest import setup_platform +from .test_sensor_v1 import HUE_DIMMER_REMOTE_1, HUE_TAP_REMOTE_1 -from tests.common import ( - assert_lists_same, - async_get_device_automations, - async_mock_service, - mock_device_registry, -) -from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 +from tests.common import assert_lists_same, async_get_device_automations REMOTES_RESPONSE = {"7": HUE_TAP_REMOTE_1, "8": HUE_DIMMER_REMOTE_1} -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - -async def test_get_triggers(hass, mock_bridge, device_reg): +async def test_get_triggers(hass, mock_bridge_v1, device_reg): """Test we get the expected triggers from a hue remote.""" - mock_bridge.mock_sensor_responses.append(REMOTES_RESPONSE) - await setup_bridge(hass, mock_bridge) + mock_bridge_v1.mock_sensor_responses.append(REMOTES_RESPONSE) + await setup_platform(hass, mock_bridge_v1, ["sensor", "binary_sensor"]) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge_v1.mock_requests) == 1 # 2 remotes, just 1 battery sensor assert len(hass.states.async_all()) == 1 @@ -88,11 +68,11 @@ async def test_get_triggers(hass, mock_bridge, device_reg): assert_lists_same(triggers, expected_triggers) -async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): +async def test_if_fires_on_state_change(hass, mock_bridge_v1, device_reg, calls): """Test for button press trigger firing.""" - mock_bridge.mock_sensor_responses.append(REMOTES_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + mock_bridge_v1.mock_sensor_responses.append(REMOTES_RESPONSE) + await setup_platform(hass, mock_bridge_v1, ["sensor", "binary_sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 1 # Set an automation with a specific tap switch trigger @@ -145,13 +125,13 @@ async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): "buttonevent": 18, "lastupdated": "2019-12-28T22:58:02", } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + await mock_bridge_v1.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 2 + assert len(mock_bridge_v1.mock_requests) == 2 assert len(calls) == 1 assert calls[0].data["some"] == "B4 - 18" @@ -162,10 +142,10 @@ async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): "buttonevent": 34, "lastupdated": "2019-12-28T22:58:05", } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + await mock_bridge_v1.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 3 + assert len(mock_bridge_v1.mock_requests) == 3 assert len(calls) == 1 diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py new file mode 100644 index 00000000000..bda963552c7 --- /dev/null +++ b/tests/components/hue/test_device_trigger_v2.py @@ -0,0 +1,82 @@ +"""The tests for Philips Hue device triggers for V2 bridge.""" +from aiohue.v2.models.button import ButtonEvent + +from homeassistant.components import hue +from homeassistant.components.hue.v2.device import async_setup_devices +from homeassistant.components.hue.v2.hue_event import async_setup_hue_events + +from .conftest import setup_platform + +from tests.common import ( + assert_lists_same, + async_capture_events, + async_get_device_automations, +) + + +async def test_hue_event(hass, mock_bridge_v2, v2_resources_test_data): + """Test hue button events.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_platform(hass, mock_bridge_v2, ["binary_sensor", "sensor"]) + await async_setup_devices(mock_bridge_v2) + await async_setup_hue_events(mock_bridge_v2) + + events = async_capture_events(hass, "hue_event") + + # Emit button update event + btn_event = { + "button": {"last_event": "short_release"}, + "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "metadata": {"control_id": 1}, + "type": "button", + } + mock_bridge_v2.api.emit_event("update", btn_event) + + # wait for the event + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["id"] == "wall_switch_with_2_controls_button" + assert events[0].data["unique_id"] == btn_event["id"] + assert events[0].data["type"] == btn_event["button"]["last_event"] + assert events[0].data["subtype"] == btn_event["metadata"]["control_id"] + + +async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device_reg): + """Test we get the expected triggers from a hue remote.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_platform(hass, mock_bridge_v2, ["binary_sensor", "sensor"]) + + # Get triggers for `Wall switch with 2 controls` + hue_wall_switch_device = device_reg.async_get_device( + {(hue.DOMAIN, "3ff06175-29e8-44a8-8fe7-af591b0025da")} + ) + triggers = await async_get_device_automations( + hass, "trigger", hue_wall_switch_device.id + ) + + trigger_batt = { + "platform": "device", + "domain": "sensor", + "device_id": hue_wall_switch_device.id, + "type": "battery_level", + "entity_id": "sensor.wall_switch_with_2_controls_battery", + } + + expected_triggers = [ + trigger_batt, + *( + { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_wall_switch_device.id, + "unique_id": hue_wall_switch_device.id, + "type": event_type, + "subtype": control_id, + } + for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN) + for control_id in range(1, 3) + ), + ] + + assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 05a7ade6948..b44a638669e 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -10,12 +10,19 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -@pytest.fixture -def mock_bridge_setup(): +@pytest.fixture(name="mock_bridge_setup") +def get_mock_bridge_setup(): """Mock bridge setup.""" with patch.object(hue, "HueBridge") as mock_bridge: - mock_bridge.return_value.async_setup = AsyncMock(return_value=True) - mock_bridge.return_value.api.config = Mock(bridgeid="mock-id") + mock_bridge.return_value.async_initialize_bridge = AsyncMock(return_value=True) + mock_bridge.return_value.api_version = 1 + mock_bridge.return_value.api.config = Mock( + bridge_id="mock-id", + mac_address="00:00:00:00:00:00", + software_version="1.0.0", + model_id="BSB002", + ) + mock_bridge.return_value.api.config.name = "Mock Hue bridge" yield mock_bridge.return_value @@ -108,11 +115,14 @@ async def test_fixing_unique_id_other_correct(hass, mock_bridge_setup): async def test_security_vuln_check(hass): """Test that we report security vulnerabilities.""" - - entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry = MockConfigEntry( + domain=hue.DOMAIN, data={"host": "0.0.0.0", "api_version": 1} + ) entry.add_to_hass(hass) - config = Mock(bridgeid="", mac="", modelid="BSB002", swversion="1935144020") + config = Mock( + bridge_id="", mac_address="", model_id="BSB002", software_version="1935144020" + ) config.name = "Hue" with patch.object( @@ -120,7 +130,9 @@ async def test_security_vuln_check(hass): "HueBridge", Mock( return_value=Mock( - async_setup=AsyncMock(return_value=True), api=Mock(config=config) + async_initialize_bridge=AsyncMock(return_value=True), + api=Mock(config=config), + api_version=1, ) ), ): diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py deleted file mode 100644 index 1e3df824a38..00000000000 --- a/tests/components/hue/test_init_multiple_bridges.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Test Hue init with multiple bridges.""" -from unittest.mock import patch - -import pytest - -from homeassistant.components import hue -from homeassistant.setup import async_setup_component - -from .conftest import create_mock_bridge - -from tests.common import MockConfigEntry - - -async def setup_component(hass): - """Hue component.""" - with patch.object(hue, "async_setup_entry", return_value=True): - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - {}, - ) - is True - ) - - -async def test_hue_activate_scene_both_responds( - hass, mock_bridge1, mock_bridge2, mock_config_entry1, mock_config_entry2 -): - """Test that makes both bridges successfully activate a scene.""" - - await setup_component(hass) - - await setup_bridge(hass, mock_bridge1, mock_config_entry1) - await setup_bridge(hass, mock_bridge2, mock_config_entry2) - - with patch.object( - mock_bridge1, "hue_activate_scene", return_value=None - ) as mock_hue_activate_scene1, patch.object( - mock_bridge2, "hue_activate_scene", return_value=None - ) as mock_hue_activate_scene2: - await hass.services.async_call( - "hue", - "hue_activate_scene", - {"group_name": "group_2", "scene_name": "my_scene"}, - blocking=True, - ) - - mock_hue_activate_scene1.assert_called_once() - mock_hue_activate_scene2.assert_called_once() - - -async def test_hue_activate_scene_one_responds( - hass, mock_bridge1, mock_bridge2, mock_config_entry1, mock_config_entry2 -): - """Test that makes only one bridge successfully activate a scene.""" - - await setup_component(hass) - - await setup_bridge(hass, mock_bridge1, mock_config_entry1) - await setup_bridge(hass, mock_bridge2, mock_config_entry2) - - with patch.object( - mock_bridge1, "hue_activate_scene", return_value=None - ) as mock_hue_activate_scene1, patch.object( - mock_bridge2, "hue_activate_scene", return_value=False - ) as mock_hue_activate_scene2: - await hass.services.async_call( - "hue", - "hue_activate_scene", - {"group_name": "group_2", "scene_name": "my_scene"}, - blocking=True, - ) - - mock_hue_activate_scene1.assert_called_once() - mock_hue_activate_scene2.assert_called_once() - - -async def test_hue_activate_scene_zero_responds( - hass, mock_bridge1, mock_bridge2, mock_config_entry1, mock_config_entry2 -): - """Test that makes no bridge successfully activate a scene.""" - - await setup_component(hass) - - await setup_bridge(hass, mock_bridge1, mock_config_entry1) - await setup_bridge(hass, mock_bridge2, mock_config_entry2) - - with patch.object( - mock_bridge1, "hue_activate_scene", return_value=False - ) as mock_hue_activate_scene1, patch.object( - mock_bridge2, "hue_activate_scene", return_value=False - ) as mock_hue_activate_scene2: - await hass.services.async_call( - "hue", - "hue_activate_scene", - {"group_name": "group_2", "scene_name": "my_scene"}, - blocking=True, - ) - - # both were retried - assert mock_hue_activate_scene1.call_count == 2 - assert mock_hue_activate_scene2.call_count == 2 - - -async def setup_bridge(hass, mock_bridge, config_entry): - """Load the Hue light platform with the provided bridge.""" - mock_bridge.config_entry = config_entry - config_entry.add_to_hass(hass) - with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): - await hass.config_entries.async_setup(config_entry.entry_id) - - -@pytest.fixture -def mock_config_entry1(hass): - """Mock a config entry.""" - return create_config_entry() - - -@pytest.fixture -def mock_config_entry2(hass): - """Mock a config entry.""" - return create_config_entry() - - -def create_config_entry(): - """Mock a config entry.""" - return MockConfigEntry( - domain=hue.DOMAIN, - data={"host": "mock-host"}, - ) - - -@pytest.fixture -def mock_bridge1(hass): - """Mock a Hue bridge.""" - return create_mock_bridge(hass) - - -@pytest.fixture -def mock_bridge2(hass): - """Mock a Hue bridge.""" - return create_mock_bridge(hass) diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light_v1.py similarity index 77% rename from tests/components/hue/test_light.py rename to tests/components/hue/test_light_v1.py index 6025b725c60..8c82a544ede 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light_v1.py @@ -4,12 +4,14 @@ from unittest.mock import Mock import aiohue -from homeassistant import config_entries from homeassistant.components import hue -from homeassistant.components.hue import light as hue_light +from homeassistant.components.hue.const import CONF_ALLOW_HUE_GROUPS +from homeassistant.components.hue.v1 import light as hue_light from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import color +from .conftest import create_config_entry + HUE_LIGHT_NS = "homeassistant.components.light.hue." GROUP_RESPONSE = { "1": { @@ -170,50 +172,43 @@ LIGHT_GAMUT = color.GamutType( LIGHT_GAMUT_TYPE = "A" -async def setup_bridge(hass, mock_bridge): +async def setup_bridge(hass, mock_bridge_v1): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host"}, - "test", - ) - mock_bridge.config_entry = config_entry - hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} + config_entry = create_config_entry() + config_entry.options = {CONF_ALLOW_HUE_GROUPS: True} + mock_bridge_v1.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group await hass.async_block_till_done() -async def test_not_load_groups_if_old_bridge(hass, mock_bridge): - """Test that we don't try to load gorups if bridge runs old software.""" - mock_bridge.api.config.apiversion = "1.12.0" - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 +async def test_not_load_groups_if_old_bridge(hass, mock_bridge_v1): + """Test that we don't try to load groups if bridge runs old software.""" + mock_bridge_v1.api.config.apiversion = "1.12.0" + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 0 -async def test_no_lights_or_groups(hass, mock_bridge): +async def test_no_lights_or_groups(hass, mock_bridge_v1): """Test the update_lights function when no lights are found.""" - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append({}) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append({}) + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 0 -async def test_lights(hass, mock_bridge): +async def test_lights(hass, mock_bridge_v1): """Test the update_lights function with some lights.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 # 2 lights assert len(hass.states.async_all()) == 2 @@ -228,12 +223,12 @@ async def test_lights(hass, mock_bridge): assert lamp_2.state == "off" -async def test_lights_color_mode(hass, mock_bridge): +async def test_lights_color_mode(hass, mock_bridge_v1): """Test that lights only report appropriate color mode.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) + await setup_bridge(hass, mock_bridge_v1) lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None @@ -245,15 +240,15 @@ async def test_lights_color_mode(hass, mock_bridge): new_light1_on = LIGHT_1_ON.copy() new_light1_on["state"] = new_light1_on["state"].copy() new_light1_on["state"]["colormode"] = "ct" - mock_bridge.mock_light_responses.append({"1": new_light1_on}) - mock_bridge.mock_group_responses.append({}) + mock_bridge_v1.mock_light_responses.append({"1": new_light1_on}) + mock_bridge_v1.mock_group_responses.append({}) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True ) # 2x light update, 1 group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None @@ -263,18 +258,13 @@ async def test_lights_color_mode(hass, mock_bridge): assert "hs_color" in lamp_1.attributes -async def test_groups(hass, mock_bridge): +async def test_groups(hass, mock_bridge_v1): """Test the update_lights function with some lights.""" - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) - mock_bridge.api.groups._v2_resources = [ - {"id_v1": "/groups/1", "id": "group-1-mock-id", "type": "room"}, - {"id_v1": "/groups/2", "id": "group-2-mock-id", "type": "room"}, - ] + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 # 2 hue group lights assert len(hass.states.async_all()) == 2 @@ -289,18 +279,18 @@ async def test_groups(hass, mock_bridge): assert lamp_2.state == "on" ent_reg = er.async_get(hass) - assert ent_reg.async_get("light.group_1").unique_id == "group-1-mock-id" - assert ent_reg.async_get("light.group_2").unique_id == "group-2-mock-id" + assert ent_reg.async_get("light.group_1").unique_id == "1" + assert ent_reg.async_get("light.group_2").unique_id == "2" -async def test_new_group_discovered(hass, mock_bridge): +async def test_new_group_discovered(hass, mock_bridge_v1): """Test if 2nd update has a new group.""" - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.allow_groups = True + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 new_group_response = dict(GROUP_RESPONSE) @@ -322,15 +312,15 @@ async def test_new_group_discovered(hass, mock_bridge): "state": {"any_on": True, "all_on": False}, } - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(new_group_response) + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(new_group_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) # 2x group update, 1x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 3 new_group = hass.states.get("light.group_3") @@ -340,13 +330,12 @@ async def test_new_group_discovered(hass, mock_bridge): assert new_group.attributes["color_temp"] == 250 -async def test_new_light_discovered(hass, mock_bridge): +async def test_new_light_discovered(hass, mock_bridge_v1): """Test if 2nd update has a new light.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 new_light_response = dict(LIGHT_RESPONSE) @@ -372,14 +361,14 @@ async def test_new_light_discovered(hass, mock_bridge): "uniqueid": "789", } - mock_bridge.mock_light_responses.append(new_light_response) + mock_bridge_v1.mock_light_responses.append(new_light_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 3 light = hass.states.get("light.hue_lamp_3") @@ -387,18 +376,18 @@ async def test_new_light_discovered(hass, mock_bridge): assert light.state == "off" -async def test_group_removed(hass, mock_bridge): +async def test_group_removed(hass, mock_bridge_v1): """Test if 2nd update has removed group.""" - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.allow_groups = True + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append({"1": GROUP_RESPONSE["1"]}) + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append({"1": GROUP_RESPONSE["1"]}) # Calling a service will trigger the updates to run await hass.services.async_call( @@ -406,7 +395,7 @@ async def test_group_removed(hass, mock_bridge): ) # 2x group update, 1x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 1 group = hass.states.get("light.group_1") @@ -416,17 +405,16 @@ async def test_group_removed(hass, mock_bridge): assert removed_group is None -async def test_light_removed(hass, mock_bridge): +async def test_light_removed(hass, mock_bridge_v1): """Test if 2nd update has removed light.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 - mock_bridge.mock_light_responses.clear() - mock_bridge.mock_light_responses.append({"1": LIGHT_RESPONSE.get("1")}) + mock_bridge_v1.mock_light_responses.clear() + mock_bridge_v1.mock_light_responses.append({"1": LIGHT_RESPONSE.get("1")}) # Calling a service will trigger the updates to run await hass.services.async_call( @@ -434,7 +422,7 @@ async def test_light_removed(hass, mock_bridge): ) # 2x light update, 1 group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 1 light = hass.states.get("light.hue_lamp_1") @@ -444,14 +432,14 @@ async def test_light_removed(hass, mock_bridge): assert removed_light is None -async def test_other_group_update(hass, mock_bridge): +async def test_other_group_update(hass, mock_bridge_v1): """Test changing one group that will impact the state of other light.""" - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.allow_groups = True + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(GROUP_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") @@ -480,15 +468,15 @@ async def test_other_group_update(hass, mock_bridge): "state": {"any_on": False, "all_on": False}, } - mock_bridge.mock_light_responses.append({}) - mock_bridge.mock_group_responses.append(updated_group_response) + mock_bridge_v1.mock_light_responses.append({}) + mock_bridge_v1.mock_group_responses.append(updated_group_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) # 2x group update, 1x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") @@ -497,13 +485,12 @@ async def test_other_group_update(hass, mock_bridge): assert group_2.state == "off" -async def test_other_light_update(hass, mock_bridge): +async def test_other_light_update(hass, mock_bridge_v1): """Test changing one light that will impact state of other light.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") @@ -534,14 +521,14 @@ async def test_other_light_update(hass, mock_bridge): "uniqueid": "123", } - mock_bridge.mock_light_responses.append(updated_light_response) + mock_bridge_v1.mock_light_responses.append(updated_light_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") @@ -551,30 +538,29 @@ async def test_other_light_update(hass, mock_bridge): assert lamp_2.attributes["brightness"] == 100 -async def test_update_timeout(hass, mock_bridge): +async def test_update_timeout(hass, mock_bridge_v1): """Test bridge marked as not available if timeout error during update.""" - mock_bridge.api.lights.update = Mock(side_effect=asyncio.TimeoutError) - mock_bridge.api.groups.update = Mock(side_effect=asyncio.TimeoutError) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 0 + mock_bridge_v1.api.lights.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge_v1.api.groups.update = Mock(side_effect=asyncio.TimeoutError) + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 -async def test_update_unauthorized(hass, mock_bridge): +async def test_update_unauthorized(hass, mock_bridge_v1): """Test bridge marked as not authorized if unauthorized during update.""" - mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 0 + mock_bridge_v1.api.lights.update = Mock(side_effect=aiohue.Unauthorized) + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 + assert len(mock_bridge_v1.handle_unauthorized_error.mock_calls) == 1 -async def test_light_turn_on_service(hass, mock_bridge): +async def test_light_turn_on_service(hass, mock_bridge_v1): """Test calling the turn on service on a light.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) + await setup_bridge(hass, mock_bridge_v1) light = hass.states.get("light.hue_lamp_2") assert light is not None assert light.state == "off" @@ -582,7 +568,7 @@ async def test_light_turn_on_service(hass, mock_bridge): updated_light_response = dict(LIGHT_RESPONSE) updated_light_response["2"] = LIGHT_2_ON - mock_bridge.mock_light_responses.append(updated_light_response) + mock_bridge_v1.mock_light_responses.append(updated_light_response) await hass.services.async_call( "light", @@ -590,10 +576,10 @@ async def test_light_turn_on_service(hass, mock_bridge): {"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300}, blocking=True, ) - # 2x light update, 1 group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + # 2x light update, 1x group update, 1 turn on request + assert len(mock_bridge_v1.mock_requests) == 4 - assert mock_bridge.mock_requests[2]["json"] == { + assert mock_bridge_v1.mock_requests[2]["json"] == { "bri": 100, "on": True, "ct": 300, @@ -614,21 +600,20 @@ async def test_light_turn_on_service(hass, mock_bridge): blocking=True, ) - assert len(mock_bridge.mock_requests) == 6 + assert len(mock_bridge_v1.mock_requests) == 5 - assert mock_bridge.mock_requests[4]["json"] == { + assert mock_bridge_v1.mock_requests[4]["json"] == { "on": True, "xy": (0.138, 0.08), "alert": "none", } -async def test_light_turn_off_service(hass, mock_bridge): +async def test_light_turn_off_service(hass, mock_bridge_v1): """Test calling the turn on service on a light.""" - mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) - mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge_v1.mock_light_responses.append(LIGHT_RESPONSE) - await setup_bridge(hass, mock_bridge) + await setup_bridge(hass, mock_bridge_v1) light = hass.states.get("light.hue_lamp_1") assert light is not None assert light.state == "on" @@ -636,16 +621,16 @@ async def test_light_turn_off_service(hass, mock_bridge): updated_light_response = dict(LIGHT_RESPONSE) updated_light_response["1"] = LIGHT_1_OFF - mock_bridge.mock_light_responses.append(updated_light_response) + mock_bridge_v1.mock_light_responses.append(updated_light_response) await hass.services.async_call( "light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 for group update, 1 turn on request - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 - assert mock_bridge.mock_requests[2]["json"] == {"on": False, "alert": "none"} + assert mock_bridge_v1.mock_requests[2]["json"] == {"on": False, "alert": "none"} assert len(hass.states.async_all()) == 2 @@ -663,8 +648,8 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), + bridge=Mock(config_entry=Mock(options={"allow_unreachable": False})), coordinator=Mock(last_update_success=True), - bridge=Mock(allow_unreachable=False), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, @@ -680,10 +665,10 @@ def test_available(): colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), - bridge=Mock(allow_unreachable=True), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, + bridge=Mock(config_entry=Mock(options={"allow_unreachable": True})), ) assert light.available is True @@ -696,10 +681,10 @@ def test_available(): colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), - bridge=Mock(allow_unreachable=False), is_group=True, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, + bridge=Mock(config_entry=Mock(options={"allow_unreachable": False})), ) assert light.available is True @@ -756,9 +741,8 @@ def test_hs_color(): assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT) -async def test_group_features(hass, mock_bridge): +async def test_group_features(hass, mock_bridge_v1): """Test group features.""" - color_temp_type = "Color temperature light" extended_color_type = "Extended color light" @@ -920,11 +904,10 @@ async def test_group_features(hass, mock_bridge): "4": light_4, } - mock_bridge.allow_groups = True - mock_bridge.mock_light_responses.append(light_response) - mock_bridge.mock_group_responses.append(group_response) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 2 + mock_bridge_v1.mock_light_responses.append(light_response) + mock_bridge_v1.mock_group_responses.append(group_response) + await setup_bridge(hass, mock_bridge_v1) + assert len(mock_bridge_v1.mock_requests) == 2 color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"] extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"] diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py new file mode 100644 index 00000000000..0a06a87f7f2 --- /dev/null +++ b/tests/components/hue/test_light_v2.py @@ -0,0 +1,353 @@ +"""Philips Hue lights platform tests for V2 bridge/api.""" + +from homeassistant.components.light import COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_platform +from .const import FAKE_DEVICE, FAKE_LIGHT, FAKE_ZIGBEE_CONNECTIVITY + + +async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): + """Test if all v2 lights get created with correct features.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "light") + # there shouldn't have been any requests at this point + assert len(mock_bridge_v2.mock_requests) == 0 + # 6 entities should be created from test data (grouped_lights are disabled by default) + assert len(hass.states.async_all()) == 6 + + # test light which supports color and color temperature + light_1 = hass.states.get("light.hue_light_with_color_and_color_temperature_1") + assert light_1 is not None + assert ( + light_1.attributes["friendly_name"] + == "Hue light with color and color temperature 1" + ) + assert light_1.state == "on" + assert light_1.attributes["brightness"] == int(46.85 / 100 * 255) + assert light_1.attributes["mode"] == "normal" + assert light_1.attributes["color_mode"] == COLOR_MODE_XY + assert set(light_1.attributes["supported_color_modes"]) == { + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_XY, + } + assert light_1.attributes["xy_color"] == (0.5614, 0.4058) + assert light_1.attributes["min_mireds"] == 153 + assert light_1.attributes["max_mireds"] == 500 + assert light_1.attributes["dynamics"] == "dynamic_palette" + + # test light which supports color temperature only + light_2 = hass.states.get("light.hue_light_with_color_temperature_only") + assert light_2 is not None + assert ( + light_2.attributes["friendly_name"] == "Hue light with color temperature only" + ) + assert light_2.state == "off" + assert light_2.attributes["mode"] == "normal" + assert light_2.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] + assert light_2.attributes["min_mireds"] == 153 + assert light_2.attributes["max_mireds"] == 454 + assert light_2.attributes["dynamics"] == "none" + + # test light which supports color only + light_3 = hass.states.get("light.hue_light_with_color_only") + assert light_3 is not None + assert light_3.attributes["friendly_name"] == "Hue light with color only" + assert light_3.state == "on" + assert light_3.attributes["brightness"] == 128 + assert light_3.attributes["mode"] == "normal" + assert light_3.attributes["supported_color_modes"] == [COLOR_MODE_XY] + assert light_3.attributes["color_mode"] == COLOR_MODE_XY + assert light_3.attributes["dynamics"] == "dynamic_palette" + + # test light which supports on/off only + light_4 = hass.states.get("light.hue_on_off_light") + assert light_4 is not None + assert light_4.attributes["friendly_name"] == "Hue on/off light" + assert light_4.state == "off" + assert light_4.attributes["mode"] == "normal" + assert light_4.attributes["supported_color_modes"] == [] + + +async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data): + """Test calling the turn on service on a light.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "light") + + test_light_id = "light.hue_light_with_color_temperature_only" + + # verify the light is off before we start + assert hass.states.get(test_light_id).state == "off" + + # now call the HA turn_on service + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "brightness_pct": 100, "color_temp": 300}, + blocking=True, + ) + + # PUT request should have been sent to device with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[0]["json"]["dimming"]["brightness"] == 100 + assert mock_bridge_v2.mock_requests[0]["json"]["color_temperature"]["mirek"] == 300 + + # Now generate update event by emitting the json we've sent as incoming event + mock_bridge_v2.mock_requests[0]["json"]["color_temperature"].pop("mirek_valid") + mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + await hass.async_block_till_done() + + # the light should now be on + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["mode"] == "normal" + assert test_light.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] + assert test_light.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP + assert test_light.attributes["brightness"] == 255 + + # test again with sending transition + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "brightness_pct": 50, "transition": 6}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 2 + assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + + +async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): + """Test calling the turn off service on a light.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "light") + + test_light_id = "light.hue_light_with_color_and_color_temperature_1" + + # verify the light is on before we start + assert hass.states.get(test_light_id).state == "on" + + # now call the HA turn_off service + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id}, + blocking=True, + ) + + # PUT request should have been sent to device with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False + + # Now generate update event by emitting the json we've sent as incoming event + mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + await hass.async_block_till_done() + + # the light should now be off + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "off" + + # test again with sending transition + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id, "transition": 6}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 2 + assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + + +async def test_light_added(hass, mock_bridge_v2): + """Test new light added to bridge.""" + await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) + + await setup_platform(hass, mock_bridge_v2, "light") + + test_entity_id = "light.hue_mocked_device" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake entity (and attached device and zigbee_connectivity) by emitting events + mock_bridge_v2.api.emit_event("add", FAKE_LIGHT) + await hass.async_block_till_done() + + # the entity should now be available + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "off" + assert test_entity.attributes["friendly_name"] == FAKE_DEVICE["metadata"]["name"] + + +async def test_light_availability(hass, mock_bridge_v2, v2_resources_test_data): + """Test light availability property.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "light") + + test_light_id = "light.hue_light_with_color_and_color_temperature_1" + + # verify entity does exist and is available before we start + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + + # Change availability by modififying the zigbee_connectivity status + for status in ("connectivity_issue", "disconnected", "connected"): + mock_bridge_v2.api.emit_event( + "update", + { + "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "status": status, + "type": "zigbee_connectivity", + }, + ) + mock_bridge_v2.api.emit_event( + "update", + { + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "type": "light", + }, + ) + await hass.async_block_till_done() + + # the entity should now be available only when zigbee is connected + test_light = hass.states.get(test_light_id) + assert test_light.state == "on" if status == "connected" else "unavailable" + + +async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): + """Test if all v2 grouped lights get created with correct features.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "light") + + # test if entities for hue groups are created and disabled by default + for entity_id in ("light.test_zone", "light.test_room"): + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(entity_id) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + # enable the entity + updated_entry = ent_reg.async_update_entity( + entity_entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload platform and check if entities are correctly there + await hass.config_entries.async_forward_entry_unload( + mock_bridge_v2.config_entry, "light" + ) + await hass.config_entries.async_forward_entry_setup( + mock_bridge_v2.config_entry, "light" + ) + await hass.async_block_till_done() + + # test light created for hue zone + test_entity = hass.states.get("light.test_zone") + assert test_entity is not None + assert test_entity.attributes["friendly_name"] == "Test Zone" + assert test_entity.state == "on" + assert test_entity.attributes["brightness"] == 119 + assert test_entity.attributes["color_mode"] == COLOR_MODE_XY + assert set(test_entity.attributes["supported_color_modes"]) == { + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_XY, + } + assert test_entity.attributes["min_mireds"] == 153 + assert test_entity.attributes["max_mireds"] == 500 + assert test_entity.attributes["is_hue_group"] is True + assert test_entity.attributes["hue_scenes"] == {"Dynamic Test Scene"} + assert test_entity.attributes["hue_type"] == "zone" + assert test_entity.attributes["lights"] == { + "Hue light with color and color temperature 1", + "Hue light with color and color temperature gradient", + "Hue light with color and color temperature 2", + } + + # test light created for hue room + test_entity = hass.states.get("light.test_room") + assert test_entity is not None + assert test_entity.attributes["friendly_name"] == "Test Room" + assert test_entity.state == "off" + assert test_entity.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] + assert test_entity.attributes["min_mireds"] == 153 + assert test_entity.attributes["max_mireds"] == 454 + assert test_entity.attributes["is_hue_group"] is True + assert test_entity.attributes["hue_scenes"] == {"Regular Test Scene"} + assert test_entity.attributes["hue_type"] == "room" + assert test_entity.attributes["lights"] == { + "Hue on/off light", + "Hue light with color temperature only", + } + + # Test calling the turn on service on a grouped light + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)}, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is True + assert ( + mock_bridge_v2.mock_requests[index]["json"]["dimming"]["brightness"] == 100 + ) + assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 + assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 + + # Now generate update events by emitting the json we've sent as incoming events + for index in range(0, 3): + mock_bridge_v2.api.emit_event( + "update", mock_bridge_v2.mock_requests[index]["json"] + ) + await hass.async_block_till_done() + + # the light should now be on and have the properties we've set + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["color_mode"] == COLOR_MODE_XY + assert test_light.attributes["brightness"] == 255 + assert test_light.attributes["xy_color"] == (0.123, 0.123) + + # Test calling the turn off service on a grouped light. + mock_bridge_v2.mock_requests.clear() + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id}, + blocking=True, + ) + + # PUT request should have been sent to ONLY the grouped_light resource with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False + + # Now generate update event by emitting the json we've sent as incoming event + mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + await hass.async_block_till_done() + + # the light should now be off + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "off" diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py new file mode 100644 index 00000000000..382a075bbbd --- /dev/null +++ b/tests/components/hue/test_migration.py @@ -0,0 +1,179 @@ +"""Test Hue migration logic.""" +from unittest.mock import patch + +from homeassistant.components import hue +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_migrate_api_key(hass): + """Test if username gets migrated to api_key.""" + config_entry = MockConfigEntry( + domain=hue.DOMAIN, + data={"host": "0.0.0.0", "api_version": 2, "username": "abcdefgh"}, + ) + await hue.migration.check_migration(hass, config_entry) + # the username property should have been migrated to api_key + assert config_entry.data == { + "host": "0.0.0.0", + "api_version": 2, + "api_key": "abcdefgh", + } + + +async def test_auto_switchover(hass): + """Test if config entry from v1 automatically switches to v2.""" + config_entry = MockConfigEntry( + domain=hue.DOMAIN, + data={"host": "0.0.0.0", "api_version": 1, "username": "abcdefgh"}, + ) + + with patch.object(hue.migration, "is_v2_bridge", retun_value=True), patch.object( + hue.migration, "handle_v2_migration" + ) as mock_mig: + await hue.migration.check_migration(hass, config_entry) + assert len(mock_mig.mock_calls) == 1 + # the api version should now be version 2 + assert config_entry.data == { + "host": "0.0.0.0", + "api_version": 2, + "api_key": "abcdefgh", + } + + +async def test_light_entity_migration( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for lights migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + # create device/entity with V1 schema in registry + device = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65")}, + ) + ent_reg.async_get_or_create( + "light", + hue.DOMAIN, + "00:17:88:01:09:aa:bb:65", + suggested_object_id="migrated_light_1", + device_id=device.id, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # migrated device should have new identifier (guid) and old style (mac) + migrated_device = dev_reg.async_get(device.id) + assert migrated_device is not None + assert migrated_device.identifiers == { + (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50"), + (hue.DOMAIN, "00:17:88:01:09:aa:bb:65"), + } + # the entity should have the new identifier (guid) + migrated_entity = ent_reg.async_get("light.migrated_light_1") + assert migrated_entity is not None + assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1" + + +async def test_sensor_entity_migration( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for sensors migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + # create device with V1 schema in registry for Hue motion sensor + device_mac = "00:17:aa:bb:cc:09:ac:c3" + device = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(hue.DOMAIN, device_mac)} + ) + + # mapping of device_class to new id + sensor_mappings = { + ("temperature", "sensor", "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b"), + ("illuminance", "sensor", "d504e7a4-9a18-4854-90fd-c5b6ac102c40"), + ("battery", "sensor", "669f609d-4860-4f1c-bc25-7a9cec1c3b6c"), + ("motion", "binary_sensor", "b6896534-016d-4052-8cb4-ef04454df62c"), + } + + # create entities with V1 schema in registry for Hue motion sensor + for dev_class, platform, new_id in sensor_mappings: + ent_reg.async_get_or_create( + platform, + hue.DOMAIN, + f"{device_mac}-{dev_class}", + suggested_object_id=f"hue_migrated_{dev_class}_sensor", + device_id=device.id, + device_class=dev_class, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # migrated device should have new identifier (guid) and old style (mac) + migrated_device = dev_reg.async_get(device.id) + assert migrated_device is not None + assert migrated_device.identifiers == { + (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6"), + (hue.DOMAIN, device_mac), + } + # the entities should have the correct V2 identifier (guid) + for dev_class, platform, new_id in sensor_mappings: + migrated_entity = ent_reg.async_get( + f"{platform}.hue_migrated_{dev_class}_sensor" + ) + assert migrated_entity is not None + assert migrated_entity.unique_id == new_id + + +async def test_group_entity_migration( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for grouped_lights migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + + # create (deviceless) entity with V1 schema in registry + ent_reg.async_get_or_create( + "light", + hue.DOMAIN, + "3", + suggested_object_id="hue_migrated_grouped_light", + config_entry=config_entry, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # the entity should have the new identifier (guid) + migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") + assert migrated_entity is not None + assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py new file mode 100644 index 00000000000..82595d3845d --- /dev/null +++ b/tests/components/hue/test_scene.py @@ -0,0 +1,114 @@ +"""Philips Hue scene platform tests for V2 bridge/api.""" + + +from .conftest import setup_platform +from .const import FAKE_SCENE + + +async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): + """Test if (config) scenes get created.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "scene") + # there shouldn't have been any requests at this point + assert len(mock_bridge_v2.mock_requests) == 0 + # 2 entities should be created from test data + assert len(hass.states.async_all()) == 2 + + # test (dynamic) scene for a hue zone + test_entity = hass.states.get("scene.test_zone_dynamic_test_scene") + assert test_entity is not None + assert test_entity.name == "Test Zone - Dynamic Test Scene" + assert test_entity.state == "scening" + assert test_entity.attributes["group_name"] == "Test Zone" + assert test_entity.attributes["group_type"] == "zone" + assert test_entity.attributes["name"] == "Dynamic Test Scene" + assert test_entity.attributes["speed"] == 0.6269841194152832 + assert test_entity.attributes["brightness"] == 46.85 + assert test_entity.attributes["is_dynamic"] is True + + # test (regular) scene for a hue room + test_entity = hass.states.get("scene.test_room_regular_test_scene") + assert test_entity is not None + assert test_entity.name == "Test Room - Regular Test Scene" + assert test_entity.state == "scening" + assert test_entity.attributes["group_name"] == "Test Room" + assert test_entity.attributes["group_type"] == "room" + assert test_entity.attributes["name"] == "Regular Test Scene" + assert test_entity.attributes["speed"] == 0.5 + assert test_entity.attributes["brightness"] == 100.0 + assert test_entity.attributes["is_dynamic"] is False + + +async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data): + """Test calling the turn on service on a scene.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "scene") + + test_entity_id = "scene.test_room_regular_test_scene" + + # call the HA turn_on service + await hass.services.async_call( + "scene", + "turn_on", + {"entity_id": test_entity_id}, + blocking=True, + ) + + # PUT request should have been sent to device with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["recall"] == {"action": "active"} + + # test again with sending transition + await hass.services.async_call( + "scene", + "turn_on", + {"entity_id": test_entity_id, "transition": 6}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 2 + assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == { + "action": "active", + "duration": 600, + } + + +async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data): + """Test scene events from bridge.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "scene") + + test_entity_id = "scene.test_room_mocked_scene" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake scene + mock_bridge_v2.api.emit_event("add", FAKE_SCENE) + await hass.async_block_till_done() + + # the entity should now be available + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "scening" + assert test_entity.name == "Test Room - Mocked Scene" + assert test_entity.attributes["brightness"] == 65.0 + + # test update + updated_resource = {**FAKE_SCENE} + updated_resource["actions"][0]["action"]["dimming"]["brightness"] = 35.0 + mock_bridge_v2.api.emit_event("update", updated_resource) + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.attributes["brightness"] == 35.0 + + # test delete + mock_bridge_v2.api.emit_event("delete", updated_resource) + await hass.async_block_till_done() + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity is None diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_v1.py similarity index 81% rename from tests/components/hue/test_sensor_base.py rename to tests/components/hue/test_sensor_v1.py index 8b8eb45a222..c35403eaac1 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_v1.py @@ -3,29 +3,17 @@ import asyncio from unittest.mock import Mock import aiohue -import pytest from homeassistant.components import hue -from homeassistant.components.hue import sensor_base -from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from homeassistant.components.hue.const import ATTR_HUE_EVENT +from homeassistant.components.hue.v1 import sensor_base from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util -from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge - -from tests.common import ( - async_capture_events, - async_fire_time_changed, - mock_device_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) +from .conftest import create_mock_bridge, setup_platform +from tests.common import async_capture_events, async_fire_time_changed PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, @@ -293,18 +281,17 @@ SENSOR_RESPONSE = { } -async def test_no_sensors(hass, mock_bridge): +async def test_no_sensors(hass, mock_bridge_v1): """Test the update_items function when no sensors are found.""" - mock_bridge.allow_groups = True - mock_bridge.mock_sensor_responses.append({}) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + mock_bridge_v1.mock_sensor_responses.append({}) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 0 -async def test_sensors_with_multiple_bridges(hass, mock_bridge): +async def test_sensors_with_multiple_bridges(hass, mock_bridge_v1): """Test the update_items function with some sensors.""" - mock_bridge_2 = create_mock_bridge(hass) + mock_bridge_2 = create_mock_bridge(hass, api_version=1) mock_bridge_2.mock_sensor_responses.append( { "1": PRESENCE_SENSOR_3_PRESENT, @@ -312,21 +299,23 @@ async def test_sensors_with_multiple_bridges(hass, mock_bridge): "3": TEMPERATURE_SENSOR_3, } ) - mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) - await setup_bridge(hass, mock_bridge) - await setup_bridge(hass, mock_bridge_2, hostname="mock-bridge-2") + mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + await setup_platform( + hass, mock_bridge_2, ["binary_sensor", "sensor"], "mock-bridge-2" + ) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge_v1.mock_requests) == 1 assert len(mock_bridge_2.mock_requests) == 1 # 3 "physical" sensors with 3 virtual sensors each + 1 battery sensor assert len(hass.states.async_all()) == 10 -async def test_sensors(hass, mock_bridge): +async def test_sensors(hass, mock_bridge_v1): """Test the update_items function with some sensors.""" - mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 # 2 "physical" sensors with 3 virtual sensors each assert len(hass.states.async_all()) == 7 @@ -366,23 +355,23 @@ async def test_sensors(hass, mock_bridge): ) -async def test_unsupported_sensors(hass, mock_bridge): +async def test_unsupported_sensors(hass, mock_bridge_v1): """Test that unsupported sensors don't get added and don't fail.""" response_with_unsupported = dict(SENSOR_RESPONSE) response_with_unsupported["7"] = UNSUPPORTED_SENSOR - mock_bridge.mock_sensor_responses.append(response_with_unsupported) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + mock_bridge_v1.mock_sensor_responses.append(response_with_unsupported) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 # 2 "physical" sensors with 3 virtual sensors each + 1 battery sensor assert len(hass.states.async_all()) == 7 -async def test_new_sensor_discovered(hass, mock_bridge): +async def test_new_sensor_discovered(hass, mock_bridge_v1): """Test if 2nd update has a new sensor.""" - mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 7 new_sensor_response = dict(SENSOR_RESPONSE) @@ -394,13 +383,13 @@ async def test_new_sensor_discovered(hass, mock_bridge): } ) - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + await mock_bridge_v1.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 2 + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 10 presence = hass.states.get("binary_sensor.bedroom_sensor_motion") @@ -411,25 +400,25 @@ async def test_new_sensor_discovered(hass, mock_bridge): assert temperature.state == "17.75" -async def test_sensor_removed(hass, mock_bridge): +async def test_sensor_removed(hass, mock_bridge_v1): """Test if 2nd update has removed sensor.""" - mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 7 - mock_bridge.mock_sensor_responses.clear() + mock_bridge_v1.mock_sensor_responses.clear() keys = ("1", "2", "3") - mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) + mock_bridge_v1.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + await mock_bridge_v1.sensor_manager.coordinator.async_refresh() # To flush out the service call to update the group await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 2 + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 3 sensor = hass.states.get("binary_sensor.living_room_sensor_motion") @@ -439,31 +428,31 @@ async def test_sensor_removed(hass, mock_bridge): assert removed_sensor is None -async def test_update_timeout(hass, mock_bridge): +async def test_update_timeout(hass, mock_bridge_v1): """Test bridge marked as not available if timeout error during update.""" - mock_bridge.api.sensors.update = Mock(side_effect=asyncio.TimeoutError) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 0 + mock_bridge_v1.api.sensors.update = Mock(side_effect=asyncio.TimeoutError) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 -async def test_update_unauthorized(hass, mock_bridge): +async def test_update_unauthorized(hass, mock_bridge_v1): """Test bridge marked as not authorized if unauthorized during update.""" - mock_bridge.api.sensors.update = Mock(side_effect=aiohue.Unauthorized) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 0 + mock_bridge_v1.api.sensors.update = Mock(side_effect=aiohue.Unauthorized) + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 + assert len(mock_bridge_v1.handle_unauthorized_error.mock_calls) == 1 -async def test_hue_events(hass, mock_bridge, device_reg): +async def test_hue_events(hass, mock_bridge_v1, device_reg): """Test that hue remotes fire events when pressed.""" - mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) - events = async_capture_events(hass, CONF_HUE_EVENT) + events = async_capture_events(hass, ATTR_HUE_EVENT) - await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) + assert len(mock_bridge_v1.mock_requests) == 1 assert len(hass.states.async_all()) == 7 assert len(events) == 0 @@ -471,8 +460,8 @@ async def test_hue_events(hass, mock_bridge, device_reg): {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) - mock_bridge.api.sensors["7"].last_event = {"type": "button"} - mock_bridge.api.sensors["8"].last_event = {"type": "button"} + mock_bridge_v1.api.sensors["7"].last_event = {"type": "button"} + mock_bridge_v1.api.sensors["8"].last_event = {"type": "button"} new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"] = dict(new_sensor_response["7"]) @@ -480,7 +469,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): "buttonevent": 18, "lastupdated": "2019-12-28T22:58:03", } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again async_fire_time_changed( @@ -488,7 +477,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 2 + assert len(mock_bridge_v1.mock_requests) == 2 assert len(hass.states.async_all()) == 7 assert len(events) == 1 assert events[-1].data == { @@ -509,7 +498,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): "buttonevent": 3002, "lastupdated": "2019-12-28T22:58:03", } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again async_fire_time_changed( @@ -517,7 +506,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 3 + assert len(mock_bridge_v1.mock_requests) == 3 assert len(hass.states.async_all()) == 7 assert len(events) == 2 assert events[-1].data == { @@ -535,7 +524,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): "buttonevent": 18, "lastupdated": "2019-12-28T22:58:02", } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again async_fire_time_changed( @@ -543,7 +532,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge_v1.mock_requests) == 4 assert len(hass.states.async_all()) == 7 assert len(events) == 2 @@ -580,7 +569,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): ], }, } - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again async_fire_time_changed( @@ -588,13 +577,13 @@ async def test_hue_events(hass, mock_bridge, device_reg): ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 5 + assert len(mock_bridge_v1.mock_requests) == 5 assert len(hass.states.async_all()) == 8 assert len(events) == 2 # A new press fires the event new_sensor_response["21"]["state"]["lastupdated"] = "2020-01-31T15:57:19" - mock_bridge.mock_sensor_responses.append(new_sensor_response) + mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again async_fire_time_changed( @@ -606,7 +595,7 @@ async def test_hue_events(hass, mock_bridge, device_reg): {(hue.DOMAIN, "ff:ff:00:0f:e7:fd:bc:b7")} ) - assert len(mock_bridge.mock_requests) == 6 + assert len(mock_bridge_v1.mock_requests) == 6 assert len(hass.states.async_all()) == 8 assert len(events) == 3 assert events[-1].data == { diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py new file mode 100644 index 00000000000..2668922590f --- /dev/null +++ b/tests/components/hue/test_sensor_v2.py @@ -0,0 +1,123 @@ +"""Philips Hue sensor platform tests for V2 bridge/api.""" + +from homeassistant.components import hue +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import setup_bridge, setup_platform +from .const import FAKE_DEVICE, FAKE_SENSOR, FAKE_ZIGBEE_CONNECTIVITY + + +async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): + """Test if all v2 sensors get created with correct features.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "sensor") + # there shouldn't have been any requests at this point + assert len(mock_bridge_v2.mock_requests) == 0 + # 6 entities should be created from test data + assert len(hass.states.async_all()) == 6 + + # test temperature sensor + sensor = hass.states.get("sensor.hue_motion_sensor_temperature") + assert sensor is not None + assert sensor.state == "18.1" + assert sensor.attributes["friendly_name"] == "Hue motion sensor: Temperature" + assert sensor.attributes["device_class"] == "temperature" + assert sensor.attributes["state_class"] == "measurement" + assert sensor.attributes["unit_of_measurement"] == "°C" + assert sensor.attributes["temperature_valid"] is True + + # test illuminance sensor + sensor = hass.states.get("sensor.hue_motion_sensor_illuminance") + assert sensor is not None + assert sensor.state == "63" + assert sensor.attributes["friendly_name"] == "Hue motion sensor: Illuminance" + assert sensor.attributes["device_class"] == "illuminance" + assert sensor.attributes["state_class"] == "measurement" + assert sensor.attributes["unit_of_measurement"] == "lx" + assert sensor.attributes["light_level"] == 18027 + assert sensor.attributes["light_level_valid"] is True + + # test battery sensor + sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery") + assert sensor is not None + assert sensor.state == "100" + assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls: Battery" + assert sensor.attributes["device_class"] == "battery" + assert sensor.attributes["state_class"] == "measurement" + assert sensor.attributes["unit_of_measurement"] == "%" + assert sensor.attributes["battery_state"] == "normal" + + # test disabled zigbee_connectivity sensor + entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(entity_id) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + +async def test_enable_sensor( + hass, mock_bridge_v2, v2_resources_test_data, mock_config_entry_v2 +): + """Test enabling of the by default disabled zigbee_connectivity sensor.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) + + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + await hass.async_block_till_done() + await hass.config_entries.async_forward_entry_setup(mock_config_entry_v2, "sensor") + + entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(entity_id) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + # enable the entity + updated_entry = ent_reg.async_update_entity( + entity_entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload platform and check if entity is correctly there + await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor") + await hass.config_entries.async_forward_entry_setup(mock_config_entry_v2, "sensor") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "connected" + assert state.attributes["mac_address"] == "00:17:88:01:0b:aa:bb:99" + + +async def test_sensor_add_update(hass, mock_bridge_v2): + """Test if sensors get added/updated from events.""" + await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) + await setup_platform(hass, mock_bridge_v2, "sensor") + + test_entity_id = "sensor.hue_mocked_device_temperature" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake sensor by emitting event + mock_bridge_v2.api.emit_event("add", FAKE_SENSOR) + await hass.async_block_till_done() + + # the entity should now be available + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "18.0" + + # test update of entity works on incoming event + updated_sensor = {**FAKE_SENSOR, "temperature": {"temperature": 22.5}} + mock_bridge_v2.api.emit_event("update", updated_sensor) + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "22.5" diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py new file mode 100644 index 00000000000..86557b85748 --- /dev/null +++ b/tests/components/hue/test_services.py @@ -0,0 +1,265 @@ +"""Test Hue services.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components import hue +from homeassistant.components.hue import bridge +from homeassistant.components.hue.const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, +) + +from .conftest import setup_bridge, setup_component + +GROUP_RESPONSE = { + "group_1": { + "name": "Group 1", + "lights": ["1", "2"], + "type": "LightGroup", + "action": { + "on": True, + "bri": 254, + "hue": 10000, + "sat": 254, + "effect": "none", + "xy": [0.5, 0.5], + "ct": 250, + "alert": "select", + "colormode": "ct", + }, + "state": {"any_on": True, "all_on": False}, + } +} +SCENE_RESPONSE = { + "scene_1": { + "name": "Cozy dinner", + "lights": ["1", "2"], + "owner": "ffffffffe0341b1b376a2389376a2389", + "recycle": True, + "locked": False, + "appdata": {"version": 1, "data": "myAppData"}, + "picture": "", + "lastupdated": "2015-12-03T10:09:22", + "version": 2, + } +} + + +async def test_hue_activate_scene(hass, mock_api_v1): + """Test successful hue_activate_scene.""" + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + "test", + options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, + ) + + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True + + assert hue_bridge.api is mock_api_v1 + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): + assert ( + await hue.services.hue_activate_scene_v1( + hue_bridge, "Group 1", "Cozy dinner" + ) + is True + ) + + assert len(mock_api_v1.mock_requests) == 3 + assert mock_api_v1.mock_requests[2]["json"]["scene"] == "scene_1" + assert "transitiontime" not in mock_api_v1.mock_requests[2]["json"] + assert mock_api_v1.mock_requests[2]["path"] == "groups/group_1/action" + + +async def test_hue_activate_scene_transition(hass, mock_api_v1): + """Test successful hue_activate_scene with transition.""" + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + "test", + options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, + ) + + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True + + assert hue_bridge.api is mock_api_v1 + + with patch("aiohue.HueBridgeV1", return_value=mock_api_v1): + assert ( + await hue.services.hue_activate_scene_v1( + hue_bridge, "Group 1", "Cozy dinner", 30 + ) + is True + ) + + assert len(mock_api_v1.mock_requests) == 3 + assert mock_api_v1.mock_requests[2]["json"]["scene"] == "scene_1" + assert mock_api_v1.mock_requests[2]["json"]["transitiontime"] == 30 + assert mock_api_v1.mock_requests[2]["path"] == "groups/group_1/action" + + +async def test_hue_activate_scene_group_not_found(hass, mock_api_v1): + """Test failed hue_activate_scene due to missing group.""" + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + "test", + options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, + ) + + mock_api_v1.mock_group_responses.append({}) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True + + assert hue_bridge.api is mock_api_v1 + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): + assert ( + await hue.services.hue_activate_scene_v1( + hue_bridge, "Group 1", "Cozy dinner" + ) + is False + ) + + +async def test_hue_activate_scene_scene_not_found(hass, mock_api_v1): + """Test failed hue_activate_scene due to missing scene.""" + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + "test", + options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, + ) + + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append({}) + + with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): + hue_bridge = bridge.HueBridge(hass, config_entry) + assert await hue_bridge.async_initialize_bridge() is True + + assert hue_bridge.api is mock_api_v1 + + with patch("aiohue.HueBridgeV1", return_value=mock_api_v1): + assert ( + await hue.services.hue_activate_scene_v1( + hue_bridge, "Group 1", "Cozy dinner" + ) + is False + ) + + +async def test_hue_multi_bridge_activate_scene_all_respond( + hass, mock_bridge_v1, mock_bridge_v2, mock_config_entry_v1, mock_config_entry_v2 +): + """Test that makes multiple bridges successfully activate a scene.""" + await setup_component(hass) + + mock_api_v1 = mock_bridge_v1.api + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) + await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) + + with patch.object( + hue.services, "hue_activate_scene_v2", return_value=True + ) as mock_hue_activate_scene2: + await hass.services.async_call( + "hue", + "hue_activate_scene", + {"group_name": "Group 1", "scene_name": "Cozy dinner"}, + blocking=True, + ) + + assert len(mock_api_v1.mock_requests) == 3 + assert mock_api_v1.mock_requests[2]["json"]["scene"] == "scene_1" + assert mock_api_v1.mock_requests[2]["path"] == "groups/group_1/action" + + mock_hue_activate_scene2.assert_called_once() + + +async def test_hue_multi_bridge_activate_scene_one_responds( + hass, mock_bridge_v1, mock_bridge_v2, mock_config_entry_v1, mock_config_entry_v2 +): + """Test that makes only one bridge successfully activate a scene.""" + await setup_component(hass) + + mock_api_v1 = mock_bridge_v1.api + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) + await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) + + with patch.object( + hue.services, "hue_activate_scene_v2", return_value=False + ) as mock_hue_activate_scene2: + await hass.services.async_call( + "hue", + "hue_activate_scene", + {"group_name": "Group 1", "scene_name": "Cozy dinner"}, + blocking=True, + ) + + assert len(mock_api_v1.mock_requests) == 3 + assert mock_api_v1.mock_requests[2]["json"]["scene"] == "scene_1" + assert mock_api_v1.mock_requests[2]["path"] == "groups/group_1/action" + mock_hue_activate_scene2.assert_called_once() + + +async def test_hue_multi_bridge_activate_scene_zero_responds( + hass, mock_bridge_v1, mock_bridge_v2, mock_config_entry_v1, mock_config_entry_v2 +): + """Test that makes no bridge successfully activate a scene.""" + await setup_component(hass) + mock_api_v1 = mock_bridge_v1.api + mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) + mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) + await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) + + with patch.object( + hue.services, "hue_activate_scene_v2", return_value=False + ) as mock_hue_activate_scene2: + await hass.services.async_call( + "hue", + "hue_activate_scene", + {"group_name": "Non existing group", "scene_name": "Non existing Scene"}, + blocking=True, + ) + + # the V1 implementation should have retried (2 calls) + assert len(mock_api_v1.mock_requests) == 2 + assert mock_hue_activate_scene2.call_count == 1 diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py new file mode 100644 index 00000000000..30f4d3634b4 --- /dev/null +++ b/tests/components/hue/test_switch.py @@ -0,0 +1,107 @@ +"""Philips Hue switch platform tests for V2 bridge/api.""" + +from .conftest import setup_platform +from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY + + +async def test_switch(hass, mock_bridge_v2, v2_resources_test_data): + """Test if (config) switches get created.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "switch") + # there shouldn't have been any requests at this point + assert len(mock_bridge_v2.mock_requests) == 0 + # 2 entities should be created from test data + assert len(hass.states.async_all()) == 2 + + # test config switch to enable/disable motion sensor + test_entity = hass.states.get("switch.hue_motion_sensor_motion") + assert test_entity is not None + assert test_entity.name == "Hue motion sensor: Motion" + assert test_entity.state == "on" + assert test_entity.attributes["device_class"] == "switch" + + +async def test_switch_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data): + """Test calling the turn on service on a switch.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "switch") + + test_entity_id = "switch.hue_motion_sensor_motion" + + # call the HA turn_on service + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": test_entity_id}, + blocking=True, + ) + + # PUT request should have been sent to device with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["enabled"] is True + + +async def test_switch_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): + """Test calling the turn off service on a switch.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, "switch") + + test_entity_id = "switch.hue_motion_sensor_motion" + + # verify the switch is on before we start + assert hass.states.get(test_entity_id).state == "on" + + # now call the HA turn_off service + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": test_entity_id}, + blocking=True, + ) + + # PUT request should have been sent to device with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"]["enabled"] is False + + # Now generate update event by emitting the json we've sent as incoming event + mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + await hass.async_block_till_done() + + # the switch should now be off + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "off" + + +async def test_switch_added(hass, mock_bridge_v2): + """Test new switch added to bridge.""" + await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) + + await setup_platform(hass, mock_bridge_v2, "switch") + + test_entity_id = "switch.hue_mocked_device_motion" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake entity (and attached device and zigbee_connectivity) by emitting events + mock_bridge_v2.api.emit_event("add", FAKE_BINARY_SENSOR) + await hass.async_block_till_done() + + # the entity should now be available + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "on" + + # test update + updated_resource = {**FAKE_BINARY_SENSOR, "enabled": False} + mock_bridge_v2.api.emit_event("update", updated_resource) + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity is not None + assert test_entity.state == "off" From f8501ded0e0fe68e0ede1ab3cb2c3c5c104ef28f Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 16 Nov 2021 21:37:28 +0100 Subject: [PATCH 0547/1452] Add entity_category to Rituals Entities (#59756) --- .../components/rituals_perfume_genie/binary_sensor.py | 2 ++ homeassistant/components/rituals_perfume_genie/select.py | 3 ++- homeassistant/components/rituals_perfume_genie/sensor.py | 3 +++ tests/components/rituals_perfume_genie/test_binary_sensor.py | 3 ++- tests/components/rituals_perfume_genie/test_select.py | 2 ++ tests/components/rituals_perfume_genie/test_sensor.py | 3 +++ 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index a529ff3dca6..be251b86311 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,6 +39,7 @@ class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py index eac95ee5ed4..9907e1d227f 100644 --- a/homeassistant/components/rituals_perfume_genie/select.py +++ b/homeassistant/components/rituals_perfume_genie/select.py @@ -5,7 +5,7 @@ from pyrituals import Diffuser from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import AREA_SQUARE_METERS +from homeassistant.const import AREA_SQUARE_METERS, ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,6 +36,7 @@ class DiffuserRoomSize(DiffuserEntity, SelectEntity): _attr_icon = "mdi:ruler-square" _attr_unit_of_measurement = AREA_SQUARE_METERS _attr_options = ["15", "30", "60", "100"] + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 878fb2f1a86..5299539bcd9 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) from homeassistant.core import HomeAssistant @@ -92,6 +93,7 @@ class DiffuserBatterySensor(DiffuserEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -110,6 +112,7 @@ class DiffuserWifiSensor(DiffuserEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/tests/components/rituals_perfume_genie/test_binary_sensor.py b/tests/components/rituals_perfume_genie/test_binary_sensor.py index f2e499655ca..769384dbbf8 100644 --- a/tests/components/rituals_perfume_genie/test_binary_sensor.py +++ b/tests/components/rituals_perfume_genie/test_binary_sensor.py @@ -1,7 +1,7 @@ """Tests for the Rituals Perfume Genie binary sensor platform.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING from homeassistant.components.rituals_perfume_genie.binary_sensor import CHARGING_SUFFIX -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON +from homeassistant.const import ATTR_DEVICE_CLASS, ENTITY_CATEGORY_DIAGNOSTIC, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry @@ -28,3 +28,4 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: entry = registry.async_get("binary_sensor.genie_battery_charging") assert entry assert entry.unique_id == f"{hublot}{CHARGING_SUFFIX}" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC diff --git a/tests/components/rituals_perfume_genie/test_select.py b/tests/components/rituals_perfume_genie/test_select.py index fb159166fb7..5c6d344adca 100644 --- a/tests/components/rituals_perfume_genie/test_select.py +++ b/tests/components/rituals_perfume_genie/test_select.py @@ -9,6 +9,7 @@ from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ENTITY_ID, ATTR_ICON, + ENTITY_CATEGORY_CONFIG, SERVICE_SELECT_OPTION, ) from homeassistant.core import HomeAssistant @@ -36,6 +37,7 @@ async def test_select_entity(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == f"{diffuser.hublot}{ROOM_SIZE_SUFFIX}" assert entry.unit_of_measurement == AREA_SQUARE_METERS + assert entry.entity_category == ENTITY_CATEGORY_CONFIG async def test_select_option(hass: HomeAssistant) -> None: diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py index 2c72d429a99..a76cf0d6c46 100644 --- a/tests/components/rituals_perfume_genie/test_sensor.py +++ b/tests/components/rituals_perfume_genie/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) from homeassistant.core import HomeAssistant @@ -59,6 +60,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non entry = registry.async_get("sensor.genie_battery") assert entry assert entry.unique_id == f"{hublot}{BATTERY_SUFFIX}" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.genie_wifi") assert state @@ -69,6 +71,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non entry = registry.async_get("sensor.genie_wifi") assert entry assert entry.unique_id == f"{hublot}{WIFI_SUFFIX}" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC async def test_sensors_diffuser_v2_no_battery_no_cartridge(hass: HomeAssistant) -> None: From b82fac1a734a95666d102da08f62ba3d9b132b8b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 17 Nov 2021 00:13:20 +0000 Subject: [PATCH 0548/1452] [ci skip] Translation update --- .../accuweather/translations/bg.json | 10 +++- .../components/aemet/translations/bg.json | 14 ++++++ .../components/airly/translations/bg.json | 1 + .../components/airly/translations/ja.json | 2 +- .../components/airvisual/translations/bg.json | 6 +++ .../components/airvisual/translations/ja.json | 3 ++ .../components/asuswrt/translations/ja.json | 3 +- .../components/braviatv/translations/ja.json | 9 ++++ .../components/climacell/translations/bg.json | 2 + .../components/co2signal/translations/ja.json | 2 +- .../coolmaster/translations/ja.json | 3 ++ .../components/daikin/translations/bg.json | 1 + .../components/denonavr/translations/ja.json | 1 + .../components/dexcom/translations/ja.json | 11 +++++ .../components/elkm1/translations/ja.json | 3 +- .../evil_genius_labs/translations/es.json | 15 ++++++ .../forked_daapd/translations/ja.json | 3 +- .../garages_amsterdam/translations/ja.json | 11 +++++ .../google_travel_time/translations/bg.json | 1 + .../google_travel_time/translations/ja.json | 9 ++++ .../components/habitica/translations/bg.json | 1 + .../components/homekit/translations/id.json | 2 +- .../homematicip_cloud/translations/ja.json | 3 ++ .../components/hue/translations/de.json | 12 ++++- .../components/hue/translations/en.json | 49 +++++++++---------- .../components/hue/translations/ja.json | 2 +- .../components/insteon/translations/ja.json | 11 +++++ .../components/jellyfin/translations/bg.json | 21 ++++++++ .../components/jellyfin/translations/es.json | 21 ++++++++ .../keenetic_ndms2/translations/ja.json | 1 + .../components/lovelace/translations/ja.json | 7 +++ .../components/luftdaten/translations/ja.json | 2 +- .../modern_forms/translations/ja.json | 1 + .../components/monoprice/translations/ja.json | 30 ++++++++++++ .../components/nam/translations/ja.json | 1 + .../components/nest/translations/es.json | 5 ++ .../components/netatmo/translations/ja.json | 14 +++++- .../components/nut/translations/ja.json | 21 ++++++++ .../openweathermap/translations/bg.json | 3 +- .../openweathermap/translations/ja.json | 13 +++++ .../components/pi_hole/translations/bg.json | 6 +++ .../components/plex/translations/ja.json | 3 +- .../components/rachio/translations/bg.json | 11 +++++ .../components/rdw/translations/es.json | 7 +++ .../components/scene/translations/id.json | 2 +- .../components/sensor/translations/it.json | 2 + .../components/sensor/translations/nl.json | 2 + .../components/sensor/translations/pl.json | 2 + .../sensor/translations/zh-Hant.json | 2 + .../smartthings/translations/ja.json | 3 ++ .../system_bridge/translations/ja.json | 1 + .../totalconnect/translations/it.json | 1 + .../totalconnect/translations/ja.json | 10 +++- .../totalconnect/translations/nl.json | 1 + .../totalconnect/translations/pl.json | 1 + .../totalconnect/translations/zh-Hant.json | 1 + .../components/tuya/translations/ja.json | 3 +- .../components/unifi/translations/ja.json | 3 ++ .../components/wallbox/translations/bg.json | 9 +++- .../components/wallbox/translations/es.json | 9 +++- .../components/wallbox/translations/it.json | 10 +++- .../components/wallbox/translations/nl.json | 10 +++- .../components/wallbox/translations/pl.json | 10 +++- .../wallbox/translations/zh-Hant.json | 10 +++- .../waze_travel_time/translations/ja.json | 18 +++++++ 65 files changed, 420 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/aemet/translations/bg.json create mode 100644 homeassistant/components/dexcom/translations/ja.json create mode 100644 homeassistant/components/evil_genius_labs/translations/es.json create mode 100644 homeassistant/components/garages_amsterdam/translations/ja.json create mode 100644 homeassistant/components/insteon/translations/ja.json create mode 100644 homeassistant/components/jellyfin/translations/bg.json create mode 100644 homeassistant/components/jellyfin/translations/es.json create mode 100644 homeassistant/components/lovelace/translations/ja.json create mode 100644 homeassistant/components/monoprice/translations/ja.json create mode 100644 homeassistant/components/rachio/translations/bg.json create mode 100644 homeassistant/components/rdw/translations/es.json create mode 100644 homeassistant/components/waze_travel_time/translations/ja.json diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json index 2ac8a444100..074602891cd 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/bg.json b/homeassistant/components/aemet/translations/bg.json new file mode 100644 index 00000000000..62d0a34441a --- /dev/null +++ b/homeassistant/components/aemet/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/bg.json b/homeassistant/components/airly/translations/bg.json index 45b4cb225aa..955cc8c1ac4 100644 --- a/homeassistant/components/airly/translations/bg.json +++ b/homeassistant/components/airly/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", "wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly." }, "step": { diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index ee44e93d394..8d48269cc7b 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "wrong_location": "\u3053\u306e\u30a8\u30ea\u30a2\u306b\u306fAirly\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093\u3002" + "wrong_location": "\u3053\u306e\u30a8\u30ea\u30a2\u306b\u3001Airly\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { "user": { diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index b2c2b26bad3..114a1547549 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -9,8 +9,14 @@ "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "geography_by_name": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "city": "\u0413\u0440\u0430\u0434", "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index 5568b2c6fa2..c43e09ae3d7 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "location_not_found": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "geography_by_coords": { "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u7def\u5ea6/\u7d4c\u5ea6\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 0b5c05b26f2..a4f3beb4771 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -14,7 +14,8 @@ "step": { "init": { "data": { - "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240" + "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", + "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)" } } } diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 28168e63cfe..4c5aa54784d 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -9,5 +9,14 @@ "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "\u7121\u8996\u3055\u308c\u305f\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/bg.json b/homeassistant/components/climacell/translations/bg.json index 6b1e4d3cba2..af84485310d 100644 --- a/homeassistant/components/climacell/translations/bg.json +++ b/homeassistant/components/climacell/translations/bg.json @@ -1,11 +1,13 @@ { "config": { "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435" diff --git a/homeassistant/components/co2signal/translations/ja.json b/homeassistant/components/co2signal/translations/ja.json index b17dc55ac62..f630e7a0659 100644 --- a/homeassistant/components/co2signal/translations/ja.json +++ b/homeassistant/components/co2signal/translations/ja.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "location": "\uff5e\u306e\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3059\u308b" + "location": "\uff5e\u306e\u30c7\u30fc\u30bf\u3092\u53d6\u5f97" }, "description": "\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001https://co2signal.com/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json index a42202307f2..58173440320 100644 --- a/homeassistant/components/coolmaster/translations/ja.json +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "no_units": "CoolMasterNet\u306e\u30db\u30b9\u30c8\u306bHVAC\u30e6\u30cb\u30c3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index 5a8e7d875f9..c0796234f9a 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "host": "\u0410\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 92442f15392..1e4d4d2cd8d 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -27,6 +27,7 @@ "step": { "init": { "data": { + "show_all_sources": "\u3059\u3079\u3066\u306e\u30bd\u30fc\u30b9\u3092\u8868\u793a", "zone2": "\u30be\u30fc\u30f32\u306e\u8a2d\u5b9a", "zone3": "\u30be\u30fc\u30f33\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json new file mode 100644 index 00000000000..c183275bc9f --- /dev/null +++ b/homeassistant/components/dexcom/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u6e2c\u5b9a\u5358\u4f4d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 4611ef75ac0..7c804b56c7a 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002" + "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", + "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002" } } } diff --git a/homeassistant/components/evil_genius_labs/translations/es.json b/homeassistant/components/evil_genius_labs/translations/es.json new file mode 100644 index 00000000000..d0f2d525bfd --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 0c549fbee01..6d019a24427 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -13,7 +13,8 @@ "step": { "init": { "data": { - "librespot_java_port": "librespot-java\u30d1\u30a4\u30d7\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7528\u30dd\u30fc\u30c8(\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u5834\u5408)" + "librespot_java_port": "librespot-java\u30d1\u30a4\u30d7\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7528\u30dd\u30fc\u30c8(\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u5834\u5408)", + "max_playlists": "\u30bd\u30fc\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u6700\u5927\u6570" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/ja.json b/homeassistant/components/garages_amsterdam/translations/ja.json new file mode 100644 index 00000000000..2bbaa94cd90 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "garage_name": "\u30ac\u30ec\u30fc\u30b8\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index 35cfa0ad1d7..d49807e49af 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "name": "\u0418\u043c\u0435" } } diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index f725fc427a6..daafb1a4c47 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -1,3 +1,12 @@ { + "options": { + "step": { + "init": { + "data": { + "units": "\u5358\u4f4d" + } + } + } + }, "title": "Google\u30de\u30c3\u30d7\u306e\u79fb\u52d5\u6642\u9593(Travel Time)" } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/bg.json b/homeassistant/components/habitica/translations/bg.json index 02c83a6e916..ce37c7da82c 100644 --- a/homeassistant/components/habitica/translations/bg.json +++ b/homeassistant/components/habitica/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "url": "URL" } } diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index 64ce23a5224..8b6fd994bc2 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -23,7 +23,7 @@ "data": { "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)" }, - "description": "Sakelar yang dapat diprogram dibuat untuk setiap perangkat yang dipilih. Saat pemicu perangkat aktif, HomeKit dapat dikonfigurasi untuk menjalankan otomatisasi atau scene.", + "description": "Sakelar yang dapat diprogram dibuat untuk setiap perangkat yang dipilih. Saat pemicu perangkat aktif, HomeKit dapat dikonfigurasi untuk menjalankan otomatisasi atau skenario.", "title": "Konfigurasi Tingkat Lanjut" }, "cameras": { diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 5b5d0d62ab9..3d3c7e9e8a3 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -16,6 +16,9 @@ "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", "pin": "PIN\u30b3\u30fc\u30c9" } + }, + "link": { + "description": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306e\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u3001\u9001\u4fe1\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Home Assistant\u306bHomematicIP\u304c\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u306e\u4f4d\u7f6e](/static/images/config_flows/config_homematicip_cloud.png)" } } } diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index bf0d2a7c756..1c9269bcfc2 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Erste Taste", + "2": "Zweite Taste", + "3": "Dritte Taste", + "4": "Vierte Taste", "button_1": "Erste Taste", "button_2": "Zweite Taste", "button_3": "Dritte Taste", @@ -47,11 +51,16 @@ "turn_on": "Einschalten" }, "trigger_type": { + "double_short_release": "Beide \"{subtype}\" losgelassen", + "initial_press": "Taste \"{subtype}\" anfangs gedr\u00fcckt", + "long_release": "Taste \"{subtype}\" nach langem Dr\u00fccken losgelassen", "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", "remote_button_short_release": "\"{subtype}\" Taste losgelassen", "remote_double_button_long_press": "Beide \"{subtype}\" nach langem Dr\u00fccken losgelassen", - "remote_double_button_short_press": "Beide \"{subtype}\" losgelassen" + "remote_double_button_short_press": "Beide \"{subtype}\" losgelassen", + "repeat": "Taste \"{subtype}\" gedr\u00fcckt gehalten", + "short_release": "Taste \"{subtype}\" nach kurzem Dr\u00fccken losgelassen" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Hue-Gruppen erlauben", + "allow_hue_scenes": "Hue-Szenen zulassen", "allow_unreachable": "Erlaube nicht erreichbaren Gl\u00fchlampen, ihren Zustand korrekt zu melden" } } diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 2f1010c88b7..f0b8e560729 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -35,33 +35,32 @@ }, "device_automation": { "trigger_subtype": { - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "double_buttons_1_3": "First and Third buttons", - "double_buttons_2_4": "Second and Fourth buttons", - "dim_down": "Dim down", - "dim_up": "Dim up", - "turn_off": "Turn off", - "turn_on": "Turn on", - "1": "First button", - "2": "Second button", - "3": "Third button", - "4": "Fourth button" + "1": "First button", + "2": "Second button", + "3": "Third button", + "4": "Fourth button", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "dim_down": "Dim down", + "dim_up": "Dim up", + "double_buttons_1_3": "First and Third buttons", + "double_buttons_2_4": "Second and Fourth buttons", + "turn_off": "Turn off", + "turn_on": "Turn on" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_double_button_long_press": "Both \"{subtype}\" released after long press", - "remote_double_button_short_press": "Both \"{subtype}\" released", - - "initial_press": "Button \"{subtype}\" pressed initially", - "repeat": "Button \"{subtype}\" held down", - "short_release": "Button \"{subtype}\" released after short press", - "long_release": "Button \"{subtype}\" released after long press", - "double_short_release": "Both \"{subtype}\" released" + "double_short_release": "Both \"{subtype}\" released", + "initial_press": "Button \"{subtype}\" pressed initially", + "long_release": "Button \"{subtype}\" released after long press", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_double_button_long_press": "Both \"{subtype}\" released after long press", + "remote_double_button_short_press": "Both \"{subtype}\" released", + "repeat": "Button \"{subtype}\" held down", + "short_release": "Button \"{subtype}\" released after short press" } }, "options": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index ce391b43101..8e15de50354 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -16,7 +16,7 @@ "title": "Hue bridge\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" }, "link": { - "description": "\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Philips Hue\u304cHome Assistant\u306b\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "description": "\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Philips Hue\u304cHome Assistant\u306b\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u306e\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", "title": "\u30ea\u30f3\u30af\u30cf\u30d6" }, "manual": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json new file mode 100644 index 00000000000..a18421fe9a1 --- /dev/null +++ b/homeassistant/components/insteon/translations/ja.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "add_x10": { + "data": { + "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/bg.json b/homeassistant/components/jellyfin/translations/bg.json new file mode 100644 index 00000000000..e99b99fdb61 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/es.json b/homeassistant/components/jellyfin/translations/es.json new file mode 100644 index 00000000000..1cc7ab64c75 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index 6edbf1013b0..b477b4a77b1 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -4,6 +4,7 @@ "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093", "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic router\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/lovelace/translations/ja.json b/homeassistant/components/lovelace/translations/ja.json new file mode 100644 index 00000000000..2d1f1af3c61 --- /dev/null +++ b/homeassistant/components/lovelace/translations/ja.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "resources": "\u30ea\u30bd\u30fc\u30b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/ja.json b/homeassistant/components/luftdaten/translations/ja.json index 48f371ae432..a6ca1a31316 100644 --- a/homeassistant/components/luftdaten/translations/ja.json +++ b/homeassistant/components/luftdaten/translations/ja.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "show_on_map": "\u5730\u56f3\u3067\u8868\u793a", + "show_on_map": "\u5730\u56f3\u306b\u8868\u793a", "station_id": "Luftdaten\u30bb\u30f3\u30b5\u30fcID" }, "title": "Luftdaten\u306e\u5b9a\u7fa9" diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json index 341cc32781d..4848a748574 100644 --- a/homeassistant/components/modern_forms/translations/ja.json +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/monoprice/translations/ja.json b/homeassistant/components/monoprice/translations/ja.json new file mode 100644 index 00000000000..45356b82d7a --- /dev/null +++ b/homeassistant/components/monoprice/translations/ja.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "source_1": "\u30bd\u30fc\u30b9#1\u306e\u540d\u524d", + "source_2": "\u30bd\u30fc\u30b9#2\u306e\u540d\u524d", + "source_3": "\u30bd\u30fc\u30b9#3\u306e\u540d\u524d", + "source_4": "\u30bd\u30fc\u30b9#4\u306e\u540d\u524d", + "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d", + "source_6": "\u30bd\u30fc\u30b9#6\u306e\u540d\u524d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u30bd\u30fc\u30b9#1\u306e\u540d\u524d", + "source_2": "\u30bd\u30fc\u30b9#2\u306e\u540d\u524d", + "source_3": "\u30bd\u30fc\u30b9#3\u306e\u540d\u524d", + "source_4": "\u30bd\u30fc\u30b9#4\u306e\u540d\u524d", + "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d" + }, + "title": "\u30bd\u30fc\u30b9\u306e\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 5a3b47683ef..5e95c9baffe 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index f9e88c9180f..7ec5f1ab2f6 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -18,6 +18,11 @@ "unknown": "Error desconocido validando el c\u00f3digo" }, "step": { + "auth": { + "data": { + "code": "Token de acceso" + } + }, "init": { "data": { "flow_impl": "Proveedor" diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 277eca2fbdb..56c111e95fa 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -8,10 +8,22 @@ }, "options": { "step": { + "public_weather": { + "data": { + "area_name": "\u30a8\u30ea\u30a2\u540d", + "mode": "\u8a08\u7b97", + "show_on_map": "\u5730\u56f3\u306b\u8868\u793a" + }, + "description": "\u30a8\u30ea\u30a2\u306e\u30d1\u30d6\u30ea\u30c3\u30af\u6c17\u8c61\u30bb\u30f3\u30b5\u30fc\u3092\u8a2d\u5b9a\u3059\u308b\u3002", + "title": "Netatmo\u30d1\u30d6\u30ea\u30c3\u30af\u6c17\u8c61\u30bb\u30f3\u30b5\u30fc" + }, "public_weather_areas": { "data": { + "new_area": "\u30a8\u30ea\u30a2\u540d", "weather_areas": "\u5929\u6c17\u4e88\u5831\u306e\u30a8\u30ea\u30a2" - } + }, + "description": "\u30d1\u30d6\u30ea\u30c3\u30af\u6c17\u8c61\u30bb\u30f3\u30b5\u30fc\u3092\u8a2d\u5b9a\u3059\u308b\u3002", + "title": "Netatmo\u30d1\u30d6\u30ea\u30c3\u30af\u6c17\u8c61\u30bb\u30f3\u30b5\u30fc" } } } diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index a42202307f2..79fa93725e1 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -1,11 +1,32 @@ { "config": { "step": { + "resources": { + "data": { + "resources": "\u30ea\u30bd\u30fc\u30b9" + }, + "title": "\u76e3\u8996\u3059\u308b\u30ea\u30bd\u30fc\u30b9\u3092\u9078\u629e" + }, + "ups": { + "data": { + "resources": "\u30ea\u30bd\u30fc\u30b9" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" } } } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u30ea\u30bd\u30fc\u30b9" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u30ea\u30bd\u30fc\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/bg.json b/homeassistant/components/openweathermap/translations/bg.json index 463ddf48132..c1816ddae03 100644 --- a/homeassistant/components/openweathermap/translations/bg.json +++ b/homeassistant/components/openweathermap/translations/bg.json @@ -4,7 +4,8 @@ "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index cbe8ba65321..d13b04e97d6 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -2,9 +2,22 @@ "config": { "step": { "user": { + "data": { + "language": "\u8a00\u8a9e", + "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + }, "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", "title": "OpenWeatherMap" } } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u8a00\u8a9e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 3493e514ef3..62a6b635be0 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -1,8 +1,14 @@ { "config": { "step": { + "api_key": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442" } diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 8d172d6ea3f..6dda0588efc 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "error": { - "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "ssl_error": "SSL\u8a3c\u660e\u66f8\u306e\u554f\u984c" }, "step": { "manual_setup": { diff --git a/homeassistant/components/rachio/translations/bg.json b/homeassistant/components/rachio/translations/bg.json new file mode 100644 index 00000000000..fdbdc5b1cdf --- /dev/null +++ b/homeassistant/components/rachio/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/es.json b/homeassistant/components/rdw/translations/es.json new file mode 100644 index 00000000000..e3f8891f3b1 --- /dev/null +++ b/homeassistant/components/rdw/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scene/translations/id.json b/homeassistant/components/scene/translations/id.json index 827c0c81f38..10ece853e7b 100644 --- a/homeassistant/components/scene/translations/id.json +++ b/homeassistant/components/scene/translations/id.json @@ -1,3 +1,3 @@ { - "title": "Scene" + "title": "Skenario" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index cc5d3534715..7985cee7cd4 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -6,6 +6,7 @@ "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_frequency": "Frequenza 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}", @@ -32,6 +33,7 @@ "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}", + "frequency": "{entity_name} cambiamenti di frequenza", "gas": "Variazioni di gas di {entity_name}", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", "illuminance": "variazioni dell'illuminazione di {entity_name}", diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index c55f1547642..fea321fe221 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", "is_energy": "Huidige {entity_name} energie", + "is_frequency": "Huidige {entity_name} frequentie", "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", "energy": "{entity_name} energieveranderingen", + "frequency": "{entity_name} frequentie verandert", "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index def1be5e06d..adff0b482a2 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "obecny poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", "is_energy": "obecna energia {entity_name}", + "is_frequency": "obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", "is_gas": "obecny poziom gazu {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", + "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", "gas": "{entity_name} wykryje zmian\u0119 poziomu gazu", "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 5c36491941a..f549d18dbc7 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -6,6 +6,7 @@ "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_frequency": "\u76ee\u524d{entity_name}\u983b\u7387", "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", @@ -32,6 +33,7 @@ "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", + "frequency": "{entity_name}\u983b\u7387\u8b8a\u66f4", "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 19dfe5d9d0e..c32e7a7364c 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -10,6 +10,9 @@ }, "pat": { "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "select_location": { + "title": "\u5834\u6240\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index 23bb84493b1..d651ca67d2f 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index dfc480ab961..437edd55a44 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "no_locations": "Nessuna posizione disponibile per questo utente, controlla le impostazioni di TotalConnect", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index c799eb7494e..dbbb20739c7 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "no_locations": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u5229\u7528\u3067\u304d\u308b\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u305b\u3093\u3002TotalConnect\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "error": { + "usercode": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u3053\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u306b\u306f\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059" + }, "step": { "locations": { "data": { "usercode": "\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" - } + }, + "description": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u5834\u6240 {location_id} \u306b\u5165\u529b\u3057\u307e\u3059", + "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" } } } diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 0ec7bb52d88..674818d8428 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", + "no_locations": "Er zijn geen locaties beschikbaar voor deze gebruiker, controleer de instellingen van TotalConnect", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index 03452569c28..5174d717c26 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "no_locations": "Brak dost\u0119pnych lokalizacji dla tego u\u017cytkownika, sprawd\u017a ustawienia TotalConnect.", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json index eb739cb5e38..beaeaa5d9bf 100644 --- a/homeassistant/components/totalconnect/translations/zh-Hant.json +++ b/homeassistant/components/totalconnect/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_locations": "\u8a72\u4f7f\u7528\u8005\u7121\u53ef\u7528\u7684\u4f4d\u5740\uff0c\u8acb\u6aa2\u67e5 TotalConnect \u8a2d\u5b9a", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index d63ac53889e..8c73deb27a3 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -38,7 +38,8 @@ "device": { "data": { "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", - "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8" + "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8", + "unit_of_measurement": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d" }, "title": "Tuya\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 1766534faa7..aec02062f2f 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -17,6 +17,9 @@ "data": { "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8" } + }, + "statistics_sensors": { + "description": "\u7d71\u8a08\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/wallbox/translations/bg.json b/homeassistant/components/wallbox/translations/bg.json index 5644c3b845c..25f3bb50845 100644 --- a/homeassistant/components/wallbox/translations/bg.json +++ b/homeassistant/components/wallbox/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -9,6 +10,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 1252e5eaca1..3adfd671804 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,12 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/wallbox/translations/it.json b/homeassistant/components/wallbox/translations/it.json index 5b8828860e7..112a0c13970 100644 --- a/homeassistant/components/wallbox/translations/it.json +++ b/homeassistant/components/wallbox/translations/it.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "reauth_invalid": "Riautenticazione non riuscita; Il numero di serie non corrisponde all'originale", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index 6ba03e7ee99..dbe8bd91f72 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", + "reauth_invalid": "Herauthenticatie mislukt; serial-nummer komt niet overeen met het origineel", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/wallbox/translations/pl.json b/homeassistant/components/wallbox/translations/pl.json index 2728f1cae31..51180d4c68e 100644 --- a/homeassistant/components/wallbox/translations/pl.json +++ b/homeassistant/components/wallbox/translations/pl.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_invalid": "Ponowne uwierzytelnienie nie powiod\u0142o si\u0119. Numer seryjny nie pasuje do orygina\u0142u.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/wallbox/translations/zh-Hant.json b/homeassistant/components/wallbox/translations/zh-Hant.json index 78a752f9a0d..3f282cda1f9 100644 --- a/homeassistant/components/wallbox/translations/zh-Hant.json +++ b/homeassistant/components/wallbox/translations/zh-Hant.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_invalid": "\u91cd\u65b0\u8a8d\u8b49\u5931\u6557\uff1b\u8207\u539f\u59cb\u5e8f\u865f\u4e0d\u7b26\u5408", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json new file mode 100644 index 00000000000..cbca4b1091f --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30ab\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "units": "\u5358\u4f4d" + } + } + } + } +} \ No newline at end of file From 7f07755f5c50261cf7b40fe50f6b493d6a8ebcab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Nov 2021 01:45:03 +0100 Subject: [PATCH 0549/1452] Use ZeroconfServiceInfo in enphase-envoy (#59738) Co-authored-by: epenet --- homeassistant/components/enphase_envoy/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index fa253ca442c..6093a30ca68 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -102,7 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" self.serial = discovery_info[zeroconf.ATTR_PROPERTIES]["serialnum"] From a68563cefd62fcb78914daea9a453e6eee3c8dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Wed, 17 Nov 2021 03:10:41 +0100 Subject: [PATCH 0550/1452] Add `configuration_url` to SMA integration (#59638) --- homeassistant/components/sma/__init__.py | 11 ++++++++++- homeassistant/components/sma/sensor.py | 18 ++---------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 1a48bee7797..6a7243b0343 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -143,7 +144,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: # Get updated device info - device_info = await sma.device_info() + sma_device_info = await sma.device_info() + device_info = DeviceInfo( + configuration_url=url, + identifiers={(DOMAIN, entry.unique_id)}, + manufacturer=sma_device_info["manufacturer"], + model=sma_device_info["type"], + name=sma_device_info["name"], + sw_version=sma_device_info["sw_version"], + ) # Get all device sensors sensor_def = await sma.get_sensors() except ( diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 853edee823c..7fa84f25bd2 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -156,7 +156,7 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self, coordinator: DataUpdateCoordinator, config_entry_unique_id: str, - device_info: dict[str, Any], + device_info: DeviceInfo, pysma_sensor: pysma.sensor.Sensor, ) -> None: """Initialize the sensor.""" @@ -164,7 +164,7 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self._sensor = pysma_sensor self._enabled_default = self._sensor.enabled self._config_entry_unique_id = config_entry_unique_id - self._device_info = device_info + self._attr_device_info = device_info if self.native_unit_of_measurement == ENERGY_KILO_WATT_HOUR: self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @@ -199,20 +199,6 @@ class SMAsensor(CoordinatorEntity, SensorEntity): f"{self._config_entry_unique_id}-{self._sensor.key}_{self._sensor.key_idx}" ) - @property - def device_info(self) -> DeviceInfo | None: - """Return the device information.""" - if not self._device_info: - return None - - return DeviceInfo( - identifiers={(DOMAIN, self._config_entry_unique_id)}, - manufacturer=self._device_info["manufacturer"], - model=self._device_info["type"], - name=self._device_info["name"], - sw_version=self._device_info["sw_version"], - ) - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" From a4826f4b6961f21bb8f05f86f36474637561733d Mon Sep 17 00:00:00 2001 From: Kilian Lackhove Date: Wed, 17 Nov 2021 07:29:37 +0100 Subject: [PATCH 0551/1452] Fix deCONZ climate offset display if offset is zero (#59803) --- homeassistant/components/deconz/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index b9401e6d5a3..4641bd26d43 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -256,7 +256,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Return the state attributes of the thermostat.""" attr = {} - if self._device.offset: + if self._device.offset is not None: attr[ATTR_OFFSET] = self._device.offset if self._device.valve is not None: From a88469ec74f2d8b68b520128b469e50f269a2d20 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 16 Nov 2021 23:29:59 -0800 Subject: [PATCH 0552/1452] Bump motioneye-client to v0.3.12 (#59811) --- homeassistant/components/motioneye/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index ae6d3108f96..e01cae08511 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -9,7 +9,7 @@ "webhook" ], "requirements": [ - "motioneye-client==0.3.11" + "motioneye-client==0.3.12" ], "codeowners": [ "@dermotduffy" diff --git a/requirements_all.txt b/requirements_all.txt index 123a9c618c9..3349956870e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,7 +1023,7 @@ mitemp_bt==0.0.5 motionblinds==0.5.7 # homeassistant.components.motioneye -motioneye-client==0.3.11 +motioneye-client==0.3.12 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 768c3c7854f..70d9644f6a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -618,7 +618,7 @@ minio==4.0.9 motionblinds==0.5.7 # homeassistant.components.motioneye -motioneye-client==0.3.11 +motioneye-client==0.3.12 # homeassistant.components.mullvad mullvad-api==1.0.0 From 593bc866f049997f0b4cf7c3ceee29cd1468d7c9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Nov 2021 09:05:25 +0100 Subject: [PATCH 0553/1452] Bump pychromecast to 10.1.0 (#59719) * Prepare for pychromecast 10 * Bump pychromecast to 10.0.0 * Bump pychromecast to 10.1.0 * Update homeassistant/components/cast/discovery.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/cast/const.py | 1 - homeassistant/components/cast/discovery.py | 18 ++-- homeassistant/components/cast/helpers.py | 93 ++++++------------- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 4 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/conftest.py | 4 - tests/components/cast/test_media_player.py | 81 ++++------------ 9 files changed, 61 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index 03ffdfbd15c..06db70b830a 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,7 +1,6 @@ """Consts for Cast integration.""" DOMAIN = "cast" -DEFAULT_PORT = 8009 # Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index a5ac4c02047..01b00c82f64 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -11,7 +11,6 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( CAST_BROWSER_KEY, CONF_KNOWN_HOSTS, - DEFAULT_PORT, INTERNAL_DISCOVERY_RUNNING_KEY, SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, @@ -21,15 +20,18 @@ from .helpers import ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) -def discover_chromecast(hass: HomeAssistant, device_info): +def discover_chromecast( + hass: HomeAssistant, cast_info: pychromecast.models.CastInfo +) -> None: """Discover a Chromecast.""" info = ChromecastInfo( - services=device_info.services, - uuid=device_info.uuid, - model_name=device_info.model_name, - friendly_name=device_info.friendly_name, - is_audio_group=device_info.port != DEFAULT_PORT, + services=cast_info.services, + uuid=cast_info.uuid, + model_name=cast_info.model_name, + friendly_name=cast_info.friendly_name, + cast_type=cast_info.cast_type, + manufacturer=cast_info.manufacturer, ) if info.uuid is None: @@ -78,6 +80,8 @@ def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None: uuid=cast_info.uuid, model_name=cast_info.model_name, friendly_name=cast_info.friendly_name, + cast_type=cast_info.cast_type, + manufacturer=cast_info.manufacturer, ), ) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 6d021d020c4..a3bf34ab3ae 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,7 +5,7 @@ from typing import Optional import attr from pychromecast import dial -from pychromecast.const import CAST_MANUFACTURERS +from pychromecast.const import CAST_TYPE_GROUP @attr.s(slots=True, frozen=True) @@ -16,89 +16,48 @@ class ChromecastInfo: """ services: set | None = attr.ib() - uuid: str | None = attr.ib( - converter=attr.converters.optional(str), default=None - ) # always convert UUID to string if not None - _manufacturer = attr.ib(type=Optional[str], default=None) - model_name: str = attr.ib(default="") - friendly_name: str | None = attr.ib(default=None) - is_audio_group = attr.ib(type=Optional[bool], default=False) + uuid: str = attr.ib(converter=attr.converters.optional(str)) + model_name: str = attr.ib() + friendly_name: str = attr.ib() + cast_type: str = attr.ib() + manufacturer: str = attr.ib() is_dynamic_group = attr.ib(type=Optional[bool], default=None) @property - def is_information_complete(self) -> bool: - """Return if all information is filled out.""" - want_dynamic_group = self.is_audio_group - have_dynamic_group = self.is_dynamic_group is not None - have_all_except_dynamic_group = all( - attr.astuple( - self, - filter=attr.filters.exclude( - attr.fields(ChromecastInfo).is_dynamic_group - ), - ) - ) - return have_all_except_dynamic_group and ( - not want_dynamic_group or have_dynamic_group - ) - - @property - def manufacturer(self) -> str | None: - """Return the manufacturer.""" - if self._manufacturer: - return self._manufacturer - if not self.model_name: - return None - return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.") + def is_audio_group(self) -> bool: + """Return if the cast is an audio group.""" + return self.cast_type == CAST_TYPE_GROUP def fill_out_missing_chromecast_info(self) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. Uses blocking HTTP / HTTPS. """ - if self.is_information_complete: + if not self.is_audio_group or self.is_dynamic_group is not None: # We have all information, no need to check HTTP API. return self # Fill out missing group information via HTTP API. - if self.is_audio_group: - is_dynamic_group = False - http_group_status = None - if self.uuid: - http_group_status = dial.get_multizone_status( - None, - services=self.services, - zconf=ChromeCastZeroconf.get_zeroconf(), - ) - if http_group_status is not None: - is_dynamic_group = any( - str(g.uuid) == self.uuid - for g in http_group_status.dynamic_groups - ) - - return ChromecastInfo( - services=self.services, - uuid=self.uuid, - friendly_name=self.friendly_name, - model_name=self.model_name, - is_audio_group=True, - is_dynamic_group=is_dynamic_group, - ) - - # Fill out some missing information (friendly_name, uuid) via HTTP dial. - http_device_status = dial.get_device_status( - None, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() + is_dynamic_group = False + http_group_status = None + http_group_status = dial.get_multizone_status( + None, + services=self.services, + zconf=ChromeCastZeroconf.get_zeroconf(), ) - if http_device_status is None: - # HTTP dial didn't give us any new information. - return self + if http_group_status is not None: + is_dynamic_group = any( + str(g.uuid) == self.uuid for g in http_group_status.dynamic_groups + ) return ChromecastInfo( services=self.services, - uuid=(self.uuid or http_device_status.uuid), - friendly_name=(self.friendly_name or http_device_status.friendly_name), - manufacturer=(self.manufacturer or http_device_status.manufacturer), - model_name=(self.model_name or http_device_status.model_name), + uuid=self.uuid, + friendly_name=self.friendly_name, + model_name=self.model_name, + cast_type=self.cast_type, + manufacturer=self.manufacturer, + is_dynamic_group=is_dynamic_group, ) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index dbbb7aa1417..9e79e395fde 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.4.0"], + "requirements": ["pychromecast==10.1.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index cfaf8843865..32c303dfd8b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -235,6 +235,8 @@ class CastDevice(MediaPlayerEntity): self._cast_info.friendly_name, None, None, + self._cast_info.cast_type, + self._cast_info.manufacturer, ), ChromeCastZeroconf.get_zeroconf(), ) @@ -833,6 +835,8 @@ class DynamicCastGroup: self._cast_info.friendly_name, None, None, + self._cast_info.cast_type, + self._cast_info.manufacturer, ), ChromeCastZeroconf.get_zeroconf(), ) diff --git a/requirements_all.txt b/requirements_all.txt index 3349956870e..1aacb26c9f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1390,7 +1390,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.4.0 +pychromecast==10.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70d9644f6a6..9d01ed04eb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==9.4.0 +pychromecast==10.1.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index dce5db51161..4c17cf74af9 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -10,10 +10,6 @@ import pytest def dial_mock(): """Mock pychromecast dial.""" dial_mock = MagicMock() - dial_mock.get_device_status.return_value.uuid = "fake_uuid" - dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" - dial_mock.get_device_status.return_value.model_name = "fake_model_name" - dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" dial_mock.get_multizone_status.return_value.dynamic_groups = [] return dial_mock diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index a038cb34aea..6c63d3bcc2b 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -8,6 +8,7 @@ from uuid import UUID import attr import pychromecast +from pychromecast.const import CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP import pytest from homeassistant.components import media_player, tts @@ -70,10 +71,10 @@ def get_fake_chromecast_info( ChromecastInfo( services=self.services, uuid=self.uuid, - manufacturer=self.manufacturer, model_name=self.model_name, friendly_name=self.friendly_name, - is_audio_group=self.is_audio_group, + cast_type=self.cast_type, + manufacturer=self.manufacturer, is_dynamic_group=self.is_dynamic_group, ) == other @@ -83,10 +84,12 @@ def get_fake_chromecast_info( return ExtendedChromecastInfo( host=host, port=port, - uuid=uuid, - friendly_name="Speaker", services={"the-service"}, - is_audio_group=port != 8009, + uuid=uuid, + model_name="Chromecast", + friendly_name="Speaker", + cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, + manufacturer="Nabu Casa", ) @@ -142,6 +145,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -157,6 +162,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ), ) @@ -195,6 +202,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -211,6 +220,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -245,64 +256,6 @@ async def test_start_discovery_called_once(hass, castbrowser_mock): assert castbrowser_mock.start_discovery.call_count == 1 -async def test_internal_discovery_callback_fill_out(hass): - """Test internal discovery automatically filling out information.""" - discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info(host="host1") - zconf = get_fake_zconf(host="host1", port=8009) - full_info = attr.evolve( - info, - model_name="google home", - friendly_name="Speaker", - uuid=FakeUUID, - manufacturer="Nabu Casa", - ) - - with patch( - "homeassistant.components.cast.helpers.dial.get_device_status", - return_value=full_info, - ), patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf, - ): - signal = MagicMock() - - async_dispatcher_connect(hass, "cast_discovered", signal) - discover_cast("the-service", info) - await hass.async_block_till_done() - - # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] - assert discover == full_info - - -async def test_internal_discovery_callback_fill_out_default_manufacturer(hass): - """Test internal discovery automatically filling out information.""" - discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info(host="host1") - zconf = get_fake_zconf(host="host1", port=8009) - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) - - with patch( - "homeassistant.components.cast.helpers.dial.get_device_status", - return_value=full_info, - ), patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf, - ): - signal = MagicMock() - - async_dispatcher_connect(hass, "cast_discovered", signal) - discover_cast("the-service", info) - await hass.async_block_till_done() - - # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] - assert discover == attr.evolve(full_info, manufacturer="Google Inc.") - - async def test_internal_discovery_callback_fill_out_fail(hass): """Test internal discovery automatically filling out information.""" discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) @@ -337,7 +290,7 @@ async def test_internal_discovery_callback_fill_out_group(hass): zconf = get_fake_zconf(host="host1", port=12345) full_info = attr.evolve( info, - model_name="", + model_name="Chromecast", friendly_name="Speaker", uuid=FakeUUID, is_dynamic_group=False, From dec54488e8a0c7eca1cb228668c0740fd7f90822 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Nov 2021 09:07:01 +0100 Subject: [PATCH 0554/1452] Enable basic type checking for cloud (#55337) * Enable basic type checking for cloud * Update mypy settings * Address review comment * Fix rebase mistakes * Correct decorator order --- homeassistant/components/cloud/client.py | 9 +++--- homeassistant/components/cloud/http_api.py | 34 +++++++++++----------- homeassistant/components/cloud/prefs.py | 1 + homeassistant/components/cloud/utils.py | 12 +++++--- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 5a10e1d1e5c..5f3abe521af 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -42,12 +42,13 @@ class CloudClient(Interface): self._websession = websession self.google_user_config = google_user_config self.alexa_user_config = alexa_user_config - self._alexa_config = None - self._google_config = None + self._alexa_config: alexa_config.AlexaConfig | None = None + self._google_config: google_config.CloudGoogleConfig | None = None @property def base_path(self) -> Path: """Return path to base dir.""" + assert self._hass.config.config_dir is not None return Path(self._hass.config.config_dir) @property @@ -56,7 +57,7 @@ class CloudClient(Interface): return self._prefs @property - def loop(self) -> asyncio.BaseEventLoop: + def loop(self) -> asyncio.AbstractEventLoop: """Return client loop.""" return self._hass.loop @@ -66,7 +67,7 @@ class CloudClient(Interface): return self._websession @property - def aiohttp_runner(self) -> aiohttp.web.AppRunner: + def aiohttp_runner(self) -> aiohttp.web.AppRunner | None: """Return client webinterface aiohttp application.""" return self._hass.http.runner diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c18db2ac683..cd682057266 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -264,8 +264,8 @@ class CloudForgotPasswordView(HomeAssistantView): return self.json_message("ok") -@websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "cloud/status"}) +@websocket_api.async_response async def websocket_cloud_status(hass, connection, msg): """Handle request for account info. @@ -298,8 +298,8 @@ def _require_cloud_login(handler): @_require_cloud_login -@websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "cloud/subscription"}) +@websocket_api.async_response async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -315,7 +315,6 @@ async def websocket_subscription(hass, connection, msg): @_require_cloud_login -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "cloud/update_prefs", @@ -331,6 +330,7 @@ async def websocket_subscription(hass, connection, msg): ), } ) +@websocket_api.async_response async def websocket_update_prefs(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -365,14 +365,14 @@ async def websocket_update_prefs(hass, connection, msg): @_require_cloud_login -@websocket_api.async_response -@_ws_handle_cloud_errors @websocket_api.websocket_command( { vol.Required("type"): "cloud/cloudhook/create", vol.Required("webhook_id"): str, } ) +@websocket_api.async_response +@_ws_handle_cloud_errors async def websocket_hook_create(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -381,14 +381,14 @@ async def websocket_hook_create(hass, connection, msg): @_require_cloud_login -@websocket_api.async_response -@_ws_handle_cloud_errors @websocket_api.websocket_command( { vol.Required("type"): "cloud/cloudhook/delete", vol.Required("webhook_id"): str, } ) +@websocket_api.async_response +@_ws_handle_cloud_errors async def websocket_hook_delete(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -430,9 +430,9 @@ async def _account_data(cloud): @websocket_api.require_admin @_require_cloud_login +@websocket_api.websocket_command({"type": "cloud/remote/connect"}) @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({"type": "cloud/remote/connect"}) async def websocket_remote_connect(hass, connection, msg): """Handle request for connect remote.""" cloud = hass.data[DOMAIN] @@ -442,9 +442,9 @@ async def websocket_remote_connect(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login +@websocket_api.websocket_command({"type": "cloud/remote/disconnect"}) @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({"type": "cloud/remote/disconnect"}) async def websocket_remote_disconnect(hass, connection, msg): """Handle request for disconnect remote.""" cloud = hass.data[DOMAIN] @@ -454,9 +454,9 @@ async def websocket_remote_disconnect(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login +@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"}) @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"}) async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] @@ -479,8 +479,6 @@ async def google_assistant_list(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login -@websocket_api.async_response -@_ws_handle_cloud_errors @websocket_api.websocket_command( { "type": "cloud/google_assistant/entities/update", @@ -491,6 +489,8 @@ async def google_assistant_list(hass, connection, msg): vol.Optional("disable_2fa"): bool, } ) +@websocket_api.async_response +@_ws_handle_cloud_errors async def google_assistant_update(hass, connection, msg): """Update google assistant config.""" cloud = hass.data[DOMAIN] @@ -507,9 +507,9 @@ async def google_assistant_update(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login +@websocket_api.websocket_command({"type": "cloud/alexa/entities"}) @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({"type": "cloud/alexa/entities"}) async def alexa_list(hass, connection, msg): """List all alexa entities.""" cloud = hass.data[DOMAIN] @@ -532,8 +532,6 @@ async def alexa_list(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login -@websocket_api.async_response -@_ws_handle_cloud_errors @websocket_api.websocket_command( { "type": "cloud/alexa/entities/update", @@ -541,6 +539,8 @@ async def alexa_list(hass, connection, msg): vol.Optional("should_expose"): vol.Any(None, bool), } ) +@websocket_api.async_response +@_ws_handle_cloud_errors async def alexa_update(hass, connection, msg): """Update alexa entity config.""" cloud = hass.data[DOMAIN] @@ -557,8 +557,8 @@ async def alexa_update(hass, connection, msg): @websocket_api.require_admin @_require_cloud_login -@websocket_api.async_response @websocket_api.websocket_command({"type": "cloud/alexa/sync"}) +@websocket_api.async_response async def alexa_sync(hass, connection, msg): """Sync with Alexa.""" cloud = hass.data[DOMAIN] @@ -581,8 +581,8 @@ async def alexa_sync(hass, connection, msg): connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") -@websocket_api.async_response @websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) +@websocket_api.async_response async def thingtalk_convert(hass, connection, msg): """Convert a query.""" cloud = hass.data[DOMAIN] diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index bb76ca3b669..edd0e5ddda4 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -283,6 +283,7 @@ class CloudPreferences: user = await self._hass.auth.async_create_system_user( "Home Assistant Cloud", [GROUP_ID_ADMIN] ) + assert user is not None await self.async_update(cloud_user=user.id) return user.id diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 715a8119af9..5908a0ac816 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -9,13 +9,17 @@ from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> dict[str, Any]: """Serialize an aiohttp response to a dictionary.""" if (body := response.body) is None: - pass + body_decoded = None elif isinstance(body, payload.StringPayload): # pylint: disable=protected-access - body = body._value.decode(body.encoding) + body_decoded = body._value.decode(body.encoding) elif isinstance(body, bytes): - body = body.decode(response.charset or "utf-8") + body_decoded = body.decode(response.charset or "utf-8") else: raise ValueError("Unknown payload encoding") - return {"status": response.status, "body": body, "headers": dict(response.headers)} + return { + "status": response.status, + "body": body_decoded, + "headers": dict(response.headers), + } diff --git a/mypy.ini b/mypy.ini index 4e374c8b1ae..9c7c481e986 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1670,9 +1670,6 @@ ignore_errors = true [mypy-homeassistant.components.climacell.*] ignore_errors = true -[mypy-homeassistant.components.cloud.*] -ignore_errors = true - [mypy-homeassistant.components.config.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 7d289984335..cf1faadc72e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -17,7 +17,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.awair.*", "homeassistant.components.blueprint.*", "homeassistant.components.climacell.*", - "homeassistant.components.cloud.*", "homeassistant.components.config.*", "homeassistant.components.conversation.*", "homeassistant.components.deconz.*", From 7e5316eb862d092a6c165cb1af6a11c9a495ec1a Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 17 Nov 2021 10:15:19 +0100 Subject: [PATCH 0555/1452] Bump velbusaio to 2021.11.7 (#59817) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 5fb3c58c3c7..63d74536378 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.11.6"], + "requirements": ["velbus-aio==2021.11.7"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1aacb26c9f0..d6eadd37ee9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ vallox-websocket-api==2.8.1 vehicle==0.2.0 # homeassistant.components.velbus -velbus-aio==2021.11.6 +velbus-aio==2021.11.7 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d01ed04eb5..b035e1faba3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ uvcclient==0.11.0 vehicle==0.2.0 # homeassistant.components.velbus -velbus-aio==2021.11.6 +velbus-aio==2021.11.7 # homeassistant.components.venstar venstarcolortouch==0.15 From 5133269e2bf2e277e869aa3b6aa88dd6c6cd8658 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Nov 2021 11:19:09 +0100 Subject: [PATCH 0556/1452] Upgrade black to 21.11b0 (#59823) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9f42ed17ab..3342f77cd8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 21.11b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 319b6478655..5e2ae42ca4a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.10b0 +black==21.11b0 codespell==2.0.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 From ac96c7bb1fa2d29eb47ca3d351b6b1bcd838f1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Wed, 17 Nov 2021 11:28:18 +0100 Subject: [PATCH 0557/1452] Move creation of DeviceInfo outside try statement in SMA (#59821) --- homeassistant/components/sma/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 6a7243b0343..06be21d7ac6 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -145,14 +145,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: # Get updated device info sma_device_info = await sma.device_info() - device_info = DeviceInfo( - configuration_url=url, - identifiers={(DOMAIN, entry.unique_id)}, - manufacturer=sma_device_info["manufacturer"], - model=sma_device_info["type"], - name=sma_device_info["name"], - sw_version=sma_device_info["sw_version"], - ) # Get all device sensors sensor_def = await sma.get_sensors() except ( @@ -161,6 +153,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) as exc: raise ConfigEntryNotReady from exc + # Create DeviceInfo object from sma_device_info + device_info = DeviceInfo( + configuration_url=url, + identifiers={(DOMAIN, entry.unique_id)}, + manufacturer=sma_device_info["manufacturer"], + model=sma_device_info["type"], + name=sma_device_info["name"], + sw_version=sma_device_info["sw_version"], + ) + # Parse legacy options if initial setup was done from yaml if entry.source == SOURCE_IMPORT: config_sensors = _parse_legacy_options(entry, sensor_def) From 3a7a4e8ffc27789c2ddb3e010d838b795042f8ef Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Nov 2021 11:49:12 +0100 Subject: [PATCH 0558/1452] Fix Netgear init error on orbi models (#59799) * fix Netgear init error on orbi models * Update sensor.py --- homeassistant/components/netgear/router.py | 2 ++ homeassistant/components/netgear/sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 7339b9ea1ac..40e26128d8d 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -172,6 +172,8 @@ class NetgearRouter: "link_rate": None, "signal": None, "ip": None, + "ssid": None, + "conn_ap_mac": None, } await self.async_update_device_trackers() diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index d8e81cd639f..9762a777194 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -65,7 +65,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): self.entity_description = SENSOR_TYPES[self._attribute] self._name = f"{self.get_device_name()} {self.entity_description.name}" self._unique_id = f"{self._mac}-{self._attribute}" - self._state = self._device[self._attribute] + self._state = self._device.get(self._attribute) @property def native_value(self): @@ -77,7 +77,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): """Update the Netgear device.""" self._device = self._router.devices[self._mac] self._active = self._device["active"] - if self._device[self._attribute] is not None: + if self._device.get(self._attribute) is not None: self._state = self._device[self._attribute] self.async_write_ha_state() From 0f64e7036f1ab9bdcde21d9e5a3876b2543a3702 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 17 Nov 2021 05:56:48 -0500 Subject: [PATCH 0559/1452] Bump zwave-js-server-python to 0.32.0 (#59727) --- homeassistant/components/zwave_js/const.py | 1 - .../components/zwave_js/discovery_data_template.py | 3 --- homeassistant/components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/sensor.py | 7 ------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 3 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 977055485da..2d16c0113c9 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -109,7 +109,6 @@ ENTITY_DESC_KEY_PRESSURE = "pressure" ENTITY_DESC_KEY_SIGNAL_STRENGTH = "signal_strength" ENTITY_DESC_KEY_TEMPERATURE = "temperature" ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature" -ENTITY_DESC_KEY_TIMESTAMP = "timestamp" ENTITY_DESC_KEY_MEASUREMENT = "measurement" ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index a550c683ea2..ea05005deae 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -26,7 +26,6 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( PRESSURE_SENSORS, SIGNAL_STRENGTH_SENSORS, TEMPERATURE_SENSORS, - TIMESTAMP_SENSORS, VOLTAGE_SENSORS, MultilevelSensorType, ) @@ -53,7 +52,6 @@ from .const import ( ENTITY_DESC_KEY_SIGNAL_STRENGTH, ENTITY_DESC_KEY_TARGET_TEMPERATURE, ENTITY_DESC_KEY_TEMPERATURE, - ENTITY_DESC_KEY_TIMESTAMP, ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_VOLTAGE, ) @@ -77,7 +75,6 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { ENTITY_DESC_KEY_PRESSURE: PRESSURE_SENSORS, ENTITY_DESC_KEY_SIGNAL_STRENGTH: SIGNAL_STRENGTH_SENSORS, ENTITY_DESC_KEY_TEMPERATURE: TEMPERATURE_SENSORS, - ENTITY_DESC_KEY_TIMESTAMP: TIMESTAMP_SENSORS, ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS, } diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 50e0a039488..a9fe3d6e4fb 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.31.3"], + "requirements": ["zwave-js-server-python==0.32.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index d8cf32d0f88..b8b8cb4b978 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -38,7 +38,6 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, TEMP_CELSIUS, @@ -71,7 +70,6 @@ from .const import ( ENTITY_DESC_KEY_SIGNAL_STRENGTH, ENTITY_DESC_KEY_TARGET_TEMPERATURE, ENTITY_DESC_KEY_TEMPERATURE, - ENTITY_DESC_KEY_TIMESTAMP, ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_VOLTAGE, SERVICE_RESET_METER, @@ -172,11 +170,6 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, ZwaveSensorEntityDescription] = { device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_TIMESTAMP: ZwaveSensorEntityDescription( - ENTITY_DESC_KEY_TIMESTAMP, - device_class=DEVICE_CLASS_TIMESTAMP, - state_class=STATE_CLASS_MEASUREMENT, - ), ENTITY_DESC_KEY_TARGET_TEMPERATURE: ZwaveSensorEntityDescription( ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE, diff --git a/requirements_all.txt b/requirements_all.txt index d6eadd37ee9..1edfbd6a25f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2501,4 +2501,4 @@ zigpy==0.41.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.3 +zwave-js-server-python==0.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b035e1faba3..1184441e6d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1469,4 +1469,4 @@ zigpy-znp==0.6.1 zigpy==0.41.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.3 +zwave-js-server-python==0.32.0 From 0ab3b10aed46a5d80f515bdb705343f26ad322f2 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 17 Nov 2021 12:31:32 +0100 Subject: [PATCH 0560/1452] Allow selection of statistics state characteristic (#49960) * Make statistics state characteristic selectable * Move computation in helper function * Add relevant config elements for clarity * Rename variables for better readability * Avoid reserved prefix ATTR_ for stats * Fix NoneType base_unit error * Add testcases for statistics characteristic * Add testcases for state_class, unitless, and characteristics * Add testcase coverage for no unit with binary * Replace error catching by an exception * Attend to review comments --- homeassistant/components/statistics/sensor.py | 368 +++++++++++------- tests/components/statistics/test_sensor.py | 191 ++++++++- 2 files changed, 403 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 5b0ad3765b1..92fc682196b 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -8,12 +8,15 @@ import voluptuous as vol from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, CONF_NAME, - EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -24,36 +27,37 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.start import async_at_start from homeassistant.util import dt as dt_util from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -ATTR_AVERAGE_CHANGE = "average_change" -ATTR_CHANGE = "change" -ATTR_CHANGE_RATE = "change_rate" -ATTR_COUNT = "count" -ATTR_MAX_AGE = "max_age" -ATTR_MAX_VALUE = "max_value" -ATTR_MEAN = "mean" -ATTR_MEDIAN = "median" -ATTR_MIN_AGE = "min_age" -ATTR_MIN_VALUE = "min_value" -ATTR_QUANTILES = "quantiles" -ATTR_SAMPLING_SIZE = "sampling_size" -ATTR_STANDARD_DEVIATION = "standard_deviation" -ATTR_TOTAL = "total" -ATTR_VARIANCE = "variance" +STAT_AVERAGE_CHANGE = "average_change" +STAT_CHANGE = "change" +STAT_CHANGE_RATE = "change_rate" +STAT_COUNT = "count" +STAT_MAX_AGE = "max_age" +STAT_MAX_VALUE = "max_value" +STAT_MEAN = "mean" +STAT_MEDIAN = "median" +STAT_MIN_AGE = "min_age" +STAT_MIN_VALUE = "min_value" +STAT_QUANTILES = "quantiles" +STAT_STANDARD_DEVIATION = "standard_deviation" +STAT_TOTAL = "total" +STAT_VARIANCE = "variance" -CONF_SAMPLING_SIZE = "sampling_size" +CONF_STATE_CHARACTERISTIC = "state_characteristic" +CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" CONF_PRECISION = "precision" CONF_QUANTILE_INTERVALS = "quantile_intervals" CONF_QUANTILE_METHOD = "quantile_method" DEFAULT_NAME = "Stats" -DEFAULT_SIZE = 20 +DEFAULT_BUFFER_SIZE = 20 DEFAULT_PRECISION = 2 DEFAULT_QUANTILE_INTERVALS = 4 DEFAULT_QUANTILE_METHOD = "exclusive" @@ -63,9 +67,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SAMPLING_SIZE, default=DEFAULT_SIZE): vol.All( - vol.Coerce(int), vol.Range(min=1) + vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_MEAN): vol.In( + [ + STAT_AVERAGE_CHANGE, + STAT_CHANGE, + STAT_CHANGE_RATE, + STAT_COUNT, + STAT_MAX_AGE, + STAT_MAX_VALUE, + STAT_MEAN, + STAT_MEDIAN, + STAT_MIN_AGE, + STAT_MIN_VALUE, + STAT_QUANTILES, + STAT_STANDARD_DEVIATION, + STAT_TOTAL, + STAT_VARIANCE, + ] ), + vol.Optional( + CONF_SAMPLES_MAX_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE + ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_MAX_AGE): cv.time_period, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), vol.Optional( @@ -83,29 +105,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - entity_id = config.get(CONF_ENTITY_ID) - name = config.get(CONF_NAME) - sampling_size = config.get(CONF_SAMPLING_SIZE) - max_age = config.get(CONF_MAX_AGE) - precision = config.get(CONF_PRECISION) - quantile_intervals = config.get(CONF_QUANTILE_INTERVALS) - quantile_method = config.get(CONF_QUANTILE_METHOD) - async_add_entities( [ StatisticsSensor( - entity_id, - name, - sampling_size, - max_age, - precision, - quantile_intervals, - quantile_method, + source_entity_id=config.get(CONF_ENTITY_ID), + name=config.get(CONF_NAME), + state_characteristic=config.get(CONF_STATE_CHARACTERISTIC), + samples_max_buffer_size=config.get(CONF_SAMPLES_MAX_BUFFER_SIZE), + samples_max_age=config.get(CONF_MAX_AGE), + precision=config.get(CONF_PRECISION), + quantile_intervals=config.get(CONF_QUANTILE_INTERVALS), + quantile_method=config.get(CONF_QUANTILE_METHOD), ) ], True, ) - return True @@ -114,33 +128,45 @@ class StatisticsSensor(SensorEntity): def __init__( self, - entity_id, + source_entity_id, name, - sampling_size, - max_age, + state_characteristic, + samples_max_buffer_size, + samples_max_age, precision, quantile_intervals, quantile_method, ): """Initialize the Statistics sensor.""" - self._entity_id = entity_id - self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" + self._source_entity_id = source_entity_id + self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name self._available = False - self._sampling_size = sampling_size - self._max_age = max_age + self._state_characteristic = state_characteristic + self._samples_max_buffer_size = samples_max_buffer_size + self._samples_max_age = samples_max_age self._precision = precision self._quantile_intervals = quantile_intervals self._quantile_method = quantile_method self._unit_of_measurement = None - self.states = deque(maxlen=self._sampling_size) - self.ages = deque(maxlen=self._sampling_size) - - self.count = 0 - self.mean = self.median = self.quantiles = self.stdev = self.variance = None - self.total = self.min = self.max = None - self.min_age = self.max_age = None - self.change = self.average_change = self.change_rate = None + self.states = deque(maxlen=self._samples_max_buffer_size) + self.ages = deque(maxlen=self._samples_max_buffer_size) + self.attr = { + STAT_COUNT: 0, + STAT_TOTAL: None, + STAT_MEAN: None, + STAT_MEDIAN: None, + STAT_STANDARD_DEVIATION: None, + STAT_VARIANCE: None, + STAT_MIN_VALUE: None, + STAT_MAX_VALUE: None, + STAT_MIN_AGE: None, + STAT_MAX_AGE: None, + STAT_CHANGE: None, + STAT_AVERAGE_CHANGE: None, + STAT_CHANGE_RATE: None, + STAT_QUANTILES: None, + } self._update_listener = None async def async_added_to_hass(self): @@ -151,9 +177,7 @@ class StatisticsSensor(SensorEntity): """Handle the sensor state changes.""" if (new_state := event.data.get("new_state")) is None: return - self._add_state_to_queue(new_state) - self.async_schedule_update_ha_state(True) @callback @@ -163,17 +187,16 @@ class StatisticsSensor(SensorEntity): self.async_on_remove( async_track_state_change_event( - self.hass, [self._entity_id], async_stats_sensor_state_listener + self.hass, + [self._source_entity_id], + async_stats_sensor_state_listener, ) ) if "recorder" in self.hass.config.components: - # Only use the database if it's configured self.hass.async_create_task(self._initialize_from_database()) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_stats_sensor_startup - ) + async_at_start(self.hass, async_stats_sensor_startup) def _add_state_to_queue(self, new_state): """Add the state to the queue.""" @@ -195,27 +218,75 @@ class StatisticsSensor(SensorEntity): ) return - self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._unit_of_measurement = self._derive_unit_of_measurement(new_state) + + def _derive_unit_of_measurement(self, new_state): + base_unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if not base_unit: + unit = None + elif self.is_binary: + unit = None + elif self._state_characteristic in ( + STAT_COUNT, + STAT_MIN_AGE, + STAT_MAX_AGE, + STAT_QUANTILES, + ): + unit = None + elif self._state_characteristic in ( + STAT_TOTAL, + STAT_MEAN, + STAT_MEDIAN, + STAT_STANDARD_DEVIATION, + STAT_MIN_VALUE, + STAT_MAX_VALUE, + STAT_CHANGE, + ): + unit = base_unit + elif self._state_characteristic == STAT_VARIANCE: + unit = base_unit + "²" + elif self._state_characteristic == STAT_AVERAGE_CHANGE: + unit = base_unit + "/sample" + elif self._state_characteristic == STAT_CHANGE_RATE: + unit = base_unit + "/s" + return unit @property def name(self): """Return the name of the sensor.""" return self._name + @property + def state_class(self): + """Return the state class of this entity.""" + if self._state_characteristic in ( + STAT_MIN_AGE, + STAT_MAX_AGE, + STAT_QUANTILES, + ): + return None + return STATE_CLASS_MEASUREMENT + @property def native_value(self): """Return the state of the sensor.""" if self.is_binary: - return self.count + return self.attr[STAT_COUNT] + if self._state_characteristic in ( + STAT_MIN_AGE, + STAT_MAX_AGE, + STAT_QUANTILES, + ): + return self.attr[self._state_characteristic] if self._precision == 0: with contextlib.suppress(TypeError, ValueError): - return int(self.mean) - return self.mean + return int(self.attr[self._state_characteristic]) + return self.attr[self._state_characteristic] @property 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 + return self._unit_of_measurement @property def available(self): @@ -230,24 +301,9 @@ class StatisticsSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" - if not self.is_binary: - return { - ATTR_SAMPLING_SIZE: self._sampling_size, - ATTR_COUNT: self.count, - ATTR_MEAN: self.mean, - ATTR_MEDIAN: self.median, - ATTR_QUANTILES: self.quantiles, - ATTR_STANDARD_DEVIATION: self.stdev, - ATTR_VARIANCE: self.variance, - ATTR_TOTAL: self.total, - ATTR_MIN_VALUE: self.min, - ATTR_MAX_VALUE: self.max, - ATTR_MIN_AGE: self.min_age, - ATTR_MAX_AGE: self.max_age, - ATTR_CHANGE: self.change, - ATTR_AVERAGE_CHANGE: self.average_change, - ATTR_CHANGE_RATE: self.change_rate, - } + if self.is_binary: + return None + return self.attr @property def icon(self): @@ -255,17 +311,17 @@ class StatisticsSensor(SensorEntity): return ICON def _purge_old(self): - """Remove states which are older than self._max_age.""" + """Remove states which are older than self._samples_max_age.""" now = dt_util.utcnow() _LOGGER.debug( "%s: purging records older then %s(%s)", self.entity_id, - dt_util.as_local(now - self._max_age), - self._max_age, + dt_util.as_local(now - self._samples_max_age), + self._samples_max_age, ) - while self.ages and (now - self.ages[0]) > self._max_age: + while self.ages and (now - self.ages[0]) > self._samples_max_age: _LOGGER.debug( "%s: purging record with datetime %s(%s)", self.entity_id, @@ -277,73 +333,91 @@ class StatisticsSensor(SensorEntity): def _next_to_purge_timestamp(self): """Find the timestamp when the next purge would occur.""" - if self.ages and self._max_age: + if self.ages and self._samples_max_age: # Take the oldest entry from the ages list and add the configured max_age. # If executed after purging old states, the result is the next timestamp # in the future when the oldest state will expire. - return self.ages[0] + self._max_age + return self.ages[0] + self._samples_max_age return None + def _update_characteristics(self): + """Calculate and update the various statistical characteristics.""" + states_count = len(self.states) + self.attr[STAT_COUNT] = states_count + + if self.is_binary: + return + + if states_count >= 2: + self.attr[STAT_STANDARD_DEVIATION] = round( + statistics.stdev(self.states), self._precision + ) + self.attr[STAT_VARIANCE] = round( + statistics.variance(self.states), self._precision + ) + else: + self.attr[STAT_STANDARD_DEVIATION] = STATE_UNKNOWN + self.attr[STAT_VARIANCE] = STATE_UNKNOWN + + if states_count > self._quantile_intervals: + self.attr[STAT_QUANTILES] = [ + round(quantile, self._precision) + for quantile in statistics.quantiles( + self.states, + n=self._quantile_intervals, + method=self._quantile_method, + ) + ] + else: + self.attr[STAT_QUANTILES] = STATE_UNKNOWN + + if states_count == 0: + self.attr[STAT_MEAN] = STATE_UNKNOWN + self.attr[STAT_MEDIAN] = STATE_UNKNOWN + self.attr[STAT_TOTAL] = STATE_UNKNOWN + self.attr[STAT_MIN_VALUE] = self.attr[STAT_MAX_VALUE] = STATE_UNKNOWN + self.attr[STAT_MIN_AGE] = self.attr[STAT_MAX_AGE] = STATE_UNKNOWN + self.attr[STAT_CHANGE] = self.attr[STAT_AVERAGE_CHANGE] = STATE_UNKNOWN + self.attr[STAT_CHANGE_RATE] = STATE_UNKNOWN + return + + self.attr[STAT_MEAN] = round(statistics.mean(self.states), self._precision) + self.attr[STAT_MEDIAN] = round(statistics.median(self.states), self._precision) + + self.attr[STAT_TOTAL] = round(sum(self.states), self._precision) + self.attr[STAT_MIN_VALUE] = round(min(self.states), self._precision) + self.attr[STAT_MAX_VALUE] = round(max(self.states), self._precision) + + self.attr[STAT_MIN_AGE] = self.ages[0] + self.attr[STAT_MAX_AGE] = self.ages[-1] + + self.attr[STAT_CHANGE] = self.states[-1] - self.states[0] + + self.attr[STAT_AVERAGE_CHANGE] = self.attr[STAT_CHANGE] + self.attr[STAT_CHANGE_RATE] = 0 + if states_count > 1: + self.attr[STAT_AVERAGE_CHANGE] /= len(self.states) - 1 + + time_diff = ( + self.attr[STAT_MAX_AGE] - self.attr[STAT_MIN_AGE] + ).total_seconds() + if time_diff > 0: + self.attr[STAT_CHANGE_RATE] = self.attr[STAT_CHANGE] / time_diff + self.attr[STAT_CHANGE] = round(self.attr[STAT_CHANGE], self._precision) + self.attr[STAT_AVERAGE_CHANGE] = round( + self.attr[STAT_AVERAGE_CHANGE], self._precision + ) + self.attr[STAT_CHANGE_RATE] = round( + self.attr[STAT_CHANGE_RATE], self._precision + ) + async def async_update(self): """Get the latest data and updates the states.""" _LOGGER.debug("%s: updating statistics", self.entity_id) - if self._max_age is not None: + if self._samples_max_age is not None: self._purge_old() - self.count = len(self.states) - - if not self.is_binary: - try: # require only one data point - self.mean = round(statistics.mean(self.states), self._precision) - self.median = round(statistics.median(self.states), self._precision) - except statistics.StatisticsError as err: - _LOGGER.debug("%s: %s", self.entity_id, err) - self.mean = self.median = STATE_UNKNOWN - - try: # require at least two data points - self.stdev = round(statistics.stdev(self.states), self._precision) - self.variance = round(statistics.variance(self.states), self._precision) - if self._quantile_intervals < self.count: - self.quantiles = [ - round(quantile, self._precision) - for quantile in statistics.quantiles( - self.states, - n=self._quantile_intervals, - method=self._quantile_method, - ) - ] - except statistics.StatisticsError as err: - _LOGGER.debug("%s: %s", self.entity_id, err) - self.stdev = self.variance = self.quantiles = STATE_UNKNOWN - - if self.states: - self.total = round(sum(self.states), self._precision) - self.min = round(min(self.states), self._precision) - self.max = round(max(self.states), self._precision) - - self.min_age = self.ages[0] - self.max_age = self.ages[-1] - - self.change = self.states[-1] - self.states[0] - self.average_change = self.change - self.change_rate = 0 - - if len(self.states) > 1: - self.average_change /= len(self.states) - 1 - - time_diff = (self.max_age - self.min_age).total_seconds() - if time_diff > 0: - self.change_rate = self.change / time_diff - - self.change = round(self.change, self._precision) - self.average_change = round(self.average_change, self._precision) - self.change_rate = round(self.change_rate, self._precision) - - else: - self.total = self.min = self.max = STATE_UNKNOWN - self.min_age = self.max_age = dt_util.utcnow() - self.change = self.average_change = STATE_UNKNOWN - self.change_rate = STATE_UNKNOWN + self._update_characteristics() # If max_age is set, ensure to update again after the defined interval. next_to_purge_timestamp = self._next_to_purge_timestamp() @@ -381,11 +455,11 @@ class StatisticsSensor(SensorEntity): with session_scope(hass=self.hass) as session: query = session.query(States).filter( - States.entity_id == self._entity_id.lower() + States.entity_id == self._source_entity_id.lower() ) - if self._max_age is not None: - records_older_then = dt_util.utcnow() - self._max_age + if self._samples_max_age is not None: + records_older_then = dt_util.utcnow() - self._samples_max_age _LOGGER.debug( "%s: retrieve records not older then %s", self.entity_id, @@ -396,7 +470,7 @@ class StatisticsSensor(SensorEntity): _LOGGER.debug("%s: retrieving all records", self.entity_id) query = query.order_by(States.last_updated.desc()).limit( - self._sampling_size + self._samples_max_buffer_size ) states = execute(query, to_native=True, validate_entity_ids=False) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 0aa1c116991..4412dad843a 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components import recorder +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.statistics.sensor import DOMAIN, StatisticsSensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -64,11 +65,18 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "binary_sensor.test_monitored", - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "binary_sensor.test_monitored", + }, + { + "platform": "statistics", + "name": "test_unitless", + "entity_id": "binary_sensor.test_monitored_unitless", + }, + ] }, ) @@ -77,12 +85,21 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() for value in values: - self.hass.states.set("binary_sensor.test_monitored", value) + self.hass.states.set( + "binary_sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set("binary_sensor.test_monitored_unitless", value) self.hass.block_till_done() state = self.hass.states.get("sensor.test") + assert state.state == str(len(values)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - assert str(len(values)) == state.state + state = self.hass.states.get("sensor.test_unitless") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None def test_sensor_source(self): """Test if source is a sensor.""" @@ -121,17 +138,18 @@ class TestStatisticsSensor(unittest.TestCase): assert self.mean == state.attributes.get("mean") assert self.count == state.attributes.get("count") assert self.total == state.attributes.get("total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert self.change == state.attributes.get("change") assert self.average_change == state.attributes.get("average_change") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + # Source sensor turns unavailable, then available with valid value, # statistics sensor should follow state = self.hass.states.get("sensor.test") self.hass.states.set( "sensor.test_monitored", STATE_UNAVAILABLE, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") @@ -445,6 +463,161 @@ class TestStatisticsSensor(unittest.TestCase): state = self.hass.states.get("sensor.test") assert state.state == str(round(sum(self.values) / len(self.values), 1)) + def test_state_characteristic_unit(self): + """Test statistics characteristic selection (via config).""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_min_age", + "entity_id": "sensor.test_monitored", + "state_characteristic": "min_age", + }, + { + "platform": "statistics", + "name": "test_variance", + "entity_id": "sensor.test_monitored", + "state_characteristic": "variance", + }, + { + "platform": "statistics", + "name": "test_average_change", + "entity_id": "sensor.test_monitored", + "state_characteristic": "average_change", + }, + { + "platform": "statistics", + "name": "test_change_rate", + "entity_id": "sensor.test_monitored", + "state_characteristic": "change_rate", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set( + "sensor.test_monitored_unitless", + value, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_min_age") + assert state.state == str(state.attributes.get("min_age")) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_variance") + assert state.state == str(state.attributes.get("variance")) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "²" + state = self.hass.states.get("sensor.test_average_change") + assert state.state == str(state.attributes.get("average_change")) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "/sample" + ) + state = self.hass.states.get("sensor.test_change_rate") + assert state.state == str(state.attributes.get("change_rate")) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "/s" + + def test_state_class(self): + """Test state class, which depends on the characteristic configured.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_normal", + "entity_id": "sensor.test_monitored", + "state_characteristic": "count", + }, + { + "platform": "statistics", + "name": "test_nan", + "entity_id": "sensor.test_monitored", + "state_characteristic": "min_age", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_normal") + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = self.hass.states.get("sensor.test_nan") + assert state.attributes.get(ATTR_STATE_CLASS) is None + + def test_unitless_source_sensor(self): + """Statistics for a unitless source sensor should never have a unit.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_unitless_1", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "count", + }, + { + "platform": "statistics", + "name": "test_unitless_2", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_unitless_3", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "change_rate", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored_unitless", + value, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_unitless_1") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_2") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_3") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + def test_initialize_from_database(self): """Test initializing the statistics from the database.""" # enable the recorder From b6dea3c6cb291c7df9282eb46e617a25e7d69c80 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 17 Nov 2021 12:53:44 +0100 Subject: [PATCH 0561/1452] Add more zwave_js binary sensor descriptions (#59474) --- .../components/zwave_js/binary_sensor.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index af094407359..5d91e9b8d93 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -148,6 +148,13 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = states=("1", "2", "3", "4"), device_class=DEVICE_CLASS_LOCK, ), + NotificationZWaveJSEntityDescription( + # NotificationType 6: Access Control - State Id's 11 (Lock jammed) + key=NOTIFICATION_ACCESS_CONTROL, + states=("11",), + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id 22 (door/window open) key=NOTIFICATION_ACCESS_CONTROL, @@ -189,6 +196,14 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = device_class=DEVICE_CLASS_PLUG, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + NotificationZWaveJSEntityDescription( + # NotificationType 8: Power Management - + # State Id's 6, 7, 8, 9 (power status) + key=NOTIFICATION_POWER_MANAGEMENT, + states=("6", "7", "8", "9"), + device_class=DEVICE_CLASS_SAFETY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), NotificationZWaveJSEntityDescription( # NotificationType 8: Power Management - # State Id's 10, 11, 17 (Battery maintenance status) @@ -198,10 +213,11 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( - # NotificationType 9: System - State Id's 1, 2, 6, 7 + # NotificationType 9: System - State Id's 1, 2, 3, 4, 6, 7 key=NOTIFICATION_SYSTEM, - states=("1", "2", "6", "7"), + states=("1", "2", "3", "4", "6", "7"), device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 10: Emergency - State Id's 1, 2, 3 From edbe54c34616a5172ad523354c5142fbd7e36ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 17 Nov 2021 14:47:22 +0100 Subject: [PATCH 0562/1452] Prefer YAML config mode in Lovelace system_health (#59835) --- homeassistant/components/lovelace/__init__.py | 1 + homeassistant/components/lovelace/system_health.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index d8fe591a0ba..6f5de83fd30 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -140,6 +140,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = { # We store a dictionary mapping url_path: config. None is the default. + "mode": mode, "dashboards": {None: default_config}, "resources": resource_collection, "yaml_dashboards": config[DOMAIN].get(CONF_DASHBOARDS, {}), diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index 29b53251f21..96ae2f47540 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -38,7 +38,9 @@ async def system_health_info(hass): else: health_info[key] = dashboard[key] - if MODE_STORAGE in modes: + if hass.data[DOMAIN][CONF_MODE] == MODE_YAML: + health_info[CONF_MODE] = MODE_YAML + elif MODE_STORAGE in modes: health_info[CONF_MODE] = MODE_STORAGE elif MODE_YAML in modes: health_info[CONF_MODE] = MODE_YAML From 29e0ef604e5effdd917bfcf851f56d0d71559a3a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 17 Nov 2021 15:08:37 +0100 Subject: [PATCH 0563/1452] Add typing to deCONZ Climate and Cover platforms (#59610) --- homeassistant/components/deconz/climate.py | 50 +++++++++++++++------- homeassistant/components/deconz/cover.py | 50 ++++++++++++++-------- tests/components/deconz/test_climate.py | 4 +- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 4641bd26d43..85ab4b17a1e 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,6 +1,9 @@ """Support for deCONZ climate devices.""" from __future__ import annotations +from collections.abc import ValuesView +from typing import Any + from pydeconz.sensor import ( THERMOSTAT_FAN_MODE_AUTO, THERMOSTAT_FAN_MODE_HIGH, @@ -42,13 +45,15 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" @@ -89,7 +94,11 @@ PRESET_MODE_TO_DECONZ = { DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ climate devices. Thermostats are based on the same device class as sensors in deCONZ. @@ -98,9 +107,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_climate(sensors=gateway.api.sensors.values()): + def async_add_climate( + sensors: list[Thermostat] + | ValuesView[Thermostat] = gateway.api.sensors.values(), + ) -> None: """Add climate devices from deCONZ.""" - entities = [] + entities: list[DeconzThermostat] = [] for sensor in sensors: @@ -131,9 +143,11 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" TYPE = DOMAIN + _device: Thermostat + _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, device, gateway): + def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None: """Set up thermostat device.""" super().__init__(device, gateway) @@ -167,7 +181,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): ) @property - def fan_modes(self) -> list: + def fan_modes(self) -> list[str]: """Return the list of available fan operation modes.""" return list(FAN_MODE_TO_DECONZ) @@ -181,7 +195,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): # HVAC control @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -192,7 +206,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): ) @property - def hvac_modes(self) -> list: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(self._hvac_mode_to_deconz) @@ -231,16 +245,20 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def current_temperature(self) -> float: """Return the current temperature.""" - return self._device.temperature + return self._device.temperature # type: ignore[no-any-return] @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return the target temperature.""" - if self._device.mode == THERMOSTAT_MODE_COOL: - return self._device.cooling_setpoint - return self._device.heating_setpoint + if self._device.mode == THERMOSTAT_MODE_COOL and self._device.cooling_setpoint: + return self._device.cooling_setpoint # type: ignore[no-any-return] - async def async_set_temperature(self, **kwargs): + if self._device.heating_setpoint: + return self._device.heating_setpoint # type: ignore[no-any-return] + + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") @@ -252,7 +270,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): await self._device.set_config(**data) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool | int]: """Return the state attributes of the thermostat.""" attr = {} diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 5cf90c4dca1..4c40e89b9f3 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,5 +1,10 @@ """Support for deCONZ covers.""" +from __future__ import annotations + +from collections.abc import ValuesView +from typing import Any + from pydeconz.light import Cover from homeassistant.components.cover import ( @@ -18,11 +23,13 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.core import callback +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 .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DEVICE_CLASS = { "Level controllable output": DEVICE_CLASS_DAMPER, @@ -31,13 +38,19 @@ DEVICE_CLASS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up covers for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_cover(lights=gateway.api.lights.values()): + def async_add_cover( + lights: list[Cover] | ValuesView[Cover] = gateway.api.lights.values(), + ) -> None: """Add cover from deCONZ.""" entities = [] @@ -66,8 +79,9 @@ class DeconzCover(DeconzDevice, CoverEntity): """Representation of a deCONZ cover.""" TYPE = DOMAIN + _device: Cover - def __init__(self, device, gateway): + def __init__(self, device: Cover, gateway: DeconzGateway) -> None: """Set up cover device.""" super().__init__(device, gateway) @@ -85,52 +99,52 @@ class DeconzCover(DeconzDevice, CoverEntity): self._attr_device_class = DEVICE_CLASS.get(self._device.type) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of the cover.""" - return 100 - self._device.lift + return 100 - self._device.lift # type: ignore[no-any-return] @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return not self._device.is_open - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: int) -> None: """Move the cover to a specific position.""" position = 100 - kwargs[ATTR_POSITION] await self._device.set_position(lift=position) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" await self._device.open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self._device.close() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" await self._device.stop() @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current tilt position of the cover.""" if self._device.tilt is not None: - return 100 - self._device.tilt + return 100 - self._device.tilt # type: ignore[no-any-return] return None - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: int) -> None: """Tilt the cover to a specific position.""" position = 100 - kwargs[ATTR_TILT_POSITION] await self._device.set_position(tilt=position) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" await self._device.set_position(tilt=0) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" await self._device.set_position(tilt=100) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" await self._device.stop() diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 92ca38fdf4d..3febbae510b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -347,7 +347,7 @@ async def test_climate_device_with_cooling_support( "0": { "config": { "battery": 25, - "coolsetpoint": None, + "coolsetpoint": 1111, "fanmode": None, "heatsetpoint": 2222, "mode": "heat", @@ -398,8 +398,10 @@ async def test_climate_device_with_cooling_support( } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.get("climate.zen_01").state == HVAC_MODE_COOL + assert hass.states.get("climate.zen_01").attributes["temperature"] == 11.1 # Verify service calls From 0339761e7264d77d3955a62bebd02bd380588a68 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 17 Nov 2021 15:11:51 +0100 Subject: [PATCH 0564/1452] Add typing to deCONZ Number and Sensor platforms (#59604) --- homeassistant/components/deconz/number.py | 53 +++++++++++----- homeassistant/components/deconz/sensor.py | 75 ++++++++++++++--------- 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 0ac355f7dd1..6c3ca08710c 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import ValuesView from dataclasses import dataclass from pydeconz.sensor import PRESENCE_DELAY, Presence @@ -11,25 +12,35 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry @dataclass -class DeconzNumberEntityDescription(NumberEntityDescription): +class DeconzNumberEntityDescriptionBase: + """Required values when describing deCONZ number entities.""" + + device_property: str + suffix: str + update_key: str + max_value: int + min_value: int + step: int + + +@dataclass +class DeconzNumberEntityDescription( + NumberEntityDescription, DeconzNumberEntityDescriptionBase +): """Class describing deCONZ number entities.""" entity_category = ENTITY_CATEGORY_CONFIG - device_property: str | None = None - suffix: str | None = None - update_key: str | None = None - max_value: int | None = None - min_value: int | None = None - step: int | None = None ENTITY_DESCRIPTIONS = { @@ -47,13 +58,19 @@ ENTITY_DESCRIPTIONS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ number entity.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors=gateway.api.sensors.values()): + def async_add_sensor( + sensors: list[Presence] | ValuesView[Presence] = gateway.api.sensors.values(), + ) -> None: """Add number config sensor from deCONZ.""" entities = [] @@ -92,13 +109,19 @@ class DeconzNumber(DeconzDevice, NumberEntity): """Representation of a deCONZ number entity.""" TYPE = DOMAIN + _device: Presence - def __init__(self, device, gateway, description): + def __init__( + self, + device: Presence, + gateway: DeconzGateway, + description: DeconzNumberEntityDescription, + ) -> None: """Initialize deCONZ number entity.""" - self.entity_description = description + self.entity_description: DeconzNumberEntityDescription = description super().__init__(device, gateway) - self._attr_name = f"{self._device.name} {description.suffix}" + self._attr_name = f"{device.name} {description.suffix}" self._attr_max_value = description.max_value self._attr_min_value = description.min_value self._attr_step = description.step @@ -113,7 +136,7 @@ class DeconzNumber(DeconzDevice, NumberEntity): @property def value(self) -> float: """Return the value of the sensor property.""" - return getattr(self._device, self.entity_description.device_property) + return getattr(self._device, self.entity_description.device_property) # type: ignore[no-any-return] async def async_set_value(self, value: float) -> None: """Set sensor config.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3f8c22d43d6..e530e33e654 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,9 +1,14 @@ """Support for deCONZ sensors.""" +from __future__ import annotations + +from collections.abc import ValuesView + from pydeconz.sensor import ( AirQuality, Battery, Consumption, Daylight, + DeconzSensor as PydeconzSensor, GenericStatus, Humidity, LightLevel, @@ -22,6 +27,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -40,15 +46,17 @@ from homeassistant.const import ( PRESSURE_HPA, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_SENSORS = ( AirQuality, @@ -119,7 +127,11 @@ ENTITY_DESCRIPTIONS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @@ -127,13 +139,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): battery_handler = DeconzBatteryHandler(gateway) @callback - def async_add_sensor(sensors=gateway.api.sensors.values()): + def async_add_sensor( + sensors: list[PydeconzSensor] + | ValuesView[PydeconzSensor] = gateway.api.sensors.values(), + ) -> None: """Add sensors from deCONZ. Create DeconzBattery if sensor has a battery attribute. Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. """ - entities = [] + entities: list[DeconzBattery | DeconzSensor | DeconzTemperature] = [] for sensor in sensors: @@ -184,8 +199,9 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" TYPE = DOMAIN + _device: PydeconzSensor - def __init__(self, device, gateway): + def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: """Initialize deCONZ binary sensor.""" super().__init__(device, gateway) @@ -193,19 +209,19 @@ class DeconzSensor(DeconzDevice, SensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the sensor's state.""" keys = {"on", "reachable", "state"} if self._device.changed_keys.intersection(keys): super().async_update_callback() @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._device.state + return self._device.state # type: ignore[no-any-return] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool | float | int | None]: """Return the state attributes of the sensor.""" attr = {} @@ -243,8 +259,9 @@ class DeconzTemperature(DeconzDevice, SensorEntity): """ TYPE = DOMAIN + _device: PydeconzSensor - def __init__(self, device, gateway): + def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: """Initialize deCONZ temperature sensor.""" super().__init__(device, gateway) @@ -252,29 +269,30 @@ class DeconzTemperature(DeconzDevice, SensorEntity): self._attr_name = f"{self._device.name} Temperature" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device.""" return f"{self.serial}-temperature" @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the sensor's state.""" keys = {"temperature", "reachable"} if self._device.changed_keys.intersection(keys): super().async_update_callback() @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._device.secondary_temperature + return self._device.secondary_temperature # type: ignore[no-any-return] class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" TYPE = DOMAIN + _device: PydeconzSensor - def __init__(self, device, gateway): + def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: """Initialize deCONZ battery level sensor.""" super().__init__(device, gateway) @@ -282,14 +300,14 @@ class DeconzBattery(DeconzDevice, SensorEntity): self._attr_name = f"{self._device.name} Battery Level" @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the battery's state, if needed.""" keys = {"battery", "reachable"} if self._device.changed_keys.intersection(keys): super().async_update_callback() @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device. Normally there should only be one battery sensor per device from deCONZ. @@ -305,12 +323,12 @@ class DeconzBattery(DeconzDevice, SensorEntity): return f"{self.serial}-battery" @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the battery.""" - return self._device.battery + return self._device.battery # type: ignore[no-any-return] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes of the battery.""" attr = {} @@ -325,21 +343,20 @@ class DeconzBattery(DeconzDevice, SensorEntity): class DeconzSensorStateTracker: """Track sensors without a battery state and signal when battery state exist.""" - def __init__(self, sensor, gateway): + def __init__(self, sensor: PydeconzSensor, gateway: DeconzGateway) -> None: """Set up tracker.""" self.sensor = sensor self.gateway = gateway sensor.register_callback(self.async_update_callback) @callback - def close(self): + def close(self) -> None: """Clean up tracker.""" self.sensor.remove_callback(self.async_update_callback) - self.gateway = None self.sensor = None @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Sensor state updated.""" if "battery" in self.sensor.changed_keys: async_dispatcher_send( @@ -352,13 +369,13 @@ class DeconzSensorStateTracker: class DeconzBatteryHandler: """Creates and stores trackers for sensors without a battery state.""" - def __init__(self, gateway): + def __init__(self, gateway: DeconzGateway) -> None: """Set up battery handler.""" self.gateway = gateway - self._trackers = set() + self._trackers: set[DeconzSensorStateTracker] = set() @callback - def create_tracker(self, sensor): + def create_tracker(self, sensor: PydeconzSensor) -> None: """Create new tracker for battery state.""" for tracker in self._trackers: if sensor == tracker.sensor: @@ -366,7 +383,7 @@ class DeconzBatteryHandler: self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) @callback - def remove_tracker(self, sensor): + def remove_tracker(self, sensor: PydeconzSensor) -> None: """Remove tracker of battery state.""" for tracker in self._trackers: if sensor == tracker.sensor: From 569d5967994fdef83e18cb8a51353ed7cf1b8e0e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 17 Nov 2021 15:22:59 +0100 Subject: [PATCH 0565/1452] Add typing to deCONZ Lock and Logbook platforms (#59605) --- .../components/deconz/device_trigger.py | 27 ++++++++------- homeassistant/components/deconz/lock.py | 32 +++++++++++++----- homeassistant/components/deconz/logbook.py | 33 +++++++++---------- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index d1abbed0928..ae539ee5d48 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,4 +1,7 @@ """Provides device automations for deconz events.""" + +from __future__ import annotations + import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA @@ -14,10 +17,11 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import DOMAIN -from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE +from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, DeconzAlarmEvent, DeconzEvent CONF_SUBTYPE = "subtype" @@ -613,16 +617,19 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -def _get_deconz_event_from_device_id(hass, device_id): - """Resolve deconz event from device id.""" +def _get_deconz_event_from_device( + hass: HomeAssistant, + device: dr.DeviceEntry, +) -> DeconzAlarmEvent | DeconzEvent: + """Resolve deconz event from device.""" for gateway in hass.data.get(DOMAIN, {}).values(): - for deconz_event in gateway.events: - - if device_id == deconz_event.device_id: + if device.id == deconz_event.device_id: return deconz_event - return None + raise InvalidDeviceAutomationConfig( + f'No deconz_event tied to device "{device.name}" found' + ) async def async_validate_trigger_config(hass, config): @@ -658,11 +665,7 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = REMOTES[device.model][trigger] - deconz_event = _get_deconz_event_from_device_id(hass, device.id) - if deconz_event is None: - raise InvalidDeviceAutomationConfig( - f'No deconz_event tied to device "{device.name}" found' - ) + deconz_event = _get_deconz_event_from_device(hass, device) event_id = deconz_event.serial diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index fb344e54176..7bdae3e36ed 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -1,23 +1,36 @@ """Support for deCONZ locks.""" +from __future__ import annotations + +from collections.abc import ValuesView +from typing import Any + from pydeconz.light import Lock from pydeconz.sensor import DoorLock from homeassistant.components.lock import DOMAIN, LockEntity -from homeassistant.core import callback +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 .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up locks for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_lock_from_light(lights=gateway.api.lights.values()): + def async_add_lock_from_light( + lights: list[Lock] | ValuesView[Lock] = gateway.api.lights.values(), + ) -> None: """Add lock from deCONZ.""" entities = [] @@ -41,7 +54,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) @callback - def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()): + def async_add_lock_from_sensor( + sensors: list[DoorLock] | ValuesView[DoorLock] = gateway.api.sensors.values(), + ) -> None: """Add lock from deCONZ.""" entities = [] @@ -72,16 +87,17 @@ class DeconzLock(DeconzDevice, LockEntity): """Representation of a deCONZ lock.""" TYPE = DOMAIN + _device: DoorLock | Lock @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is on.""" - return self._device.is_locked + return self._device.is_locked # type: ignore[no-any-return] - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" await self._device.lock() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" await self._device.unlock() diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 0d7ad67dda6..1c41feda7da 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -5,15 +5,11 @@ from collections.abc import Callable from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.event import Event from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN -from .deconz_event import ( - CONF_DECONZ_ALARM_EVENT, - CONF_DECONZ_EVENT, - DeconzAlarmEvent, - DeconzEvent, -) +from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT from .device_trigger import ( CONF_BOTH_BUTTONS, CONF_BOTTOM_BUTTONS, @@ -57,7 +53,7 @@ from .device_trigger import ( CONF_TURN_OFF, CONF_TURN_ON, REMOTES, - _get_deconz_event_from_device_id, + _get_deconz_event_from_device, ) ACTIONS = { @@ -108,9 +104,11 @@ INTERFACES = { } -def _get_device_event_description(modelid: str, event: str) -> tuple: +def _get_device_event_description( + modelid: str, event: int +) -> tuple[str | None, str | None]: """Get device event description.""" - device_event_descriptions: dict = REMOTES[modelid] + device_event_descriptions = REMOTES[modelid] for event_type_tuple, event_dict in device_event_descriptions.items(): if event == event_dict.get(CONF_EVENT): @@ -124,16 +122,16 @@ def _get_device_event_description(modelid: str, event: str) -> tuple: @callback def async_describe_events( hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict]], None], + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], ) -> None: """Describe logbook events.""" + device_registry = dr.async_get(hass) @callback - def async_describe_deconz_alarm_event(event: Event) -> dict: + def async_describe_deconz_alarm_event(event: Event) -> dict[str, str]: """Describe deCONZ logbook alarm event.""" - deconz_alarm_event: DeconzAlarmEvent | None = _get_deconz_event_from_device_id( - hass, event.data[ATTR_DEVICE_ID] - ) + device = device_registry.devices[event.data[ATTR_DEVICE_ID]] + deconz_alarm_event = _get_deconz_event_from_device(hass, device) data = event.data[CONF_EVENT] @@ -143,11 +141,10 @@ def async_describe_events( } @callback - def async_describe_deconz_event(event: Event) -> dict: + def async_describe_deconz_event(event: Event) -> dict[str, str]: """Describe deCONZ logbook event.""" - deconz_event: DeconzEvent | None = _get_deconz_event_from_device_id( - hass, event.data[ATTR_DEVICE_ID] - ) + device = device_registry.devices[event.data[ATTR_DEVICE_ID]] + deconz_event = _get_deconz_event_from_device(hass, device) action = None interface = None From cac54d8e398dabd946891cc1e9085ab72dd859a3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Nov 2021 15:26:23 +0100 Subject: [PATCH 0566/1452] bump motionblinds to 0.5.8 (#59834) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 7ff87c1b58b..96951f78a5b 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.7"], + "requirements": ["motionblinds==0.5.8"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1edfbd6a25f..26b5f9bff6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ minio==4.0.9 mitemp_bt==0.0.5 # homeassistant.components.motion_blinds -motionblinds==0.5.7 +motionblinds==0.5.8 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1184441e6d0..b6466be2278 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -615,7 +615,7 @@ millheater==0.8.0 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.5.7 +motionblinds==0.5.8 # homeassistant.components.motioneye motioneye-client==0.3.12 From 5ae311b111cdd316de7b4e1af2410a757587bc9f Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 17 Nov 2021 16:05:50 +0100 Subject: [PATCH 0567/1452] Fix invalid string syntax in OwnTracks config flow translations (#59838) --- homeassistant/components/owntracks/translations/hu.json | 2 +- homeassistant/components/owntracks/translations/lb.json | 2 +- homeassistant/components/owntracks/translations/nl.json | 2 +- homeassistant/components/owntracks/translations/pl.json | 2 +- homeassistant/components/owntracks/translations/ru.json | 2 +- homeassistant/components/owntracks/translations/sv.json | 2 +- homeassistant/components/owntracks/translations/uk.json | 2 +- homeassistant/components/owntracks/translations/zh-Hans.json | 2 +- homeassistant/components/owntracks/translations/zh-Hant.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json index 84a40a1a593..e99b11a9e7e 100644 --- a/homeassistant/components/owntracks/translations/hu.json +++ b/homeassistant/components/owntracks/translations/hu.json @@ -4,7 +4,7 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { - "default": "\n\nAndroidon, nyissa meg [az OwnTracks appot]({android_url}), majd v\u00e1lassza ki a Preferences -> Connection men\u00fct. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyissa meg [az OwnTracks appot]({ios_url}), kattintson az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zze meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." + "default": "\n\nAndroidon, nyissa meg [az OwnTracks appot]({android_url}), majd v\u00e1lassza ki a Preferences -> Connection men\u00fct. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS-en, nyissa meg [az OwnTracks appot]({ios_url}), kattintson az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nN\u00e9zze meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/lb.json b/homeassistant/components/owntracks/translations/lb.json index f2e9ea664d3..a5fa4ddc2f7 100644 --- a/homeassistant/components/owntracks/translations/lb.json +++ b/homeassistant/components/owntracks/translations/lb.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." }, "create_entry": { - "default": "\n\nOp Android, an [der OwnTracks App]({android_url}), g\u00e9i an Preferences -> Connection. \u00c4nnert folgend Astellungen:\n- Mode: Private HTTP\n- Host {webhook_url}\n- Identification:\n - Username: ``\n - Device ID: ``\n\nOp IOS, an [der OwnTracks App]({ios_url}), klick op (i) Ikon uewen l\u00e9nks -> Settings. \u00c4nnert folgend Astellungen:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication:\n- UserID: ``\n\n{secret}\n\nKuck w.e.g. [Dokumentatioun]({docs_url}) fir m\u00e9i Informatiounen." + "default": "\n\nOp Android, an [der OwnTracks App]({android_url}), g\u00e9i an Preferences -> Connection. \u00c4nnert folgend Astellungen:\n- Mode: Private HTTP\n- Host {webhook_url}\n- Identification:\n - Username: `''`\n - Device ID: `''`\n\nOp IOS, an [der OwnTracks App]({ios_url}), klick op (i) Ikon uewen l\u00e9nks -> Settings. \u00c4nnert folgend Astellungen:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication:\n- UserID: `''`\n\n{secret}\n\nKuck w.e.g. [Dokumentatioun]({docs_url}) fir m\u00e9i Informatiounen." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/nl.json b/homeassistant/components/owntracks/translations/nl.json index 2d97724661c..ff6cadcbf25 100644 --- a/homeassistant/components/owntracks/translations/nl.json +++ b/homeassistant/components/owntracks/translations/nl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: ``\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." + "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: `''`\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/pl.json b/homeassistant/components/owntracks/translations/pl.json index c4499800eda..98c8779fe1f 100644 --- a/homeassistant/components/owntracks/translations/pl.json +++ b/homeassistant/components/owntracks/translations/pl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { - "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: ``\n - ID urz\u0105dzenia: ``\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: ``\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index 09fdba77266..ed1e084090d 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index fd2162f153b..8642a32f889 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." + "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: `''`\n - Enhets-ID: `''` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `''` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index e6a6fc26068..c355b745d1d 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." }, "create_entry": { - "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: `''`\n- Device ID: `''` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/zh-Hans.json b/homeassistant/components/owntracks/translations/zh-Hans.json index 6954b838d04..a6c01626c76 100644 --- a/homeassistant/components/owntracks/translations/zh-Hans.json +++ b/homeassistant/components/owntracks/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n\u5728 Android \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({android_url})\uff0c\u524d\u5f80 Preferences -> Connection\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u5728 iOS \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({ios_url})\uff0c\u70b9\u51fb\u5de6\u4e0a\u89d2\u7684 (i) \u56fe\u6807-> Settings\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" + "default": "\n\n\u5728 Android \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({android_url})\uff0c\u524d\u5f80 Preferences -> Connection\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\n\u5728 iOS \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({ios_url})\uff0c\u70b9\u51fb\u5de6\u4e0a\u89d2\u7684 (i) \u56fe\u6807-> Settings\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json index 6c92b557797..2803182629a 100644 --- a/homeassistant/components/owntracks/translations/zh-Hant.json +++ b/homeassistant/components/owntracks/translations/zh-Hant.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { - "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a `''`\n - Device ID\uff1a`''`\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: `''`\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { From edd068d6ebd549be99a88f0349072541efa6375b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 17 Nov 2021 12:26:32 -0500 Subject: [PATCH 0568/1452] Add explicit unit mapping for zwave_js meters and sensors (#59659) * Add explicit unit mapping for zwave_js meters and sensors * review comment * fix * alternate approach --- .../zwave_js/discovery_data_template.py | 212 ++++++++++++++++-- homeassistant/components/zwave_js/sensor.py | 112 +++++---- homeassistant/const.py | 3 + 3 files changed, 261 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index ea05005deae..2d8e895d9df 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -11,6 +11,13 @@ from zwave_js_server.const.command_class.meter import ( ENERGY_TOTAL_INCREASING_METER_TYPES, POWER_FACTOR_METER_TYPES, POWER_METER_TYPES, + UNIT_AMPERE as METER_UNIT_AMPERE, + UNIT_CUBIC_FEET, + UNIT_CUBIC_METER as METER_UNIT_CUBIC_METER, + UNIT_KILOWATT_HOUR, + UNIT_US_GALLON, + UNIT_VOLT as METER_UNIT_VOLT, + UNIT_WATT as METER_UNIT_WATT, VOLTAGE_METER_TYPES, ElectricScale, MeterScaleType, @@ -26,16 +33,97 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( PRESSURE_SENSORS, SIGNAL_STRENGTH_SENSORS, TEMPERATURE_SENSORS, + UNIT_AMPERE as SENSOR_UNIT_AMPERE, + UNIT_BTU_H, + UNIT_CELSIUS, + UNIT_CENTIMETER, + UNIT_CUBIC_FEET_PER_MINUTE, + UNIT_CUBIC_METER as SENSOR_UNIT_CUBIC_METER, + UNIT_CUBIC_METER_PER_HOUR, + UNIT_DECIBEL, + UNIT_DEGREES, + UNIT_DENSITY, + UNIT_FAHRENHEIT, + UNIT_FEET, + UNIT_GALLONS, + UNIT_HERTZ, + UNIT_INCHES_OF_MERCURY, + UNIT_INCHES_PER_HOUR, + UNIT_KILOGRAM, + UNIT_KILOHERTZ, + UNIT_LITER, + UNIT_LUX, + UNIT_M_S, + UNIT_METER, + UNIT_MICROGRAM_PER_CUBIC_METER, + UNIT_MILLIAMPERE, + UNIT_MILLIMETER_HOUR, + UNIT_MILLIVOLT, + UNIT_MPH, + UNIT_PARTS_MILLION, + UNIT_PERCENTAGE_VALUE, + UNIT_POUND_PER_SQUARE_INCH, + UNIT_POUNDS, + UNIT_POWER_LEVEL, + UNIT_RSSI, + UNIT_SECOND, + UNIT_SYSTOLIC, + UNIT_VOLT as SENSOR_UNIT_VOLT, + UNIT_WATT as SENSOR_UNIT_WATT, + UNIT_WATT_PER_SQUARE_METER, VOLTAGE_SENSORS, + MultilevelSensorScaleType, MultilevelSensorType, ) from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from zwave_js_server.util.command_class.meter import get_meter_scale_type from zwave_js_server.util.command_class.multilevel_sensor import ( + get_multilevel_sensor_scale_type, get_multilevel_sensor_type, ) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_MILLIVOLT, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + FREQUENCY_KILOHERTZ, + IRRADIATION_WATTS_PER_SQUARE_METER, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_METERS, + LIGHT_LUX, + MASS_KILOGRAMS, + MASS_POUNDS, + PERCENTAGE, + POWER_BTU_PER_HOUR, + POWER_WATT, + PRECIPITATION_INCHES_PER_HOUR, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_INHG, + PRESSURE_MMHG, + PRESSURE_PSI, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_SECONDS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + VOLUME_GALLONS, + VOLUME_LITERS, +) + from .const import ( ENTITY_DESC_KEY_BATTERY, ENTITY_DESC_KEY_CO, @@ -78,6 +166,58 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS, } +METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { + ELECTRIC_CURRENT_AMPERE: METER_UNIT_AMPERE, + VOLUME_CUBIC_FEET: UNIT_CUBIC_FEET, + VOLUME_CUBIC_METERS: METER_UNIT_CUBIC_METER, + VOLUME_GALLONS: UNIT_US_GALLON, + ENERGY_KILO_WATT_HOUR: UNIT_KILOWATT_HOUR, + ELECTRIC_POTENTIAL_VOLT: METER_UNIT_VOLT, + POWER_WATT: METER_UNIT_WATT, +} + +MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { + ELECTRIC_CURRENT_AMPERE: SENSOR_UNIT_AMPERE, + POWER_BTU_PER_HOUR: UNIT_BTU_H, + TEMP_CELSIUS: UNIT_CELSIUS, + LENGTH_CENTIMETERS: UNIT_CENTIMETER, + VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE, + VOLUME_CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR, + SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL, + DEGREE: UNIT_DEGREES, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: { + *UNIT_DENSITY, + *UNIT_MICROGRAM_PER_CUBIC_METER, + }, + TEMP_FAHRENHEIT: UNIT_FAHRENHEIT, + LENGTH_FEET: UNIT_FEET, + VOLUME_GALLONS: UNIT_GALLONS, + FREQUENCY_HERTZ: UNIT_HERTZ, + PRESSURE_INHG: UNIT_INCHES_OF_MERCURY, + PRECIPITATION_INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR, + MASS_KILOGRAMS: UNIT_KILOGRAM, + FREQUENCY_KILOHERTZ: UNIT_KILOHERTZ, + VOLUME_LITERS: UNIT_LITER, + LIGHT_LUX: UNIT_LUX, + LENGTH_METERS: UNIT_METER, + ELECTRIC_CURRENT_MILLIAMPERE: UNIT_MILLIAMPERE, + PRECIPITATION_MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR, + ELECTRIC_POTENTIAL_MILLIVOLT: UNIT_MILLIVOLT, + SPEED_MILES_PER_HOUR: UNIT_MPH, + SPEED_METERS_PER_SECOND: UNIT_M_S, + CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION, + PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI}, + MASS_POUNDS: UNIT_POUNDS, + PRESSURE_PSI: UNIT_POUND_PER_SQUARE_INCH, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL, + TIME_SECONDS: UNIT_SECOND, + PRESSURE_MMHG: UNIT_SYSTOLIC, + ELECTRIC_POTENTIAL_VOLT: SENSOR_UNIT_VOLT, + POWER_WATT: SENSOR_UNIT_WATT, + IRRADIATION_WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, +} + @dataclass class ZwaveValueID: @@ -154,10 +294,8 @@ class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): value.node, self.dependent_value ), } - for key in self.lookup_table: - data["lookup_table"][key] = self._get_value_from_id( - value.node, self.lookup_table[key] - ) + for key, value_id in self.lookup_table.items(): + data["lookup_table"][key] = self._get_value_from_id(value.node, value_id) return data @@ -183,17 +321,45 @@ class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): return None +@dataclass +class NumericSensorDataTemplateData: + """Class to represent returned data from NumericSensorDataTemplate.""" + + entity_description_key: str | None = None + unit_of_measurement: str | None = None + + class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): """Data template class for Z-Wave Sensor entities.""" - def resolve_data(self, value: ZwaveValue) -> str | None: + @staticmethod + def find_key_from_matching_set( + enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType, + set_map: dict[ + str, set[MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType] + ], + ) -> str | None: + """Find a key in a set map that matches a given enum value.""" + for key, value_set in set_map.items(): + for value_in_set in value_set: + # Since these are IntEnums and the different classes reuse the same + # values, we need to match the class as well + if ( + value_in_set.__class__ == enum_value.__class__ + and value_in_set == enum_value + ): + return key + return None + + def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData: """Resolve helper class data for a discovered value.""" if value.command_class == CommandClass.BATTERY: - return ENTITY_DESC_KEY_BATTERY + return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) if value.command_class == CommandClass.METER: scale_type = get_meter_scale_type(value) + unit = self.find_key_from_matching_set(scale_type, METER_UNIT_MAP) # We do this because even though these are energy scales, they don't meet # the unit requirements for the energy device class. if scale_type in ( @@ -201,28 +367,36 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): ElectricScale.KILOVOLT_AMPERE_HOUR, ElectricScale.KILOVOLT_AMPERE_REACTIVE_HOUR, ): - return ENTITY_DESC_KEY_TOTAL_INCREASING + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_TOTAL_INCREASING, unit + ) # We do this because even though these are power scales, they don't meet # the unit requirements for the power device class. if scale_type == ElectricScale.KILOVOLT_AMPERE_REACTIVE: - return ENTITY_DESC_KEY_MEASUREMENT + return NumericSensorDataTemplateData(ENTITY_DESC_KEY_MEASUREMENT, unit) - for key, scale_type_set in METER_DEVICE_CLASS_MAP.items(): - if scale_type in scale_type_set: - return key + return NumericSensorDataTemplateData( + self.find_key_from_matching_set(scale_type, METER_DEVICE_CLASS_MAP), + unit, + ) if value.command_class == CommandClass.SENSOR_MULTILEVEL: sensor_type = get_multilevel_sensor_type(value) + scale_type = get_multilevel_sensor_scale_type(value) + unit = self.find_key_from_matching_set( + scale_type, MULTILEVEL_SENSOR_UNIT_MAP + ) if sensor_type == MultilevelSensorType.TARGET_TEMPERATURE: - return ENTITY_DESC_KEY_TARGET_TEMPERATURE - for ( - key, - sensor_type_set, - ) in MULTILEVEL_SENSOR_DEVICE_CLASS_MAP.items(): - if sensor_type in sensor_type_set: - return key + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, unit + ) + key = self.find_key_from_matching_set( + sensor_type, MULTILEVEL_SENSOR_DEVICE_CLASS_MAP + ) + if key: + return NumericSensorDataTemplateData(key, unit) - return None + return NumericSensorDataTemplateData() @dataclass diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index b8b8cb4b978..549df9f6264 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Mapping -from dataclasses import dataclass import logging from typing import cast @@ -25,6 +24,9 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.components.zwave_js.discovery_data_template import ( + NumericSensorDataTemplateData, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -40,8 +42,6 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform @@ -89,98 +89,91 @@ STATUS_ICON: dict[NodeStatus, str] = { } -@dataclass -class ZwaveSensorEntityDescription(SensorEntityDescription): - """Base description of a Zwave Sensor entity.""" - - info: ZwaveDiscoveryInfo | None = None - - -ENTITY_DESCRIPTION_KEY_MAP: dict[str, ZwaveSensorEntityDescription] = { - ENTITY_DESC_KEY_BATTERY: ZwaveSensorEntityDescription( +ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = { + ENTITY_DESC_KEY_BATTERY: SensorEntityDescription( ENTITY_DESC_KEY_BATTERY, device_class=DEVICE_CLASS_BATTERY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_CURRENT: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_CURRENT: SensorEntityDescription( ENTITY_DESC_KEY_CURRENT, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_VOLTAGE: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription( ENTITY_DESC_KEY_VOLTAGE, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_ENERGY_MEASUREMENT: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), - ENTITY_DESC_KEY_POWER: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_POWER: SensorEntityDescription( ENTITY_DESC_KEY_POWER, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_POWER_FACTOR: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( ENTITY_DESC_KEY_POWER_FACTOR, device_class=DEVICE_CLASS_POWER_FACTOR, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_CO: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_CO: SensorEntityDescription( ENTITY_DESC_KEY_CO, device_class=DEVICE_CLASS_CO, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_CO2: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_CO2: SensorEntityDescription( ENTITY_DESC_KEY_CO2, device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_HUMIDITY: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( ENTITY_DESC_KEY_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_ILLUMINANCE: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( ENTITY_DESC_KEY_ILLUMINANCE, device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_PRESSURE: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription( ENTITY_DESC_KEY_PRESSURE, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_SIGNAL_STRENGTH: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_TEMPERATURE: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription( ENTITY_DESC_KEY_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_TARGET_TEMPERATURE: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription( ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE, state_class=None, ), - ENTITY_DESC_KEY_MEASUREMENT: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_MEASUREMENT, device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), - ENTITY_DESC_KEY_TOTAL_INCREASING: ZwaveSensorEntityDescription( + ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_TOTAL_INCREASING, device_class=None, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -201,25 +194,42 @@ async def async_setup_entry( """Add Z-Wave Sensor.""" entities: list[ZWaveBaseEntity] = [] + if info.platform_data: + data: NumericSensorDataTemplateData = info.platform_data + else: + data = NumericSensorDataTemplateData() entity_description = ENTITY_DESCRIPTION_KEY_MAP.get( - info.platform_data - ) or ZwaveSensorEntityDescription("base_sensor") - entity_description.info = info + data.entity_description_key or "", SensorEntityDescription("base_sensor") + ) if info.platform_hint == "string_sensor": - entities.append(ZWaveStringSensor(config_entry, client, entity_description)) + entities.append( + ZWaveStringSensor(config_entry, client, info, entity_description) + ) elif info.platform_hint == "numeric_sensor": entities.append( - ZWaveNumericSensor(config_entry, client, entity_description) + ZWaveNumericSensor( + config_entry, + client, + info, + entity_description, + data.unit_of_measurement, + ) ) elif info.platform_hint == "list_sensor": - entities.append(ZWaveListSensor(config_entry, client, entity_description)) + entities.append( + ZWaveListSensor(config_entry, client, info, entity_description) + ) elif info.platform_hint == "config_parameter": entities.append( - ZWaveConfigParameterSensor(config_entry, client, entity_description) + ZWaveConfigParameterSensor( + config_entry, client, info, entity_description + ) ) elif info.platform_hint == "meter": - entities.append(ZWaveMeterSensor(config_entry, client, entity_description)) + entities.append( + ZWaveMeterSensor(config_entry, client, info, entity_description) + ) else: LOGGER.warning( "Sensor not implemented for %s/%s", @@ -269,12 +279,14 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): self, config_entry: ConfigEntry, client: ZwaveClient, - entity_description: ZwaveSensorEntityDescription, + info: ZwaveDiscoveryInfo, + entity_description: SensorEntityDescription, + unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveSensorBase entity.""" - assert entity_description.info - super().__init__(config_entry, client, entity_description.info) + super().__init__(config_entry, client, info) self.entity_description = entity_description + self._attr_native_unit_of_measurement = unit_of_measurement # Entity class attributes self._attr_force_update = True @@ -312,12 +324,10 @@ class ZWaveNumericSensor(ZwaveSensorBase): @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" + if self._attr_native_unit_of_measurement is not None: + return self._attr_native_unit_of_measurement if self.info.primary_value.metadata.unit is None: return None - if self.info.primary_value.metadata.unit == "C": - return TEMP_CELSIUS - if self.info.primary_value.metadata.unit == "F": - return TEMP_FAHRENHEIT return str(self.info.primary_value.metadata.unit) @@ -365,10 +375,14 @@ class ZWaveListSensor(ZwaveSensorBase): self, config_entry: ConfigEntry, client: ZwaveClient, - entity_description: ZwaveSensorEntityDescription, + info: ZwaveDiscoveryInfo, + entity_description: SensorEntityDescription, + unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveListSensor entity.""" - super().__init__(config_entry, client, entity_description) + super().__init__( + config_entry, client, info, entity_description, unit_of_measurement + ) # Entity class attributes self._attr_name = self.generate_name( @@ -405,10 +419,14 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): self, config_entry: ConfigEntry, client: ZwaveClient, - entity_description: ZwaveSensorEntityDescription, + info: ZwaveDiscoveryInfo, + entity_description: SensorEntityDescription, + unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveConfigParameterSensor entity.""" - super().__init__(config_entry, client, entity_description) + super().__init__( + config_entry, client, info, entity_description, unit_of_measurement + ) self._primary_value = cast(ConfigurationValue, self.info.primary_value) # Entity class attributes diff --git a/homeassistant/const.py b/homeassistant/const.py index 6c808ffd3b3..43e07036cb8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -421,6 +421,7 @@ ATTR_TEMPERATURE: Final = "temperature" POWER_WATT: Final = "W" POWER_KILO_WATT: Final = "kW" POWER_VOLT_AMPERE: Final = "VA" +POWER_BTU_PER_HOUR: Final = "BTU/h" # Energy units ENERGY_WATT_HOUR: Final = "Wh" @@ -472,6 +473,7 @@ LENGTH_MILES: Final = "mi" # Frequency units FREQUENCY_HERTZ: Final = "Hz" +FREQUENCY_KILOHERTZ: Final = "kHz" FREQUENCY_MEGAHERTZ: Final = "MHz" FREQUENCY_GIGAHERTZ: Final = "GHz" @@ -482,6 +484,7 @@ PRESSURE_KPA: Final = "kPa" PRESSURE_BAR: Final = "bar" PRESSURE_CBAR: Final = "cbar" PRESSURE_MBAR: Final = "mbar" +PRESSURE_MMHG: Final = "mmHg" PRESSURE_INHG: Final = "inHg" PRESSURE_PSI: Final = "psi" From 6175f1b6f3f441e3d0708ba3b61dbfdc6ce7e86f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Nov 2021 11:45:08 -0800 Subject: [PATCH 0569/1452] Bump frontend to 20211117.0 (#59851) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d2db6138171..d662d3300ab 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211109.0" + "home-assistant-frontend==20211117.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index be20a0b027f..ab6da7293f0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211109.0 +home-assistant-frontend==20211117.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 26b5f9bff6f..493fe9782bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211109.0 +home-assistant-frontend==20211117.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6466be2278..c08e1aded43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -509,7 +509,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211109.0 +home-assistant-frontend==20211117.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 279ded356257f9a2c75ffba80b7a2fb41928f449 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 17 Nov 2021 16:37:53 -0700 Subject: [PATCH 0570/1452] Ensure `last_event_sensor_type` in SimpliSafe entities is lowercased (#59864) --- homeassistant/components/simplisafe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 404c6e96fd4..a773304896d 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -588,7 +588,7 @@ class SimpliSafeEntity(CoordinatorEntity): self._attr_extra_state_attributes = { ATTR_LAST_EVENT_INFO: event.get("info"), ATTR_LAST_EVENT_SENSOR_NAME: event.get("sensorName"), - ATTR_LAST_EVENT_SENSOR_TYPE: device_type.name, + ATTR_LAST_EVENT_SENSOR_TYPE: device_type.name.lower(), ATTR_LAST_EVENT_TIMESTAMP: event.get("eventTimestamp"), ATTR_SYSTEM_ID: system.system_id, } From 9a85c8d8941b1a5065ca37768f1d5f190b36101e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 18 Nov 2021 00:12:48 +0000 Subject: [PATCH 0571/1452] [ci skip] Translation update --- .../components/airthings/translations/id.json | 9 +++ .../components/airtouch4/translations/id.json | 6 +- .../airvisual/translations/sensor.id.json | 20 +++++++ .../alarm_control_panel/translations/id.json | 4 ++ .../ambee/translations/sensor.id.json | 3 +- .../amberelectric/translations/id.json | 22 +++++++ .../aurora_abb_powerone/translations/id.json | 15 ++++- .../binary_sensor/translations/nl.json | 3 + .../components/bosch_shc/translations/id.json | 21 ++++++- .../buienradar/translations/id.json | 11 ++++ .../components/button/translations/id.json | 11 ++++ .../components/cast/translations/id.json | 6 ++ .../components/cloud/translations/id.json | 1 + .../cloudflare/translations/id.json | 3 +- .../components/co2signal/translations/id.json | 8 ++- .../components/coinbase/translations/id.json | 21 ++++++- .../crownstone/translations/id.json | 59 ++++++++++++++++--- .../components/daikin/translations/id.json | 1 + .../demo/translations/select.id.json | 7 +++ .../components/denonavr/translations/id.json | 1 + .../devolo_home_control/translations/id.json | 3 +- .../devolo_home_network/translations/id.json | 8 ++- .../components/dlna_dmr/translations/id.json | 31 +++++++++- .../components/dsmr/translations/id.json | 13 +++- .../components/efergy/translations/id.json | 21 +++++++ .../components/energy/translations/id.json | 3 + .../environment_canada/translations/id.json | 6 +- .../components/epson/translations/id.json | 3 +- .../components/esphome/translations/id.json | 7 +++ .../evil_genius_labs/translations/id.json | 1 + .../components/flipr/translations/id.json | 3 + .../components/flux_led/translations/id.json | 19 ++++++ .../homeassistant/translations/id.json | 1 + .../components/homekit/translations/id.json | 4 +- .../homekit_controller/translations/id.json | 2 + .../components/honeywell/translations/id.json | 3 +- .../components/hue/translations/ca.json | 12 +++- .../components/hue/translations/et.json | 12 +++- .../components/hue/translations/hu.json | 12 +++- .../components/hue/translations/id.json | 12 +++- .../components/hue/translations/nl.json | 12 +++- .../components/hue/translations/no.json | 12 +++- .../components/hue/translations/ru.json | 12 +++- .../components/hue/translations/zh-Hant.json | 12 +++- .../components/motioneye/translations/id.json | 2 + .../components/mqtt/translations/id.json | 2 +- .../components/octoprint/translations/id.json | 5 +- .../components/owntracks/translations/nl.json | 2 +- .../components/owntracks/translations/pl.json | 2 +- .../components/owntracks/translations/ru.json | 2 +- .../components/owntracks/translations/sl.json | 2 +- .../components/owntracks/translations/sv.json | 2 +- .../components/owntracks/translations/uk.json | 2 +- .../components/ridwell/translations/id.json | 4 ++ .../components/sensor/translations/id.json | 2 + .../components/sensor/translations/no.json | 2 + .../components/shelly/translations/id.json | 5 +- .../synology_dsm/translations/id.json | 7 ++- .../totalconnect/translations/no.json | 1 + .../components/tuya/translations/id.json | 20 ++++++- .../tuya/translations/select.id.json | 38 +++++++++++- .../tuya/translations/sensor.id.json | 14 +++++ .../components/venstar/translations/id.json | 23 ++++++++ .../vlc_telnet/translations/id.json | 9 +++ .../components/wallbox/translations/id.json | 11 +++- .../components/wallbox/translations/no.json | 10 +++- .../components/watttime/translations/id.json | 14 ++++- .../components/wled/translations/id.json | 9 +++ .../xiaomi_miio/translations/id.json | 13 +++- .../yamaha_musiccast/translations/id.json | 9 ++- .../components/zwave_js/translations/id.json | 16 ++++- 71 files changed, 626 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.id.json create mode 100644 homeassistant/components/amberelectric/translations/id.json create mode 100644 homeassistant/components/button/translations/id.json create mode 100644 homeassistant/components/demo/translations/select.id.json create mode 100644 homeassistant/components/efergy/translations/id.json create mode 100644 homeassistant/components/energy/translations/id.json create mode 100644 homeassistant/components/flux_led/translations/id.json create mode 100644 homeassistant/components/tuya/translations/sensor.id.json create mode 100644 homeassistant/components/venstar/translations/id.json diff --git a/homeassistant/components/airthings/translations/id.json b/homeassistant/components/airthings/translations/id.json index 44648dab3d2..b019ddd0aed 100644 --- a/homeassistant/components/airthings/translations/id.json +++ b/homeassistant/components/airthings/translations/id.json @@ -7,6 +7,15 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "description": "Masuk di {url} untuk menemukan kredensial Anda", + "id": "ID", + "secret": "Kode Rahasia" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/id.json b/homeassistant/components/airtouch4/translations/id.json index c8236f5ec73..9af558b9a45 100644 --- a/homeassistant/components/airtouch4/translations/id.json +++ b/homeassistant/components/airtouch4/translations/id.json @@ -4,13 +4,15 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "no_units": "Tidak dapat menemukan Grup AirTouch 4 apa pun." }, "step": { "user": { "data": { "host": "Host" - } + }, + "title": "Siapkan detail koneksi AirTouch 4 Anda." } } } diff --git a/homeassistant/components/airvisual/translations/sensor.id.json b/homeassistant/components/airvisual/translations/sensor.id.json new file mode 100644 index 00000000000..ad6c9c64b3d --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.id.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Karbon monoksida", + "n2": "Nitrogen dioksida", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Sulfur Dioksida" + }, + "airvisual__pollutant_level": { + "good": "Bagus", + "hazardous": "Berbahaya", + "moderate": "Sedang", + "unhealthy": "Tidak sehat", + "unhealthy_sensitive": "Tidak sehat untuk kelompok sensitif", + "very_unhealthy": "Sangat tidak sehat" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/id.json b/homeassistant/components/alarm_control_panel/translations/id.json index f1676ce8c75..ee079ff7435 100644 --- a/homeassistant/components/alarm_control_panel/translations/id.json +++ b/homeassistant/components/alarm_control_panel/translations/id.json @@ -4,6 +4,7 @@ "arm_away": "Aktifkan {entity_name} untuk keluar", "arm_home": "Aktifkan {entity_name} untuk di rumah", "arm_night": "Aktifkan {entity_name} untuk malam", + "arm_vacation": "Aktifkan {entity_name} untuk liburan", "disarm": "Nonaktifkan {entity_name}", "trigger": "Picu {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} diaktifkan untuk keluar", "is_armed_home": "{entity_name} diaktifkan untuk di rumah", "is_armed_night": "{entity_name} diaktifkan untuk malam", + "is_armed_vacation": "{entity_name} diaktifkan untuk liburan", "is_disarmed": "{entity_name} dinonaktifkan", "is_triggered": "{entity_name} dipicu" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} diaktifkan untuk keluar", "armed_home": "{entity_name} diaktifkan untuk di rumah", "armed_night": "{entity_name} diaktifkan untuk malam", + "armed_vacation": "{entity_name} diaktifkan untuk liburan", "disarmed": "{entity_name} dinonaktifkan", "triggered": "{entity_name} dipicu" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Diaktifkan khusus", "armed_home": "Diaktifkan untuk di rumah", "armed_night": "Diaktifkan untuk malam", + "armed_vacation": "Diaktifkan untuk liburan", "arming": "Mengaktifkan", "disarmed": "Dinonaktifkan", "disarming": "Dinonaktifkan", diff --git a/homeassistant/components/ambee/translations/sensor.id.json b/homeassistant/components/ambee/translations/sensor.id.json index 61bdea468ee..5cb74694da5 100644 --- a/homeassistant/components/ambee/translations/sensor.id.json +++ b/homeassistant/components/ambee/translations/sensor.id.json @@ -3,7 +3,8 @@ "ambee__risk": { "high": "Tinggi", "low": "Rendah", - "moderate": "Sedang" + "moderate": "Sedang", + "very high": "Sangat Tinggi" } } } \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/id.json b/homeassistant/components/amberelectric/translations/id.json new file mode 100644 index 00000000000..4920ee7b177 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Nama Situs", + "site_nmi": "Situs NMI" + }, + "description": "Pilih NMI dari situs yang ingin ditambahkan", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "Token API", + "site_id": "ID Site" + }, + "description": "Buka {api_url} untuk membuat kunci API", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/id.json b/homeassistant/components/aurora_abb_powerone/translations/id.json index ddfbf74ffeb..4157502c2ad 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/id.json +++ b/homeassistant/components/aurora_abb_powerone/translations/id.json @@ -1,10 +1,23 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "no_serial_ports": "Tidak ada port com yang ditemukan. Perlu perangkat RS485 yang valid untuk berkomunikasi." }, "error": { + "cannot_connect": "Tidak dapat terhubung, periksa port serial, alamat, koneksi listrik dan apakah inverter sedang nyala (di siang hari)", + "cannot_open_serial_port": "Tidak dapat membuka port serial, periksa dan coba lagi", + "invalid_serial_port": "Port serial bukan perangkat yang valid atau tidak dapat dibuka", "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "address": "Alamat Inverter", + "port": "Port Adaptor RS485 atau USB-RS485" + }, + "description": "Inverter harus terhubung melalui adaptor RS485, pilih port serial dan alamat inverter seperti yang dikonfigurasi pada panel LCD" + } } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 3f5aff1585e..f3d8a263187 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -32,6 +32,7 @@ "is_not_powered": "{entity_name} is niet van stroom voorzien...", "is_not_present": "{entity_name} is niet aanwezig", "is_not_running": "{entity_name} is niet langer actief", + "is_not_tampered": "{entity_name} detecteert geen sabotage", "is_not_unsafe": "{entity_name} is veilig", "is_occupied": "{entity_name} bezet is", "is_off": "{entity_name} is uitgeschakeld", @@ -44,6 +45,7 @@ "is_running": "{entity_name} is actief", "is_smoke": "{entity_name} detecteert rook", "is_sound": "{entity_name} detecteert geluid", + "is_tampered": "{entity_name} detecteert sabotage", "is_unsafe": "{entity_name} is onveilig", "is_update": "{entity_name} heeft een update beschikbaar", "is_vibration": "{entity_name} detecteert trillingen" @@ -54,6 +56,7 @@ "connected": "{entity_name} verbonden", "gas": "{entity_name} begon gas te detecteren", "hot": "{entity_name} werd heet", + "is_not_tampered": "{entity_name} gestopt met het detecteren van sabotage", "is_tampered": "{entity_name} begonnen met het detecteren van sabotage", "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", diff --git a/homeassistant/components/bosch_shc/translations/id.json b/homeassistant/components/bosch_shc/translations/id.json index c2167eb0f20..db20e06cd76 100644 --- a/homeassistant/components/bosch_shc/translations/id.json +++ b/homeassistant/components/bosch_shc/translations/id.json @@ -7,13 +7,32 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "pairing_failed": "Pairing gagal; periksa apakah Bosch Smart Home Controller dalam mode pairing (LED berkedip) dan kata sandi Anda benar.", + "session_error": "Kesalahan sesi: API mengembalikan hasil Non-OK.", "unknown": "Kesalahan yang tidak diharapkan" }, "flow_title": "Bosch SHC: {name}", "step": { + "confirm_discovery": { + "description": "Tekan tombol sisi depan Bosch Smart Home Controller hingga LED mulai berkedip.\nSiap melanjutkan penyiapan {model} @ {host} dengan Home Assistant?" + }, + "credentials": { + "data": { + "password": "Kata Sandi dari Smart Home Controller" + } + }, "reauth_confirm": { + "description": "Integrasi bosch_shc perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Siapkan Bosch Smart Home Controller Anda untuk memungkinkan pemantauan dan kontrol dengan Home Assistant.", + "title": "Parameter autentikasi SHC" } } - } + }, + "title": "Bosch SHC" } \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/id.json b/homeassistant/components/buienradar/translations/id.json index a4331fced9f..46eb80123cc 100644 --- a/homeassistant/components/buienradar/translations/id.json +++ b/homeassistant/components/buienradar/translations/id.json @@ -14,5 +14,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Kode negara negara untuk menampilkan gambar kamera.", + "delta": "Interval waktu pembaruan gambar kamera dalam detik", + "timeframe": "Waktu mendatang dalam menit untuk mendapatkan prakiraan curah hujan" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/button/translations/id.json b/homeassistant/components/button/translations/id.json new file mode 100644 index 00000000000..bb51d4003da --- /dev/null +++ b/homeassistant/components/button/translations/id.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Tekan tombol {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} telah ditekan" + } + }, + "title": "Tombol" +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index b0a54f52897..e81b25a4844 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -25,12 +25,18 @@ }, "step": { "advanced_options": { + "data": { + "ignore_cec": "Abaikan CEC", + "uuid": "UUID yang diizinkan" + }, + "description": "UUID yang Diizinkan - Daftar UUID perangkat Cast yang dipisahkan koma untuk ditambahkan ke Home Assistant. Gunakan hanya jika Anda tidak ingin menambahkan semua perangkat cast yang tersedia.\nAbaikan CEC - Daftar Chromecast yang dipisahkan koma yang harus mengabaikan data CEC untuk menentukan input aktif. Daftar ini akan diteruskan ke pychromecast.IGNORE_CEC.", "title": "Konfigurasi Google Cast tingkat lanjut" }, "basic_options": { "data": { "known_hosts": "Host yang dikenal" }, + "description": "Host yang Dikenal - Daftar nama host atau alamat IP perangkat cast, dipisahkan dengan tanda koma, gunakan jika penemuan mDNS tidak berfungsi.", "title": "Konfigurasi Google Cast" } } diff --git a/homeassistant/components/cloud/translations/id.json b/homeassistant/components/cloud/translations/id.json index 1cff542796c..a8f6d7b4b67 100644 --- a/homeassistant/components/cloud/translations/id.json +++ b/homeassistant/components/cloud/translations/id.json @@ -10,6 +10,7 @@ "relayer_connected": "Relayer Terhubung", "remote_connected": "Terhubung Jarak Jauh", "remote_enabled": "Kontrol Jarak Jauh Diaktifkan", + "remote_server": "Server Daring", "subscription_expiration": "Masa Kedaluwarsa Langganan" } } diff --git a/homeassistant/components/cloudflare/translations/id.json b/homeassistant/components/cloudflare/translations/id.json index 73f0455273c..b3f49244658 100644 --- a/homeassistant/components/cloudflare/translations/id.json +++ b/homeassistant/components/cloudflare/translations/id.json @@ -14,7 +14,8 @@ "step": { "reauth_confirm": { "data": { - "api_token": "Token API" + "api_token": "Token API", + "description": "Autentikasi ulang dengan akun Cloudflare Anda." } }, "records": { diff --git a/homeassistant/components/co2signal/translations/id.json b/homeassistant/components/co2signal/translations/id.json index 76e72a93fd5..e323b25db2d 100644 --- a/homeassistant/components/co2signal/translations/id.json +++ b/homeassistant/components/co2signal/translations/id.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "api_ratelimit": "Batas Tingkat API terlampaui", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { + "api_ratelimit": "Batas Tingkat API terlampaui", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -22,8 +24,10 @@ }, "user": { "data": { - "api_key": "Token Akses" - } + "api_key": "Token Akses", + "location": "Dapatkan data untuk" + }, + "description": "Kunjungi https://co2signal.com/ untuk meminta token." } } } diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index e0d93019507..0f83a78044b 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -11,14 +11,31 @@ "step": { "user": { "data": { - "api_key": "Kunci API" - } + "api_key": "Kunci API", + "api_token": "Kode Rahasia API", + "currencies": "Mata Uang Saldo Akun", + "exchange_rates": "Nilai Tukar" + }, + "description": "Silakan masukkan detail kunci API Anda sesuai yang disediakan oleh Coinbase.", + "title": "Detail Kunci API Coinbase" } } }, "options": { "error": { + "currency_unavaliable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", + "exchange_rate_unavaliable": "Satu atau beberapa nilai tukar yang diminta tidak disediakan oleh Coinbase.", "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Saldo dompet untuk dilaporkan.", + "exchange_base": "Mata uang dasar untuk sensor nilai tukar.", + "exchange_rate_currencies": "Nilai tukar untuk dilaporkan." + }, + "description": "Sesuaikan Opsi Coinbase" + } } } } \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/id.json b/homeassistant/components/crownstone/translations/id.json index b7fbfdc8c7d..aef98346fd2 100644 --- a/homeassistant/components/crownstone/translations/id.json +++ b/homeassistant/components/crownstone/translations/id.json @@ -1,9 +1,12 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "usb_setup_complete": "Penyiapan USB Crownstone selesai.", + "usb_setup_unsuccessful": "Penyiapan USB Crownstone tidak berhasil." }, "error": { + "account_not_verified": "Akun tidak diverifikasi. Aktifkan akun Anda melalui email aktivasi dari Crownstone.", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -11,42 +14,82 @@ "usb_config": { "data": { "usb_path": "Jalur Perangkat USB" - } + }, + "description": "Pilih port serial dongle USB Crownstone, atau pilih 'Jangan gunakan USB' jika Anda tidak ingin menyiapkan dongle USB.\n\nCari perangkat dengan VID 10C4 dan PID EA60.", + "title": "Konfigurasi dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Jalur Perangkat USB" - } + }, + "description": "Masukkan jalur dongle USB Crownstone secara manual.", + "title": "Jalur manual dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Pilih Crownstone Sphere tempat USB berada.", + "title": "Crownstone USB Sphere" }, "user": { "data": { "email": "Email", "password": "Kata Sandi" - } + }, + "title": "Akun Crownstone" } } }, "options": { "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere tempat USB berada", + "use_usb_option": "Gunakan dongle USB Crownstone untuk transmisi data lokal" + } + }, "usb_config": { "data": { "usb_path": "Jalur Perangkat USB" - } + }, + "description": "Pilih port serial dongle USB Crownstone. \n\nCari perangkat dengan VID 10C4 dan PID EA60.", + "title": "Konfigurasi dongle USB Crownstone" }, "usb_config_option": { "data": { "usb_path": "Jalur Perangkat USB" - } + }, + "description": "Pilih port serial dongle USB Crownstone. \n\nCari perangkat dengan VID 10C4 dan PID EA60.", + "title": "Konfigurasi dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Jalur Perangkat USB" - } + }, + "description": "Masukkan jalur dongle USB Crownstone secara manual.", + "title": "Jalur manual dongle USB Crownstone" }, "usb_manual_config_option": { "data": { "usb_manual_path": "Jalur Perangkat USB" - } + }, + "description": "Masukkan jalur dongle USB Crownstone secara manual.", + "title": "Jalur manual dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Pilih Crownstone Sphere tempat USB berada.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Pilih Crownstone Sphere tempat USB berada.", + "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/daikin/translations/id.json b/homeassistant/components/daikin/translations/id.json index 8b7cfb5460e..8a35b8e113c 100644 --- a/homeassistant/components/daikin/translations/id.json +++ b/homeassistant/components/daikin/translations/id.json @@ -5,6 +5,7 @@ "cannot_connect": "Gagal terhubung" }, "error": { + "api_password": "Autentikasi tidak valid, gunakan Kunci API atau Kata Sandi.", "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/demo/translations/select.id.json b/homeassistant/components/demo/translations/select.id.json new file mode 100644 index 00000000000..410fad95ba8 --- /dev/null +++ b/homeassistant/components/demo/translations/select.id.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Kecepatan Cahaya" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json index 0bafe289842..b2543d1d877 100644 --- a/homeassistant/components/denonavr/translations/id.json +++ b/homeassistant/components/denonavr/translations/id.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Tampilkan semua sumber", + "update_audyssey": "Perbarui pengaturan Audyssey", "zone2": "Siapkan Zona 2", "zone3": "Siapkan Zona 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/id.json b/homeassistant/components/devolo_home_control/translations/id.json index 41d2100b6ed..a4db1b3d6af 100644 --- a/homeassistant/components/devolo_home_control/translations/id.json +++ b/homeassistant/components/devolo_home_control/translations/id.json @@ -5,7 +5,8 @@ "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "invalid_auth": "Autentikasi tidak valid" + "invalid_auth": "Autentikasi tidak valid", + "reauth_failed": "Gunakan pengguna mydevolo yang sama seperti sebelumnya." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_network/translations/id.json b/homeassistant/components/devolo_home_network/translations/id.json index 187ccebf754..0950f6a2711 100644 --- a/homeassistant/components/devolo_home_network/translations/id.json +++ b/homeassistant/components/devolo_home_network/translations/id.json @@ -1,18 +1,24 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "home_control": "Unit Central devolo Home Control tidak berfungsi dengan integrasi ini." }, "error": { "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{product} ({name})", "step": { "user": { "data": { "ip_address": "Alamat IP" }, "description": "Ingin memulai penyiapan?" + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan perangkat jaringan rumah devolo dengan nama host `{host_name}` ke Home Assistant?", + "title": "Menemukan perangkat jaringan rumah devolo" } } } diff --git a/homeassistant/components/dlna_dmr/translations/id.json b/homeassistant/components/dlna_dmr/translations/id.json index 483d422d040..bf4049af584 100644 --- a/homeassistant/components/dlna_dmr/translations/id.json +++ b/homeassistant/components/dlna_dmr/translations/id.json @@ -1,23 +1,48 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "could_not_connect": "Gagal terhubung ke perangkat DLNA", + "discovery_error": "Gagal menemukan perangkat DLNA yang cocok", + "incomplete_config": "Konfigurasi tidak memiliki variabel yang diperlukan", + "non_unique_id": "Beberapa perangkat ditemukan dengan ID unik yang sama", + "not_dmr": "Perangkat bukan Digital Media Renderer yang didukung" + }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "could_not_connect": "Gagal terhubung ke perangkat DLNA", + "not_dmr": "Perangkat bukan Digital Media Renderer yang didukung" }, "flow_title": "{name}", "step": { "confirm": { "description": "Ingin memulai penyiapan?" }, + "import_turn_on": { + "description": "Nyalakan perangkat dan klik kirim untuk melanjutkan migrasi" + }, "manual": { "data": { "url": "URL" - } + }, + "description": "URL ke file XML deskripsi perangkat", + "title": "Koneksi perangkat DLNA DMR manual" }, "user": { "data": { "host": "Host", "url": "URL" - } + }, + "description": "Pilih perangkat untuk dikonfigurasi atau biarkan kosong untuk memasukkan URL", + "title": "Perangkat DLNA DMR yang ditemukan" + } + } + }, + "options": { + "step": { + "init": { + "title": "Konfigurasi Digital Media Renderer DLNA" } } } diff --git a/homeassistant/components/dsmr/translations/id.json b/homeassistant/components/dsmr/translations/id.json index 2e56dd3b0a6..2c1eeccca17 100644 --- a/homeassistant/components/dsmr/translations/id.json +++ b/homeassistant/components/dsmr/translations/id.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_communicate": "Gagal berkomunikasi", "cannot_connect": "Gagal terhubung" }, "error": { "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_communicate": "Gagal berkomunikasi", "cannot_connect": "Gagal terhubung" }, "step": { "setup_network": { "data": { + "dsmr_version": "Pilih versi DSMR", "host": "Host", "port": "Port" - } + }, + "title": "Pilih alamat koneksi" + }, + "setup_serial": { + "data": { + "dsmr_version": "Pilih versi DSMR", + "port": "Pilih perangkat" + }, + "title": "Perangkat" }, "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/efergy/translations/id.json b/homeassistant/components/efergy/translations/id.json new file mode 100644 index 00000000000..234e5122db2 --- /dev/null +++ b/homeassistant/components/efergy/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/id.json b/homeassistant/components/energy/translations/id.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/id.json b/homeassistant/components/environment_canada/translations/id.json index 8334056d960..683dc8964c2 100644 --- a/homeassistant/components/environment_canada/translations/id.json +++ b/homeassistant/components/environment_canada/translations/id.json @@ -1,7 +1,10 @@ { "config": { "error": { + "bad_station_id": "ID Stasiun tidak valid, tidak ada, atau tidak ditemukan di basis data ID stasiun", "cannot_connect": "Gagal terhubung", + "error_response": "Kesalahan balasan dari Environment Canada", + "too_many_attempts": "Koneksi ke Environment Canada dibatasi; Coba lagi dalam 60 detik", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { @@ -11,7 +14,8 @@ "latitude": "Lintang", "longitude": "Bujur", "station": "ID stasiun cuaca" - } + }, + "title": "Environment Canada: lokasi cuaca dan bahasa" } } } diff --git a/homeassistant/components/epson/translations/id.json b/homeassistant/components/epson/translations/id.json index fd6c2bc2491..6538f89ab14 100644 --- a/homeassistant/components/epson/translations/id.json +++ b/homeassistant/components/epson/translations/id.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "powered_off": "Apakah proyektor dinyalakan? Anda perlu menyalakan proyektor untuk konfigurasi awal." }, "step": { "user": { diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index 0258df3feef..aa8e9960981 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -8,6 +8,7 @@ "error": { "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", "invalid_auth": "Autentikasi tidak valid", + "invalid_psk": "Kunci enkripsi transport tidak valid. Pastikan kuncinya sesuai dengan yang ada pada konfigurasi Anda", "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -23,6 +24,12 @@ "title": "Perangkat node ESPHome yang ditemukan" }, "encryption_key": { + "data": { + "noise_psk": "Kunci enkripsi" + }, + "description": "Masukkan kunci enkripsi yang Anda atur dalam konfigurasi Anda untuk {name}." + }, + "reauth_confirm": { "data": { "noise_psk": "Kunci enkripsi" } diff --git a/homeassistant/components/evil_genius_labs/translations/id.json b/homeassistant/components/evil_genius_labs/translations/id.json index 859922f9fde..66c930e348b 100644 --- a/homeassistant/components/evil_genius_labs/translations/id.json +++ b/homeassistant/components/evil_genius_labs/translations/id.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/flipr/translations/id.json b/homeassistant/components/flipr/translations/id.json index 63751867097..4e44860293d 100644 --- a/homeassistant/components/flipr/translations/id.json +++ b/homeassistant/components/flipr/translations/id.json @@ -9,6 +9,9 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "flipr_id": { + "description": "Pilih ID Flipr Anda dari daftar" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/flux_led/translations/id.json b/homeassistant/components/flux_led/translations/id.json new file mode 100644 index 00000000000..74ada0b83af --- /dev/null +++ b/homeassistant/components/flux_led/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index 8f3d9484c27..f795a47ee20 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -10,6 +10,7 @@ "os_version": "Versi Sistem Operasi", "python_version": "Versi Python", "timezone": "Zona Waktu", + "user": "Pengguna", "version": "Versi", "virtualenv": "Lingkungan Virtual" } diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index 8b6fd994bc2..d849d2164cc 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -21,13 +21,15 @@ "step": { "advanced": { "data": { - "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)" + "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)", + "devices": "Perangkat (Pemicu)" }, "description": "Sakelar yang dapat diprogram dibuat untuk setiap perangkat yang dipilih. Saat pemicu perangkat aktif, HomeKit dapat dikonfigurasi untuk menjalankan otomatisasi atau skenario.", "title": "Konfigurasi Tingkat Lanjut" }, "cameras": { "data": { + "camera_audio": "Kamera yang mendukung audio", "camera_copy": "Kamera yang mendukung aliran H.264 asli" }, "description": "Periksa semua kamera yang mendukung streaming H.264 asli. Jika kamera tidak mengeluarkan aliran H.264, sistem akan mentranskode video ke H.264 untuk HomeKit. Proses transcoding membutuhkan CPU kinerja tinggi dan tidak mungkin bekerja pada komputer papan tunggal.", diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 839169fc6a9..57754bea50b 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Kode HomeKit salah. Periksa dan coba lagi.", + "insecure_setup_code": "Kode penyiapan yang diminta tidak aman karena sifatnya yang sepele. Aksesori ini gagal memenuhi persyaratan keamanan dasar.", "max_peers_error": "Perangkat menolak untuk menambahkan pemasangan karena tidak memiliki penyimpanan pemasangan yang tersedia.", "pairing_failed": "Terjadi kesalahan yang tidak tertangani saat mencoba memasangkan dengan perangkat ini. Ini mungkin kegagalan sementara atau perangkat Anda mungkin tidak didukung saat ini.", "unable_to_pair": "Gagal memasangkan, coba lagi.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Izinkan pemasangan dengan kode penyiapan yang tidak aman.", "pairing_code": "Kode Pemasangan" }, "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json index ee1540cc787..b8333336f71 100644 --- a/homeassistant/components/honeywell/translations/id.json +++ b/homeassistant/components/honeywell/translations/id.json @@ -8,7 +8,8 @@ "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Masukkan kredensial yang digunakan untuk masuk ke mytotalconnectcomfort.com." } } } diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 47bb10b2abb..2e177b7ee76 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Primer bot\u00f3", + "2": "Segon bot\u00f3", + "3": "Tercer bot\u00f3", + "4": "Quart bot\u00f3", "button_1": "Primer bot\u00f3", "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", @@ -47,11 +51,16 @@ "turn_on": "Enc\u00e9n" }, "trigger_type": { + "double_short_release": "Ambd\u00f3s \"{subtype}\" alliberats", + "initial_press": "Bot\u00f3 \"{subtype}\" premut inicialment", + "long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_double_button_long_press": "Ambd\u00f3s \"{subtype}\" alliberats despr\u00e9s d'una estona premuts", - "remote_double_button_short_press": "Ambd\u00f3s \"{subtype}\" alliberats" + "remote_double_button_short_press": "Ambd\u00f3s \"{subtype}\" alliberats", + "repeat": "Bot\u00f3 \"{subtype}\" mantingut premut", + "short_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s de pr\u00e9mer breument" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Permet grups Hue", + "allow_hue_scenes": "Permet escenes Hue", "allow_unreachable": "Permet que bombetes no accessibles puguin informar del seu estat correctament" } } diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index afde880690e..76ae73b7121 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Esimene nupp", + "2": "Teine nupp", + "3": "Kolmas nupp", + "4": "Neljas nupp", "button_1": "Esimene nupp", "button_2": "Teine nupp", "button_3": "Kolmas nupp", @@ -47,11 +51,16 @@ "turn_on": "L\u00fclita sisse" }, "trigger_type": { + "double_short_release": "\"{subtype}\" nupp vabastatati", + "initial_press": "Nuppu \"{subtype}\" on vajutatud", + "long_release": "Nupp \"{subtype}\" vabastati p\u00e4rast pikka vajutust", "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", "remote_button_short_press": "\"{subtype}\" nupp on vajutatud", "remote_button_short_release": "\"{subtype}\" nupp vabastati", "remote_double_button_long_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati p\u00e4rast pikka vajutust", - "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati" + "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati", + "repeat": "Nuppu \" {subtype} \" hoitakse all", + "short_release": "Nupp \" {subtype} \" vabastati p\u00e4rast l\u00fchikest vajutust" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Luba Hue r\u00fchmad", + "allow_hue_scenes": "Luba Hue stseenid", "allow_unreachable": "Luba k\u00e4ttesaamatutel pirnidel oma olekust \u00f5igesti teatada" } } diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 917ec094ced..5c825d0bb67 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "1. gomb", + "2": "2. gomb", + "3": "3. gomb", + "4": "4. gomb", "button_1": "Els\u0151 gomb", "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", @@ -47,11 +51,16 @@ "turn_on": "Bekapcsol\u00e1s" }, "trigger_type": { + "double_short_release": "Mindk\u00e9t \"{subtype}\" felengedve", + "initial_press": "\"{subtype}\" lenyomva el\u0151sz\u00f6r", + "long_release": "\"{subtype}\" felengedve hossz\u00fa nyomva tart\u00e1s ut\u00e1n", "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_double_button_long_press": "Mindk\u00e9t \"{subtype}\" hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en megjelent", - "remote_double_button_short_press": "Mindk\u00e9t \"{subtype}\" megjelent" + "remote_double_button_short_press": "Mindk\u00e9t \"{subtype}\" megjelent", + "repeat": "\"{subtype}\"gomb lenyomva tartava", + "short_release": "\"{subtype}\" felengedve r\u00f6vid nyomva tart\u00e1s ut\u00e1n" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Hue csoportok enged\u00e9lyez\u00e9se", + "allow_hue_scenes": "Hue jelenetek enged\u00e9lyez\u00e9se", "allow_unreachable": "Hagyja, hogy az el\u00e9rhetetlen izz\u00f3k helyesen jelents\u00e9k \u00e1llapotukat" } } diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index c9e0bcd75d4..1084d980ea5 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Tombol pertama", + "2": "Tombol kedua", + "3": "Tombol ketiga", + "4": "Tombol keempat", "button_1": "Tombol pertama", "button_2": "Tombol kedua", "button_3": "Tombol ketiga", @@ -47,11 +51,16 @@ "turn_on": "Nyalakan" }, "trigger_type": { + "double_short_release": "Kedua \"{subtype}\" dilepaskan", + "initial_press": "Tombol \"{subtype}\" awalnya ditekan", + "long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", "remote_button_short_press": "Tombol \"{subtype}\" ditekan", "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas" + "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas", + "repeat": "Tombol \"{subtype}\" ditekan terus", + "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Izinkan grup Hue", + "allow_hue_scenes": "Izinkan skenario Hue", "allow_unreachable": "Izinkan bohlam yang tidak dapat dijangkau untuk melaporkan statusnya dengan benar" } } diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 0938c18e1ea..12eeaf71af0 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Eerste knop", + "2": "Tweede knop", + "3": "Derde knop", + "4": "Vierde knop", "button_1": "Eerste knop", "button_2": "Tweede knop", "button_3": "Derde knop", @@ -47,11 +51,16 @@ "turn_on": "Inschakelen" }, "trigger_type": { + "double_short_release": "Beide \" {subtype} \" losgelaten", + "initial_press": "Knop \" {subtype} \" aanvankelijk ingedrukt", + "long_release": "Knop \"{subtype}\" losgelaten na lang indrukken", "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang drukken", "remote_button_short_press": "\"{subtype}\" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_double_button_long_press": "Beide \"{subtype}\" losgelaten na lang indrukken", - "remote_double_button_short_press": "Beide \"{subtype}\" losgelaten" + "remote_double_button_short_press": "Beide \"{subtype}\" losgelaten", + "repeat": "Knop \" {subtype} \" ingedrukt gehouden", + "short_release": "Knop \"{subtype}\" losgelaten na kort indrukken" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Sta Hue-groepen toe", + "allow_hue_scenes": "Sta Hue sc\u00e8nes toe", "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden" } } diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index e2b52628749..c813f80d198 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "F\u00f8rste knapp", + "2": "Andre knapp", + "3": "Tredje knapp", + "4": "Fjerde knapp", "button_1": "F\u00f8rste knapp", "button_2": "Andre knapp", "button_3": "Tredje knapp", @@ -47,11 +51,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { + "double_short_release": "Begge \"{subtype}\" er utgitt", + "initial_press": "Knappen \"{subtype}\" ble f\u00f8rst trykket", + "long_release": "Knapp \"{subtype}\" slippes etter lang trykk", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_double_button_long_press": "Begge \"{subtype}\" utgitt etter lang trykk", - "remote_double_button_short_press": "Begge \"{subtype}\" utgitt" + "remote_double_button_short_press": "Begge \"{subtype}\" utgitt", + "repeat": "Knappen \" {subtype} \" holdt nede", + "short_release": "Knapp \"{subtype}\" slippes etter kort trykk" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Tillat Hue-grupper", + "allow_hue_scenes": "Tillat Hue-scener", "allow_unreachable": "Tillat uoppn\u00e5elige p\u00e6rer \u00e5 rapportere sin tilstand riktig" } } diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 81cbe6a385f..237d91cc817 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", @@ -47,11 +51,16 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c" }, "trigger_type": { + "double_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "initial_press": "{subtype} \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" + "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "repeat": "{subtype} \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b Hue", + "allow_hue_scenes": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u0446\u0435\u043d\u044b Hue", "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" } } diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index f1b8a70f070..32207430e90 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "\u7b2c\u4e00\u500b\u6309\u9215", + "2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "3": "\u7b2c\u4e09\u500b\u6309\u9215", + "4": "\u7b2c\u56db\u500b\u6309\u9215", "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", @@ -47,11 +51,16 @@ "turn_on": "\u958b\u555f" }, "trigger_type": { + "double_short_release": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", + "initial_press": "\u6309\u9215 \"{subtype}\" \u6700\u521d\u6309\u4e0b", + "long_release": "\u6309\u9215 \"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_double_button_long_press": "\"{subtype}\" \u4e00\u8d77\u9577\u6309\u5f8c\u91cb\u653e", - "remote_double_button_short_press": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e" + "remote_double_button_short_press": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", + "repeat": "\u6309\u9215 \"{subtype}\" \u6309\u4e0b", + "short_release": "\u6309\u9215 \"{subtype}\" \u77ed\u6309\u5f8c\u91cb\u653e" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "\u5141\u8a31 Hue \u7fa4\u7d44", + "allow_hue_scenes": "\u5141\u8a31 Hue \u5834\u666f", "allow_unreachable": "\u5141\u8a31\u7121\u6cd5\u9023\u7dda\u7684\u71c8\u6ce1\u6b63\u78ba\u56de\u5831\u5176\u72c0\u614b" } } diff --git a/homeassistant/components/motioneye/translations/id.json b/homeassistant/components/motioneye/translations/id.json index 0278ac26195..ac4aa89c8ef 100644 --- a/homeassistant/components/motioneye/translations/id.json +++ b/homeassistant/components/motioneye/translations/id.json @@ -15,6 +15,8 @@ "data": { "admin_password": "Kata Sandi Admin", "admin_username": "Nama Pengguna Admin", + "surveillance_password": "Kata Sandi Surveillance", + "surveillance_username": "Nama Pengguna Surveillance", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 14e047c1694..338ca6fa6fe 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -67,7 +67,7 @@ "data": { "discovery": "Aktifkan penemuan" }, - "description": "Pilih opsi MQTT." + "description": "Penemuan - Jika penemuan diaktifkan (disarankan), Home Assistant akan secara otomatis menemukan perangkat dan entitas yang mempublikasikan konfigurasinya di broker MQTT. Jika penemuan dinonaktifkan, semua konfigurasi harus dilakukan secara manual.\nPesan birth - Pesan birth akan dikirim setiap kali Home Assistant terhubung (kembali) ke broker MQTT.\nPesan will akan dikirim setiap kali Home Assistant kehilangan koneksi ke broker, baik dalam kasus bersih (misalnya Home Assistant dimatikan) dan dalam kasus (misalnya Home Assistant mogok atau kehilangan koneksi jaringan) terputus yang tidak bersih." } } } diff --git a/homeassistant/components/octoprint/translations/id.json b/homeassistant/components/octoprint/translations/id.json index 2f6f333f799..a728c0c9401 100644 --- a/homeassistant/components/octoprint/translations/id.json +++ b/homeassistant/components/octoprint/translations/id.json @@ -1,15 +1,18 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "user": { "data": { + "host": "Host", "username": "Nama Pengguna" } } diff --git a/homeassistant/components/owntracks/translations/nl.json b/homeassistant/components/owntracks/translations/nl.json index ff6cadcbf25..65189e6b0be 100644 --- a/homeassistant/components/owntracks/translations/nl.json +++ b/homeassistant/components/owntracks/translations/nl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: `''`\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." + "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification':\n - Username: `''`\n - Device ID: `''`\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: `''`\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/pl.json b/homeassistant/components/owntracks/translations/pl.json index 98c8779fe1f..09bd29b99f2 100644 --- a/homeassistant/components/owntracks/translations/pl.json +++ b/homeassistant/components/owntracks/translations/pl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { - "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkow'nika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index ed1e084090d..09fdba77266 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sl.json b/homeassistant/components/owntracks/translations/sl.json index 9c75e9a9537..f038345498e 100644 --- a/homeassistant/components/owntracks/translations/sl.json +++ b/homeassistant/components/owntracks/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n V Androidu odprite aplikacijo OwnTracks ( {android_url} ) in pojdite na {android_url} nastavitve - > povezave. Spremenite naslednje nastavitve: \n - Na\u010din: zasebni HTTP \n - gostitelj: {webhook_url} \n - Identifikacija: \n - Uporabni\u0161ko ime: ` ` \n - ID naprave: ` ` \n\n V iOS-ju odprite aplikacijo OwnTracks ( {ios_url} ), tapnite ikono (i) v zgornjem levem kotu - > nastavitve. Spremenite naslednje nastavitve: \n - na\u010din: HTTP \n - URL: {webhook_url} \n - Vklopite preverjanje pristnosti \n - UserID: ` ` \n\n {secret} \n \n Za ve\u010d informacij si oglejte [dokumentacijo] ( {docs_url} )." + "default": "\n\nV Androidu odprite aplikacijo OwnTracks ({android_url}) in pojdite na {android_url} nastavitve - > povezave. Spremenite naslednje nastavitve: \n - Na\u010din: zasebni HTTP \n - gostitelj: {webhook_url} \n - Identifikacija: \n - Uporabni\u0161ko ime: `''` \n - ID naprave: `''` \n\n V iOS-ju odprite aplikacijo OwnTracks ({ios_url}), tapnite ikono (i) v zgornjem levem kotu - > nastavitve. Spremenite naslednje nastavitve: \n - na\u010din: HTTP \n - URL: {webhook_url} \n - Vklopite preverjanje pristnosti \n - UserID: `''` \n\n {secret} \n \n Za ve\u010d informacij si oglejte [dokumentacijo] ({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index 8642a32f889..fd2162f153b 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: `''`\n - Enhets-ID: `''` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `''` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." + "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index c355b745d1d..e6a6fc26068 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." }, "create_entry": { - "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: `''`\n- Device ID: `''` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." }, "step": { "user": { diff --git a/homeassistant/components/ridwell/translations/id.json b/homeassistant/components/ridwell/translations/id.json index 8cfa56b690a..d19dabb2356 100644 --- a/homeassistant/components/ridwell/translations/id.json +++ b/homeassistant/components/ridwell/translations/id.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, "error": { "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index cea3f890430..e65fe367ca2 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Level konsentasi karbonmonoksida {entity_name} saat ini", "is_current": "Arus {entity_name} saat ini", "is_energy": "Energi {entity_name} saat ini", + "is_frequency": "Frekuensi {entity_name} saat ini", "is_gas": "Gas {entity_name} saat ini", "is_humidity": "Kelembaban {entity_name} saat ini", "is_illuminance": "Pencahayaan {entity_name} saat ini", @@ -32,6 +33,7 @@ "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", "current": "Perubahan arus {entity_name}", "energy": "Perubahan energi {entity_name}", + "frequency": "Perubahan frekuensi {entity_name}", "gas": "Perubahan gas {entity_name}", "humidity": "Perubahan kelembaban {entity_name}", "illuminance": "Perubahan pencahayaan {entity_name}", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 1580a716dee..df3786b1415 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5", "is_current": "Gjeldende {entity_name} str\u00f8m", "is_energy": "Gjeldende {entity_name} effekt", + "is_frequency": "Gjeldende {entity_name} -frekvens", "is_gas": "Gjeldende {entity_name} gass", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "energy": "{entity_name} effektendringer", + "frequency": "{entity_name} frekvensendringer", "gas": "{entity_name} gass endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index 2f385796fd1..f4ddf62aa21 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -33,12 +33,13 @@ "button": "Tombol", "button1": "Tombol pertama", "button2": "Tombol kedua", - "button3": "Tombol ketiga" + "button3": "Tombol ketiga", + "button4": "Tombol keempat" }, "trigger_type": { "double": "{subtype} diklik dua kali", "long": "{subtype} diklik lama", - "long_push": "Push lama {subtype}", + "long_push": "Push lama {subtype}", "long_single": "{subtype} diklik lama kemudian diklik sekali", "single": "{subtype} diklik sekali", "single_long": "{subtype} diklik sekali kemudian diklik lama", diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index 89b29b9ae1f..8169a6be4bf 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "reconfigure_successful": "Konfigurasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -35,13 +36,15 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, + "description": "Alasan: {details}", "title": "Autentikasi Ulang Integrasi Synology DSM" }, "reauth_confirm": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "title": "Autentikasi Ulang Integrasi Synology DSM" }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index 839d901047b..c1624f08259 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", + "no_locations": "Ingen plasseringer er tilgjengelige for denne brukeren, sjekk TotalConnect-innstillingene", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json index 06f99cc3a8e..91bf29f3ec8 100644 --- a/homeassistant/components/tuya/translations/id.json +++ b/homeassistant/components/tuya/translations/id.json @@ -6,15 +6,33 @@ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { - "invalid_auth": "Autentikasi tidak valid" + "invalid_auth": "Autentikasi tidak valid", + "login_error": "Kesalahan masuk ({code}): {msg}" }, "flow_title": "Konfigurasi Tuya", "step": { + "login": { + "data": { + "access_id": "ID Akses", + "access_secret": "Kode Rahasia Akses", + "country_code": "Kode Negara", + "endpoint": "Zona Ketersediaan", + "password": "Kata Sandi", + "tuya_app_type": "Aplikasi Seluler", + "username": "Akun" + }, + "description": "Masukkan kredensial Tuya Anda", + "title": "Tuya" + }, "user": { "data": { + "access_id": "ID Akses Tuya IoT", + "access_secret": "Kode Rahasia Akses Tuya IoT", "country_code": "Negara", "password": "Kata Sandi", "platform": "Aplikasi tempat akun Anda terdaftar", + "region": "Wilayah", + "tuya_project_type": "Jenis proyek awan Tuya", "username": "Akun" }, "description": "Masukkan kredensial Tuya Anda.", diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json index caa94b14c89..fa65d7a36d3 100644 --- a/homeassistant/components/tuya/translations/select.id.json +++ b/homeassistant/components/tuya/translations/select.id.json @@ -1,9 +1,45 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Dinonaktifkan", + "1": "50Hz", + "2": "60Hz" + }, + "tuya__basic_nightvision": { + "0": "Otomatis", + "1": "Mati", + "2": "Nyala" + }, + "tuya__decibel_sensitivity": { + "0": "Sensitivitas rendah", + "1": "Sensitivitas tinggi" + }, + "tuya__ipc_work_mode": { + "0": "Mode daya rendah", + "1": "Mode kerja terus menerus" + }, + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Pijar", + "led": "LED" + }, "tuya__light_mode": { - "none": "Mati" + "none": "Mati", + "pos": "Menunjukkan lokasi sakelar", + "relay": "Menunjukkan status sakelar nyala/mati" + }, + "tuya__motion_sensitivity": { + "0": "Sensitivitas rendah", + "1": "Sensitivitas sedang", + "2": "Sensitivitas tinggi" + }, + "tuya__record_mode": { + "1": "Rekam peristiwa saja", + "2": "Perekaman terus menerus" }, "tuya__relay_status": { + "last": "Ingat status terakhir", + "memory": "Ingat status terakhir", "off": "Mati", "on": "Nyala", "power_off": "Mati", diff --git a/homeassistant/components/tuya/translations/sensor.id.json b/homeassistant/components/tuya/translations/sensor.id.json new file mode 100644 index 00000000000..cf9fc8bd738 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.id.json @@ -0,0 +1,14 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Suhu mendidih", + "cooling": "Mendinginkan", + "heating": "Memanaskan", + "heating_temp": "Suhu pemanas", + "reserve_1": "Cadangan 1", + "reserve_2": "Cadangan 2", + "reserve_3": "Cadangan 3", + "standby": "Siaga" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/id.json b/homeassistant/components/venstar/translations/id.json new file mode 100644 index 00000000000..1f64e8aa6a5 --- /dev/null +++ b/homeassistant/components/venstar/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "pin": "Kode PIN", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke Termostat Venstar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/id.json b/homeassistant/components/vlc_telnet/translations/id.json index d0b5d58f167..4037cb21b4d 100644 --- a/homeassistant/components/vlc_telnet/translations/id.json +++ b/homeassistant/components/vlc_telnet/translations/id.json @@ -14,6 +14,15 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Ingin terhubung ke add-on {addon}?" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Harap masukkan kata sandi yang benar untuk host: {host}" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/wallbox/translations/id.json b/homeassistant/components/wallbox/translations/id.json index becbcbe817f..08611ab3c2e 100644 --- a/homeassistant/components/wallbox/translations/id.json +++ b/homeassistant/components/wallbox/translations/id.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "reauth_invalid": "Autentikasi ulang gagal; Nomor seri tidak cocok dengan aslinya", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "password": "Kata Sandi", + "station": "Nomor Seri Stasiun", "username": "Nama Pengguna" } } diff --git a/homeassistant/components/wallbox/translations/no.json b/homeassistant/components/wallbox/translations/no.json index 42368703121..74bbb1f39d5 100644 --- a/homeassistant/components/wallbox/translations/no.json +++ b/homeassistant/components/wallbox/translations/no.json @@ -1,14 +1,22 @@ { "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", "invalid_auth": "Ugyldig godkjenning", + "reauth_invalid": "Re-autentisering mislyktes; Serienummeret samsvarer ikke med originalen", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/watttime/translations/id.json b/homeassistant/components/watttime/translations/id.json index afce5debec3..6cb1cc1c646 100644 --- a/homeassistant/components/watttime/translations/id.json +++ b/homeassistant/components/watttime/translations/id.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_coordinates": "Tidak ada data untuk garis lintang/bujur" }, "step": { "coordinates": { @@ -26,6 +27,7 @@ "data": { "password": "Kata Sandi" }, + "description": "Masukkan kembali kata sandi untuk {username} :", "title": "Autentikasi Ulang Integrasi" }, "user": { @@ -36,5 +38,15 @@ "description": "Masukkan nama pengguna dan kata sandi Anda:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Tampilkan lokasi yang dipantau di peta" + }, + "title": "Konfigurasikan WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json index 122cfd9da0b..621b11e4af5 100644 --- a/homeassistant/components/wled/translations/id.json +++ b/homeassistant/components/wled/translations/id.json @@ -20,5 +20,14 @@ "title": "Peranti WLED yang ditemukan" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Pertahankan cahaya master, bahkan dengan 1 segmen LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index a6217b52eb1..4aae8d6396c 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -3,12 +3,18 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "incomplete_info": "Informasi tidak lengkap untuk menyiapkan perangkat, tidak ada host atau token yang disediakan.", + "not_xiaomi_miio": "Perangkat (masih) tidak didukung oleh Xiaomi Miio.", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", + "cloud_credentials_incomplete": "Kredensial cloud tidak lengkap, isi nama pengguna, kata sandi, dan negara", + "cloud_login_error": "Tidak dapat masuk ke Xiaomi Miio Cloud, periksa kredensialnya.", + "cloud_no_devices": "Tidak ada perangkat yang ditemukan di akun cloud Xiaomi Miio ini.", "no_device_selected": "Tidak ada perangkat yang dipilih, pilih satu perangkat.", - "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi." + "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi.", + "wrong_token": "Kesalahan checksum, token salah" }, "flow_title": "{name}", "step": { @@ -53,9 +59,11 @@ "host": "Alamat IP", "token": "Token API" }, + "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara.", "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" }, "reauth_confirm": { + "description": "Integrasi Xiaomi Miio perlu mengautentikasi ulang akun Anda untuk memperbarui token atau menambahkan kredensial cloud yang hilang.", "title": "Autentikasi Ulang Integrasi" }, "select": { @@ -75,6 +83,9 @@ } }, "options": { + "error": { + "cloud_credentials_incomplete": "Kredensial cloud tidak lengkap, isi nama pengguna, kata sandi, dan negara" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/id.json b/homeassistant/components/yamaha_musiccast/translations/id.json index 72a79af2041..9f1a68abdfd 100644 --- a/homeassistant/components/yamaha_musiccast/translations/id.json +++ b/homeassistant/components/yamaha_musiccast/translations/id.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "yxc_control_url_missing": "URL kontrol tidak diberikan dalam deskripsi SSDP." + }, + "error": { + "no_musiccast_device": "Perangkat ini tampaknya bukan Perangkat MusicCast." }, "flow_title": "MusicCast: {name}", "step": { @@ -11,7 +15,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Siapkan MusicCast untuk diintegrasikan dengan Home Assistant." } } } diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 0d796cc6491..19f94f0ab1f 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -68,7 +68,8 @@ "refresh_value": "Segarkan nilai untuk {entity_name}", "reset_meter": "Setel ulang pengukur di {subtype}", "set_config_parameter": "Tetapkan nilai parameter konfigurasi {subtype}", - "set_lock_usercode": "Setel usercode pada {entity_name}" + "set_lock_usercode": "Setel usercode pada {entity_name}", + "set_value": "Setel nilai Nilai Z-Wave" }, "condition_type": { "config_parameter": "Nilai parameter konfigurasi {subtype}", @@ -76,7 +77,14 @@ "value": "Nilai saat ini dari Nilai Z-Wave" }, "trigger_type": { - "state.node_status": "Status node berubah" + "event.notification.entry_control": "Mengirim notifikasi Entry Control", + "event.notification.notification": "Mengirim notifikasi", + "event.value_notification.basic": "Peristiwa Basic CC pada {subtype}", + "event.value_notification.central_scene": "Aksi Central Scene pada {subtype}", + "event.value_notification.scene_activation": "Aktivasi Skenario di {subtype}", + "state.node_status": "Status node berubah", + "zwave_js.value_updated.config_parameter": "Perubahan nilai pada parameter konfigurasi {subtype}", + "zwave_js.value_updated.value": "Perubahan nilai pada Nilai Z-Wave JS" } }, "options": { @@ -105,6 +113,10 @@ "emulate_hardware": "Emulasikan Perangkat Keras", "log_level": "Tingkat log", "network_key": "Kunci Jaringan", + "s0_legacy_key": "Kunci S0 (Warisan)", + "s2_access_control_key": "Kunci Kontrol Akses S2", + "s2_authenticated_key": "Kunci Autentikasi S2", + "s2_unauthenticated_key": "Kunci S2 Tidak Diautentikasi", "usb_path": "Jalur Perangkat USB" }, "title": "Masukkan konfigurasi add-on Z-Wave JS" From 1c11e7061d498602370a80a7b370d181b3b9f588 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 18 Nov 2021 02:00:19 +0100 Subject: [PATCH 0572/1452] Bump Nettigo Air Monitor backend library (#59675) --- homeassistant/components/nam/__init__.py | 37 ++- homeassistant/components/nam/config_flow.py | 136 ++++++++--- homeassistant/components/nam/manifest.json | 2 +- homeassistant/components/nam/strings.json | 23 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/__init__.py | 10 +- tests/components/nam/test_config_flow.py | 245 ++++++++++++++++++-- tests/components/nam/test_init.py | 22 +- tests/components/nam/test_sensor.py | 27 ++- 10 files changed, 425 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 5052ffbaf1f..98152956fb5 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -1,14 +1,16 @@ """The Nettigo Air Monitor component.""" from __future__ import annotations +import asyncio import logging from typing import cast -from aiohttp import ClientSession -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ClientConnectorError, ClientError import async_timeout from nettigo_air_monitor import ( ApiError, + AuthFailed, + ConnectionOptions, InvalidSensorData, NAMSensors, NettigoAirMonitor, @@ -16,13 +18,18 @@ from nettigo_air_monitor import ( from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( ATTR_SDS011, @@ -41,10 +48,20 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nettigo as config entry.""" host: str = entry.data[CONF_HOST] + username: str | None = entry.data.get(CONF_USERNAME) + password: str | None = entry.data.get(CONF_PASSWORD) websession = async_get_clientsession(hass) - coordinator = NAMDataUpdateCoordinator(hass, websession, host, entry.unique_id) + options = ConnectionOptions(host=host, username=username, password=password) + try: + nam = await NettigoAirMonitor.create(websession, options) + except AuthFailed as err: + raise ConfigEntryAuthFailed from err + except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) @@ -81,14 +98,12 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - session: ClientSession, - host: str, + nam: NettigoAirMonitor, unique_id: str | None, ) -> None: """Initialize.""" - self.host = host - self.nam = NettigoAirMonitor(session, host) self._unique_id = unique_id + self.nam = nam super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL @@ -102,6 +117,8 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): # get the data 4 times, so we use a longer than usual timeout here. async with async_timeout.timeout(30): data = await self.nam.async_update() + # We do not need to catch AuthFailed exception here because sensor data is + # always available without authorization. except (ApiError, ClientConnectorError, InvalidSensorData) as error: raise UpdateFailed(error) from error @@ -120,5 +137,5 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): name=DEFAULT_NAME, sw_version=self.nam.software_version, manufacturer=MANUFACTURER, - configuration_url=f"http://{self.host}/", + configuration_url=f"http://{self.nam.host}/", ) diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 86a30f95af6..0e98e4f7ef5 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -3,16 +3,23 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import Any from aiohttp.client_exceptions import ClientConnectorError import async_timeout -from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor +from nettigo_air_monitor import ( + ApiError, + AuthFailed, + CannotGetMac, + ConnectionOptions, + NettigoAirMonitor, +) import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -21,6 +28,23 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +AUTH_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +async def async_get_mac(hass: HomeAssistant, host: str, data: dict[str, Any]) -> str: + """Get device MAC address.""" + websession = async_get_clientsession(hass) + + options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) + nam = await NettigoAirMonitor.create(websession, options) + # Device firmware uses synchronous code and doesn't respond to http queries + # when reading data from sensors. The nettigo-air-monitor library tries to get + # the data 4 times, so we use a longer than usual timeout here. + async with async_timeout.timeout(30): + return await nam.async_get_mac_address() + class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Nettigo Air Monitor.""" @@ -29,18 +53,22 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self.host: str | None = None + self.host: str + self.entry: config_entries.ConfigEntry async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.host = user_input[CONF_HOST] + try: - mac = await self._async_get_mac(cast(str, self.host)) + mac = await async_get_mac(self.hass, self.host, {}) + except AuthFailed: + return await self.async_step_credentials() except (ApiError, ClientConnectorError, asyncio.TimeoutError): errors["base"] = "cannot_connect" except CannotGetMac: @@ -49,36 +77,65 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(format_mac(mac)) self._abort_if_unique_id_configured({CONF_HOST: self.host}) return self.async_create_entry( - title=cast(str, self.host), + title=self.host, data=user_input, ) return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=""): str, - } - ), + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), errors=errors, ) + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the credentials step.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + mac = await async_get_mac(self.hass, self.host, user_input) + except AuthFailed: + errors["base"] = "invalid_auth" + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except CannotGetMac: + return self.async_abort(reason="device_unsupported") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + return self.async_create_entry( + title=self.host, + data={**user_input, CONF_HOST: self.host}, + ) + + return self.async_show_form( + step_id="credentials", data_schema=AUTH_SCHEMA, errors=errors + ) + async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] + self.context["title_placeholders"] = {"host": self.host} # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) try: - mac = await self._async_get_mac(self.host) + mac = await async_get_mac(self.hass, self.host, {}) + except AuthFailed: + return await self.async_step_credentials() except (ApiError, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="cannot_connect") except CannotGetMac: @@ -87,21 +144,17 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(format_mac(mac)) self._abort_if_unique_id_configured({CONF_HOST: self.host}) - self.context["title_placeholders"] = { - ATTR_NAME: discovery_info[ATTR_NAME].split(".")[0] - } - return await self.async_step_confirm_discovery() async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle discovery confirm.""" - errors: dict = {} + errors: dict[str, str] = {} if user_input is not None: return self.async_create_entry( - title=cast(str, self.host), + title=self.host, data={CONF_HOST: self.host}, ) @@ -109,16 +162,39 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="confirm_discovery", - description_placeholders={CONF_HOST: self.host}, + description_placeholders={"host": self.host}, errors=errors, ) - async def _async_get_mac(self, host: str) -> str: - """Get device MAC address.""" - websession = async_get_clientsession(self.hass) - nam = NettigoAirMonitor(websession, host) - # Device firmware uses synchronous code and doesn't respond to http queries - # when reading data from sensors. The nettigo-air-monitor library tries to get - # the data 4 times, so we use a longer than usual timeout here. - async with async_timeout.timeout(30): - return await nam.async_get_mac_address() + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]): + self.entry = entry + self.host = data[CONF_HOST] + self.context["title_placeholders"] = {"host": self.host} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await async_get_mac(self.hass, self.host, user_input) + except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError): + return self.async_abort(reason="reauth_unsuccessful") + else: + self.hass.config_entries.async_update_entry( + self.entry, data={**user_input, CONF_HOST: self.host} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={"host": self.host}, + data_schema=AUTH_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 114fc4dd48d..1aab1cf4613 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==1.1.1"], + "requirements": ["nettigo-air-monitor==1.2.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index e8994a346bf..dab6eefb095 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{host}", "step": { "user": { "description": "Set up Nettigo Air Monitor integration.", @@ -8,17 +8,34 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "credentials": { + "description": "Please enter the username and password.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "description": "Please enter the correct username and password for host: {host}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "confirm_discovery": { "description": "Do you want to set up Nettigo Air Monitor at {host}?" } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "device_unsupported": "The device is unsupported." + "device_unsupported": "The device is unsupported.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 493fe9782bf..bfe22c2a1cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1056,7 +1056,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.1.1 +nettigo-air-monitor==1.2.1 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c08e1aded43..0c7fe4f9dbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -642,7 +642,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.1.1 +nettigo-air-monitor==1.2.1 # homeassistant.components.nexia nexia==0.9.11 diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 8106f97ef31..eb723405076 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -1,5 +1,5 @@ """Tests for the Nettigo Air Monitor integration.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.nam.const import DOMAIN @@ -52,9 +52,11 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: # Remove conc_co2_ppm value nam_data["sensordatavalues"].pop(6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, + update_response = Mock(json=AsyncMock(return_value=nam_data)) + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index b0ecb20da11..3dbbe416831 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -2,21 +2,22 @@ import asyncio from unittest.mock import patch -from nettigo_air_monitor import ApiError, CannotGetMac +from nettigo_air_monitor import ApiError, AuthFailed, CannotGetMac import pytest from homeassistant import data_entry_flow from homeassistant.components.nam.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from tests.common import MockConfigEntry -DISCOVERY_INFO = {"host": "10.10.2.3", "name": "NAM-12345"} +DISCOVERY_INFO = {"host": "10.10.2.3"} VALID_CONFIG = {"host": "10.10.2.3"} +VALID_AUTH = {"username": "fake_username", "password": "fake_password"} -async def test_form_create_entry(hass): - """Test that the user step works.""" +async def test_form_create_entry_without_auth(hass): + """Test that the user step without auth works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -24,13 +25,12 @@ async def test_form_create_entry(hass): assert result["step_id"] == SOURCE_USER assert result["errors"] == {} - with patch( + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( "homeassistant.components.nam.async_setup_entry", return_value=True ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -43,10 +43,153 @@ async def test_form_create_entry(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_create_entry_with_auth(hass): + """Test that the user step with auth works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=AuthFailed("Auth Error"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "credentials" + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_AUTH, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"]["host"] == "10.10.2.3" + assert result["data"]["username"] == "fake_username" + assert result["data"]["password"] == "fake_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_successful(hass): + """Test starting a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=VALID_AUTH, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_unsuccessful(hass): + """Test starting a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=ApiError("API Error"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=VALID_AUTH, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_unsuccessful" + + @pytest.mark.parametrize( "error", [ - (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (ApiError("API Error"), "cannot_connect"), + (AuthFailed("Auth Error"), "invalid_auth"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_with_auth_errors(hass, error): + """Test we handle errors when auth is required.""" + exc, base_error = error + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=AuthFailed("Auth Error"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "credentials" + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=exc, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_AUTH, + ) + + assert result["errors"] == {"base": base_error} + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("API Error"), "cannot_connect"), (asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], @@ -55,7 +198,7 @@ async def test_form_errors(hass, error): """Test we handle errors.""" exc, base_error = error with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): @@ -70,11 +213,10 @@ async def test_form_errors(hass, error): async def test_form_abort(hass): """Test we handle abort after error.""" - with patch( + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", side_effect=CannotGetMac("Cannot get MAC address from device"), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -85,6 +227,34 @@ async def test_form_abort(hass): assert result["reason"] == "device_unsupported" +async def test_form_with_auth_abort(hass): + """Test we handle abort after error.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=AuthFailed("Auth Error"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "credentials" + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=CannotGetMac("Cannot get MAC address from device"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_AUTH, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "device_unsupported" + + async def test_form_already_configured(hass): """Test that errors are shown when duplicates are added.""" entry = MockConfigEntry( @@ -96,7 +266,7 @@ async def test_form_already_configured(hass): DOMAIN, context={"source": SOURCE_USER} ) - with patch( + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ): @@ -114,7 +284,7 @@ async def test_form_already_configured(hass): async def test_zeroconf(hass): """Test we get the form.""" - with patch( + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ): @@ -131,7 +301,7 @@ async def test_zeroconf(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert context["title_placeholders"]["name"] == "NAM-12345" + assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True with patch( @@ -150,6 +320,48 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_with_auth(hass): + """Test that the zeroconf step with auth works.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=AuthFailed("Auth Error"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "credentials" + assert result["errors"] == {} + assert context["title_placeholders"]["host"] == "10.10.2.3" + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_AUTH, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"]["host"] == "10.10.2.3" + assert result["data"]["username"] == "fake_username" + assert result["data"]["password"] == "fake_password" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_host_already_configured(hass): """Test that errors are shown when host is already configured.""" entry = MockConfigEntry( @@ -170,7 +382,7 @@ async def test_zeroconf_host_already_configured(hass): @pytest.mark.parametrize( "error", [ - (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (ApiError("API Error"), "cannot_connect"), (CannotGetMac("Cannot get MAC address from device"), "device_unsupported"), ], ) @@ -178,10 +390,9 @@ async def test_zeroconf_errors(hass, error): """Test we handle errors.""" exc, reason = error with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): - result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 97392cbaff8..3223a394f68 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -1,7 +1,7 @@ """Test init of Nettigo Air Monitor integration.""" from unittest.mock import patch -from nettigo_air_monitor import ApiError +from nettigo_air_monitor import ApiError, AuthFailed from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.nam.const import DOMAIN @@ -33,7 +33,7 @@ async def test_config_not_ready(hass): ) with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=ApiError("API Error"), ): entry.add_to_hass(hass) @@ -41,6 +41,24 @@ async def test_config_not_ready(hass): assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_config_auth_failed(hass): + """Test for setup failure if the auth fails.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.initialize", + side_effect=AuthFailed("Authorization has failed"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_unload_entry(hass): """Test successful unload of entry.""" entry = await init_integration(hass) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 68c0044e590..aa05930f727 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -1,6 +1,6 @@ """Test sensor of Nettigo Air Monitor integration.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from nettigo_air_monitor import ApiError @@ -373,9 +373,10 @@ async def test_incompleta_data_after_device_restart(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=INCOMPLETE_NAM_DATA, + update_response = Mock(json=AsyncMock(return_value=INCOMPLETE_NAM_DATA)) + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -395,8 +396,8 @@ async def test_availability(hass): assert state.state == "7.6" future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", side_effect=ApiError("API Error"), ): async_fire_time_changed(hass, future) @@ -407,9 +408,10 @@ async def test_availability(hass): assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=12) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, + update_response = Mock(json=AsyncMock(return_value=nam_data)) + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -426,9 +428,10 @@ async def test_manual_update_entity(hass): await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, + update_response = Mock(json=AsyncMock(return_value=nam_data)) + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, ) as mock_get_data: await hass.services.async_call( "homeassistant", From 5d0c75888650d697d45bc6a4d3a8a9e1a3fa5c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Thu, 18 Nov 2021 02:56:04 +0100 Subject: [PATCH 0573/1452] Bump pysma to 0.6.9 (#59848) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 13e29c8227c..4b48ce6aef1 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.7"], + "requirements": ["pysma==0.6.9"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index bfe22c2a1cd..bdbc0e9d757 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1799,7 +1799,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.7 +pysma==0.6.9 # homeassistant.components.smappee pysmappee==0.2.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c7fe4f9dbc..e050d3521b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1094,7 +1094,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.7 +pysma==0.6.9 # homeassistant.components.smappee pysmappee==0.2.27 From e180f1e30214172e0a2907d82a82fae85301328d Mon Sep 17 00:00:00 2001 From: alim4r <7687869+alim4r@users.noreply.github.com> Date: Thu, 18 Nov 2021 04:35:48 +0100 Subject: [PATCH 0574/1452] Add input_number state to prometheus metrics (#56507) * Add input_number to prometheus metrics * Add prometheus input_number tests * Removed unused import from test --- .../components/prometheus/__init__.py | 9 +++++++ tests/components/prometheus/test_init.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 7c531a292b1..e0c82061c2c 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -276,6 +276,15 @@ class PrometheusMetrics: value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) + def _handle_input_number(self, state): + metric = self._metric( + "input_number_state", + self.prometheus_cli.Gauge, + "State of the input number", + ) + value = self.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + def _handle_device_tracker(self, state): metric = self._metric( "device_tracker_state", diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 827190a4b41..0fdbc23bb07 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -7,6 +7,7 @@ import unittest.mock as mock import pytest from homeassistant.components import climate, humidifier, sensor +from homeassistant.components.demo.number import DemoNumber from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus from homeassistant.const import ( @@ -99,6 +100,17 @@ async def prometheus_client(hass, hass_client, namespace): sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" await sensor5.async_update_ha_state() + number1 = DemoNumber(None, "Threshold", 5.2, None, False, 0, 10, 0.1) + number1.hass = hass + number1.entity_id = "input_number.threshold" + await number1.async_update_ha_state() + + number2 = DemoNumber(None, None, 60, None, False, 0, 100) + number2.hass = hass + number2.entity_id = "input_number.brightness" + number2._attr_name = None + await number2.async_update_ha_state() + return await hass_client() @@ -229,6 +241,18 @@ async def test_view_empty_namespace(hass, hass_client): 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body ) + assert ( + 'input_number_state{domain="input_number",' + 'entity="input_number.threshold",' + 'friendly_name="Threshold"} 5.2' in body + ) + + assert ( + 'input_number_state{domain="input_number",' + 'entity="input_number.brightness",' + 'friendly_name="None"} 60.0' in body + ) + async def test_view_default_namespace(hass, hass_client): """Test prometheus metrics view.""" From 94bfa5272d2a52181f85095f88121dd963898ff0 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Wed, 17 Nov 2021 20:30:58 -0800 Subject: [PATCH 0575/1452] Remove the need for generics in greeneye_monitor.sensor (#58782) * Remove the need for generics in greeneye_monitor.sensor * Remove unused imports * Store monitor and use a property instead --- .../components/greeneye_monitor/sensor.py | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index de71e3c27fa..57c5c79891d 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,7 +1,7 @@ """Support for the sensors in a GreenEye Monitor.""" from __future__ import annotations -from typing import Any, Generic, Optional, TypeVar, cast +from typing import Any, Optional, Union, cast import greeneye @@ -97,16 +97,15 @@ async def async_setup_platform( async_add_entities(entities) -T = TypeVar( - "T", +UnderlyingSensorType = Union[ greeneye.monitor.Channel, greeneye.monitor.Monitor, greeneye.monitor.PulseCounter, greeneye.monitor.TemperatureSensor, -) +] -class GEMSensor(Generic[T], SensorEntity): +class GEMSensor(SensorEntity): """Base class for GreenEye Monitor sensors.""" _attr_should_poll = False @@ -117,7 +116,7 @@ class GEMSensor(Generic[T], SensorEntity): """Construct the entity.""" self._monitor_serial_number = monitor_serial_number self._attr_name = name - self._sensor: T | None = None + self._monitor: greeneye.monitor.Monitor | None = None self._sensor_type = sensor_type self._number = number self._attr_unique_id = ( @@ -145,21 +144,21 @@ class GEMSensor(Generic[T], SensorEntity): monitors.remove_listener(self._on_new_monitor) def _try_connect_to_monitor(self, monitors: greeneye.Monitors) -> bool: - monitor = monitors.monitors.get(self._monitor_serial_number) - if not monitor: + self._monitor = monitors.monitors.get(self._monitor_serial_number) + if not self._sensor: return False - self._sensor = self._get_sensor(monitor) self._sensor.add_listener(self.async_write_ha_state) self.async_write_ha_state() return True - def _get_sensor(self, monitor: greeneye.monitor.Monitor) -> T: + @property + def _sensor(self) -> UnderlyingSensorType | None: raise NotImplementedError() -class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): +class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" _attr_native_unit_of_measurement = UNIT_WATTS @@ -172,10 +171,9 @@ class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): super().__init__(monitor_serial_number, name, "current", number) self._net_metering = net_metering - def _get_sensor( - self, monitor: greeneye.monitor.Monitor - ) -> greeneye.monitor.Channel: - return monitor.channels[self._number - 1] + @property + def _sensor(self) -> greeneye.monitor.Channel | None: + return self._monitor.channels[self._number - 1] if self._monitor else None @property def native_value(self) -> float | None: @@ -199,7 +197,7 @@ class CurrentSensor(GEMSensor[greeneye.monitor.Channel]): return {DATA_WATT_SECONDS: watt_seconds} -class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): +class PulseCounter(GEMSensor): """Entity showing rate of change in one pulse counter of the monitor.""" _attr_icon = COUNTER_ICON @@ -219,10 +217,9 @@ class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): self._time_unit = time_unit self._attr_native_unit_of_measurement = f"{counted_quantity}/{self._time_unit}" - def _get_sensor( - self, monitor: greeneye.monitor.Monitor - ) -> greeneye.monitor.PulseCounter: - return monitor.pulse_counters[self._number - 1] + @property + def _sensor(self) -> greeneye.monitor.PulseCounter | None: + return self._monitor.pulse_counters[self._number - 1] if self._monitor else None @property def native_value(self) -> float | None: @@ -261,7 +258,7 @@ class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]): return {DATA_PULSES: self._sensor.pulses} -class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): +class TemperatureSensor(GEMSensor): """Entity showing temperature from one temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE @@ -273,10 +270,13 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): super().__init__(monitor_serial_number, name, "temp", number) self._attr_native_unit_of_measurement = unit - def _get_sensor( - self, monitor: greeneye.monitor.Monitor - ) -> greeneye.monitor.TemperatureSensor: - return monitor.temperature_sensors[self._number - 1] + @property + def _sensor(self) -> greeneye.monitor.TemperatureSensor | None: + return ( + self._monitor.temperature_sensors[self._number - 1] + if self._monitor + else None + ) @property def native_value(self) -> float | None: @@ -287,7 +287,7 @@ class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]): return cast(Optional[float], self._sensor.temperature) -class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): +class VoltageSensor(GEMSensor): """Entity showing voltage.""" _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT @@ -297,11 +297,10 @@ class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]): """Construct the entity.""" super().__init__(monitor_serial_number, name, "volts", number) - def _get_sensor( - self, monitor: greeneye.monitor.Monitor - ) -> greeneye.monitor.Monitor: + @property + def _sensor(self) -> greeneye.monitor.Monitor | None: """Wire the updates to the monitor itself, since there is no voltage element in the API.""" - return monitor + return self._monitor @property def native_value(self) -> float | None: From 5f96ed19d9f28aba7de008edbfb5bc0c13f29d17 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 18 Nov 2021 13:23:25 +0800 Subject: [PATCH 0576/1452] Remove -bb option from tox and ci (#59846) --- .github/workflows/ci.yaml | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 890fed4694b..e99ef401519 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -686,7 +686,7 @@ jobs: - name: Run pytest run: | . venv/bin/activate - python3 -X dev -bb -m pytest \ + python3 -X dev -m pytest \ -qq \ --timeout=9 \ --durations=10 \ diff --git a/tox.ini b/tox.ini index 5def410cb3b..0532d67b247 100644 --- a/tox.ini +++ b/tox.ini @@ -8,14 +8,14 @@ basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt pip_version = pip>=8.0.3,<20.3 commands = - {envpython} -X dev -bb -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} + {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt [testenv:cov] commands = - {envpython} -X dev -bb -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} + {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt From 5c01ed7edf7e0eb81ad3e87a146133d1eac41ead Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Nov 2021 10:21:08 +0100 Subject: [PATCH 0577/1452] Bump actions/setup-python from 2.2.2 to 2.3.0 (#59873) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.2.2 to 2.3.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.2.2...v2.3.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 22 +++++++++++----------- .github/workflows/translations.yaml | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 72a57ea1421..20bed0fffcc 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -101,7 +101,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e99ef401519..c35aea46f91 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -86,7 +86,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -126,7 +126,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -166,7 +166,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -228,7 +228,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -271,7 +271,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -314,7 +314,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -354,7 +354,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -397,7 +397,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -448,7 +448,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -519,7 +519,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 76e5ba6d719..6d11b3abae2 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.2 + uses: actions/setup-python@v2.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From cfc02fc9f46da614225d4b3c59347c341079ddb6 Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Thu, 18 Nov 2021 10:59:04 +0100 Subject: [PATCH 0578/1452] Update minio dependency to 5.0.10 (#59878) * Update minio dependency to 5.0.10 * Update minio dependency in manifest file --- homeassistant/components/minio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index 45ba422c331..ba5ba4cd0a8 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -2,7 +2,7 @@ "domain": "minio", "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", - "requirements": ["minio==4.0.9"], + "requirements": ["minio==5.0.10"], "codeowners": ["@tkislan"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index bdbc0e9d757..ea76ecc4d0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ miflora==0.7.0 millheater==0.8.0 # homeassistant.components.minio -minio==4.0.9 +minio==5.0.10 # homeassistant.components.mitemp_bt mitemp_bt==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e050d3521b5..f3ab9745637 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -612,7 +612,7 @@ micloud==0.4 millheater==0.8.0 # homeassistant.components.minio -minio==4.0.9 +minio==5.0.10 # homeassistant.components.motion_blinds motionblinds==0.5.8 From bb731fad5db122392effa323fc98287e6144a7d7 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 18 Nov 2021 18:11:46 +0800 Subject: [PATCH 0579/1452] Bump httpx from 0.19.0 to 0.21.0 (#59723) * Bump httpx from 0.19.0 to 0.21.0 * Bump respx from 0.17.0 to 0.19.0 --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ab6da7293f0..c80b4a0416c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 home-assistant-frontend==20211117.0 -httpx==0.19.0 +httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 diff --git a/requirements.txt b/requirements.txt index 0049df1cf7a..a7e86ca6dea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.19.0 +httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 PyJWT==2.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index 36cb9785ef6..7f7e36ac8d9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,7 +28,7 @@ pytest-xdist==2.4.0 pytest==6.2.5 requests_mock==1.9.2 responses==0.12.0 -respx==0.17.0 +respx==0.19.0 stdlib-list==0.7.0 tqdm==4.49.0 types-atomicwrites==1.4.1 diff --git a/setup.py b/setup.py index e08fbdf43e5..af23dc52159 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", - "httpx==0.19.0", + "httpx==0.21.0", "ifaddr==0.1.7", "jinja2==3.0.3", "PyJWT==2.1.0", From bfafeb7965d21aa3744ca3426e1682c50de5126c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Nov 2021 11:28:35 +0100 Subject: [PATCH 0580/1452] Don't mock all of pychromecast in tests (#59839) --- tests/components/cast/conftest.py | 49 +++----- tests/components/cast/test_config_flow.py | 15 +-- tests/components/cast/test_media_player.py | 140 +++++++++++++++------ 3 files changed, 127 insertions(+), 77 deletions(-) diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 4c17cf74af9..901f7fb74fb 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -17,13 +17,7 @@ def dial_mock(): @pytest.fixture() def castbrowser_mock(): """Mock pychromecast CastBrowser.""" - return MagicMock() - - -@pytest.fixture() -def castbrowser_constructor_mock(): - """Mock pychromecast CastBrowser constructor.""" - return MagicMock() + return MagicMock(spec=pychromecast.discovery.CastBrowser) @pytest.fixture() @@ -32,37 +26,29 @@ def mz_mock(): return MagicMock() -@pytest.fixture() -def pycast_mock(castbrowser_mock, castbrowser_constructor_mock): - """Mock pychromecast.""" - pycast_mock = MagicMock() - pycast_mock.IDLE_APP_ID = pychromecast.IDLE_APP_ID - pycast_mock.IGNORE_CEC = [] - pycast_mock.const = pychromecast.const - pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock - pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock - pycast_mock.discovery.AbstractCastListener = ( - pychromecast.discovery.AbstractCastListener - ) - return pycast_mock - - @pytest.fixture() def quick_play_mock(): """Mock pychromecast quick_play.""" return MagicMock() +@pytest.fixture() +def get_chromecast_mock(): + """Mock pychromecast get_chromecast_from_cast_info.""" + return MagicMock() + + @pytest.fixture(autouse=True) -def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): +def cast_mock( + dial_mock, mz_mock, quick_play_mock, castbrowser_mock, get_chromecast_mock +): """Mock pychromecast.""" + ignore_cec_orig = list(pychromecast.IGNORE_CEC) + with patch( - "homeassistant.components.cast.media_player.pychromecast", pycast_mock - ), patch( - "homeassistant.components.cast.discovery.pychromecast", pycast_mock - ), patch( - "homeassistant.components.cast.helpers.dial", dial_mock - ), patch( + "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", + castbrowser_mock, + ), patch("homeassistant.components.cast.helpers.dial", dial_mock), patch( "homeassistant.components.cast.media_player.MultizoneManager", return_value=mz_mock, ), patch( @@ -71,5 +57,10 @@ def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): ), patch( "homeassistant.components.cast.media_player.quick_play", quick_play_mock, + ), patch( + "homeassistant.components.cast.media_player.pychromecast.get_chromecast_from_cast_info", + get_chromecast_mock, ): yield + + pychromecast.IGNORE_CEC = list(ignore_cec_orig) diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 7c3fb774722..1ad89c7a8e5 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -245,7 +245,7 @@ async def test_option_flow(hass, parameter_data): assert dict(config_entry.data) == expected_data -async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): +async def test_known_hosts(hass, castbrowser_mock): """Test known hosts is passed to pychromecasts.""" result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_USER} @@ -257,12 +257,9 @@ async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock) await hass.async_block_till_done() config_entry = hass.config_entries.async_entries("cast")[0] - assert castbrowser_mock.start_discovery.call_count == 1 - castbrowser_constructor_mock.assert_called_once_with( - ANY, ANY, ["192.168.0.1", "192.168.0.2"] - ) + assert castbrowser_mock.return_value.start_discovery.call_count == 1 + castbrowser_mock.assert_called_once_with(ANY, ANY, ["192.168.0.1", "192.168.0.2"]) castbrowser_mock.reset_mock() - castbrowser_constructor_mock.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( @@ -272,8 +269,8 @@ async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock) await hass.async_block_till_done() - castbrowser_mock.start_discovery.assert_not_called() - castbrowser_constructor_mock.assert_not_called() - castbrowser_mock.host_browser.update_hosts.assert_called_once_with( + castbrowser_mock.return_value.start_discovery.assert_not_called() + castbrowser_mock.assert_not_called() + castbrowser_mock.return_value.host_browser.update_hosts.assert_called_once_with( ["192.168.0.11", "192.168.0.12"] ) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 6c63d3bcc2b..3690cb7a2e0 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -46,6 +46,13 @@ FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2") FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4") FakeGroupUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e3") +FAKE_HOST_SERVICE = pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_HOST, ("127.0.0.1", 8009) +) +FAKE_MDNS_SERVICE = pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "the-service" +) + def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" @@ -56,7 +63,7 @@ def get_fake_chromecast(info: ChromecastInfo): def get_fake_chromecast_info( - host="192.168.178.42", port=8009, uuid: UUID | None = FakeUUID + host="192.168.178.42", port=8009, service=None, uuid: UUID | None = FakeUUID ): """Generate a Fake ChromecastInfo with the specified arguments.""" @@ -81,10 +88,14 @@ def get_fake_chromecast_info( ) return super().__eq__(other) + if service is None: + service = pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_HOST, (host, port) + ) return ExtendedChromecastInfo( host=host, port=port, - services={"the-service"}, + services={service}, uuid=uuid, model_name="Chromecast", friendly_name="Speaker", @@ -136,10 +147,12 @@ async def async_setup_cast_internal_discovery(hass, config=None): discovery_callback = cast_browser.call_args[0][0].add_cast remove_callback = cast_browser.call_args[0][0].remove_cast - def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: + def discover_chromecast( + service: pychromecast.discovery.ServiceInfo, info: ChromecastInfo + ) -> None: """Discover a chromecast device.""" browser.devices[info.uuid] = pychromecast.discovery.CastInfo( - {service_name}, + {service}, info.uuid, info.model_name, info.friendly_name, @@ -148,7 +161,7 @@ async def async_setup_cast_internal_discovery(hass, config=None): info.cast_type, info.manufacturer, ) - discovery_callback(info.uuid, service_name) + discovery_callback(info.uuid, "") def remove_chromecast(service_name: str, info: ChromecastInfo) -> None: """Remove a chromecast device.""" @@ -194,9 +207,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf discovery_callback = cast_browser.call_args[0][0].add_cast - service_name = "the-service" browser.devices[info.uuid] = pychromecast.discovery.CastInfo( - {service_name}, + {FAKE_MDNS_SERVICE}, info.uuid, info.model_name, info.friendly_name, @@ -205,7 +217,7 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.cast_type, info.manufacturer, ) - discovery_callback(info.uuid, service_name) + discovery_callback(info.uuid, FAKE_MDNS_SERVICE[1]) await hass.async_block_till_done() await hass.async_block_till_done() @@ -214,7 +226,7 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" browser.devices[info.uuid] = pychromecast.discovery.CastInfo( - {service_name}, + {FAKE_MDNS_SERVICE}, info.uuid, info.model_name, info.friendly_name, @@ -223,7 +235,7 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.cast_type, info.manufacturer, ) - discovery_callback(info.uuid, service_name) + discovery_callback(info.uuid, FAKE_MDNS_SERVICE[1]) return chromecast, discover_chromecast @@ -250,16 +262,16 @@ def get_status_callbacks(chromecast_mock, mz_mock=None): async def test_start_discovery_called_once(hass, castbrowser_mock): """Test pychromecast.start_discovery called exactly once.""" await async_setup_cast(hass) - assert castbrowser_mock.start_discovery.call_count == 1 + assert castbrowser_mock.return_value.start_discovery.call_count == 1 await async_setup_cast(hass) - assert castbrowser_mock.start_discovery.call_count == 1 + assert castbrowser_mock.return_value.start_discovery.call_count == 1 async def test_internal_discovery_callback_fill_out_fail(hass): """Test internal discovery automatically filling out information.""" discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info(host="host1") + info = get_fake_chromecast_info(host="host1", service=FAKE_MDNS_SERVICE) zconf = get_fake_zconf(host="host1", port=8009) full_info = ( info # attr.evolve(info, model_name="", friendly_name="Speaker", uuid=FakeUUID) @@ -275,7 +287,7 @@ async def test_internal_discovery_callback_fill_out_fail(hass): signal = MagicMock() async_dispatcher_connect(hass, "cast_discovered", signal) - discover_cast("the-service", info) + discover_cast(FAKE_MDNS_SERVICE, info) await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing @@ -286,7 +298,7 @@ async def test_internal_discovery_callback_fill_out_fail(hass): async def test_internal_discovery_callback_fill_out_group(hass): """Test internal discovery automatically filling out information.""" discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info(host="host1", port=12345) + info = get_fake_chromecast_info(host="host1", port=12345, service=FAKE_MDNS_SERVICE) zconf = get_fake_zconf(host="host1", port=12345) full_info = attr.evolve( info, @@ -306,7 +318,7 @@ async def test_internal_discovery_callback_fill_out_group(hass): signal = MagicMock() async_dispatcher_connect(hass, "cast_discovered", signal) - discover_cast("the-service", info) + discover_cast(FAKE_MDNS_SERVICE, info) await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing @@ -318,12 +330,12 @@ async def test_stop_discovery_called_on_stop(hass, castbrowser_mock): """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config await async_setup_cast(hass, {}) - assert castbrowser_mock.start_discovery.call_count == 1 + assert castbrowser_mock.return_value.start_discovery.call_count == 1 # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert castbrowser_mock.stop_discovery.call_count == 1 + assert castbrowser_mock.return_value.stop_discovery.call_count == 1 async def test_create_cast_device_without_uuid(hass): @@ -362,7 +374,12 @@ async def test_manual_cast_chromecasts_uuid(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_2, ): - discover_cast("service2", cast_2) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service2" + ), + cast_2, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 0 @@ -371,7 +388,12 @@ async def test_manual_cast_chromecasts_uuid(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - discover_cast("service1", cast_1) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service1" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -390,7 +412,12 @@ async def test_auto_cast_chromecasts(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - discover_cast("service2", cast_2) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service2" + ), + cast_2, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -399,13 +426,18 @@ async def test_auto_cast_chromecasts(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_2, ): - discover_cast("service1", cast_1) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service1" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 2 -async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): +async def test_discover_dynamic_group(hass, dial_mock, get_chromecast_mock, caplog): """Test dynamic group does not create device or entity.""" cast_1 = get_fake_chromecast_info(host="host_1", port=23456, uuid=FakeUUID) cast_2 = get_fake_chromecast_info(host="host_2", port=34567, uuid=FakeUUID2) @@ -421,7 +453,7 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): tmp2.uuid = FakeUUID2 dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2] - pycast_mock.get_chromecast_from_cast_info.assert_not_called() + get_chromecast_mock.assert_not_called() discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery( hass ) @@ -431,11 +463,16 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - discover_cast("service", cast_1) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs - pycast_mock.get_chromecast_from_cast_info.assert_called() - pycast_mock.get_chromecast_from_cast_info.reset_mock() + get_chromecast_mock.assert_called() + get_chromecast_mock.reset_mock() assert add_dev1.call_count == 0 assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None @@ -444,11 +481,16 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_2, ): - discover_cast("service", cast_2) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service" + ), + cast_2, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs - pycast_mock.get_chromecast_from_cast_info.assert_called() - pycast_mock.get_chromecast_from_cast_info.reset_mock() + get_chromecast_mock.assert_called() + get_chromecast_mock.reset_mock() assert add_dev1.call_count == 0 assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None @@ -457,10 +499,15 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - discover_cast("service", cast_1) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs - pycast_mock.get_chromecast_from_cast_info.assert_not_called() + get_chromecast_mock.assert_not_called() assert add_dev1.call_count == 0 assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None @@ -471,7 +518,12 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - remove_cast("service", cast_1) + remove_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs @@ -492,7 +544,12 @@ async def test_update_cast_chromecasts(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, ): - discover_cast("service1", cast_1) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service1" + ), + cast_1, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -501,7 +558,12 @@ async def test_update_cast_chromecasts(hass): "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_2, ): - discover_cast("service2", cast_2) + discover_cast( + pychromecast.discovery.ServiceInfo( + pychromecast.const.SERVICE_TYPE_MDNS, "service2" + ), + cast_2, + ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -1406,7 +1468,7 @@ async def test_entry_setup_empty_config(hass: HomeAssistant): assert config_entry.data["ignore_cec"] == [] -async def test_entry_setup_single_config(hass: HomeAssistant, pycast_mock): +async def test_entry_setup_single_config(hass: HomeAssistant): """Test deprecated yaml config with a single config media_player.""" await async_setup_component( hass, "cast", {"cast": {"media_player": {"uuid": "bla", "ignore_cec": "cast1"}}} @@ -1417,10 +1479,10 @@ async def test_entry_setup_single_config(hass: HomeAssistant, pycast_mock): assert config_entry.data["uuid"] == ["bla"] assert config_entry.data["ignore_cec"] == ["cast1"] - assert pycast_mock.IGNORE_CEC == ["cast1"] + assert pychromecast.IGNORE_CEC == ["cast1"] -async def test_entry_setup_list_config(hass: HomeAssistant, pycast_mock): +async def test_entry_setup_list_config(hass: HomeAssistant): """Test deprecated yaml config with multiple media_players.""" await async_setup_component( hass, @@ -1439,4 +1501,4 @@ async def test_entry_setup_list_config(hass: HomeAssistant, pycast_mock): config_entry = hass.config_entries.async_entries("cast")[0] assert set(config_entry.data["uuid"]) == {"bla", "blu"} assert set(config_entry.data["ignore_cec"]) == {"cast1", "cast2", "cast3"} - assert set(pycast_mock.IGNORE_CEC) == {"cast1", "cast2", "cast3"} + assert set(pychromecast.IGNORE_CEC) == {"cast1", "cast2", "cast3"} From 87f2eb3bd72ece5a1548648771b4763a78e59912 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 11:33:53 +0100 Subject: [PATCH 0581/1452] Upgrade sentry-sdk to 1.5.0 (#59842) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 68e6e05405b..9d8575dbc4e 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.4.3"], + "requirements": ["sentry-sdk==1.5.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ea76ecc4d0b..fdc4013ea50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2125,7 +2125,7 @@ sense-hat==2.2.0 sense_energy==0.9.3 # homeassistant.components.sentry -sentry-sdk==1.4.3 +sentry-sdk==1.5.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3ab9745637..e8567435edd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ screenlogicpy==0.4.1 sense_energy==0.9.3 # homeassistant.components.sentry -sentry-sdk==1.4.3 +sentry-sdk==1.5.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 1609c0cc2c0b0652ad43d06151a0d78bcd06af02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Nov 2021 11:51:32 +0100 Subject: [PATCH 0582/1452] Sort some entity_registry code (#59876) * Sort some entity_registry code * Sort some more entity_registry code * Tweak --- .../components/config/entity_registry.py | 22 ++-- homeassistant/helpers/entity.py | 26 ++-- homeassistant/helpers/entity_platform.py | 18 +-- homeassistant/helpers/entity_registry.py | 120 +++++++++--------- 4 files changed, 93 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 9b6fc2af82a..62f2aec232c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -69,9 +69,9 @@ async def websocket_get_entity(hass, connection, msg): vol.Required("type"): "config/entity_registry/update", vol.Required("entity_id"): cv.entity_id, # If passed in, we update value. Passing None will remove old value. - vol.Optional("name"): vol.Any(str, None), - vol.Optional("icon"): vol.Any(str, None), vol.Optional("area_id"): vol.Any(str, None), + vol.Optional("icon"): vol.Any(str, None), + vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), @@ -92,7 +92,7 @@ async def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("name", "icon", "area_id", "disabled_by"): + for key in ("area_id", "disabled_by", "icon", "name"): if key in msg: changes[key] = msg[key] @@ -168,15 +168,15 @@ async def websocket_remove_entity(hass, connection, msg): def _entry_dict(entry): """Convert entry to API format.""" return { + "area_id": entry.area_id, "config_entry_id": entry.config_entry_id, "device_id": entry.device_id, - "area_id": entry.area_id, "disabled_by": entry.disabled_by, - "entity_id": entry.entity_id, - "name": entry.name, - "icon": entry.icon, - "platform": entry.platform, "entity_category": entry.entity_category, + "entity_id": entry.entity_id, + "icon": entry.icon, + "name": entry.name, + "platform": entry.platform, } @@ -184,8 +184,8 @@ def _entry_dict(entry): def _entry_ext_dict(entry): """Convert entry to API format.""" data = _entry_dict(entry) - data["original_name"] = entry.original_name - data["original_icon"] = entry.original_icon - data["unique_id"] = entry.unique_id data["capabilities"] = entry.capabilities + data["original_icon"] = entry.original_icon + data["original_name"] = entry.original_name + data["unique_id"] = entry.unique_id return data diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 650ce0701a6..a0ad081a782 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -534,26 +534,26 @@ class Entity(ABC): entry = self.registry_entry # pylint: disable=consider-using-ternary - if (name := (entry and entry.name) or self.name) is not None: - attr[ATTR_FRIENDLY_NAME] = name - - if (icon := (entry and entry.icon) or self.icon) is not None: - attr[ATTR_ICON] = icon - - if (entity_picture := self.entity_picture) is not None: - attr[ATTR_ENTITY_PICTURE] = entity_picture - if assumed_state := self.assumed_state: attr[ATTR_ASSUMED_STATE] = assumed_state - if (supported_features := self.supported_features) is not None: - attr[ATTR_SUPPORTED_FEATURES] = supported_features + if (attribution := self.attribution) is not None: + attr[ATTR_ATTRIBUTION] = attribution if (device_class := self.device_class) is not None: attr[ATTR_DEVICE_CLASS] = str(device_class) - if (attribution := self.attribution) is not None: - attr[ATTR_ATTRIBUTION] = attribution + if (entity_picture := self.entity_picture) is not None: + attr[ATTR_ENTITY_PICTURE] = entity_picture + + if (icon := (entry and entry.icon) or self.icon) is not None: + attr[ATTR_ICON] = icon + + if (name := (entry and entry.name) or self.name) is not None: + attr[ATTR_FRIENDLY_NAME] = name + + if (supported_features := self.supported_features) is not None: + attr[ATTR_SUPPORTED_FEATURES] = supported_features end = timer() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c44eb96026d..2d555b82c10 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -509,18 +509,18 @@ class EntityPlatform: self.domain, self.platform_name, entity.unique_id, - suggested_object_id=suggested_object_id, - config_entry=self.config_entry, - device_id=device_id, - known_object_ids=self.entities.keys(), - disabled_by=disabled_by, capabilities=entity.capability_attributes, - supported_features=entity.supported_features, + config_entry=self.config_entry, device_class=entity.device_class, - unit_of_measurement=entity.unit_of_measurement, - original_name=entity.name, - original_icon=entity.icon, + device_id=device_id, + disabled_by=disabled_by, entity_category=entity.entity_category, + known_object_ids=self.entities.keys(), + original_icon=entity.icon, + original_name=entity.name, + suggested_object_id=suggested_object_id, + supported_features=entity.supported_features, + unit_of_measurement=entity.unit_of_measurement, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7d3bdf58a92..9575e6722ed 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -64,12 +64,12 @@ STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity # to external services. ENTITY_DESCRIBING_ATTRIBUTES = { + "capabilities", + "device_class", "entity_id", "name", "original_name", - "capabilities", "supported_features", - "device_class", "unit_of_measurement", } @@ -81,11 +81,12 @@ class RegistryEntry: entity_id: str = attr.ib() unique_id: str = attr.ib() platform: str = attr.ib() - name: str | None = attr.ib(default=None) - icon: str | None = attr.ib(default=None) - device_id: str | None = attr.ib(default=None) area_id: str | None = attr.ib(default=None) + capabilities: Mapping[str, Any] | None = attr.ib(default=None) config_entry_id: str | None = attr.ib(default=None) + device_class: str | None = attr.ib(default=None) + device_id: str | None = attr.ib(default=None) + domain: str = attr.ib(init=False, repr=False) disabled_by: str | None = attr.ib( default=None, validator=attr.validators.in_( @@ -99,15 +100,14 @@ class RegistryEntry: ) ), ) - capabilities: Mapping[str, Any] | None = attr.ib(default=None) - supported_features: int = attr.ib(default=0) - device_class: str | None = attr.ib(default=None) - unit_of_measurement: str | None = attr.ib(default=None) - # As set by integration - original_name: str | None = attr.ib(default=None) - original_icon: str | None = attr.ib(default=None) entity_category: str | None = attr.ib(default=None) - domain: str = attr.ib(init=False, repr=False) + icon: str | None = attr.ib(default=None) + name: str | None = attr.ib(default=None) + # As set by integration + original_icon: str | None = attr.ib(default=None) + original_name: str | None = attr.ib(default=None) + supported_features: int = attr.ib(default=0) + unit_of_measurement: str | None = attr.ib(default=None) @domain.default def _domain_default(self) -> str: @@ -127,22 +127,22 @@ class RegistryEntry: if self.capabilities is not None: attrs.update(self.capabilities) - if self.supported_features is not None: - attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features - if self.device_class is not None: attrs[ATTR_DEVICE_CLASS] = self.device_class - if self.unit_of_measurement is not None: - attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement + icon = self.icon or self.original_icon + if icon is not None: + attrs[ATTR_ICON] = icon name = self.name or self.original_name if name is not None: attrs[ATTR_FRIENDLY_NAME] = name - icon = self.icon or self.original_icon - if icon is not None: - attrs[ATTR_ICON] = icon + if self.supported_features is not None: + attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features + + if self.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) @@ -271,16 +271,16 @@ class EntityRegistry: if entity_id: return self._async_update_entity( entity_id, - config_entry_id=config_entry_id or UNDEFINED, - device_id=device_id or UNDEFINED, area_id=area_id or UNDEFINED, capabilities=capabilities or UNDEFINED, - supported_features=supported_features or UNDEFINED, + config_entry_id=config_entry_id or UNDEFINED, device_class=device_class or UNDEFINED, - unit_of_measurement=unit_of_measurement or UNDEFINED, - original_name=original_name or UNDEFINED, - original_icon=original_icon or UNDEFINED, + device_id=device_id or UNDEFINED, entity_category=entity_category or UNDEFINED, + original_icon=original_icon or UNDEFINED, + original_name=original_name or UNDEFINED, + supported_features=supported_features or UNDEFINED, + unit_of_measurement=unit_of_measurement or UNDEFINED, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -443,19 +443,19 @@ class EntityRegistry: old_values = {} # Dict with old key/value pairs for attr_name, value in ( - ("name", name), - ("icon", icon), - ("config_entry_id", config_entry_id), - ("device_id", device_id), ("area_id", area_id), - ("disabled_by", disabled_by), ("capabilities", capabilities), - ("supported_features", supported_features), + ("config_entry_id", config_entry_id), ("device_class", device_class), - ("unit_of_measurement", unit_of_measurement), - ("original_name", original_name), - ("original_icon", original_icon), + ("device_id", device_id), + ("disabled_by", disabled_by), ("entity_category", entity_category), + ("icon", icon), + ("name", name), + ("original_icon", original_icon), + ("original_name", original_name), + ("supported_features", supported_features), + ("unit_of_measurement", unit_of_measurement), ): if value is not UNDEFINED and value != getattr(old, attr_name): new_values[attr_name] = value @@ -526,22 +526,22 @@ class EntityRegistry: continue entities[entity["entity_id"]] = RegistryEntry( - entity_id=entity["entity_id"], - config_entry_id=entity.get("config_entry_id"), - device_id=entity.get("device_id"), area_id=entity.get("area_id"), - unique_id=entity["unique_id"], - platform=entity["platform"], - name=entity.get("name"), - icon=entity.get("icon"), - disabled_by=entity.get("disabled_by"), capabilities=entity.get("capabilities") or {}, - supported_features=entity.get("supported_features", 0), + config_entry_id=entity.get("config_entry_id"), device_class=entity.get("device_class"), - unit_of_measurement=entity.get("unit_of_measurement"), - original_name=entity.get("original_name"), - original_icon=entity.get("original_icon"), + device_id=entity.get("device_id"), + disabled_by=entity.get("disabled_by"), entity_category=entity.get("entity_category"), + entity_id=entity["entity_id"], + icon=entity.get("icon"), + name=entity.get("name"), + original_icon=entity.get("original_icon"), + original_name=entity.get("original_name"), + platform=entity["platform"], + supported_features=entity.get("supported_features", 0), + unique_id=entity["unique_id"], + unit_of_measurement=entity.get("unit_of_measurement"), ) self.entities = entities @@ -559,22 +559,22 @@ class EntityRegistry: data["entities"] = [ { - "entity_id": entry.entity_id, - "config_entry_id": entry.config_entry_id, - "device_id": entry.device_id, "area_id": entry.area_id, - "unique_id": entry.unique_id, - "platform": entry.platform, - "name": entry.name, - "icon": entry.icon, - "disabled_by": entry.disabled_by, "capabilities": entry.capabilities, - "supported_features": entry.supported_features, + "config_entry_id": entry.config_entry_id, "device_class": entry.device_class, - "unit_of_measurement": entry.unit_of_measurement, - "original_name": entry.original_name, - "original_icon": entry.original_icon, + "device_id": entry.device_id, + "disabled_by": entry.disabled_by, "entity_category": entry.entity_category, + "entity_id": entry.entity_id, + "icon": entry.icon, + "name": entry.name, + "original_icon": entry.original_icon, + "original_name": entry.original_name, + "platform": entry.platform, + "supported_features": entry.supported_features, + "unique_id": entry.unique_id, + "unit_of_measurement": entry.unit_of_measurement, } for entry in self.entities.values() ] From a41d3367244e6cf2bd198a9bd1180393ba173509 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 18 Nov 2021 13:13:45 +0100 Subject: [PATCH 0583/1452] Scsgate logging from warn(deprecated) to warning (#59862) --- homeassistant/components/scsgate/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 175f40c3eba..366a2930764 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -180,7 +180,9 @@ class SCSGateScenarioSwitch: elif isinstance(message, ScenarioTriggeredMessage): scenario_id = message.scenario else: - self._logger.warn("Scenario switch: received unknown message %s", message) + self._logger.warning( + "Scenario switch: received unknown message %s", message + ) return self._hass.bus.fire( From 92ca94e91536400f8b34a88ab41bb504c0f1bd01 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Thu, 18 Nov 2021 14:00:01 +0100 Subject: [PATCH 0584/1452] Add cover platform to bosch_shc integration (#51443) Co-authored-by: Artem Draft Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/bosch_shc/__init__.py | 2 +- homeassistant/components/bosch_shc/cover.py | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/bosch_shc/cover.py diff --git a/.coveragerc b/.coveragerc index 6750675da26..bbe00155b2f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,7 @@ omit = homeassistant/components/bosch_shc/__init__.py homeassistant/components/bosch_shc/binary_sensor.py homeassistant/components/bosch_shc/const.py + homeassistant/components/bosch_shc/cover.py homeassistant/components/bosch_shc/entity.py homeassistant/components/bosch_shc/sensor.py homeassistant/components/braviatv/__init__.py diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index f68a2b68467..2909350fc1c 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -19,7 +19,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = ["binary_sensor", "cover", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py new file mode 100644 index 00000000000..91543e586bf --- /dev/null +++ b/homeassistant/components/bosch_shc/cover.py @@ -0,0 +1,86 @@ +"""Platform for cover integration.""" +from boschshcpy import SHCSession, SHCShutterControl + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_SHUTTER, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) + +from .const import DATA_SESSION, DOMAIN +from .entity import SHCEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the SHC cover platform.""" + + entities = [] + session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] + + for cover in session.device_helper.shutter_controls: + entities.append( + ShutterControlCover( + device=cover, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + if entities: + async_add_entities(entities) + + +class ShutterControlCover(SHCEntity, CoverEntity): + """Representation of a SHC shutter control device.""" + + _attr_device_class = DEVICE_CLASS_SHUTTER + _attr_supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + + @property + def current_cover_position(self): + """Return the current cover position.""" + return round(self._device.level * 100.0) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._device.stop() + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self.current_cover_position == 0 + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return ( + self._device.operation_state + == SHCShutterControl.ShutterControlService.State.OPENING + ) + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return ( + self._device.operation_state + == SHCShutterControl.ShutterControlService.State.CLOSING + ) + + def open_cover(self, **kwargs): + """Open the cover.""" + self._device.level = 1.0 + + def close_cover(self, **kwargs): + """Close cover.""" + self._device.level = 0.0 + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + self._device.level = position / 100.0 From 01efe1eba29d291f1b783e502a32bbd11992b67b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 14:11:44 +0100 Subject: [PATCH 0585/1452] Add datetime object as valid StateType (#52671) Co-authored-by: Martin Hjelmare --- .../components/cert_expiry/sensor.py | 8 ++- homeassistant/components/sensor/__init__.py | 65 ++++++++++++++++- tests/components/picnic/test_sensor.py | 24 +++---- tests/components/risco/test_sensor.py | 2 +- tests/components/sensor/test_init.py | 70 +++++++++++++++++++ tests/components/xiaomi_miio/test_vacuum.py | 12 ++++ 6 files changed, 162 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 7b6445a2f35..0aa67993180 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -1,5 +1,7 @@ """Counter for the days until an HTTPS (TLS) certificate will expire.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import voluptuous as vol @@ -85,8 +87,8 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" @property - def native_value(self): + def native_value(self) -> datetime | None: """Return the state of the sensor.""" if self.coordinator.data: - return self.coordinator.data.isoformat() + return self.coordinator.data return None diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 4ae6e2129ed..58a9fe4022e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,11 +4,12 @@ from __future__ import annotations from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import inspect import logging from typing import Any, Final, cast, final +import ciso8601 import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -182,6 +183,9 @@ class SensorEntity(Entity): _last_reset_reported = False _temperature_conversion_reported = False + # Temporary private attribute to track if deprecation has been logged. + __datetime_as_string_deprecation_logged = False + @property def state_class(self) -> str | None: """Return the state class of this entity, from STATE_CLASSES, if any.""" @@ -236,7 +240,7 @@ class SensorEntity(Entity): return None @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | date | datetime: """Return the value reported by the sensor.""" return self._attr_native_value @@ -273,6 +277,61 @@ class SensorEntity(Entity): """Return the state of the sensor and perform unit conversions, if needed.""" unit_of_measurement = self.native_unit_of_measurement value = self.native_value + device_class = self.device_class + + # We have an old non-datetime value, warn about it and convert it during + # the deprecation period. + if ( + value is not None + and device_class in (DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP) + and not isinstance(value, (date, datetime)) + ): + # Deprecation warning for date/timestamp device classes + if not self.__datetime_as_string_deprecation_logged: + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "%s is providing a string for its state, while the device " + "class is '%s', this is not valid and will be unsupported " + "from Home Assistant 2022.2. Please %s", + self.entity_id, + device_class, + report_issue, + ) + self.__datetime_as_string_deprecation_logged = True + + # Anyways, lets validate the date at least.. + try: + value = ciso8601.parse_datetime(str(value)) + except (ValueError, IndexError) as error: + raise ValueError( + f"Invalid date/datetime: {self.entity_id} provide state '{value}', " + f"while it has device class '{device_class}'" + ) from error + + # Convert the date object to a standardized state string. + if device_class == DEVICE_CLASS_DATE: + return value.date().isoformat() + return value.isoformat(timespec="seconds") + + # Received a datetime + if value is not None and device_class == DEVICE_CLASS_TIMESTAMP: + try: + return value.isoformat(timespec="seconds") # type: ignore + except (AttributeError, TypeError) as err: + raise ValueError( + f"Invalid datetime: {self.entity_id} has a timestamp device class" + f"but does not provide a datetime state but {type(value)}" + ) from err + + # Received a date value + if value is not None and device_class == DEVICE_CLASS_DATE: + try: + return value.isoformat() # type: ignore + except (AttributeError, TypeError) as err: + raise ValueError( + f"Invalid date: {self.entity_id} has a date device class" + f"but does not provide a date state but {type(value)}" + ) from err units = self.hass.config.units if ( @@ -304,7 +363,7 @@ class SensorEntity(Entity): 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) + temp = units.temperature(float(value), unit_of_measurement) # type: ignore value = round(temp) if prec == 0 else round(temp, prec) return value diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 2f8fb4cec53..58426e310ed 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -210,44 +210,44 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): ) self._assert_sensor( "sensor.picnic_selected_slot_start", - "2021-03-03T14:45:00.000+01:00", + "2021-03-03T14:45:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_end", - "2021-03-03T15:45:00.000+01:00", + "2021-03-03T15:45:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_max_order_time", - "2021-03-02T22:00:00.000+01:00", + "2021-03-02T22:00:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0") self._assert_sensor( "sensor.picnic_last_order_slot_start", - "2021-02-26T20:15:00.000+01:00", + "2021-02-26T20:15:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_slot_end", - "2021-02-26T21:15:00.000+01:00", + "2021-02-26T21:15:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") self._assert_sensor( "sensor.picnic_last_order_eta_start", - "2021-02-26T20:54:00.000+01:00", + "2021-02-26T20:54:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_eta_end", - "2021-02-26T21:14:00.000+01:00", + "2021-02-26T21:14:00+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_delivery_time", - "2021-02-26T20:54:05.221+01:00", + "2021-02-26T20:54:05+01:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( @@ -305,10 +305,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): # Assert delivery time is not available, but eta is self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00.000+01:00" + "sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00+01:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00.000+01:00" + "sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00+01:00" ) async def test_sensors_use_detailed_eta_if_available(self): @@ -333,10 +333,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): delivery_response["delivery_id"] ) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20.452+01:00" + "sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20+01:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20.452+01:00" + "sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20+01:00" ) async def test_sensors_no_data(self): diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index eb7ed990bd9..4286a7d09c9 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -147,7 +147,7 @@ def _check_state(hass, category, entity_id): event_index = CATEGORIES_TO_EVENTS[category] event = TEST_EVENTS[event_index] state = hass.states.get(entity_id) - assert state.state == event.time + assert state.state == dt.parse_datetime(event.time).isoformat() assert state.attributes["category_id"] == event.category_id assert state.attributes["category_name"] == event.category_name assert state.attributes["type_id"] == event.type_id diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index c3808b44b06..d3fb6e89229 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,11 +1,16 @@ """The test for sensor device automation.""" +from datetime import date, datetime, timezone + import pytest from pytest import approx from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_DATE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -107,3 +112,68 @@ async def test_deprecated_unit_of_measurement(hass, caplog, enable_custom_integr "tests.components.sensor.test_init is setting 'unit_of_measurement' on an " "instance of SensorEntityDescription" ) in caplog.text + + +async def test_datetime_conversion(hass, caplog, enable_custom_integrations): + """Test conversion of datetime.""" + test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) + test_date = date(2017, 12, 19) + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", native_value=test_timestamp, device_class=DEVICE_CLASS_TIMESTAMP + ) + platform.ENTITIES["1"] = platform.MockSensor( + name="Test", native_value=test_date, device_class=DEVICE_CLASS_DATE + ) + platform.ENTITIES["2"] = platform.MockSensor( + name="Test", native_value=None, device_class=DEVICE_CLASS_TIMESTAMP + ) + platform.ENTITIES["3"] = platform.MockSensor( + name="Test", native_value=None, device_class=DEVICE_CLASS_DATE + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(platform.ENTITIES["0"].entity_id) + assert state.state == test_timestamp.isoformat() + + state = hass.states.get(platform.ENTITIES["1"].entity_id) + assert state.state == test_date.isoformat() + + state = hass.states.get(platform.ENTITIES["2"].entity_id) + assert state.state == STATE_UNKNOWN + + state = hass.states.get(platform.ENTITIES["3"].entity_id) + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "device_class,native_value", + [ + (DEVICE_CLASS_DATE, "2021-11-09"), + (DEVICE_CLASS_TIMESTAMP, "2021-01-09T12:00:00+00:00"), + ], +) +async def test_deprecated_datetime_str( + hass, caplog, enable_custom_integrations, device_class, native_value +): + """Test warning on deprecated str for a date(time) value.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", native_value=native_value, device_class=device_class + ) + + 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 == native_value + assert ( + "is providing a string for its state, while the device class is " + f"'{device_class}', this is not valid and will be unsupported " + "from Home Assistant 2022.2." + ) in caplog.text diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 10f1dd649c8..f2fef4bba4b 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -78,6 +78,12 @@ def mirobo_is_got_error_fixture(): mock_vacuum.status().battery = 82 mock_vacuum.status().clean_area = 123.43218 mock_vacuum.status().clean_time = timedelta(hours=2, minutes=35, seconds=34) + mock_vacuum.last_clean_details().start = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) + mock_vacuum.last_clean_details().end = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) mock_vacuum.consumable_status().main_brush_left = timedelta( hours=12, minutes=35, seconds=34 ) @@ -136,6 +142,12 @@ def mirobo_old_speeds_fixture(request): mock_vacuum.status().battery = 32 mock_vacuum.fan_speed_presets.return_value = request.param mock_vacuum.status().fanspeed = list(request.param.values())[0] + mock_vacuum.last_clean_details().start = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) + mock_vacuum.last_clean_details().end = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum From b13e4e995308fac64d3298b2397f4a0dd1c478d2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Nov 2021 14:34:07 +0100 Subject: [PATCH 0586/1452] Small tweak of TriggerEntity typing (#59875) --- homeassistant/components/template/trigger_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index c80620b0453..d4bc96e43bf 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -21,7 +21,7 @@ from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE class TriggerEntity(update_coordinator.CoordinatorEntity): """Template entity based on trigger data.""" - domain = "" + domain: str extra_template_keys: tuple | None = None extra_template_keys_complex: tuple | None = None From 9ab8622d7260dc6cf4a65e4ccebd74d841759f21 Mon Sep 17 00:00:00 2001 From: bwduncan Date: Thu, 18 Nov 2021 14:18:25 +0000 Subject: [PATCH 0587/1452] Fix Nissan Leaf default states (#59866) * Fix default states and add device_class. Car data is initialised to zero, which means that graphs have an ugly drop to zero in them when HA is restarted. We should report "None" when the state is unknown. We need to use availability to signal whether binary_sensors have sensible data or not. We can remove the custom icons and use the defaults provided by using appropriate device_class. * Make isort happy. * Explicitly return None * Remove feature from bugfix PR. --- homeassistant/components/nissan_leaf/__init__.py | 12 ++++++------ .../components/nissan_leaf/binary_sensor.py | 10 ++++++++++ homeassistant/components/nissan_leaf/sensor.py | 5 +++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 44727ebccdd..d450eaf7dad 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -213,12 +213,12 @@ class LeafDataStore: self.car_config = car_config self.force_miles = car_config[CONF_FORCE_MILES] self.data = {} - self.data[DATA_CLIMATE] = False - self.data[DATA_BATTERY] = 0 - self.data[DATA_CHARGING] = False - self.data[DATA_RANGE_AC] = 0 - self.data[DATA_RANGE_AC_OFF] = 0 - self.data[DATA_PLUGGED_IN] = False + self.data[DATA_CLIMATE] = None + self.data[DATA_BATTERY] = None + self.data[DATA_CHARGING] = None + self.data[DATA_RANGE_AC] = None + self.data[DATA_RANGE_AC_OFF] = None + self.data[DATA_PLUGGED_IN] = None self.next_update = None self.last_check = None self.request_in_progress = False diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 3d2064dad4c..d2387e0e9a2 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -30,6 +30,11 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): """Sensor name.""" return f"{self.car.leaf.nickname} Plug Status" + @property + def available(self): + """Sensor availability.""" + return self.car.data[DATA_PLUGGED_IN] is not None + @property def is_on(self): """Return true if plugged in.""" @@ -51,6 +56,11 @@ class LeafChargingSensor(LeafEntity, BinarySensorEntity): """Sensor name.""" return f"{self.car.leaf.nickname} Charging Status" + @property + def available(self): + """Sensor availability.""" + return self.car.data[DATA_CHARGING] is not None + @property def is_on(self): """Return true if charging.""" diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 4074cd47f50..bed4d264bd4 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -52,6 +52,8 @@ class LeafBatterySensor(LeafEntity, SensorEntity): @property def native_value(self): """Battery state percentage.""" + if self.car.data[DATA_BATTERY] is None: + return None return round(self.car.data[DATA_BATTERY]) @property @@ -96,6 +98,9 @@ class LeafRangeSensor(LeafEntity, SensorEntity): else: ret = self.car.data[DATA_RANGE_AC_OFF] + if ret is None: + return None + if not self.car.hass.config.units.is_metric or self.car.force_miles: ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) From ec6a67d17ab00c9b2c0bd35188c5b784c0712faf Mon Sep 17 00:00:00 2001 From: bwduncan Date: Thu, 18 Nov 2021 14:23:57 +0000 Subject: [PATCH 0588/1452] Use correct Nissan leaf device_class (#59889) --- .../components/nissan_leaf/binary_sensor.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index d2387e0e9a2..13fe666a3a8 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -1,7 +1,11 @@ """Plugged In Status Support for the Nissan Leaf.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + BinarySensorEntity, +) from . import DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity @@ -25,6 +29,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): """Plugged In Sensor class.""" + _attr_device_class = DEVICE_CLASS_PLUG + @property def name(self): """Sensor name.""" @@ -40,17 +46,12 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): """Return true if plugged in.""" return self.car.data[DATA_PLUGGED_IN] - @property - def icon(self): - """Icon handling.""" - if self.car.data[DATA_PLUGGED_IN]: - return "mdi:power-plug" - return "mdi:power-plug-off" - class LeafChargingSensor(LeafEntity, BinarySensorEntity): """Charging Sensor class.""" + _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + @property def name(self): """Sensor name.""" @@ -65,10 +66,3 @@ class LeafChargingSensor(LeafEntity, BinarySensorEntity): def is_on(self): """Return true if charging.""" return self.car.data[DATA_CHARGING] - - @property - def icon(self): - """Icon handling.""" - if self.car.data[DATA_CHARGING]: - return "mdi:flash" - return "mdi:flash-off" From 4a83ee5daba675649d78052e7e3179b4d1188bf3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 15:32:39 +0100 Subject: [PATCH 0589/1452] Use native datetime value in ESPHome sensors (#59896) --- homeassistant/components/esphome/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index b2758c91b68..c3f9eff6060 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,6 +1,7 @@ """Support for esphome sensors.""" from __future__ import annotations +from datetime import datetime import math from aioesphomeapi import ( @@ -78,14 +79,14 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): return self._static_info.force_update @esphome_state_property - def native_value(self) -> str | None: + def native_value(self) -> datetime | str | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None if self._state.missing_state: return None if self.device_class == DEVICE_CLASS_TIMESTAMP: - return dt.utc_from_timestamp(self._state.state).isoformat() + return dt.utc_from_timestamp(self._state.state) return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @property From 5e07bc38c1385e3dc8b02ff1da85831a73a1d287 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 15:32:50 +0100 Subject: [PATCH 0590/1452] Use native date value in Twente Milieu sensors (#59897) --- homeassistant/components/twentemilieu/sensor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index bb3176e5834..259f3c4e969 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -12,7 +12,6 @@ from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -104,8 +103,6 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): ) @property - def native_value(self) -> StateType: + def native_value(self) -> date | None: """Return the state of the sensor.""" - if pickup := self.coordinator.data.get(self.entity_description.waste_type): - return pickup.isoformat() - return None + return self.coordinator.data.get(self.entity_description.waste_type) From 3dc0b9537ca5bf8c9e11c521e2af5d9d67e1c4f6 Mon Sep 17 00:00:00 2001 From: Ullrich Neiss Date: Thu, 18 Nov 2021 16:06:32 +0100 Subject: [PATCH 0591/1452] Move Kostal Plenticore writable settings from sensor to select widget or switch (#56529) * Move "Battery:SmartBatteryControl:Enable" from a simple sensor to a switch Add "Battery:TimeControl:Enable" as a switch If you want to change charging behavior you need to turn off both switches, before you can enable the function you want. (Same as on Plenticore UI) * removed: @property def assumed_state(self) -> bool was copied from an switchbot integration, does not make sense or does deliver valuable information Tried to set constant properties in the constructor * correct typo, add new line at eof * Initial state of switch was missing after (re)starting HA. Now working. * Reformatted with black * correct syntax errors from test run 09.10.2021 * reformat * update 15.10.2021 * Set select value is working * update 05.11.2021 * data correctly received * working completly * remove old switch definitions, now replaced by select widget * correct complaints from workflow run on 11/11/2021 * Add explanatory comment for switch and select * Correct comments * Removed function async def async_read_data(self, module_id: str, data_id: str) from class SettingDataUpdateCoordinator * Add Mixin class for read/write * try to make select.py less "stale" * new dev environment 2 * new dev environment 2 * correct syntax * minor coding standard correction * Remove BOM * Remove BOM on select.py * Updated .coveragerc --- .coveragerc | 2 + .../components/kostal_plenticore/__init__.py | 2 +- .../components/kostal_plenticore/const.py | 58 +++++- .../components/kostal_plenticore/helper.py | 117 +++++++++++- .../components/kostal_plenticore/select.py | 129 +++++++++++++ .../components/kostal_plenticore/switch.py | 171 ++++++++++++++++++ 6 files changed, 472 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/kostal_plenticore/select.py create mode 100644 homeassistant/components/kostal_plenticore/switch.py diff --git a/.coveragerc b/.coveragerc index bbe00155b2f..2b247c2c923 100644 --- a/.coveragerc +++ b/.coveragerc @@ -549,6 +549,8 @@ omit = homeassistant/components/kostal_plenticore/const.py homeassistant/components/kostal_plenticore/helper.py homeassistant/components/kostal_plenticore/sensor.py + homeassistant/components/kostal_plenticore/switch.py + homeassistant/components/kostal_plenticore/select.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/* diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index f00e6ee1327..8e2beb73cc2 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -11,7 +11,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "switch", "select"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 68c2baffbdb..b025738a7b8 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,4 +1,6 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" +from collections import namedtuple +from typing import NamedTuple from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -688,11 +690,59 @@ SENSOR_SETTINGS_DATA = [ {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:battery-negative"}, "format_round", ), - ( +] + +# Defines all entities for switches. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - on Value (str) +# - on Label (str) +# - off Value (str) +# - off Label (str) +SWITCH = namedtuple( + "SWITCH", "module_id data_id name is_on on_value on_label off_value off_label" +) +SWITCH_SETTINGS_DATA = [ + SWITCH( "devices:local", "Battery:Strategy", - "Battery Strategy", - {}, - "format_round", + "Battery Strategy:", + "1", + "1", + "Automatic", + "2", + "Automatic economical", ), ] + + +class SelectData(NamedTuple): + """Representation of a SelectData tuple.""" + + module_id: str + data_id: str + name: str + options: list + is_on: str + + +# Defines all entities for select widgets. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - options +# - entity is enabled by default (bool) +SELECT_SETTINGS_DATA = [ + SelectData( + "devices:local", + "battery_charge", + "Battery Charging / Usage mode", + ["None", "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable"], + "1", + ) +] diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 32dfc9b2fd9..264d7e90efb 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,11 +3,16 @@ from __future__ import annotations import asyncio from collections import defaultdict +from collections.abc import Iterable from datetime import datetime, timedelta import logging from aiohttp.client_exceptions import ClientError -from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException +from kostal.plenticore import ( + PlenticoreApiClient, + PlenticoreApiException, + PlenticoreAuthenticationException, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant @@ -112,6 +117,38 @@ class Plenticore: _LOGGER.debug("Logged out from %s", self.host) +class DataUpdateCoordinatorMixin: + """Base implementation for read and write data.""" + + async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: + """Write settings back to Plenticore.""" + client = self._plenticore.client + + if client is None: + return False + + try: + val = await client.get_setting_values(module_id, data_id) + except PlenticoreApiException: + return False + else: + return val + + async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: + """Write settings back to Plenticore.""" + client = self._plenticore.client + + if client is None: + return False + + try: + await client.set_setting_values(module_id, value) + except PlenticoreApiException: + return False + else: + return True + + class PlenticoreUpdateCoordinator(DataUpdateCoordinator): """Base implementation of DataUpdateCoordinator for Plenticore data.""" @@ -171,7 +208,9 @@ class ProcessDataUpdateCoordinator(PlenticoreUpdateCoordinator): } -class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator): +class SettingDataUpdateCoordinator( + PlenticoreUpdateCoordinator, DataUpdateCoordinatorMixin +): """Implementation of PlenticoreUpdateCoordinator for settings data.""" async def _async_update_data(self) -> dict[str, dict[str, str]]: @@ -183,9 +222,83 @@ class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator): _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) fetched_data = await client.get_setting_values(self._fetch) + return fetched_data + + +class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator): + """Base implementation of DataUpdateCoordinator for Plenticore data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + name: str, + update_inverval: timedelta, + plenticore: Plenticore, + ) -> None: + """Create a new update coordinator for plenticore data.""" + super().__init__( + hass=hass, + logger=logger, + name=name, + update_interval=update_inverval, + ) + # data ids to poll + self._fetch = defaultdict(list) + self._plenticore = plenticore + + def start_fetch_data(self, module_id: str, data_id: str, all_options: str) -> None: + """Start fetching the given data (module-id and entry-id).""" + self._fetch[module_id].append(data_id) + self._fetch[module_id].append(all_options) + + # Force an update of all data. Multiple refresh calls + # are ignored by the debouncer. + async def force_refresh(event_time: datetime) -> None: + await self.async_request_refresh() + + async_call_later(self.hass, 2, force_refresh) + + def stop_fetch_data(self, module_id: str, data_id: str, all_options: str) -> None: + """Stop fetching the given data (module-id and entry-id).""" + self._fetch[module_id].remove(all_options) + self._fetch[module_id].remove(data_id) + + +class SelectDataUpdateCoordinator( + PlenticoreSelectUpdateCoordinator, DataUpdateCoordinatorMixin +): + """Implementation of PlenticoreUpdateCoordinator for select data.""" + + async def _async_update_data(self) -> dict[str, dict[str, str]]: + client = self._plenticore.client + + if client is None: + return {} + + _LOGGER.debug("Fetching select %s for %s", self.name, self._fetch) + + fetched_data = await self.async_get_currentoption(self._fetch) return fetched_data + async def async_get_currentoption( + self, + module_id: str | dict[str, Iterable[str]], + ) -> dict[str, dict[str, str]]: + """Get current option.""" + for mid, pids in module_id.items(): + all_options = pids[1] + for all_option in all_options: + if all_option != "None": + val = await self.async_read_data(mid, all_option) + for option in val.values(): + if option[all_option] == "1": + fetched = {mid: {pids[0]: all_option}} + return fetched + + return {mid: {pids[0]: "None"}} + class PlenticoreDataFormatter: """Provides method to format values of process or settings data.""" diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py new file mode 100644 index 00000000000..1f9d11dc334 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/select.py @@ -0,0 +1,129 @@ +"""Platform for Kostal Plenticore select widgets.""" +from __future__ import annotations + +from abc import ABC +from datetime import timedelta +import logging + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, SELECT_SETTINGS_DATA +from .helper import Plenticore, SelectDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Add kostal plenticore Select widget.""" + plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + PlenticoreDataSelect( + hass=hass, + plenticore=plenticore, + entry_id=entry.entry_id, + platform_name=entry.title, + device_class="kostal_plenticore__battery", + module_id=select.module_id, + data_id=select.data_id, + name=select.name, + current_option="None", + options=select.options, + is_on=select.is_on, + device_info=plenticore.device_info, + unique_id=f"{entry.entry_id}_{select.module_id}", + ) + for select in SELECT_SETTINGS_DATA + ) + + +class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): + """Representation of a Plenticore Switch.""" + + def __init__( + self, + hass: HomeAssistant, + plenticore: Plenticore, + entry_id: str, + platform_name: str, + device_class: str | None, + module_id: str, + data_id: str, + name: str, + current_option: str | None, + options: list[str], + is_on: str, + device_info: DeviceInfo, + unique_id: str, + ) -> None: + """Create a new switch Entity for Plenticore process data.""" + super().__init__( + coordinator=SelectDataUpdateCoordinator( + hass, + _LOGGER, + "Select Data", + timedelta(seconds=30), + plenticore, + ) + ) + self.plenticore = plenticore + self.entry_id = entry_id + self.platform_name = platform_name + self._attr_device_class = device_class + self.module_id = module_id + self.data_id = data_id + self._attr_options = options + self.all_options = options + self._attr_current_option = current_option + self._is_on = is_on + self._device_info = device_info + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_unique_id = unique_id + + @property + def available(self) -> bool: + """Return if entity is available.""" + is_available = ( + super().available + and self.coordinator.data is not None + and self.module_id in self.coordinator.data + and self.data_id in self.coordinator.data[self.module_id] + ) + + if is_available: + self._attr_current_option = self.coordinator.data[self.module_id][ + self.data_id + ] + + return is_available + + async def async_added_to_hass(self) -> None: + """Register this entity on the Update Coordinator.""" + await super().async_added_to_hass() + self.coordinator.start_fetch_data( + self.module_id, self.data_id, self.all_options + ) + + async def async_will_remove_from_hass(self) -> None: + """Unregister this entity from the Update Coordinator.""" + self.coordinator.stop_fetch_data(self.module_id, self.data_id, self.all_options) + await super().async_will_remove_from_hass() + + async def async_select_option(self, option: str) -> None: + """Update the current selected option.""" + self._attr_current_option = option + for all_option in self._attr_options: + if all_option != "None": + await self.coordinator.async_write_data( + self.module_id, {all_option: "0"} + ) + if option != "None": + await self.coordinator.async_write_data(self.module_id, {option: "1"}) + self.async_write_ha_state() diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py new file mode 100644 index 00000000000..9598e12aac0 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -0,0 +1,171 @@ +"""Platform for Kostal Plenticore switches.""" +from __future__ import annotations + +from abc import ABC +from datetime import timedelta +import logging +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, SWITCH_SETTINGS_DATA +from .helper import SettingDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Add kostal plenticore Switch.""" + plenticore = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + available_settings_data = await plenticore.client.get_settings() + settings_data_update_coordinator = SettingDataUpdateCoordinator( + hass, + _LOGGER, + "Settings Data", + timedelta(seconds=30), + plenticore, + ) + for switch in SWITCH_SETTINGS_DATA: + if switch.module_id not in available_settings_data or switch.data_id not in ( + setting.id for setting in available_settings_data[switch.module_id] + ): + _LOGGER.debug( + "Skipping non existing setting data %s/%s", + switch.module_id, + switch.data_id, + ) + continue + + entities.append( + PlenticoreDataSwitch( + settings_data_update_coordinator, + entry.entry_id, + entry.title, + switch.module_id, + switch.data_id, + switch.name, + switch.is_on, + switch.on_value, + switch.on_label, + switch.off_value, + switch.off_label, + plenticore.device_info, + f"{entry.title} {switch.name}", + f"{entry.entry_id}_{switch.module_id}_{switch.data_id}", + ) + ) + + async_add_entities(entities) + + +class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): + """Representation of a Plenticore Switch.""" + + def __init__( + self, + coordinator, + entry_id: str, + platform_name: str, + module_id: str, + data_id: str, + name: str, + is_on: str, + on_value: str, + on_label: str, + off_value: str, + off_label: str, + device_info: DeviceInfo, + attr_name: str, + attr_unique_id: str, + ): + """Create a new switch Entity for Plenticore process data.""" + super().__init__(coordinator) + self.entry_id = entry_id + self.platform_name = platform_name + self.module_id = module_id + self.data_id = data_id + self._last_run_success: bool | None = None + self._name = name + self._is_on = is_on + self._attr_name = attr_name + self.on_value = on_value + self.on_label = on_label + self.off_value = off_value + self.off_label = off_label + self._attr_unique_id = attr_unique_id + + self._device_info = device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.data is not None + and self.module_id in self.coordinator.data + and self.data_id in self.coordinator.data[self.module_id] + ) + + async def async_added_to_hass(self) -> None: + """Register this entity on the Update Coordinator.""" + await super().async_added_to_hass() + self.coordinator.start_fetch_data(self.module_id, self.data_id) + + async def async_will_remove_from_hass(self) -> None: + """Unregister this entity from the Update Coordinator.""" + self.coordinator.stop_fetch_data(self.module_id, self.data_id) + await super().async_will_remove_from_hass() + + async def async_turn_on(self, **kwargs) -> None: + """Turn device on.""" + if await self.coordinator.async_write_data( + self.module_id, {self.data_id: self.on_value} + ): + self._last_run_success = True + self.coordinator.name = f"{self.platform_name} {self._name} {self.on_label}" + await self.coordinator.async_request_refresh() + else: + self._last_run_success = False + + async def async_turn_off(self, **kwargs) -> None: + """Turn device off.""" + if await self.coordinator.async_write_data( + self.module_id, {self.data_id: self.off_value} + ): + self._last_run_success = True + self.coordinator.name = ( + f"{self.platform_name} {self._name} {self.off_label}" + ) + await self.coordinator.async_request_refresh() + else: + self._last_run_success = False + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self._device_info + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + if self.coordinator.data[self.module_id][self.data_id] == self._is_on: + self.coordinator.name = f"{self.platform_name} {self._name} {self.on_label}" + else: + self.coordinator.name = ( + f"{self.platform_name} {self._name} {self.off_label}" + ) + return bool(self.coordinator.data[self.module_id][self.data_id] == self._is_on) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return {"last_run_success": self._last_run_success} From 7cc7bbb76d5929e09c02fc8b54f19ffb550cc1e2 Mon Sep 17 00:00:00 2001 From: rianadon Date: Thu, 18 Nov 2021 07:08:42 -0800 Subject: [PATCH 0592/1452] Add speed to units system (#58437) * Use speed units in unit system * Use more obvious conversion factor for unit system speed test * Use wind_speed instead of speed, use m/s --- homeassistant/const.py | 1 + homeassistant/util/unit_system.py | 22 +++++++++++++++ tests/helpers/test_template.py | 9 ++++++- tests/util/test_unit_system.py | 45 +++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 43e07036cb8..da48646beed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -686,6 +686,7 @@ PRESSURE: Final = "pressure" VOLUME: Final = "volume" TEMPERATURE: Final = "temperature" SPEED: Final = "speed" +WIND_SPEED: Final = "wind_speed" ILLUMINANCE: Final = "illuminance" WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index bdd47112dde..1956faea84d 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -17,6 +17,8 @@ from homeassistant.const import ( PRESSURE, PRESSURE_PA, PRESSURE_PSI, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMPERATURE, @@ -24,10 +26,12 @@ from homeassistant.const import ( VOLUME, VOLUME_GALLONS, VOLUME_LITERS, + WIND_SPEED, ) from homeassistant.util import ( distance as distance_util, pressure as pressure_util, + speed as speed_util, temperature as temperature_util, volume as volume_util, ) @@ -42,6 +46,8 @@ PRESSURE_UNITS = pressure_util.VALID_UNITS VOLUME_UNITS = volume_util.VALID_UNITS +WIND_SPEED_UNITS = speed_util.VALID_UNITS + TEMPERATURE_UNITS: tuple[str, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) @@ -49,6 +55,8 @@ def is_valid_unit(unit: str, unit_type: str) -> bool: """Check if the unit is valid for it's type.""" if unit_type == LENGTH: units = LENGTH_UNITS + elif unit_type == WIND_SPEED: + units = WIND_SPEED_UNITS elif unit_type == TEMPERATURE: units = TEMPERATURE_UNITS elif unit_type == MASS: @@ -71,6 +79,7 @@ class UnitSystem: name: str, temperature: str, length: str, + wind_speed: str, volume: str, mass: str, pressure: str, @@ -81,6 +90,7 @@ class UnitSystem: for unit, unit_type in ( (temperature, TEMPERATURE), (length, LENGTH), + (wind_speed, WIND_SPEED), (volume, VOLUME), (mass, MASS), (pressure, PRESSURE), @@ -97,6 +107,7 @@ class UnitSystem: self.mass_unit = mass self.pressure_unit = pressure self.volume_unit = volume + self.wind_speed_unit = wind_speed @property def is_metric(self) -> bool: @@ -130,6 +141,14 @@ class UnitSystem: pressure, from_unit, self.pressure_unit ) + def wind_speed(self, wind_speed: float | None, from_unit: str) -> float: + """Convert the given wind_speed to this unit system.""" + if not isinstance(wind_speed, Number): + raise TypeError(f"{wind_speed!s} is not a numeric value.") + + # type ignore: https://github.com/python/mypy/issues/7207 + return speed_util.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore + def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" if not isinstance(volume, Number): @@ -146,6 +165,7 @@ class UnitSystem: PRESSURE: self.pressure_unit, TEMPERATURE: self.temperature_unit, VOLUME: self.volume_unit, + WIND_SPEED: self.wind_speed_unit, } @@ -153,6 +173,7 @@ METRIC_SYSTEM = UnitSystem( CONF_UNIT_SYSTEM_METRIC, TEMP_CELSIUS, LENGTH_KILOMETERS, + SPEED_METERS_PER_SECOND, VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, @@ -162,6 +183,7 @@ IMPERIAL_SYSTEM = UnitSystem( CONF_UNIT_SYSTEM_IMPERIAL, TEMP_FAHRENHEIT, LENGTH_MILES, + SPEED_MILES_PER_HOUR, VOLUME_GALLONS, MASS_POUNDS, PRESSURE_PSI, diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index dd0c4c96a11..f871fc88d3a 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -14,6 +14,7 @@ from homeassistant.const import ( LENGTH_METERS, MASS_GRAMS, PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, VOLUME_LITERS, ) @@ -34,7 +35,13 @@ from tests.common import ( def _set_up_units(hass): """Set up the tests.""" hass.config.units = UnitSystem( - "custom", TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA + "custom", + TEMP_CELSIUS, + LENGTH_METERS, + SPEED_KILOMETERS_PER_HOUR, + VOLUME_LITERS, + MASS_GRAMS, + PRESSURE_PA, ) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index f32e731f9b3..5c7ac660f9b 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -9,10 +9,12 @@ from homeassistant.const import ( MASS_GRAMS, PRESSURE, PRESSURE_PA, + SPEED_METERS_PER_SECOND, TEMP_CELSIUS, TEMPERATURE, VOLUME, VOLUME_LITERS, + WIND_SPEED, ) from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem @@ -27,6 +29,7 @@ def test_invalid_units(): SYSTEM_NAME, INVALID_UNIT, LENGTH_METERS, + SPEED_METERS_PER_SECOND, VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, @@ -37,6 +40,7 @@ def test_invalid_units(): SYSTEM_NAME, TEMP_CELSIUS, INVALID_UNIT, + SPEED_METERS_PER_SECOND, VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, @@ -48,6 +52,7 @@ def test_invalid_units(): TEMP_CELSIUS, LENGTH_METERS, INVALID_UNIT, + VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, ) @@ -57,6 +62,18 @@ def test_invalid_units(): SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, + SPEED_METERS_PER_SECOND, + INVALID_UNIT, + MASS_GRAMS, + PRESSURE_PA, + ) + + with pytest.raises(ValueError): + UnitSystem( + SYSTEM_NAME, + TEMP_CELSIUS, + LENGTH_METERS, + SPEED_METERS_PER_SECOND, VOLUME_LITERS, INVALID_UNIT, PRESSURE_PA, @@ -67,6 +84,7 @@ def test_invalid_units(): SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, + SPEED_METERS_PER_SECOND, VOLUME_LITERS, MASS_GRAMS, INVALID_UNIT, @@ -79,6 +97,8 @@ def test_invalid_value(): METRIC_SYSTEM.length("25a", LENGTH_KILOMETERS) with pytest.raises(TypeError): METRIC_SYSTEM.temperature("50K", TEMP_CELSIUS) + with pytest.raises(TypeError): + METRIC_SYSTEM.wind_speed("50km/h", SPEED_METERS_PER_SECOND) with pytest.raises(TypeError): METRIC_SYSTEM.volume("50L", VOLUME_LITERS) with pytest.raises(TypeError): @@ -89,6 +109,7 @@ def test_as_dict(): """Test that the as_dict() method returns the expected dictionary.""" expected = { LENGTH: LENGTH_KILOMETERS, + WIND_SPEED: SPEED_METERS_PER_SECOND, TEMPERATURE: TEMP_CELSIUS, VOLUME: VOLUME_LITERS, MASS: MASS_GRAMS, @@ -142,6 +163,29 @@ def test_length_to_imperial(): assert IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) == 3.106855 +def test_wind_speed_unknown_unit(): + """Test wind_speed conversion with unknown from unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.length(5, "turtles") + + +def test_wind_speed_to_metric(): + """Test length conversion to metric system.""" + assert METRIC_SYSTEM.wind_speed(100, METRIC_SYSTEM.wind_speed_unit) == 100 + # 1 m/s is about 2.237 mph + assert METRIC_SYSTEM.wind_speed( + 2237, IMPERIAL_SYSTEM.wind_speed_unit + ) == pytest.approx(1000, abs=0.1) + + +def test_wind_speed_to_imperial(): + """Test wind_speed conversion to imperial system.""" + assert IMPERIAL_SYSTEM.wind_speed(100, IMPERIAL_SYSTEM.wind_speed_unit) == 100 + assert IMPERIAL_SYSTEM.wind_speed( + 1000, METRIC_SYSTEM.wind_speed_unit + ) == pytest.approx(2237, abs=0.1) + + def test_pressure_same_unit(): """Test no conversion happens if to unit is same as from unit.""" assert METRIC_SYSTEM.pressure(5, METRIC_SYSTEM.pressure_unit) == 5 @@ -172,6 +216,7 @@ def test_pressure_to_imperial(): def test_properties(): """Test the unit properties are returned as expected.""" assert METRIC_SYSTEM.length_unit == LENGTH_KILOMETERS + assert METRIC_SYSTEM.wind_speed_unit == SPEED_METERS_PER_SECOND assert METRIC_SYSTEM.temperature_unit == TEMP_CELSIUS assert METRIC_SYSTEM.mass_unit == MASS_GRAMS assert METRIC_SYSTEM.volume_unit == VOLUME_LITERS From 28ff1b9d9e69ab635f5552407e4c74320a89ddbe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Nov 2021 16:15:54 +0100 Subject: [PATCH 0593/1452] Use native date value in Renault sensors (#59900) Co-authored-by: epenet --- homeassistant/components/renault/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index d06ae497cf1..3dddfafab07 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from typing import TYPE_CHECKING, cast from renault_api.kamereon.enums import ChargeState, PlugState @@ -67,7 +68,7 @@ class RenaultSensorEntityDescription( icon_lambda: Callable[[RenaultSensor[T]], str] | None = None condition_lambda: Callable[[RenaultVehicleProxy], bool] | None = None requires_fuel: bool = False - value_lambda: Callable[[RenaultSensor[T]], StateType] | None = None + value_lambda: Callable[[RenaultSensor[T]], StateType | datetime] | None = None async def async_setup_entry( @@ -106,7 +107,7 @@ class RenaultSensor(RenaultDataEntity[T], SensorEntity): return self.entity_description.icon_lambda(self) @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of this entity.""" if self.data is None: return None @@ -153,12 +154,12 @@ def _get_rounded_value(entity: RenaultSensor[T]) -> float: return round(cast(float, entity.data)) -def _get_utc_value(entity: RenaultSensor[T]) -> str: +def _get_utc_value(entity: RenaultSensor[T]) -> datetime: """Return the UTC value of this entity.""" original_dt = parse_datetime(cast(str, entity.data)) if TYPE_CHECKING: assert original_dt is not None - return as_utc(original_dt).isoformat() + return as_utc(original_dt) SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( From 329904dfbb1c8416ade02252f98c6a021c35e186 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Nov 2021 16:49:36 +0100 Subject: [PATCH 0594/1452] Add type hints to SSDP (#59840) --- homeassistant/components/ssdp/__init__.py | 78 +++++++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c937f210368..a84b2e690da 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta from enum import Enum from ipaddress import IPv4Address, IPv6Address import logging -from typing import Any, Callable, Mapping +from typing import Any, Callable, Final, Mapping, TypedDict, cast from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource @@ -32,7 +32,7 @@ SCAN_INTERVAL = timedelta(seconds=60) IPV4_BROADCAST = IPv4Address("255.255.255.255") # Attributes for accessing info from SSDP response -ATTR_SSDP_LOCATION = "ssdp_location" +ATTR_SSDP_LOCATION: Final = "ssdp_location" ATTR_SSDP_ST = "ssdp_st" ATTR_SSDP_NT = "ssdp_nt" ATTR_SSDP_UDN = "ssdp_udn" @@ -56,7 +56,7 @@ ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" # Attributes for accessing info added by Home Assistant -ATTR_HA_MATCHING_DOMAINS = "x-homeassistant-matching-domains" +ATTR_HA_MATCHING_DOMAINS: Final = "x_homeassistant_matching_domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] @@ -85,6 +85,58 @@ SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { _LOGGER = logging.getLogger(__name__) +class _HaServiceInfoDescription(TypedDict, total=True): + """Keys added by HA.""" + + x_homeassistant_matching_domains: set[str] + + +class _SsdpDescriptionBase(TypedDict, total=True): + """Compulsory keys for SSDP info.""" + + ssdp_usn: str + ssdp_st: str + + +class SsdpDescription(_SsdpDescriptionBase, total=False): + """SSDP info with optional keys.""" + + ssdp_location: str + ssdp_nt: str + ssdp_udn: str + ssdp_ext: str + ssdp_server: str + + +class _UpnpDescriptionBase(TypedDict, total=True): + """Compulsory keys for UPnP info.""" + + deviceType: str + friendlyName: str + manufacturer: str + modelName: str + UDN: str + + +class UpnpDescription(_UpnpDescriptionBase, total=False): + """UPnP info with optional keys.""" + + manufacturerURL: str + modelDescription: str + modelNumber: str + modelURL: str + serialNumber: str + UPC: str + iconList: dict[str, list[dict[str, str]]] + serviceList: dict[str, list[dict[str, str]]] + deviceList: dict[str, Any] + presentationURL: str + + +class SsdpServiceInfo(SsdpDescription, UpnpDescription, _HaServiceInfoDescription): + """Prepared info from ssdp/upnp entries.""" + + @bind_hass async def async_register_callback( hass: HomeAssistant, @@ -102,7 +154,7 @@ async def async_register_callback( @bind_hass async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name hass: HomeAssistant, udn: str, st: str -) -> dict[str, str] | None: +) -> SsdpServiceInfo | None: """Fetch the discovery info cache.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn_st(udn, st) @@ -111,7 +163,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_st( # pylint: disable=invalid-name hass: HomeAssistant, st: str -) -> list[dict[str, str]]: +) -> list[SsdpServiceInfo]: """Fetch all the entries matching the st.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_st(st) @@ -120,7 +172,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_udn( hass: HomeAssistant, udn: str -) -> list[dict[str, str]]: +) -> list[SsdpServiceInfo]: """Fetch all the entries matching the udn.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn(udn) @@ -141,7 +193,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_process_callbacks( callbacks: list[SsdpCallback], - discovery_info: dict[str, str], + discovery_info: SsdpServiceInfo, ssdp_change: SsdpChange, ) -> None: for callback in callbacks: @@ -427,7 +479,7 @@ class Scanner: async def _async_headers_to_discovery_info( self, headers: Mapping[str, Any] - ) -> dict[str, Any]: + ) -> SsdpServiceInfo: """Combine the headers and description into discovery_info. Building this is a bit expensive so we only do it on demand. @@ -443,7 +495,7 @@ class Scanner: async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name self, udn: str, st: str - ) -> dict[str, Any] | None: + ) -> SsdpServiceInfo | None: """Return discovery_info for a udn and st.""" if headers := self._all_headers_from_ssdp_devices.get((udn, st)): return await self._async_headers_to_discovery_info(headers) @@ -451,7 +503,7 @@ class Scanner: async def async_get_discovery_info_by_st( # pylint: disable=invalid-name self, st: str - ) -> list[dict[str, Any]]: + ) -> list[SsdpServiceInfo]: """Return matching discovery_infos for a st.""" return [ await self._async_headers_to_discovery_info(headers) @@ -459,7 +511,7 @@ class Scanner: if udn_st[1] == st ] - async def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, Any]]: + async def async_get_discovery_info_by_udn(self, udn: str) -> list[SsdpServiceInfo]: """Return matching discovery_infos for a udn.""" return [ await self._async_headers_to_discovery_info(headers) @@ -470,7 +522,7 @@ class Scanner: def discovery_info_from_headers_and_description( info_with_desc: CaseInsensitiveDict, -) -> dict[str, Any]: +) -> SsdpServiceInfo: """Convert headers and description to discovery_info.""" info = { DISCOVERY_MAPPING.get(k.lower(), k): v @@ -485,7 +537,7 @@ def discovery_info_from_headers_and_description( if ATTR_SSDP_ST not in info and ATTR_SSDP_NT in info: info[ATTR_SSDP_ST] = info[ATTR_SSDP_NT] - return info + return cast(SsdpServiceInfo, info) def _udn_from_usn(usn: str | None) -> str | None: From a211b8ca8f01a3b0f08079672b3707f3224d976e Mon Sep 17 00:00:00 2001 From: PlusPlus-ua Date: Thu, 18 Nov 2021 17:53:34 +0200 Subject: [PATCH 0595/1452] Bugfix in Tuya Number value scaling (#59903) --- homeassistant/components/tuya/base.py | 4 ++++ homeassistant/components/tuya/number.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index d61c83b17ad..f4bc0dc561f 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -46,6 +46,10 @@ class IntegerTypeData: """Scale a value.""" return value * 1.0 / (10 ** self.scale) + def scale_value_back(self, value: float | int) -> int: + """Return raw value for scaled.""" + return int(value * (10 ** self.scale)) + def remap_value_to( self, value: float, diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index c724f6e79a3..8db2e4debd5 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -259,9 +259,9 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): # and determine unit of measurement if self._status_range.type == "Integer": self._type_data = IntegerTypeData.from_json(self._status_range.values) - self._attr_max_value = self._type_data.max - self._attr_min_value = self._type_data.min - self._attr_step = self._type_data.step + self._attr_max_value = self._type_data.max_scaled + self._attr_min_value = self._type_data.min_scaled + self._attr_step = self._type_data.step_scaled if description.unit_of_measurement is None: self._attr_unit_of_measurement = self._type_data.unit @@ -290,7 +290,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): [ { "code": self.entity_description.key, - "value": self._type_data.scale_value(value), + "value": self._type_data.scale_value_back(value), } ] ) From cc3f17979673a824d92b4d9fe648e65d984b8ec1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Nov 2021 09:57:31 -0600 Subject: [PATCH 0596/1452] Ensure powerview setup is retried on 502 error (#59847) --- .../components/hunterdouglas_powerview/__init__.py | 9 ++++----- .../components/hunterdouglas_powerview/const.py | 9 +++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index a8461f4da5c..a9c620a4baa 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -89,12 +89,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (await shades.get_resources())[SHADE_DATA] ) except HUB_EXCEPTIONS as err: - _LOGGER.error("Connection error to PowerView hub: %s", hub_address) - raise ConfigEntryNotReady from err - + raise ConfigEntryNotReady( + f"Connection error to PowerView hub: {hub_address}: {err}" + ) from err if not device_info: - _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) - raise ConfigEntryNotReady + raise ConfigEntryNotReady(f"Unable to initialize PowerView hub: {hub_address}") async def async_update_data(): """Fetch data from shade endpoint.""" diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index e827b055995..ea87150a9ca 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -3,7 +3,7 @@ import asyncio from aiohttp.client_exceptions import ServerDisconnectedError -from aiopvapi.helpers.aiorequest import PvApiConnectionError +from aiopvapi.helpers.aiorequest import PvApiConnectionError, PvApiResponseStatusError DOMAIN = "hunterdouglas_powerview" @@ -62,7 +62,12 @@ PV_SHADE_DATA = "pv_shade_data" PV_ROOM_DATA = "pv_room_data" COORDINATOR = "coordinator" -HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnectionError) +HUB_EXCEPTIONS = ( + ServerDisconnectedError, + asyncio.TimeoutError, + PvApiConnectionError, + PvApiResponseStatusError, +) LEGACY_DEVICE_SUB_REVISION = 1 LEGACY_DEVICE_REVISION = 0 From d18c250acfff9fe612e53d37a64bf57a57a6b902 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Nov 2021 17:15:40 +0100 Subject: [PATCH 0597/1452] Add minor version support to storage.Store (#59882) --- .../components/onboarding/__init__.py | 8 +- homeassistant/components/person/__init__.py | 2 +- homeassistant/helpers/storage.py | 48 +++++- .../google_assistant/test_helpers.py | 3 + tests/helpers/test_storage.py | 157 ++++++++++++++++++ tests/test_config.py | 1 + 6 files changed, 206 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index c2ab9a495bf..6ffee319d33 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -20,14 +20,14 @@ STORAGE_VERSION = 4 class OnboadingStorage(Store): """Store onboarding data.""" - async def _async_migrate_func(self, old_version, old_data): + async def _async_migrate_func(self, old_major_version, old_minor_version, old_data): """Migrate to the new version.""" # From version 1 -> 2, we automatically mark the integration step done - if old_version < 2: + if old_major_version < 2: old_data["done"].append(STEP_INTEGRATION) - if old_version < 3: + if old_major_version < 3: old_data["done"].append(STEP_CORE_CONFIG) - if old_version < 4: + if old_major_version < 4: old_data["done"].append(STEP_ANALYTICS) return old_data diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 23b9bafcc47..3ec08ae518e 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -148,7 +148,7 @@ UPDATE_FIELDS = { class PersonStore(Store): """Person storage.""" - async def _async_migrate_func(self, old_version, old_data): + async def _async_migrate_func(self, old_major_version, old_minor_version, old_data): """Migrate to the new version. Migrate storage to use format of collection helper. diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 59cf4ff8c22..ba276969299 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from contextlib import suppress +import inspect from json import JSONEncoder import logging import os @@ -75,11 +76,13 @@ class Store: key: str, private: bool = False, *, - encoder: type[JSONEncoder] | None = None, atomic_writes: bool = False, + encoder: type[JSONEncoder] | None = None, + minor_version: int = 1, ) -> None: """Initialize storage class.""" self.version = version + self.minor_version = minor_version self.key = key self.hass = hass self._private = private @@ -99,8 +102,8 @@ class Store: async def async_load(self) -> dict | list | None: """Load data. - If the expected version does not match the given version, the migrate - function will be invoked with await migrate_func(version, config). + If the expected version and minor version do not match the given versions, the + migrate function will be invoked with migrate_func(version, minor_version, config). Will ensure that when a call comes in while another one is in progress, the second call will wait and return the result of the first call. @@ -137,7 +140,15 @@ class Store: if data == {}: return None - if data["version"] == self.version: + + # Add minor_version if not set + if "minor_version" not in data: + data["minor_version"] = 1 + + if ( + data["version"] == self.version + and data["minor_version"] == self.minor_version + ): stored = data["data"] else: _LOGGER.info( @@ -146,13 +157,29 @@ class Store: data["version"], self.version, ) - stored = await self._async_migrate_func(data["version"], data["data"]) + if len(inspect.signature(self._async_migrate_func).parameters) == 2: + # pylint: disable-next=no-value-for-parameter + stored = await self._async_migrate_func(data["version"], data["data"]) + else: + try: + stored = await self._async_migrate_func( + data["version"], data["minor_version"], data["data"] + ) + except NotImplementedError: + if data["version"] != self.version: + raise + stored = data["data"] return stored async def async_save(self, data: dict | list) -> None: """Save data.""" - self._data = {"version": self.version, "key": self.key, "data": data} + self._data = { + "version": self.version, + "minor_version": self.minor_version, + "key": self.key, + "data": data, + } if self.hass.state == CoreState.stopping: self._async_ensure_final_write_listener() @@ -163,7 +190,12 @@ class Store: @callback def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None: """Save data with an optional delay.""" - self._data = {"version": self.version, "key": self.key, "data_func": data_func} + self._data = { + "version": self.version, + "minor_version": self.minor_version, + "key": self.key, + "data_func": data_func, + } self._async_cleanup_delay_listener() self._async_ensure_final_write_listener() @@ -248,7 +280,7 @@ class Store: atomic_writes=self._atomic_writes, ) - async def _async_migrate_func(self, old_version, old_data): + async def _async_migrate_func(self, old_major_version, old_minor_version, old_data): """Migrate to the new version.""" raise NotImplementedError diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 9e54e6cff3f..a260ef03948 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -169,6 +169,7 @@ async def test_agent_user_id_storage(hass, hass_storage): hass_storage["google_assistant"] = { "version": 1, + "minor_version": 1, "key": "google_assistant", "data": {"agent_user_ids": {"agent_1": {}}}, } @@ -178,6 +179,7 @@ async def test_agent_user_id_storage(hass, hass_storage): assert hass_storage["google_assistant"] == { "version": 1, + "minor_version": 1, "key": "google_assistant", "data": {"agent_user_ids": {"agent_1": {}}}, } @@ -188,6 +190,7 @@ async def test_agent_user_id_storage(hass, hass_storage): assert hass_storage["google_assistant"] == { "version": 1, + "minor_version": 1, "key": "google_assistant", "data": data, } diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 61bf9fa8d0e..d6a78340127 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -17,6 +17,9 @@ from homeassistant.util import dt from tests.common import async_fire_time_changed MOCK_VERSION = 1 +MOCK_VERSION_2 = 2 +MOCK_MINOR_VERSION_1 = 1 +MOCK_MINOR_VERSION_2 = 2 MOCK_KEY = "storage-test" MOCK_DATA = {"hello": "world"} MOCK_DATA2 = {"goodbye": "cruel world"} @@ -28,6 +31,30 @@ def store(hass): yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) +@pytest.fixture +def store_v_1_1(hass): + """Fixture of a store that prevents writing on Home Assistant stop.""" + yield storage.Store( + hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + + +@pytest.fixture +def store_v_1_2(hass): + """Fixture of a store that prevents writing on Home Assistant stop.""" + yield storage.Store( + hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2 + ) + + +@pytest.fixture +def store_v_2_1(hass): + """Fixture of a store that prevents writing on Home Assistant stop.""" + yield storage.Store( + hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + + async def test_loading(hass, store): """Test we can save and load data.""" await store.async_save(MOCK_DATA) @@ -78,6 +105,7 @@ async def test_saving_with_delay(hass, store, hass_storage): await hass.async_block_till_done() assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": MOCK_DATA, } @@ -101,6 +129,7 @@ async def test_saving_on_final_write(hass, hass_storage): await hass.async_block_till_done() assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": MOCK_DATA, } @@ -148,6 +177,7 @@ async def test_loading_while_delay(hass, store, hass_storage): await store.async_save({"delay": "no"}) assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -155,6 +185,7 @@ async def test_loading_while_delay(hass, store, hass_storage): store.async_delay_save(lambda: {"delay": "yes"}, 1) assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -170,6 +201,7 @@ async def test_writing_while_writing_delay(hass, store, hass_storage): await store.async_save({"delay": "no"}) assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -178,6 +210,7 @@ async def test_writing_while_writing_delay(hass, store, hass_storage): await hass.async_block_till_done() assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -196,6 +229,7 @@ async def test_multiple_delay_save_calls(hass, store, hass_storage): await store.async_save({"delay": "no"}) assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -204,6 +238,7 @@ async def test_multiple_delay_save_calls(hass, store, hass_storage): await hass.async_block_till_done() assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"delay": "no"}, } @@ -221,6 +256,7 @@ async def test_multiple_save_calls(hass, store, hass_storage): await asyncio.gather(*tasks) assert hass_storage[store.key] == { "version": MOCK_VERSION, + "minor_version": 1, "key": MOCK_KEY, "data": {"savecount": 5}, } @@ -252,6 +288,7 @@ async def test_migrator_existing_config(hass, store, hass_storage): assert hass_storage[store.key] == { "key": MOCK_KEY, "version": MOCK_VERSION, + "minor_version": 1, "data": data, } @@ -277,5 +314,125 @@ async def test_migrator_transforming_config(hass, store, hass_storage): assert hass_storage[store.key] == { "key": MOCK_KEY, "version": MOCK_VERSION, + "minor_version": 1, "data": data, } + + +async def test_minor_version_default(hass, store, hass_storage): + """Test minor version default.""" + + await store.async_save(MOCK_DATA) + assert hass_storage[store.key]["minor_version"] == 1 + + +async def test_minor_version(hass, store_v_1_2, hass_storage): + """Test minor version.""" + + await store_v_1_2.async_save(MOCK_DATA) + assert hass_storage[store_v_1_2.key]["minor_version"] == MOCK_MINOR_VERSION_2 + + +async def test_migrate_major_not_implemented_raises(hass, store, store_v_2_1): + """Test migrating between major versions fails if not implemented.""" + + await store_v_2_1.async_save(MOCK_DATA) + with pytest.raises(NotImplementedError): + await store.async_load() + + +async def test_migrate_minor_not_implemented( + hass, hass_storage, store_v_1_1, store_v_1_2 +): + """Test migrating between minor versions does not fail if not implemented.""" + + assert store_v_1_1.key == store_v_1_2.key + + await store_v_1_1.async_save(MOCK_DATA) + assert hass_storage[store_v_1_1.key] == { + "key": MOCK_KEY, + "version": MOCK_VERSION, + "minor_version": MOCK_MINOR_VERSION_1, + "data": MOCK_DATA, + } + data = await store_v_1_2.async_load() + assert hass_storage[store_v_1_1.key]["data"] == data + + await store_v_1_2.async_save(MOCK_DATA) + assert hass_storage[store_v_1_2.key] == { + "key": MOCK_KEY, + "version": MOCK_VERSION, + "minor_version": MOCK_MINOR_VERSION_2, + "data": MOCK_DATA, + } + + +async def test_migration(hass, hass_storage, store_v_1_2): + """Test migration.""" + calls = 0 + + class CustomStore(storage.Store): + async def _async_migrate_func( + self, old_major_version, old_minor_version, old_data: dict + ): + nonlocal calls + calls += 1 + assert old_major_version == store_v_1_2.version + assert old_minor_version == store_v_1_2.minor_version + return old_data + + await store_v_1_2.async_save(MOCK_DATA) + assert hass_storage[store_v_1_2.key] == { + "key": MOCK_KEY, + "version": MOCK_VERSION, + "minor_version": MOCK_MINOR_VERSION_2, + "data": MOCK_DATA, + } + assert calls == 0 + + legacy_store = CustomStore(hass, 2, store_v_1_2.key, minor_version=1) + data = await legacy_store.async_load() + assert calls == 1 + assert hass_storage[store_v_1_2.key]["data"] == data + + await legacy_store.async_save(MOCK_DATA) + assert hass_storage[legacy_store.key] == { + "key": MOCK_KEY, + "version": 2, + "minor_version": 1, + "data": MOCK_DATA, + } + + +async def test_legacy_migration(hass, hass_storage, store_v_1_2): + """Test legacy migration method signature.""" + calls = 0 + + class LegacyStore(storage.Store): + async def _async_migrate_func(self, old_version, old_data: dict): + nonlocal calls + calls += 1 + assert old_version == store_v_1_2.version + return old_data + + await store_v_1_2.async_save(MOCK_DATA) + assert hass_storage[store_v_1_2.key] == { + "key": MOCK_KEY, + "version": MOCK_VERSION, + "minor_version": MOCK_MINOR_VERSION_2, + "data": MOCK_DATA, + } + assert calls == 0 + + legacy_store = LegacyStore(hass, 2, store_v_1_2.key, minor_version=1) + data = await legacy_store.async_load() + assert calls == 1 + assert hass_storage[store_v_1_2.key]["data"] == data + + await legacy_store.async_save(MOCK_DATA) + assert hass_storage[legacy_store.key] == { + "key": MOCK_KEY, + "version": 2, + "minor_version": 1, + "data": MOCK_DATA, + } diff --git a/tests/test_config.py b/tests/test_config.py index 441029d27dc..9f2cc56b1b7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -444,6 +444,7 @@ async def test_updating_configuration(hass, hass_storage): }, "key": "core.config", "version": 1, + "minor_version": 1, } hass_storage["core.config"] = dict(core_data) await config_util.async_process_ha_core_config( From 24f3fd35c9950c703ab35d8a74ec4b86a139162f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 17:37:52 +0100 Subject: [PATCH 0598/1452] Fix shorthand native value type in sensor entity component (#59908) --- homeassistant/components/sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 58a9fe4022e..ca083a0480d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -174,7 +174,7 @@ class SensorEntity(Entity): 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_native_value: StateType | date | datetime = None _attr_state_class: str | None _attr_state: None = None # Subclasses of SensorEntity should not set this _attr_unit_of_measurement: None = ( From f751d6e064d70a9ce233cadb1d38b2ce05af6be9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Nov 2021 10:45:38 -0600 Subject: [PATCH 0599/1452] Add additional bulb ouis to flux_led (#59868) --- homeassistant/components/flux_led/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index cd37897af67..0b9b7f00e8c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -24,6 +24,10 @@ "macaddress": "B4E842*", "hostname": "[ba][lk]*" }, + { + "macaddress": "F0FE6B*", + "hostname": "[ba][lk]*" + }, { "macaddress": "8CCE4E*", "hostname": "lwip*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d21fc8d63b0..f11cf63df9c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -91,6 +91,11 @@ DHCP = [ "macaddress": "B4E842*", "hostname": "[ba][lk]*" }, + { + "domain": "flux_led", + "macaddress": "F0FE6B*", + "hostname": "[ba][lk]*" + }, { "domain": "flux_led", "macaddress": "8CCE4E*", From 9ccee205cad31ffcb6562d3ed3783c8707ae7943 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Nov 2021 19:14:00 +0100 Subject: [PATCH 0600/1452] Use ZeroconfServiceInfo in gogogate2 (#59746) Co-authored-by: epenet --- .../components/gogogate2/config_flow.py | 9 +++++--- .../components/gogogate2/test_config_flow.py | 21 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 6fd61b79795..3695703b509 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -7,6 +7,7 @@ from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( @@ -35,10 +36,12 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): self._ip_address = None self._device_type = None - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> data_entry_flow.FlowResult: """Handle homekit discovery.""" - await self.async_set_unique_id(discovery_info["properties"]["id"]) - return await self._async_discovery_handler(discovery_info["host"]) + await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["id"]) + return await self._async_discovery_handler(discovery_info[zeroconf.ATTR_HOST]) async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 18ed7334b8d..74997e827aa 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -6,6 +6,7 @@ from ismartgate.common import ApiError from ismartgate.const import GogoGate2ApiErrorCode from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, @@ -106,7 +107,9 @@ async def test_form_homekit_unique_id_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} @@ -126,7 +129,9 @@ async def test_form_homekit_unique_id_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -143,7 +148,9 @@ async def test_form_homekit_ip_address_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -154,7 +161,9 @@ async def test_form_homekit_ip_address(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} @@ -227,7 +236,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} From fc330f797d4a9cd459563d2a5b917975bebd9f89 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 19:58:31 +0100 Subject: [PATCH 0601/1452] Use native datetime value in Forecast Solar sensors (#59913) --- homeassistant/components/forecast_solar/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index cd672311c52..c1ebbaf60bd 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -61,7 +61,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): ) @property - def native_value(self) -> StateType: + def native_value(self) -> datetime | StateType: """Return the state of the sensor.""" if self.entity_description.state is None: state: StateType | datetime = getattr( @@ -70,6 +70,4 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): else: state = self.entity_description.state(self.coordinator.data) - if isinstance(state, datetime): - return state.isoformat() return state From 3adb9e4143f827a934d3ade5d6f3eea23f8d7da3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 20:03:04 +0100 Subject: [PATCH 0602/1452] Use native datetime value in Tasmota sensors (#59915) --- homeassistant/components/tasmota/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 348ff741c9b..678d3eaf4fa 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -259,10 +259,10 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return class_or_icon.get(ICON) @property - def native_value(self) -> str | None: + def native_value(self) -> datetime | str | None: """Return the state of the entity.""" if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: - return self._state_timestamp.isoformat() + return self._state_timestamp return self._state @property From 4e4f6ffa07f3bd1eb0e98e9373983338cdff399d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 20:03:16 +0100 Subject: [PATCH 0603/1452] Use native datetime value in Jewish Calendar sensors (#59917) --- homeassistant/components/jewish_calendar/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 09a56a9e26a..6db80036614 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -170,10 +170,8 @@ class JewishCalendarSensor(SensorEntity): self._holiday_attrs: dict[str, str] = {} @property - def native_value(self) -> StateType: + def native_value(self) -> datetime | StateType: """Return the state of the sensor.""" - if isinstance(self._state, datetime): - return self._state.isoformat() return self._state async def async_update(self) -> None: @@ -262,11 +260,11 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): _attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType | None: + def native_value(self) -> datetime | None: """Return the state of the sensor.""" if self._state is None: return None - return dt_util.as_utc(self._state).isoformat() + return dt_util.as_utc(self._state) def get_state( self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate From fc296119357cebf1c4349aa993920227668a45a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 20:03:30 +0100 Subject: [PATCH 0604/1452] Use native datetime value in Uptime sensors (#59916) --- homeassistant/components/uptime/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index db06b09ea18..9a48b09e452 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_native_value: str = dt_util.now().isoformat() + self._attr_native_value = dt_util.now() From 5593dd40060138e37d569fc37b4bc54fcb3c2b38 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 20:03:40 +0100 Subject: [PATCH 0605/1452] Use native datetime value in WLED sensors (#59907) --- homeassistant/components/wled/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 2453c9d1604..d1c1bc2d96c 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta from typing import Callable from wled import Device as WLEDDevice @@ -37,7 +37,7 @@ from .models import WLEDEntity class WLEDSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[WLEDDevice], StateType] + value_fn: Callable[[WLEDDevice], datetime | StateType] @dataclass @@ -77,9 +77,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_TIMESTAMP, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda device: (utcnow() - timedelta(seconds=device.info.uptime)) - .replace(microsecond=0) - .isoformat(), + value_fn=lambda device: (utcnow() - timedelta(seconds=device.info.uptime)), ), WLEDSensorEntityDescription( key="free_heap", @@ -157,6 +155,6 @@ class WLEDSensorEntity(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_{description.key}" @property - def native_value(self) -> StateType: + def native_value(self) -> datetime | StateType: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) From 8a0c5aa50b6683a63b01ff922532973335c482e1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 20:07:38 +0100 Subject: [PATCH 0606/1452] Use native datetime value in RDW sensors (#59914) --- homeassistant/components/rdw/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 1ce63dfaead..06ad446fccb 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import date from typing import Callable from vehicle import Vehicle @@ -15,7 +16,6 @@ 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 homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -28,7 +28,7 @@ from .const import CONF_LICENSE_PLATE, DOMAIN, ENTRY_TYPE_SERVICE class RDWSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[Vehicle], str | float | None] + value_fn: Callable[[Vehicle], date | str | float | None] @dataclass @@ -43,13 +43,13 @@ SENSORS: tuple[RDWSensorEntityDescription, ...] = ( key="apk_expiration", name="APK Expiration", device_class=DEVICE_CLASS_DATE, - value_fn=lambda vehicle: vehicle.apk_expiration.isoformat(), + value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", name="Ascription Date", device_class=DEVICE_CLASS_DATE, - value_fn=lambda vehicle: vehicle.ascription_date.isoformat(), + value_fn=lambda vehicle: vehicle.ascription_date, ), ) @@ -98,6 +98,6 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): ) @property - def native_value(self) -> StateType: + def native_value(self) -> date | str | float | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) From 1ecd9c94590b361a0eccb6ad5849c012639f4e72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Nov 2021 22:42:21 +0100 Subject: [PATCH 0607/1452] Use native datetime value in OctoPrint sensors (#59927) --- homeassistant/components/octoprint/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 2a3e8c773ff..68313a16bd3 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -1,7 +1,7 @@ """Support for monitoring OctoPrint sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from pyoctoprintapi import OctoprintJobInfo, OctoprintPrinterInfo @@ -159,7 +159,7 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): super().__init__(coordinator, "Estimated Finish Time", device_id) @property - def native_value(self): + def native_value(self) -> datetime | None: """Return sensor state.""" job: OctoprintJobInfo = self.coordinator.data["job"] if ( @@ -171,7 +171,7 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): read_time = self.coordinator.data["last_read_time"] - return (read_time + timedelta(seconds=job.progress.print_time_left)).isoformat() + return read_time + timedelta(seconds=job.progress.print_time_left) class OctoPrintStartTimeSensor(OctoPrintSensorBase): @@ -186,7 +186,7 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): super().__init__(coordinator, "Start Time", device_id) @property - def native_value(self): + def native_value(self) -> datetime | None: """Return sensor state.""" job: OctoprintJobInfo = self.coordinator.data["job"] @@ -199,7 +199,7 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): read_time = self.coordinator.data["last_read_time"] - return (read_time - timedelta(seconds=job.progress.print_time)).isoformat() + return read_time - timedelta(seconds=job.progress.print_time) class OctoPrintTemperatureSensor(OctoPrintSensorBase): From 05eb2f3e5c63eca56f82721782261ca25a37663e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Nov 2021 22:52:57 +0100 Subject: [PATCH 0608/1452] Yeelight add state check to set_mode service (#59884) --- homeassistant/components/yeelight/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index e71b75755c0..ca2752a7a7d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -842,6 +842,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_mode(self, mode: str): """Set a power mode.""" await self._bulb.async_set_power_mode(PowerMode[mode.upper()]) + self._async_schedule_state_check(True) @_async_cmd async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER): From 958c199d8011cf3db86df9f5a2336fc256c5458d Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 18 Nov 2021 23:00:42 +0100 Subject: [PATCH 0609/1452] Brunt package update with async, data update coordinator and config flow (#49714) * implemented config_flow and dataupdatecoordinator * implemented config flow, dataupdatecoordinator and tests. * undid extra vscode task * fixed pylint errors * updates based on review * fix mypy in reauth * fast interval to 5 sec * fixed test patches and others from review * added released package * deleted wrong line from coveragerc * updates to config and tests * fixed test patch --- .coveragerc | 2 + homeassistant/components/brunt/__init__.py | 77 ++++++ homeassistant/components/brunt/config_flow.py | 119 +++++++++ homeassistant/components/brunt/const.py | 17 ++ homeassistant/components/brunt/cover.py | 231 ++++++++++++------ homeassistant/components/brunt/manifest.json | 3 +- homeassistant/components/brunt/strings.json | 29 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/brunt/__init__.py | 1 + tests/components/brunt/test_config_flow.py | 180 ++++++++++++++ 12 files changed, 586 insertions(+), 79 deletions(-) create mode 100644 homeassistant/components/brunt/config_flow.py create mode 100644 homeassistant/components/brunt/const.py create mode 100644 homeassistant/components/brunt/strings.json create mode 100644 tests/components/brunt/__init__.py create mode 100644 tests/components/brunt/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2b247c2c923..1adede05ca4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -138,7 +138,9 @@ omit = homeassistant/components/broadlink/updater.py homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* + homeassistant/components/brunt/__init__.py homeassistant/components/brunt/cover.py + homeassistant/components/brunt/const.py homeassistant/components/bsblan/climate.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index f89d57cdec1..37c9fd73632 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -1 +1,78 @@ """The brunt component.""" +from __future__ import annotations + +import logging + +from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError +import async_timeout +from brunt import BruntClientAsync + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Brunt using config flow.""" + session = async_get_clientsession(hass) + bapi = BruntClientAsync( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + session=session, + ) + try: + await bapi.async_login() + except ServerDisconnectedError as exc: + raise ConfigEntryNotReady("Brunt not ready to connect.") from exc + except ClientResponseError as exc: + raise ConfigEntryAuthFailed( + f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}." + ) from exc + + async def async_update_data(): + """Fetch data from the Brunt endpoint for all Things. + + Error 403 is the API response for any kind of authentication error (failed password or email) + Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account. + """ + try: + async with async_timeout.timeout(10): + things = await bapi.async_get_things(force=True) + return {thing.SERIAL: thing for thing in things} + except ServerDisconnectedError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + except ClientResponseError as err: + if err.status == 403: + raise ConfigEntryAuthFailed() from err + if err.status == 401: + _LOGGER.warning("Device not found, will reload Brunt integration") + await hass.config_entries.async_reload(entry.entry_id) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="brunt", + update_method=async_update_data, + update_interval=REGULAR_INTERVAL, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: 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 diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py new file mode 100644 index 00000000000..636a9affddd --- /dev/null +++ b/homeassistant/components/brunt/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for brunt integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import ClientResponseError +from aiohttp.client_exceptions import ServerDisconnectedError +from brunt import BruntClientAsync +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +async def validate_input(user_input: dict[str, Any]) -> dict[str, str] | None: + """Login to the brunt api and return errors if any.""" + errors = None + bapi = BruntClientAsync( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + try: + await bapi.async_login() + except ClientResponseError as exc: + if exc.status == 403: + _LOGGER.warning("Brunt Credentials are incorrect") + errors = {"base": "invalid_auth"} + else: + _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + errors = {"base": "unknown"} + except ServerDisconnectedError: + _LOGGER.warning("Cannot connect to Brunt") + errors = {"base": "cannot_connect"} + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + errors = {"base": "unknown"} + finally: + await bapi.async_close() + return errors + + +class BruntConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Brunt.""" + + VERSION = 1 + + _reauth_entry: ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = await validate_input(user_input) + if errors is not None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + ) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + assert self._reauth_entry + username = self._reauth_entry.data[CONF_USERNAME] + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + description_placeholders={"username": username}, + ) + user_input[CONF_USERNAME] = username + errors = await validate_input(user_input) + if errors is not None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + errors=errors, + description_placeholders={"username": username}, + ) + + self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import config from configuration.yaml.""" + await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + return await self.async_step_user(import_config) diff --git a/homeassistant/components/brunt/const.py b/homeassistant/components/brunt/const.py new file mode 100644 index 00000000000..4ffaf2875c9 --- /dev/null +++ b/homeassistant/components/brunt/const.py @@ -0,0 +1,17 @@ +"""Constants for Brunt.""" +from datetime import timedelta + +DOMAIN = "brunt" +ATTR_REQUEST_POSITION = "request_position" +NOTIFICATION_ID = "brunt_notification" +NOTIFICATION_TITLE = "Brunt Cover Setup" +ATTRIBUTION = "Based on an unofficial Brunt SDK." +PLATFORMS = ["cover"] +DATA_BAPI = "bapi" +DATA_COOR = "coordinator" + +CLOSED_POSITION = 0 +OPEN_POSITION = 100 + +REGULAR_INTERVAL = timedelta(seconds=20) +FAST_INTERVAL = timedelta(seconds=5) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 650ce9c05c6..cc0ecd0feab 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,84 +1,134 @@ """Support for Brunt Blind Engine covers.""" from __future__ import annotations +from collections.abc import MutableMapping import logging +from typing import Any -from brunt import BruntAPI -import voluptuous as vol +from aiohttp.client_exceptions import ClientResponseError +from brunt import BruntClientAsync, Thing from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_WINDOW, - PLATFORM_SCHEMA, + DEVICE_CLASS_SHADE, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, CoverEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + ATTR_REQUEST_POSITION, + ATTRIBUTION, + CLOSED_POSITION, + DATA_BAPI, + DATA_COOR, + DOMAIN, + FAST_INTERVAL, + OPEN_POSITION, + REGULAR_INTERVAL, +) _LOGGER = logging.getLogger(__name__) COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -ATTR_REQUEST_POSITION = "request_position" -NOTIFICATION_ID = "brunt_notification" -NOTIFICATION_TITLE = "Brunt Cover Setup" -ATTRIBUTION = "Based on an unofficial Brunt SDK." -CLOSED_POSITION = 0 -OPEN_POSITION = 100 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the brunt platform.""" - - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - bapi = BruntAPI(username=username, password=password) - try: - if not (things := bapi.getThings()["things"]): - _LOGGER.error("No things present in account") - else: - add_entities( - [ - BruntDevice(bapi, thing["NAME"], thing["thingUri"]) - for thing in things - ], - True, - ) - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - hass.components.persistent_notification.create( - "Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Component setup, run import config flow for each entry in config.""" + _LOGGER.warning( + "Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) + ) -class BruntDevice(CoverEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the brunt platform.""" + bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI] + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COOR] + + async_add_entities( + BruntDevice(coordinator, serial, thing, bapi, entry.entry_id) + for serial, thing in coordinator.data.items() + ) + + +class BruntDevice(CoordinatorEntity, CoverEntity): """ Representation of a Brunt cover device. Contains the common logic for all Brunt devices. """ - _attr_device_class = DEVICE_CLASS_WINDOW - _attr_supported_features = COVER_FEATURES - - def __init__(self, bapi, name, thing_uri): + def __init__( + self, + coordinator: DataUpdateCoordinator, + serial: str, + thing: Thing, + bapi: BruntClientAsync, + entry_id: str, + ) -> None: """Init the Brunt device.""" + super().__init__(coordinator) + self._attr_unique_id = serial self._bapi = bapi - self._attr_name = name - self._thing_uri = thing_uri + self._thing = thing + self._entry_id = entry_id - self._state = {} + self._remove_update_listener = None + + self._attr_name = self._thing.NAME + self._attr_device_class = DEVICE_CLASS_SHADE + self._attr_supported_features = COVER_FEATURES + self._attr_attribution = ATTRIBUTION + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + name=self._attr_name, + via_device=(DOMAIN, self._entry_id), + manufacturer="Brunt", + sw_version=self._thing.FW_VERSION, + model=self._thing.MODEL, + ) + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.async_add_listener(self._brunt_update_listener) + ) + + @property + def current_cover_position(self) -> int | None: + """ + Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + pos = self.coordinator.data[self.unique_id].currentPosition + return int(pos) if pos is not None else None @property def request_cover_position(self) -> int | None: @@ -89,8 +139,8 @@ class BruntDevice(CoverEntity): to Brunt, at times there is a diff of 1 to current None is unknown, 0 is closed, 100 is fully open. """ - pos = self._state.get("requestPosition") - return int(pos) if pos else None + pos = self.coordinator.data[self.unique_id].requestPosition + return int(pos) if pos is not None else None @property def move_state(self) -> int | None: @@ -99,37 +149,64 @@ class BruntDevice(CoverEntity): None is unknown, 0 when stopped, 1 when opening, 2 when closing """ - mov = self._state.get("moveState") - return int(mov) if mov else None + mov = self.coordinator.data[self.unique_id].moveState + return int(mov) if mov is not None else None - def update(self): - """Poll the current state of the device.""" - try: - self._state = self._bapi.getState(thingUri=self._thing_uri).get("thing") - self._attr_available = True - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - self._attr_available = False - self._attr_is_opening = self.move_state == 1 - self._attr_is_closing = self.move_state == 2 - pos = self._state.get("currentPosition") - self._attr_current_cover_position = int(pos) if pos else None - self._attr_is_closed = self.current_cover_position == CLOSED_POSITION - self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, + @property + def is_opening(self) -> bool: + """Return if the cover is opening or not.""" + return self.move_state == 1 + + @property + def is_closing(self) -> bool: + """Return if the cover is closing or not.""" + return self.move_state == 2 + + @property + def extra_state_attributes(self) -> MutableMapping[str, Any]: + """Return the detailed device state attributes.""" + return { ATTR_REQUEST_POSITION: self.request_cover_position, } - def open_cover(self, **kwargs): + @property + def is_closed(self) -> bool: + """Return true if cover is closed, else False.""" + return self.current_cover_position == CLOSED_POSITION + + async def async_open_cover(self, **kwargs: Any) -> None: """Set the cover to the open position.""" - self._bapi.changeRequestPosition(OPEN_POSITION, thingUri=self._thing_uri) + await self._async_update_cover(OPEN_POSITION) - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Set the cover to the closed position.""" - self._bapi.changeRequestPosition(CLOSED_POSITION, thingUri=self._thing_uri) + await self._async_update_cover(CLOSED_POSITION) - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover to a specific position.""" - self._bapi.changeRequestPosition( - kwargs[ATTR_POSITION], thingUri=self._thing_uri - ) + await self._async_update_cover(int(kwargs[ATTR_POSITION])) + + async def _async_update_cover(self, position: int) -> None: + """Set the cover to the new position and wait for the update to be reflected.""" + try: + await self._bapi.async_change_request_position( + position, thingUri=self._thing.thingUri + ) + except ClientResponseError as exc: + raise HomeAssistantError( + f"Unable to reposition {self._thing.NAME}" + ) from exc + self.coordinator.update_interval = FAST_INTERVAL + await self.coordinator.async_request_refresh() + + @callback + def _brunt_update_listener(self) -> None: + """Update the update interval after each refresh.""" + if ( + self.request_cover_position + == self._bapi.last_requested_positions[self._thing.thingUri] + and self.move_state == 0 + ): + self.coordinator.update_interval = REGULAR_INTERVAL + else: + self.coordinator.update_interval = FAST_INTERVAL diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index ba7d1ba117d..976b017ca09 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -1,8 +1,9 @@ { "domain": "brunt", "name": "Brunt Blind Engine", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==0.1.3"], + "requirements": ["brunt==1.0.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/brunt/strings.json b/homeassistant/components/brunt/strings.json new file mode 100644 index 00000000000..37b2f95bc08 --- /dev/null +++ b/homeassistant/components/brunt/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup your Brunt integration", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please reenter the password for: {username}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } + } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b620366a31b..5d865465e61 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -43,6 +43,7 @@ FLOWS = [ "braviatv", "broadlink", "brother", + "brunt", "bsblan", "buienradar", "canary", diff --git a/requirements_all.txt b/requirements_all.txt index fdc4013ea50..7b060b33723 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==0.1.3 +brunt==1.0.0 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8567435edd..d399d56e424 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -277,6 +277,9 @@ broadlink==0.18.0 # homeassistant.components.brother brother==1.1.0 +# homeassistant.components.brunt +brunt==1.0.0 + # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/tests/components/brunt/__init__.py b/tests/components/brunt/__init__.py new file mode 100644 index 00000000000..15060cbaf4c --- /dev/null +++ b/tests/components/brunt/__init__.py @@ -0,0 +1 @@ +"""Brunt tests.""" diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py new file mode 100644 index 00000000000..f053a6d18b0 --- /dev/null +++ b/tests/components/brunt/test_config_flow.py @@ -0,0 +1,180 @@ +"""Test the Brunt config flow.""" +from unittest.mock import Mock, patch + +from aiohttp import ClientResponseError +from aiohttp.client_exceptions import ServerDisconnectedError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.brunt.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +CONFIG = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} + + +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}, data=None + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + return_value=None, + ), patch( + "homeassistant.components.brunt.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass): + """Test we get the form.""" + + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + return_value=None, + ), patch( + "homeassistant.components.brunt.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 + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_duplicate_login(hass): + """Test uniqueness of username.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + title="test-username", + unique_id="test-username", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + return_value=None, + ), patch( + "homeassistant.components.brunt.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form_duplicate_login(hass): + """Test uniqueness of username.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + title="test-username", + unique_id="test-username", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + return_value=None, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "side_effect, error_message", + [ + (ServerDisconnectedError, "cannot_connect"), + (ClientResponseError(Mock(), None, status=403), "invalid_auth"), + (ClientResponseError(Mock(), None, status=401), "unknown"), + (Exception, "unknown"), + ], +) +async def test_form_error(hass, side_effect, error_message): + """Test we handle cannot connect.""" + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": error_message} + + +@pytest.mark.parametrize( + "side_effect, result_type, password, step_id, reason", + [ + (None, data_entry_flow.RESULT_TYPE_ABORT, "test", None, "reauth_successful"), + ( + Exception, + data_entry_flow.RESULT_TYPE_FORM, + CONFIG[CONF_PASSWORD], + "reauth_confirm", + None, + ), + ], +) +async def test_reauth(hass, side_effect, result_type, password, step_id, reason): + """Test uniqueness of username.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + title="test-username", + unique_id="test-username", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=None, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + with patch( + "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", + return_value=None, + side_effect=side_effect, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"password": "test"}, + ) + assert result3["type"] == result_type + assert entry.data["password"] == password + assert result3.get("step_id", None) == step_id + assert result3.get("reason", None) == reason From a3139595bba2c756ad3c53bbd8cb56ddebe5b16f Mon Sep 17 00:00:00 2001 From: Paul Frank Date: Thu, 18 Nov 2021 23:04:21 +0100 Subject: [PATCH 0610/1452] Remove zwave_js cover workaround (#59939) --- homeassistant/components/zwave_js/cover.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 1dee8b0bad3..c94c54e1948 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,7 +1,6 @@ """Support for Z-Wave cover devices.""" from __future__ import annotations -import asyncio import logging from typing import Any, cast @@ -222,12 +221,6 @@ class ZWaveTiltCover(ZWaveCover): tilt_value, percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]), ) - # The following 2 lines are a workaround for this issue: - # https://github.com/zwave-js/node-zwave-js/issues/3611 - # As soon as the issue is fixed, and minimum server schema is bumped - # the 2 lines should be removed. - await asyncio.sleep(2.5) - await self.info.node.async_refresh_cc_values(tilt_value.command_class) async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" From 8fb84270d5d2f30aa1e27ebbffad79b2d637f40a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Nov 2021 00:21:27 +0100 Subject: [PATCH 0611/1452] Fix Tuya back scaling in Climate and Humidifer entities (#59909) --- homeassistant/components/tuya/climate.py | 6 ++++-- homeassistant/components/tuya/humidifier.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index b26ff34bc6d..cb70fc5515a 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -322,7 +322,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): [ { "code": self._set_humidity_dpcode, - "value": self._set_humidity_type.scale_value(humidity), + "value": self._set_humidity_type.scale_value_back(humidity), } ] ) @@ -364,7 +364,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): { "code": self._set_temperature_dpcode, "value": round( - self._set_temperature_type.scale_value(kwargs["temperature"]) + self._set_temperature_type.scale_value_back( + kwargs["temperature"] + ) ), } ] diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index b9fc10790e3..3169000fcba 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -165,7 +165,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): [ { "code": self.entity_description.humidity, - "value": self._set_humidity_type.scale_value(humidity), + "value": self._set_humidity_type.scale_value_back(humidity), } ] ) From 0fb21af07ff89d7793f3f89c09cd1c8d3ae28b24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Nov 2021 17:21:51 -0600 Subject: [PATCH 0612/1452] Strip out deleted entities when configuring homekit (#59844) --- .../components/homekit/config_flow.py | 29 +++++--- tests/components/homekit/test_config_flow.py | 73 ++++++++++++++++++- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index a79db949ab0..79a0e71f969 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -330,13 +330,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=self.hk_options) all_supported_devices = await _async_get_supported_devices(self.hass) + # Strip out devices that no longer exist to prevent error in the UI + devices = [ + device_id + for device_id in self.hk_options.get(CONF_DEVICES, []) + if device_id in all_supported_devices + ] return self.async_show_form( step_id="advanced", data_schema=vol.Schema( { - vol.Optional( - CONF_DEVICES, default=self.hk_options.get(CONF_DEVICES, []) - ): cv.multi_select(all_supported_devices) + vol.Optional(CONF_DEVICES, default=devices): cv.multi_select( + all_supported_devices + ) } ), ) @@ -445,13 +451,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) data_schema = {} - entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: - entity_schema = vol.In - else: - if entities: - include_exclude_mode = MODE_INCLUDE - else: + entity_schema = vol.In + # Strip out entities that no longer exist to prevent error in the UI + entities = [ + entity_id + for entity_id in entity_filter.get(CONF_INCLUDE_ENTITIES, []) + if entity_id in all_supported_entities + ] + if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: + include_exclude_mode = MODE_INCLUDE + if not entities: include_exclude_mode = MODE_EXCLUDE entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) data_schema[ diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index e5d395a1f29..f1564f9e3ae 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -365,7 +365,24 @@ async def test_options_flow_devices( mock_hap, hass, demo_cleanup, device_reg, entity_reg, mock_get_source_ip ): """Test devices can be bridged.""" - config_entry = _mock_config_entry_with_options_populated() + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "devices": ["notexist"], + "filter": { + "include_domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + }, + ) config_entry.add_to_hass(hass) demo_config_entry = MockConfigEntry(domain="domain") @@ -491,6 +508,60 @@ async def test_options_flow_devices_preserved_when_advanced_off( } +async def test_options_flow_with_non_existant_entity(hass, mock_get_source_ip): + """Test config flow options in include mode.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "filter": { + "include_entities": ["climate.not_exist", "climate.front_gate"], + }, + }, + ) + config_entry.add_to_hass(hass) + hass.states.async_set("climate.front_gate", "off") + hass.states.async_set("climate.new", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + + entities = result["data_schema"]({})["entities"] + assert "climate.not_exist" not in entities + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.new", "climate.front_gate"], + "include_exclude_mode": "include", + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["fan", "vacuum"], + "include_entities": ["climate.new", "climate.front_gate"], + }, + } + + async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): """Test config flow options in include mode.""" From 442597928ea70ec60f6ca95c3b065d25675d898c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Nov 2021 15:56:22 -0800 Subject: [PATCH 0613/1452] Store: copy pending data (#59934) --- homeassistant/helpers/storage.py | 5 +++ .../fixtures/automation_saved_traces.json | 1 + .../trace/fixtures/script_saved_traces.json | 1 + tests/components/unifi/test_controller.py | 2 +- tests/components/unifi/test_device_tracker.py | 2 +- tests/components/unifi/test_init.py | 3 +- tests/components/unifi/test_switch.py | 5 +-- tests/helpers/test_storage.py | 34 +++++++++++++++---- 8 files changed, 42 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index ba276969299..87d8044c044 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from contextlib import suppress +from copy import deepcopy import inspect from json import JSONEncoder import logging @@ -133,6 +134,10 @@ class Store: # If we didn't generate data yet, do it now. if "data_func" in data: data["data"] = data.pop("data_func")() + + # We make a copy because code might assume it's safe to mutate loaded data + # and we don't want that to mess with what we're trying to store. + data = deepcopy(data) else: data = await self.hass.async_add_executor_job( json_util.load_json, self.path diff --git a/tests/components/trace/fixtures/automation_saved_traces.json b/tests/components/trace/fixtures/automation_saved_traces.json index 45bcfffc157..7f6ed56a8bc 100644 --- a/tests/components/trace/fixtures/automation_saved_traces.json +++ b/tests/components/trace/fixtures/automation_saved_traces.json @@ -1,5 +1,6 @@ { "version": 1, + "minor_version": 1, "key": "trace.saved_traces", "data": { "automation.sun": [ diff --git a/tests/components/trace/fixtures/script_saved_traces.json b/tests/components/trace/fixtures/script_saved_traces.json index 91677b2a47e..ccd2902d726 100644 --- a/tests/components/trace/fixtures/script_saved_traces.json +++ b/tests/components/trace/fixtures/script_saved_traces.json @@ -1,5 +1,6 @@ { "version": 1, + "minor_version": 1, "key": "trace.saved_traces", "data": { "script.sun": [ diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 41864f5dc3e..0f3447b3dd9 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -49,7 +49,7 @@ import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed -DEFAULT_CONFIG_ENTRY_ID = 1 +DEFAULT_CONFIG_ENTRY_ID = "1" DEFAULT_HOST = "1.2.3.4" DEFAULT_SITE = "site_id" diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 5ab61ce21a7..384db693f1c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1089,7 +1089,7 @@ async def test_restoring_client(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id="1", ) registry = er.async_get(hass) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 1d1fcad9cbe..85733d6d686 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -14,7 +14,7 @@ from .test_controller import ( setup_unifi_integration, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, flush_store async def test_setup_with_no_config(hass): @@ -110,6 +110,7 @@ async def test_wireless_clients(hass, hass_storage, aioclient_mock): config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client_1, client_2] ) + await flush_store(hass.data[unifi.UNIFI_WIRELESS_CLIENTS]._store) for mac in [ "00:00:00:00:00:00", diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 85850062583..796434c5cd9 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .test_controller import ( CONTROLLER_HOST, + DEFAULT_CONFIG_ENTRY_ID, DESCRIPTION, ENTRY_CONFIG, setup_unifi_integration, @@ -857,7 +858,7 @@ async def test_restore_client_succeed(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id=DEFAULT_CONFIG_ENTRY_ID, ) registry = er.async_get(hass) @@ -947,7 +948,7 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id=DEFAULT_CONFIG_ENTRY_ID, ) registry = er.async_get(hass) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index d6a78340127..0478c17e299 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -28,13 +28,13 @@ MOCK_DATA2 = {"goodbye": "cruel world"} @pytest.fixture def store(hass): """Fixture of a store that prevents writing on Home Assistant stop.""" - yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) + return storage.Store(hass, MOCK_VERSION, MOCK_KEY) @pytest.fixture def store_v_1_1(hass): """Fixture of a store that prevents writing on Home Assistant stop.""" - yield storage.Store( + return storage.Store( hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -42,7 +42,7 @@ def store_v_1_1(hass): @pytest.fixture def store_v_1_2(hass): """Fixture of a store that prevents writing on Home Assistant stop.""" - yield storage.Store( + return storage.Store( hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2 ) @@ -50,7 +50,7 @@ def store_v_1_2(hass): @pytest.fixture def store_v_2_1(hass): """Fixture of a store that prevents writing on Home Assistant stop.""" - yield storage.Store( + return storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -91,8 +91,8 @@ async def test_loading_parallel(hass, store, hass_storage, caplog): results = await asyncio.gather(store.async_load(), store.async_load()) - assert results[0] is MOCK_DATA - assert results[1] is MOCK_DATA + assert results[0] == MOCK_DATA + assert results[0] is results[1] assert caplog.text.count(f"Loading data for {store.key}") @@ -436,3 +436,25 @@ async def test_legacy_migration(hass, hass_storage, store_v_1_2): "minor_version": 1, "data": MOCK_DATA, } + + +async def test_changing_delayed_written_data(hass, store, hass_storage): + """Test changing data that is written with delay.""" + data_to_store = {"hello": "world"} + store.async_delay_save(lambda: data_to_store, 1) + assert store.key not in hass_storage + + loaded_data = await store.async_load() + assert loaded_data == data_to_store + assert loaded_data is not data_to_store + + loaded_data["hello"] = "earth" + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert hass_storage[store.key] == { + "version": MOCK_VERSION, + "minor_version": 1, + "key": MOCK_KEY, + "data": {"hello": "world"}, + } From 29dc9de08fcd5a7290b65fa0e1a415691610d63e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 19 Nov 2021 00:14:01 +0000 Subject: [PATCH 0614/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 6 ++++ .../components/adax/translations/ja.json | 3 +- .../components/adguard/translations/ja.json | 1 + .../components/agent_dvr/translations/ja.json | 3 +- .../ambient_station/translations/ja.json | 15 ++++++++ .../components/arcam_fmj/translations/ja.json | 1 + .../components/asuswrt/translations/ja.json | 7 +++- .../components/atag/translations/ja.json | 3 +- .../components/august/translations/ja.json | 9 ++++- .../components/auth/translations/ja.json | 3 ++ .../components/axis/translations/ja.json | 5 ++- .../components/blebox/translations/ja.json | 4 +++ .../components/blink/translations/ja.json | 5 +++ .../bmw_connected_drive/translations/ja.json | 11 ++++++ .../components/bond/translations/ja.json | 1 + .../components/bosch_shc/translations/ja.json | 3 ++ .../components/braviatv/translations/ja.json | 6 ++++ .../components/broadlink/translations/ja.json | 1 + .../components/brother/translations/ja.json | 8 +++++ .../components/brunt/translations/en.json | 29 ++++++++++++++++ .../components/brunt/translations/id.json | 29 ++++++++++++++++ .../components/bsblan/translations/ja.json | 5 +++ .../components/canary/translations/ja.json | 11 ++++++ .../cert_expiry/translations/ja.json | 8 +++++ .../components/control4/translations/ja.json | 3 ++ .../components/deconz/translations/ja.json | 19 +++++++++-- .../demo/translations/select.id.json | 4 ++- .../devolo_home_control/translations/ja.json | 7 ++++ .../components/dexcom/translations/ja.json | 9 +++++ .../components/directv/translations/ja.json | 11 ++++++ .../components/dlna_dmr/translations/id.json | 9 +++++ .../components/doorbird/translations/ja.json | 15 ++++++++ .../components/dsmr/translations/ja.json | 6 +++- .../components/ecobee/translations/ja.json | 4 +++ .../components/econet/translations/ja.json | 11 ++++++ .../components/elgato/translations/ja.json | 4 +++ .../components/elkm1/translations/ja.json | 1 + .../emulated_roku/translations/id.json | 1 + .../enphase_envoy/translations/ja.json | 3 +- .../environment_canada/translations/id.json | 1 + .../components/esphome/translations/id.json | 3 +- .../components/esphome/translations/ja.json | 6 +++- .../components/ezviz/translations/ja.json | 21 ++++++++++++ .../faa_delays/translations/ja.json | 18 ++++++++++ .../components/fan/translations/ja.json | 6 ++++ .../fireservicerota/translations/ja.json | 8 +++++ .../components/flipr/translations/id.json | 11 ++++-- .../components/flipr/translations/ja.json | 3 ++ .../components/flo/translations/ja.json | 3 +- .../components/flume/translations/id.json | 4 ++- .../components/flume/translations/ja.json | 17 ++++++++++ .../components/flux_led/translations/id.json | 17 ++++++++++ .../forecast_solar/translations/id.json | 20 ++++++++++- .../forked_daapd/translations/ja.json | 9 ++++- .../components/foscam/translations/ja.json | 3 ++ .../components/freebox/translations/ja.json | 3 +- .../freedompro/translations/id.json | 4 ++- .../components/fritz/translations/id.json | 23 +++++++++++-- .../components/fritz/translations/ja.json | 30 +++++++++++++++- .../components/fritzbox/translations/ja.json | 11 ++++++ .../fritzbox_callmonitor/translations/ja.json | 10 +++++- .../garages_amsterdam/translations/id.json | 11 +++++- .../geonetnz_volcano/translations/ja.json | 12 +++++++ .../components/glances/translations/ja.json | 25 ++++++++++++++ .../components/goalzero/translations/id.json | 4 +++ .../components/goalzero/translations/ja.json | 3 +- .../components/gogogate2/translations/ja.json | 11 ++++++ .../growatt_server/translations/id.json | 15 ++++++-- .../growatt_server/translations/ja.json | 3 ++ .../components/guardian/translations/id.json | 3 ++ .../components/guardian/translations/ja.json | 11 ++++++ .../components/harmony/translations/ja.json | 3 ++ .../components/hassio/translations/ja.json | 3 +- .../components/heos/translations/ja.json | 3 +- .../hisense_aehw4a1/translations/ja.json | 9 +++++ .../components/hive/translations/ja.json | 9 +++++ .../components/hlk_sw16/translations/ja.json | 3 +- .../components/homekit/translations/ja.json | 3 +- .../homekit_controller/translations/ja.json | 16 +++++++++ .../homematicip_cloud/translations/ja.json | 6 ++-- .../components/honeywell/translations/id.json | 3 +- .../components/honeywell/translations/ja.json | 3 ++ .../huawei_lte/translations/id.json | 4 ++- .../huawei_lte/translations/ja.json | 17 ++++++++++ .../components/hue/translations/ja.json | 21 ++++++++++++ .../components/hue/translations/pl.json | 4 +++ .../huisbaasje/translations/ja.json | 11 ++++++ .../components/hyperion/translations/ja.json | 9 ++++- .../components/ialarm/translations/ja.json | 3 +- .../components/iaqualink/translations/ja.json | 13 +++++++ .../components/icloud/translations/ja.json | 6 ++++ .../components/insteon/translations/ja.json | 22 ++++++++++++ .../components/iotawatt/translations/id.json | 3 +- .../components/iotawatt/translations/ja.json | 3 +- .../components/ipma/translations/ja.json | 5 +++ .../components/ipp/translations/ja.json | 9 +++++ .../components/isy994/translations/id.json | 8 +++++ .../components/isy994/translations/ja.json | 4 ++- .../components/jellyfin/translations/ja.json | 11 ++++++ .../keenetic_ndms2/translations/id.json | 4 ++- .../keenetic_ndms2/translations/ja.json | 3 +- .../components/kmtronic/translations/ja.json | 3 +- .../components/kodi/translations/ja.json | 9 ++++- .../components/konnected/translations/ja.json | 13 ++++++- .../kostal_plenticore/translations/ja.json | 3 +- .../components/life360/translations/ja.json | 8 +++++ .../components/litejet/translations/ja.json | 13 +++++++ .../litterrobot/translations/ja.json | 11 ++++++ .../logi_circle/translations/ja.json | 9 +++++ .../components/mazda/translations/ja.json | 12 +++++++ .../components/melcloud/translations/ja.json | 11 ++++++ .../components/mikrotik/translations/ja.json | 5 +++ .../components/mill/translations/ja.json | 11 ++++++ .../minecraft_server/translations/ja.json | 7 +++- .../modem_callerid/translations/ja.json | 3 ++ .../components/monoprice/translations/ja.json | 1 + .../moon/translations/sensor.ja.json | 1 + .../motion_blinds/translations/id.json | 7 ++++ .../components/motioneye/translations/id.json | 14 ++++++++ .../components/motioneye/translations/ja.json | 6 ++++ .../components/mqtt/translations/id.json | 9 +++-- .../components/mqtt/translations/ja.json | 7 ++++ .../components/myq/translations/id.json | 4 ++- .../components/myq/translations/ja.json | 17 ++++++++++ .../components/mysensors/translations/id.json | 1 + .../components/mysensors/translations/ja.json | 13 +++++-- .../components/nam/translations/ca.json | 21 ++++++++++-- .../components/nam/translations/de.json | 21 ++++++++++-- .../components/nam/translations/en.json | 21 ++++++++++-- .../components/nam/translations/et.json | 21 ++++++++++-- .../components/nam/translations/id.json | 24 +++++++++++-- .../components/nam/translations/ja.json | 18 +++++++++- .../components/nam/translations/no.json | 21 ++++++++++-- .../components/nam/translations/pl.json | 18 +++++++++- .../components/nam/translations/ru.json | 21 ++++++++++-- .../components/nam/translations/zh-Hant.json | 21 ++++++++++-- .../components/nanoleaf/translations/id.json | 4 +++ .../components/nanoleaf/translations/ja.json | 2 +- .../components/nest/translations/id.json | 4 ++- .../components/nexia/translations/ja.json | 3 +- .../nfandroidtv/translations/ja.json | 2 +- .../components/notion/translations/ja.json | 6 ++++ .../components/nuheat/translations/ja.json | 11 ++++++ .../components/nuki/translations/ja.json | 3 +- .../components/nut/translations/ja.json | 4 ++- .../components/nzbget/translations/ja.json | 4 ++- .../components/omnilogic/translations/ja.json | 9 +++++ .../components/onewire/translations/ja.json | 3 +- .../components/onvif/translations/ja.json | 16 +++++++-- .../opentherm_gw/translations/ja.json | 16 +++++++++ .../components/openuv/translations/id.json | 7 ++++ .../ovo_energy/translations/ja.json | 6 ++++ .../components/owntracks/translations/ja.json | 1 + .../p1_monitor/translations/id.json | 3 +- .../philips_js/translations/id.json | 9 +++++ .../components/pi_hole/translations/ja.json | 3 +- .../components/picnic/translations/ja.json | 11 ++++++ .../components/plaato/translations/ja.json | 1 + .../components/plex/translations/ja.json | 13 +++++++ .../components/plugwise/translations/ja.json | 12 +++++++ .../plum_lightpad/translations/ja.json | 11 ++++++ .../components/point/translations/ja.json | 10 +++++- .../components/poolsense/translations/ja.json | 11 ++++++ .../components/powerwall/translations/ja.json | 9 ++++- .../progettihwsw/translations/ja.json | 3 +- .../components/prosegur/translations/id.json | 1 + .../components/prosegur/translations/ja.json | 6 ++-- .../components/ps4/translations/ja.json | 1 + .../rainmachine/translations/ja.json | 4 ++- .../components/rfxtrx/translations/ja.json | 9 +++++ .../components/ring/translations/ja.json | 5 +++ .../components/risco/translations/ja.json | 11 ++++++ .../translations/ja.json | 11 ++++++ .../components/roku/translations/ja.json | 11 ++++++ .../components/roomba/translations/ja.json | 14 +++++++- .../ruckus_unleashed/translations/ja.json | 3 +- .../components/samsungtv/translations/ja.json | 7 ++++ .../screenlogic/translations/ja.json | 3 ++ .../components/sense/translations/ja.json | 1 + .../components/sharkiq/translations/ja.json | 16 +++++++++ .../components/shelly/translations/ja.json | 8 +++++ .../components/sia/translations/ja.json | 4 +++ .../simplisafe/translations/ja.json | 11 +++++- .../components/sma/translations/ja.json | 3 +- .../smart_meter_texas/translations/ja.json | 11 ++++++ .../components/smarthab/translations/ja.json | 11 ++++++ .../smartthings/translations/ja.json | 3 ++ .../components/smarttub/translations/ja.json | 4 +++ .../components/solaredge/translations/ja.json | 6 ++-- .../components/solarlog/translations/ja.json | 11 ++++++ .../components/soma/translations/ja.json | 8 +++++ .../somfy_mylink/translations/ja.json | 3 +- .../components/sonarr/translations/ja.json | 3 +- .../components/songpal/translations/ja.json | 10 ++++++ .../components/spider/translations/ja.json | 11 ++++++ .../squeezebox/translations/ja.json | 5 ++- .../srp_energy/translations/ja.json | 9 +++++ .../components/starline/translations/ja.json | 29 ++++++++++++++++ .../components/subaru/translations/ja.json | 11 ++++++ .../surepetcare/translations/ja.json | 11 ++++++ .../components/switchbot/translations/ja.json | 3 +- .../components/syncthru/translations/ja.json | 7 ++++ .../synology_dsm/translations/ja.json | 13 ++++++- .../system_bridge/translations/ja.json | 3 +- .../components/tile/translations/ja.json | 11 ++++++ .../components/tractive/translations/ja.json | 7 ++++ .../components/tradfri/translations/ja.json | 2 ++ .../transmission/translations/ja.json | 7 ++-- .../components/tuya/translations/ja.json | 1 + .../components/unifi/translations/ja.json | 5 +++ .../components/upb/translations/ja.json | 14 ++++++++ .../components/upcloud/translations/ja.json | 11 ++++++ .../components/velbus/translations/ja.json | 11 ++++++ .../components/verisure/translations/ja.json | 16 +++++++++ .../components/vesync/translations/ja.json | 3 ++ .../components/vilfo/translations/ja.json | 3 ++ .../components/vizio/translations/ja.json | 10 ++++++ .../components/volumio/translations/ja.json | 3 +- .../components/wallbox/translations/ja.json | 9 +++++ .../components/watttime/translations/ja.json | 6 ++++ .../components/wemo/translations/ja.json | 7 ++++ .../components/whirlpool/translations/ja.json | 11 ++++++ .../components/wiffi/translations/ja.json | 14 ++++++++ .../components/withings/translations/ja.json | 8 +++++ .../components/wled/translations/ja.json | 3 ++ .../components/wolflink/translations/ja.json | 11 ++++++ .../xiaomi_miio/translations/ja.json | 11 +++++- .../yale_smart_alarm/translations/ja.json | 8 ++++- .../components/yeelight/translations/ja.json | 3 ++ .../components/zha/translations/ja.json | 34 +++++++++++++++++++ .../zoneminder/translations/ja.json | 8 ++++- 231 files changed, 1863 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/ambient_station/translations/ja.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/ja.json create mode 100644 homeassistant/components/brunt/translations/en.json create mode 100644 homeassistant/components/brunt/translations/id.json create mode 100644 homeassistant/components/canary/translations/ja.json create mode 100644 homeassistant/components/directv/translations/ja.json create mode 100644 homeassistant/components/doorbird/translations/ja.json create mode 100644 homeassistant/components/econet/translations/ja.json create mode 100644 homeassistant/components/ezviz/translations/ja.json create mode 100644 homeassistant/components/faa_delays/translations/ja.json create mode 100644 homeassistant/components/flume/translations/ja.json create mode 100644 homeassistant/components/fritzbox/translations/ja.json create mode 100644 homeassistant/components/geonetnz_volcano/translations/ja.json create mode 100644 homeassistant/components/glances/translations/ja.json create mode 100644 homeassistant/components/gogogate2/translations/ja.json create mode 100644 homeassistant/components/guardian/translations/ja.json create mode 100644 homeassistant/components/hisense_aehw4a1/translations/ja.json create mode 100644 homeassistant/components/huisbaasje/translations/ja.json create mode 100644 homeassistant/components/iaqualink/translations/ja.json create mode 100644 homeassistant/components/jellyfin/translations/ja.json create mode 100644 homeassistant/components/litterrobot/translations/ja.json create mode 100644 homeassistant/components/mazda/translations/ja.json create mode 100644 homeassistant/components/melcloud/translations/ja.json create mode 100644 homeassistant/components/mill/translations/ja.json create mode 100644 homeassistant/components/myq/translations/ja.json create mode 100644 homeassistant/components/nuheat/translations/ja.json create mode 100644 homeassistant/components/opentherm_gw/translations/ja.json create mode 100644 homeassistant/components/picnic/translations/ja.json create mode 100644 homeassistant/components/plugwise/translations/ja.json create mode 100644 homeassistant/components/plum_lightpad/translations/ja.json create mode 100644 homeassistant/components/poolsense/translations/ja.json create mode 100644 homeassistant/components/risco/translations/ja.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/ja.json create mode 100644 homeassistant/components/roku/translations/ja.json create mode 100644 homeassistant/components/sharkiq/translations/ja.json create mode 100644 homeassistant/components/smart_meter_texas/translations/ja.json create mode 100644 homeassistant/components/smarthab/translations/ja.json create mode 100644 homeassistant/components/solarlog/translations/ja.json create mode 100644 homeassistant/components/songpal/translations/ja.json create mode 100644 homeassistant/components/spider/translations/ja.json create mode 100644 homeassistant/components/subaru/translations/ja.json create mode 100644 homeassistant/components/surepetcare/translations/ja.json create mode 100644 homeassistant/components/syncthru/translations/ja.json create mode 100644 homeassistant/components/tile/translations/ja.json create mode 100644 homeassistant/components/upb/translations/ja.json create mode 100644 homeassistant/components/upcloud/translations/ja.json create mode 100644 homeassistant/components/velbus/translations/ja.json create mode 100644 homeassistant/components/verisure/translations/ja.json create mode 100644 homeassistant/components/whirlpool/translations/ja.json create mode 100644 homeassistant/components/wiffi/translations/ja.json create mode 100644 homeassistant/components/wolflink/translations/ja.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index f10fb0798e5..439df99f9fc 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -11,9 +11,15 @@ "title": "Abode\u306eMFA\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json index 5233293be0a..1d914a84b18 100644 --- a/homeassistant/components/adax/translations/ja.json +++ b/homeassistant/components/adax/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "account_id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index 71898e93b82..5b8d5def9c4 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -7,6 +7,7 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8" }, "description": "\u76e3\u8996\u3068\u5236\u5fa1\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001AdGuardHome\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u3057\u307e\u3059\u3002" diff --git a/homeassistant/components/agent_dvr/translations/ja.json b/homeassistant/components/agent_dvr/translations/ja.json index 4b063c675b2..0ef445eda34 100644 --- a/homeassistant/components/agent_dvr/translations/ja.json +++ b/homeassistant/components/agent_dvr/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" }, "title": "Agent DVR\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/ambient_station/translations/ja.json b/homeassistant/components/ambient_station/translations/ja.json new file mode 100644 index 00000000000..f7cddd6c5a0 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "step": { + "user": { + "data": { + "app_key": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30ad\u30fc" + }, + "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ja.json b/homeassistant/components/arcam_fmj/translations/ja.json index 54801fefa0d..045f155378d 100644 --- a/homeassistant/components/arcam_fmj/translations/ja.json +++ b/homeassistant/components/arcam_fmj/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{host}", "step": { "user": { "data": { diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index a4f3beb4771..5ec55700a0e 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "pwd_and_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u306e\u307f\u63d0\u4f9b" + }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "ssh_key": "SSH\u30ad\u30fc \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9 (\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u4ee3\u308f\u308a)" }, "description": "\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059" } diff --git a/homeassistant/components/atag/translations/ja.json b/homeassistant/components/atag/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/atag/translations/ja.json +++ b/homeassistant/components/atag/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index 6944a72ef1d..86d32ce8aff 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -1,9 +1,16 @@ { "config": { "step": { + "reauth_validate": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user_validate": { "data": { - "login_method": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5" + "login_method": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u5b50\u30e1\u30fc\u30eb\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u8a71\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f \"+NNNNNNNNN\" \u5f62\u5f0f\u306e\u96fb\u8a71\u756a\u53f7\u3067\u3059\u3002" }, diff --git a/homeassistant/components/auth/translations/ja.json b/homeassistant/components/auth/translations/ja.json index ffeda27c9a5..182e56114d6 100644 --- a/homeassistant/components/auth/translations/ja.json +++ b/homeassistant/components/auth/translations/ja.json @@ -20,6 +20,9 @@ "title": "\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u901a\u77e5" }, "totp": { + "error": { + "invalid_code": "\u7121\u52b9\u306a\u30b3\u30fc\u30c9\u3067\u3059\u3001\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u5e38\u306b\u767a\u751f\u3059\u308b\u5834\u5408\u306f\u3001Home Assistant\u306e\u30b7\u30b9\u30c6\u30e0\u6642\u8a08\u304c\u6b63\u78ba\u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "init": { "description": "\u30bf\u30a4\u30e0\u30d9\u30fc\u30b9\u306e\u30ef\u30f3\u30bf\u30a4\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u4f7f\u7528\u3057\u30662\u8981\u7d20\u8a8d\u8a3c\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u3001\u8a8d\u8a3c\u30a2\u30d7\u30ea\u3067QR\u30b3\u30fc\u30c9\u3092\u30b9\u30ad\u30e3\u30f3\u3057\u307e\u3059\u3002\u8a8d\u8a3c\u30a2\u30d7\u30ea\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001[Google \u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0](https://support.google.com/accounts/answer/1066447)\u307e\u305f\u306f\u3001[Authy](https://authy.com/)\u306e\u3069\u3061\u3089\u304b\u3092\u63a8\u5968\u3057\u307e\u3059\u3002\n\n{qr_code}\n\n\u30b3\u30fc\u30c9\u3092\u30b9\u30ad\u30e3\u30f3\u3057\u305f\u5f8c\u3001\u30a2\u30d7\u30ea\u304b\u30896\u6841\u306e\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002QR\u30b3\u30fc\u30c9\u306e\u30b9\u30ad\u30e3\u30f3\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30b3\u30fc\u30c9 **`{code}`** \u3092\u4f7f\u7528\u3057\u3066\u624b\u52d5\u3067\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index 15a22f76432..b95bf43490b 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -3,10 +3,13 @@ "abort": { "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" }, "title": "Axis\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index ab0faf90da9..5ccec9230d6 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "flow_title": "{name} ({host})", "step": { "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + }, "description": "BleBox\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", "title": "BleBox\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index dc544bbcb75..c9bb1f3a111 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -6,6 +6,11 @@ "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, "title": "2\u8981\u7d20\u8a8d\u8a3c" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json index a42202307f2..cf7f15de753 100644 --- a/homeassistant/components/bond/translations/ja.json +++ b/homeassistant/components/bond/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 88c11c531b4..c1bfa7efbf5 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Bosch Smart Home Controller\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b(LED\u304c\u70b9\u6ec5)\u3053\u3068\u3068\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u3044\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "flow_title": "Bosch SHC: {name}", "step": { "credentials": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 4c5aa54784d..116a3129a86 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "no_ip_control": "\u30c6\u30ec\u30d3\u3067IP\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30c6\u30ec\u30d3\u304c\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002" + }, + "error": { + "unsupported_model": "\u304a\u4f7f\u3044\u306e\u30c6\u30ec\u30d3\u306e\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + }, "step": { "authorize": { "description": "\u30bd\u30cb\u30fc\u88fd\u306e\u30c6\u30ec\u30d3 \u30d6\u30e9\u30d3\u30a2\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index e7a65b8d84b..299cb227fe9 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({model} at {host})", "step": { "auth": { "title": "\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u8a8d\u8a3c" diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index 45aafc0b296..487c2bbf2f6 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "unsupported_model": "\u3053\u306e\u30d7\u30ea\u30f3\u30bf\u30fc\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + }, "error": { + "snmp_error": "SNMP\u30b5\u30fc\u30d0\u30fc\u304c\u30aa\u30d5\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" + }, "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/brunt/translations/en.json b/homeassistant/components/brunt/translations/en.json new file mode 100644 index 00000000000..82fdda36822 --- /dev/null +++ b/homeassistant/components/brunt/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please reenter the password for: {username}", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Setup your Brunt integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/id.json b/homeassistant/components/brunt/translations/id.json new file mode 100644 index 00000000000..21b4d381ed5 --- /dev/null +++ b/homeassistant/components/brunt/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Masukkan kembali kata sandi untuk: {username}", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Siapkan integrasi Brunt Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index cd2ddf38c39..427d83bfb94 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -2,6 +2,11 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, "description": "BSB-Lan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/canary/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/ja.json b/homeassistant/components/cert_expiry/translations/ja.json index f10ff948420..864b632d81f 100644 --- a/homeassistant/components/cert_expiry/translations/ja.json +++ b/homeassistant/components/cert_expiry/translations/ja.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "import_failed": "\u30b3\u30f3\u30d5\u30a3\u30b0\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, "error": { + "connection_refused": "\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u63a5\u7d9a\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f", "connection_timeout": "\u3053\u306e\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, "title": "\u30c6\u30b9\u30c8\u3059\u308b\u8a3c\u660e\u66f8\u3092\u5b9a\u7fa9\u3059\u308b" } } diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json index c3fd320ad24..a7d0d5dc3e8 100644 --- a/homeassistant/components/control4/translations/ja.json +++ b/homeassistant/components/control4/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "Control4\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8a73\u7d30\u3068\u3001\u30ed\u30fc\u30ab\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 65cb1e46428..20ba31ec60f 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -7,13 +7,15 @@ "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, + "flow_title": "{host}", "step": { "link": { "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" }, "manual_input": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } @@ -22,7 +24,20 @@ "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "close": "\u9589\u3058\u308b", + "dim_down": "\u8584\u6697\u304f\u3059\u308b", + "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", + "left": "\u5de6", + "right": "\u53f3", + "side_1": "\u30b5\u30a4\u30c91", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" + }, + "trigger_type": { + "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b" } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.id.json b/homeassistant/components/demo/translations/select.id.json index 410fad95ba8..7f3e9109995 100644 --- a/homeassistant/components/demo/translations/select.id.json +++ b/homeassistant/components/demo/translations/select.id.json @@ -1,7 +1,9 @@ { "state": { "demo__speed": { - "light_speed": "Kecepatan Cahaya" + "light_speed": "Kecepatan Cahaya", + "ludicrous_speed": "Kecepatan Menggelikan", + "ridiculous_speed": "Kecepatan Konyol" } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json index def5524cdbb..6c371a08ae7 100644 --- a/homeassistant/components/devolo_home_control/translations/ja.json +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -2,6 +2,13 @@ "config": { "error": { "reauth_failed": "\u4ee5\u524d\u306e\u3068\u540c\u3058mydevolo\u30e6\u30fc\u30b6\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "zeroconf_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index c183275bc9f..327868e0b9c 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/directv/translations/ja.json b/homeassistant/components/directv/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/directv/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/id.json b/homeassistant/components/dlna_dmr/translations/id.json index bf4049af584..152c4f73a56 100644 --- a/homeassistant/components/dlna_dmr/translations/id.json +++ b/homeassistant/components/dlna_dmr/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "alternative_integration": "Perangkat dapat didukung lebih baik lewat integrasi lainnya", "cannot_connect": "Gagal terhubung", "could_not_connect": "Gagal terhubung ke perangkat DLNA", "discovery_error": "Gagal menemukan perangkat DLNA yang cocok", @@ -40,8 +41,16 @@ } }, "options": { + "error": { + "invalid_url": "URL tidak valid" + }, "step": { "init": { + "data": { + "callback_url_override": "URL panggilan balik pendengar peristiwa", + "listen_port": "Port pendengar peristiwa (acak jika tidak diatur)", + "poll_availability": "Polling untuk ketersediaan perangkat" + }, "title": "Konfigurasi Digital Media Renderer DLNA" } } diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json new file mode 100644 index 00000000000..57fdc081a18 --- /dev/null +++ b/homeassistant/components/doorbird/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index fabdf8bc3af..d92dceb1a2a 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -10,7 +10,8 @@ "setup_network": { "data": { "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e", - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" }, "title": "\u63a5\u7d9a\u30a2\u30c9\u30ec\u30b9\u306e\u9078\u629e" }, @@ -22,6 +23,9 @@ "title": "\u30c7\u30d0\u30a4\u30b9" }, "setup_serial_manual_path": { + "data": { + "port": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "title": "\u30d1\u30b9" }, "user": { diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 64ba77ca99e..55bdacb325e 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -2,7 +2,11 @@ "config": { "step": { "authorize": { + "description": "\u3053\u306e\u30a2\u30d7\u30ea\u3092 https://www.ecobee.com/consumerportal/index.html \u3067PIN\u30b3\u30fc\u30c9\u3067\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n {pin}\n\n\u6b21\u306b\u3001\u9001\u4fe1(submit) \u3092\u62bc\u3057\u307e\u3059\u3002", "title": "ecobee.com\u306e\u30a2\u30d7\u30ea\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "title": "ecobee API\u30ad\u30fc" } } } diff --git a/homeassistant/components/econet/translations/ja.json b/homeassistant/components/econet/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/econet/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index e244ae5f1d2..304ec628c87 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -2,6 +2,10 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, "description": "Elgato Key Light\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 7c804b56c7a..8e155e2dd56 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002" } } diff --git a/homeassistant/components/emulated_roku/translations/id.json b/homeassistant/components/emulated_roku/translations/id.json index 9ffcedf5d19..30aa33240de 100644 --- a/homeassistant/components/emulated_roku/translations/id.json +++ b/homeassistant/components/emulated_roku/translations/id.json @@ -9,6 +9,7 @@ "advertise_ip": "Umumkan Alamat IP", "advertise_port": "Umumkan Port", "host_ip": "Alamat IP Host", + "listen_port": "Port untuk Mendengarkan", "name": "Nama", "upnp_bind_multicast": "Bind multicast (True/False)" }, diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index 850e34f88bf..006bff4d921 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -4,7 +4,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/environment_canada/translations/id.json b/homeassistant/components/environment_canada/translations/id.json index 683dc8964c2..df3f087332e 100644 --- a/homeassistant/components/environment_canada/translations/id.json +++ b/homeassistant/components/environment_canada/translations/id.json @@ -15,6 +15,7 @@ "longitude": "Bujur", "station": "ID stasiun cuaca" }, + "description": "Salah satu dari ID stasiun atau lintang/bujur harus ditentukan. Lintang/bujur default yang digunakan adalah nilai yang dikonfigurasi dalam instalasi Home Assistant Anda. Stasiun cuaca terdekat dengan koordinat akan digunakan jika koordinat ditentukan. Jika menggunakan kode stasiun, formatnya harus berupa: PP/kode, di mana PP adalah provinsi dua huruf dan kode adalah ID stasiun. Daftar ID stasiun dapat ditemukan di sini: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Informasi cuaca dapat diperoleh dalam bahasa Inggris atau Prancis.", "title": "Environment Canada: lokasi cuaca dan bahasa" } } diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index aa8e9960981..e099405bf0d 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -32,7 +32,8 @@ "reauth_confirm": { "data": { "noise_psk": "Kunci enkripsi" - } + }, + "description": "Perangkat ESPHome {name} mengaktifkan enkripsi transport atau telah mengubah kunci enkripsi. Masukkan kunci yang diperbarui." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 41484fdf874..a374a3a7e38 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -8,6 +8,9 @@ "flow_title": "{name}", "step": { "authenticate": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "{name} \u3067\u8a2d\u5b9a\u3057\u305f\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "discovery_confirm": { @@ -28,7 +31,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" }, "description": "\u3042\u306a\u305f\u306e[ESPHome](https://esphomelib.com/)\u306e\u30ce\u30fc\u30c9\u306e\u63a5\u7d9a\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/ezviz/translations/ja.json b/homeassistant/components/ezviz/translations/ja.json new file mode 100644 index 00000000000..94fe0f846e2 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user_custom_url": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/ja.json b/homeassistant/components/faa_delays/translations/ja.json new file mode 100644 index 00000000000..420618e8f66 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u3053\u306e\u7a7a\u6e2f\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, + "error": { + "invalid_airport": "\u7a7a\u6e2f\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059" + }, + "step": { + "user": { + "data": { + "id": "\u7a7a\u6e2f" + }, + "description": "IATA\u5f62\u5f0f\u3067\u7c73\u56fd\u306e\u7a7a\u6e2f\u30b3\u30fc\u30c9(US Airport Code)\u3092\u5165\u529b\u3057\u307e\u3059" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/translations/ja.json b/homeassistant/components/fan/translations/ja.json index 15dd3796187..d3546ca6721 100644 --- a/homeassistant/components/fan/translations/ja.json +++ b/homeassistant/components/fan/translations/ja.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/fireservicerota/translations/ja.json b/homeassistant/components/fireservicerota/translations/ja.json index fa8742d5a87..d453028ca2a 100644 --- a/homeassistant/components/fireservicerota/translations/ja.json +++ b/homeassistant/components/fireservicerota/translations/ja.json @@ -2,7 +2,15 @@ "config": { "step": { "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u304c\u7121\u52b9\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u518d\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } } } } diff --git a/homeassistant/components/flipr/translations/id.json b/homeassistant/components/flipr/translations/id.json index 4e44860293d..0f1758f2d8f 100644 --- a/homeassistant/components/flipr/translations/id.json +++ b/homeassistant/components/flipr/translations/id.json @@ -6,17 +6,24 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "no_flipr_id_found": "Tidak ada id flipr yang terkait dengan akun Anda untuk saat ini. Anda harus memverifikasinya dengan aplikasi seluler Flipr terlebih dahulu.", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "flipr_id": { - "description": "Pilih ID Flipr Anda dari daftar" + "data": { + "flipr_id": "ID Flipr" + }, + "description": "Pilih ID Flipr Anda dari daftar", + "title": "Pilih ID Flipr Anda" }, "user": { "data": { "email": "Email", "password": "Kata Sandi" - } + }, + "description": "Hubungkan menggunakan akun Flipr Anda.", + "title": "Hubungkan ke Flipr" } } } diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index 06337da8b6d..8c91a4e364f 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -12,6 +12,9 @@ "title": "Flipr\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "\u3042\u306a\u305f\u306eFlipr\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3057\u307e\u3059\u3002", "title": "Flipr\u306b\u63a5\u7d9a" } diff --git a/homeassistant/components/flo/translations/ja.json b/homeassistant/components/flo/translations/ja.json index a42202307f2..9f68231f0d2 100644 --- a/homeassistant/components/flo/translations/ja.json +++ b/homeassistant/components/flo/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/flume/translations/id.json b/homeassistant/components/flume/translations/id.json index f72e27ece8d..9aea48afae0 100644 --- a/homeassistant/components/flume/translations/id.json +++ b/homeassistant/components/flume/translations/id.json @@ -13,7 +13,9 @@ "reauth_confirm": { "data": { "password": "Kata Sandi" - } + }, + "description": "Kata sandi untuk {username} tidak lagi berlaku.", + "title": "Autentikasi ulang Akun Flume Anda" }, "user": { "data": { diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json new file mode 100644 index 00000000000..07d401c0bc8 --- /dev/null +++ b/homeassistant/components/flume/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/id.json b/homeassistant/components/flux_led/translations/id.json index 74ada0b83af..84c993365ac 100644 --- a/homeassistant/components/flux_led/translations/id.json +++ b/homeassistant/components/flux_led/translations/id.json @@ -8,10 +8,27 @@ "error": { "cannot_connect": "Gagal terhubung" }, + "flow_title": "{model} {id} ({ipaddr})", "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {model} {id} ({ipaddr})?" + }, "user": { "data": { "host": "Host" + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Efek Khusus: Daftar berisi 1 hingga 16 warna [R,G,B]. Contoh: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Efek Khusus: Kecepatan dalam persen untuk efek perubahan warna.", + "custom_effect_transition": "Efek Khusus: Jenis transisi antara warna.", + "mode": "Mode kecerahan yang dipilih." } } } diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json index 130f66db7f5..27ef16e0266 100644 --- a/homeassistant/components/forecast_solar/translations/id.json +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -3,10 +3,28 @@ "step": { "user": { "data": { + "azimuth": "Azimuth (360 derajat, 0 = Utara, 90 = Timur, 180 = Selatan, 270 = Barat)", + "declination": "Deklinasi (0 = Horizontal, 90 = Vertikal)", "latitude": "Lintang", "longitude": "Bujur", + "modules power": "Total daya puncak modul surya Anda dalam Watt", "name": "Nama" - } + }, + "description": "Isi data panel surya Anda. Rujuk ke dokumentasi jika bidang isian tidak jelas." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Kunci API Forecast.Solar (opsional)", + "azimuth": "Azimuth (360 derajat, 0 = Utara, 90 = Timur, 180 = Selatan, 270 = Barat)", + "damping": "Faktor redaman: menyesuaikan hasil di pagi dan sore hari", + "declination": "Deklinasi (0 = Horizontal, 90 = Vertikal)", + "modules power": "Total daya puncak modul surya Anda dalam Watt" + }, + "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Solar.Forecast. Rujuk ke dokumentasi jika bidang isian tidak jelas." } } } diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 6d019a24427..45f27956f02 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -1,9 +1,16 @@ { "config": { + "error": { + "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "API\u30d1\u30b9\u30ef\u30fc\u30c9(\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", + "port": "API\u30dd\u30fc\u30c8" }, "title": "forked-daapd\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 5e0b6473557..402b0f2c897 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -7,6 +7,9 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "rtsp_port": "RTSP\u30dd\u30fc\u30c8", "stream": "\u30b9\u30c8\u30ea\u30fc\u30e0" } } diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json index ee911e0a3a9..de9812fb598 100644 --- a/homeassistant/components/freebox/translations/ja.json +++ b/homeassistant/components/freebox/translations/ja.json @@ -6,7 +6,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/freedompro/translations/id.json b/homeassistant/components/freedompro/translations/id.json index 9676af6d8f9..e05abfb7d14 100644 --- a/homeassistant/components/freedompro/translations/id.json +++ b/homeassistant/components/freedompro/translations/id.json @@ -11,7 +11,9 @@ "user": { "data": { "api_key": "Kunci API" - } + }, + "description": "Masukkan kunci API yang diperoleh dari https://home.freedompro.eu.", + "title": "Kunci API Freedompro" } } } diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index 1a3140da624..5aae1443d02 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -18,13 +18,17 @@ "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "FRITZ!Box ditemukan: {name} \n\nSiapkan FRITZ!Box Tools untuk mengontrol {name}", + "title": "Siapkan FRITZ!Box Tools." }, "reauth_confirm": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Perbarui kredensial FRITZ!Box Tools untuk: {host} . \n\nFRITZ!Box Tools tidak dapat masuk ke FRITZ!Box Anda.", + "title": "Memperbarui FRITZ!Box Tools - kredensial" }, "start_config": { "data": { @@ -32,7 +36,9 @@ "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" - } + }, + "description": "Siapkan FRITZ!Box Tools untuk mengontrol FRITZ!Box Anda.\nDiperlukan minimal: nama pengguna dan kata sandi.", + "title": "Siapkan FRITZ!Box Tools - wajib" }, "user": { "data": { @@ -40,6 +46,17 @@ "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" + }, + "description": "Siapkan FRITZ!Box Tools untuk mengontrol FRITZ!Box Anda.\nDiperlukan minimal: nama pengguna dan kata sandi.", + "title": "Siapkan FRITZ!Box Tools." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Wakti dalam detik untuk mempertimbangkan perangkat sebagai 'di rumah'" } } } diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index e8940bef26a..7c362d1c8e7 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -1,5 +1,33 @@ { "config": { - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "start_config": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json new file mode 100644 index 00000000000..b1790f8547c --- /dev/null +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index e8940bef26a..2ca204485ec 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -1,5 +1,13 @@ { "config": { - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/id.json b/homeassistant/components/garages_amsterdam/translations/id.json index 37a312250a1..f12cfd6fc80 100644 --- a/homeassistant/components/garages_amsterdam/translations/id.json +++ b/homeassistant/components/garages_amsterdam/translations/id.json @@ -4,6 +4,15 @@ "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "garage_name": "Nama garasi" + }, + "title": "Pilih garasi untuk dipantau" + } } - } + }, + "title": "Garasi Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/ja.json b/homeassistant/components/geonetnz_volcano/translations/ja.json new file mode 100644 index 00000000000..7f698056b09 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "\u534a\u5f84" + }, + "title": "\u30d5\u30a3\u30eb\u30bf\u30fc\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/ja.json b/homeassistant/components/glances/translations/ja.json new file mode 100644 index 00000000000..6d220a900d1 --- /dev/null +++ b/homeassistant/components/glances/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "wrong_version": "\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3\u306e\u307f)" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "version": "Glances API\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3)" + }, + "title": "Glances\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + }, + "options": { + "step": { + "init": { + "description": "Glances\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json index 5bab8fa03a2..d5897a2d944 100644 --- a/homeassistant/components/goalzero/translations/id.json +++ b/homeassistant/components/goalzero/translations/id.json @@ -11,6 +11,10 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "confirm_discovery": { + "description": "Dianjurkan untuk menggunakan reservasi DHCP pada router Anda. Jika tidak diatur, perangkat mungkin tidak tersedia hingga Home Assistant mendeteksi alamat IP baru. Lihat panduan pengguna router Anda.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index 60af7c9c461..4f3b4f0b2bf 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -7,7 +7,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/gogogate2/translations/ja.json b/homeassistant/components/gogogate2/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/gogogate2/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/id.json b/homeassistant/components/growatt_server/translations/id.json index 59975607fb7..dcc75c42fa0 100644 --- a/homeassistant/components/growatt_server/translations/id.json +++ b/homeassistant/components/growatt_server/translations/id.json @@ -1,17 +1,28 @@ { "config": { + "abort": { + "no_plants": "Tidak ada pembangkit yang ditemukan di akun ini" + }, "error": { "invalid_auth": "Autentikasi tidak valid" }, "step": { + "plant": { + "data": { + "plant_id": "Pembangkit" + }, + "title": "Pilih pembangkit Anda" + }, "user": { "data": { "name": "Nama", "password": "Kata Sandi", "url": "URL", "username": "Nama Pengguna" - } + }, + "title": "Masukkan informasi Growatt Anda" } } - } + }, + "title": "Server Growatt" } \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json index 38a843d14fa..0eba932ebaf 100644 --- a/homeassistant/components/growatt_server/translations/ja.json +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -8,6 +8,9 @@ "title": "\u30d7\u30e9\u30f3\u30c8\u3092\u9078\u629e" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "Growatt\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index b5b75321037..8193386fb62 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -6,6 +6,9 @@ "cannot_connect": "Gagal terhubung" }, "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan perangkat Guardian ini?" + }, "user": { "data": { "ip_address": "Alamat IP", diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json new file mode 100644 index 00000000000..3a61126ea94 --- /dev/null +++ b/homeassistant/components/guardian/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index d17b8c8fe15..78f02028ea3 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -5,6 +5,9 @@ "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index 88a27fb36bb..102cc549b5f 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -2,7 +2,8 @@ "system_health": { "info": { "healthy": "\u5143\u6c17", - "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0" + "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0", + "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059" } } } \ No newline at end of file diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 954da767d10..94a8c761f93 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -5,7 +5,8 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Heos\u30c7\u30d0\u30a4\u30b9(\u3067\u304d\u308c\u3070\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u6709\u7dda\u3067\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9)\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "Heos\u30c7\u30d0\u30a4\u30b9(\u3067\u304d\u308c\u3070\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u6709\u7dda\u3067\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9)\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Heos\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/ja.json b/homeassistant/components/hisense_aehw4a1/translations/ja.json new file mode 100644 index 00000000000..a9edf17eedc --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Hisense AEH-W4A1\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 5de338fbb7b..0e1c75dd769 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "unknown_entry": "\u65e2\u5b58\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, + "error": { + "invalid_password": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, "step": { "2fa": { "data": { @@ -11,9 +14,15 @@ "title": "\u30cf\u30a4\u30d6(Hive)2\u8981\u7d20\u8a8d\u8a3c\u3002" }, "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "Hive\u30ed\u30b0\u30a4\u30f3" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } diff --git a/homeassistant/components/hlk_sw16/translations/ja.json b/homeassistant/components/hlk_sw16/translations/ja.json index a42202307f2..9f68231f0d2 100644 --- a/homeassistant/components/hlk_sw16/translations/ja.json +++ b/homeassistant/components/hlk_sw16/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index f8b122b3643..717ac52192f 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -8,7 +8,8 @@ }, "cameras": { "data": { - "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9" + "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9", + "camera_copy": "H.264\u306e\u30cd\u30a4\u30c6\u30a3\u30d6\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308b\u30ab\u30e1\u30e9" } }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 4f2de96bc8d..0aa22ee0b55 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -4,6 +4,22 @@ "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, + "error": { + "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "pair": { + "data": { + "pairing_code": "\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9" + } + }, + "user": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 3d3c7e9e8a3..0f6f12ebf00 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -15,10 +15,12 @@ "data": { "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", "pin": "PIN\u30b3\u30fc\u30c9" - } + }, + "title": "HomematicIP Access point\u3092\u9078\u629e" }, "link": { - "description": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306e\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u3001\u9001\u4fe1\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Home Assistant\u306bHomematicIP\u304c\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u306e\u4f4d\u7f6e](/static/images/config_flows/config_homematicip_cloud.png)" + "description": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306e\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u3001\u9001\u4fe1(submit)\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u3001Home Assistant\u306bHomematicIP\u304c\u767b\u9332\u3055\u308c\u307e\u3059\u3002\n\n![\u30d6\u30ea\u30c3\u30b8\u306e\u30dc\u30bf\u30f3\u306e\u4f4d\u7f6e](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u30ea\u30f3\u30af \u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8" } } } diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json index b8333336f71..62151da1481 100644 --- a/homeassistant/components/honeywell/translations/id.json +++ b/homeassistant/components/honeywell/translations/id.json @@ -9,7 +9,8 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan kredensial yang digunakan untuk masuk ke mytotalconnectcomfort.com." + "description": "Masukkan kredensial yang digunakan untuk masuk ke mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (AS)" } } } diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index 667f57a90ab..87ae2a44188 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "mytotalconnectcomfort.com \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3059\u308b\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Honeywell Total Connect Comfort (US)" } diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index de784fd3e94..fa586718cac 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -34,7 +34,9 @@ "data": { "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)", "recipient": "Penerima notifikasi SMS", - "track_new_devices": "Lacak perangkat baru" + "track_new_devices": "Lacak perangkat baru", + "track_wired_clients": "Lacak klien jaringan kabel", + "unauthenticated_mode": "Mode tidak diautentikasi (perubahan memerlukan pemuatan ulang)" } } } diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index a02253588df..ef1c6588f11 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,13 +1,30 @@ { "config": { + "abort": { + "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, "error": { + "connection_timeout": "\u63a5\u7d9a\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "incorrect_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", + "incorrect_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", + "invalid_url": "\u7121\u52b9\u306aURL", "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "Huawei LTE\u306e\u8a2d\u5b9a" + } } }, "options": { "step": { "init": { "data": { + "recipient": "SMS\u901a\u77e5\u306e\u53d7\u4fe1\u8005", + "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1", "unauthenticated_mode": "\u8a8d\u8a3c\u306a\u3057\u306e\u30e2\u30fc\u30c9(\u5909\u66f4\u306b\u306f\u30ea\u30ed\u30fc\u30c9\u304c\u5fc5\u8981)" } } diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 8e15de50354..b3b39dfb185 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue bridge\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", "no_bridges": "Hue bridge\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "not_hue_bridge": "Hue bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", @@ -13,6 +14,9 @@ }, "step": { "init": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "title": "Hue bridge\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" }, "link": { @@ -26,5 +30,22 @@ "title": "Hue bridges\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" } } + }, + "device_automation": { + "trigger_subtype": { + "1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_scenes": "Hue\u30b7\u30fc\u30f3\u3092\u8a31\u53ef" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index b144393c3d1..ea3e402a59b 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Pierwszy przycisk", + "2": "Drugi przycisk", + "3": "Trzeci przycisk", + "4": "Czwarty przycisk", "button_1": "pierwszy", "button_2": "drugi", "button_3": "trzeci", diff --git a/homeassistant/components/huisbaasje/translations/ja.json b/homeassistant/components/huisbaasje/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index a42202307f2..eb1da4ce43a 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "no_id": "Hyperion Ambilight\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u305d\u306eID\u3092\u30ec\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" + }, "step": { + "confirm": { + "description": "\u3053\u306eHyperion Ambilight\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f \n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}" + }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/ialarm/translations/ja.json b/homeassistant/components/ialarm/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/ialarm/translations/ja.json +++ b/homeassistant/components/ialarm/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json new file mode 100644 index 00000000000..84f6c52ac04 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "iAqualink\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "iAqualink\u306b\u63a5\u7d9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index c27c96e570a..4aad8f31345 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "trusted_device": { "data": { "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index a18421fe9a1..7b206985e6d 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -1,9 +1,31 @@ { + "config": { + "step": { + "hubv1": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + }, + "hubv2": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + } + } + }, "options": { "step": { "add_x10": { "data": { "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" + }, + "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002" + }, + "change_hub_config": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/iotawatt/translations/id.json b/homeassistant/components/iotawatt/translations/id.json index 8461b692aaa..ef50c938292 100644 --- a/homeassistant/components/iotawatt/translations/id.json +++ b/homeassistant/components/iotawatt/translations/id.json @@ -10,7 +10,8 @@ "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Perangkat IoTawatt memerlukan otentikasi. Masukkan nama pengguna dan kata sandi dan klik tombol Kirim." }, "user": { "data": { diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json index c93ce47e3f0..1d29e607aa5 100644 --- a/homeassistant/components/iotawatt/translations/ja.json +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -3,9 +3,10 @@ "step": { "auth": { "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "IoTawatt\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u8a8d\u8a3c\u304c\u5fc5\u8981\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3001\u9001\u4fe1 \u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "IoTawatt\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u8a8d\u8a3c\u304c\u5fc5\u8981\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3001\u9001\u4fe1(submit) \u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json index 91e89d74e32..3e0f61d33e1 100644 --- a/homeassistant/components/ipma/translations/ja.json +++ b/homeassistant/components/ipma/translations/ja.json @@ -2,6 +2,11 @@ "config": { "error": { "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + }, + "step": { + "user": { + "description": "\u30dd\u30eb\u30c8\u30ac\u30eb\u6d77\u6d0b\u5927\u6c17\u7814\u7a76\u6240(Instituto Portugu\u00eas do Mar e Atmosfera)" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 0ae87941243..a1e9314c1c5 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -1,10 +1,19 @@ { "config": { "abort": { + "connection_upgrade": "\u63a5\u7d9a\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9(connection upgrade)\u304c\u5fc5\u8981\u306a\u305f\u3081\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "ipp_version_error": "IPP\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30d7\u30ea\u30f3\u30bf\u30fc\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, + "error": { + "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, "description": "\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u5370\u5237\u30d7\u30ed\u30c8\u30b3\u30eb(IPP)\u3092\u4ecb\u3057\u3066\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index 099e3607d1e..aeea471514c 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -36,5 +36,13 @@ "title": "Opsi ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "Terhubung ke ISY", + "host_reachable": "Host dapat dijangkau", + "last_heartbeat": "Waktu Detak Terakhir", + "websocket_status": "Status Soket Peristiwa" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 6b33f35444a..e50b4676261 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -3,10 +3,12 @@ "error": { "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "URL" + "host": "URL", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80" } diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/id.json b/homeassistant/components/keenetic_ndms2/translations/id.json index 900745bc29e..e48a5051ea3 100644 --- a/homeassistant/components/keenetic_ndms2/translations/id.json +++ b/homeassistant/components/keenetic_ndms2/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "no_udn": "Info penemuan SSDP tidak memiliki UDN", + "not_keenetic_ndms2": "Item yang ditemukan bukan router Keenetic" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index b477b4a77b1..11f6dea5500 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -8,7 +8,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "Keenetic NDMS2\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json index a42202307f2..9f68231f0d2 100644 --- a/homeassistant/components/kmtronic/translations/ja.json +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 5633ba40f97..75e42ddec49 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "credentials": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "Kodi\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308a\u307e\u3059\u3002" }, "discovery_confirm": { @@ -13,11 +16,15 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" }, "description": "Kodi\u306e\u63a5\u7d9a\u60c5\u5831\u3067\u3059\u3002\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u3067 \"HTTP\u306b\u3088\u308bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u5fc5\u305a\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "ws_port": { + "data": { + "ws_port": "\u30dd\u30fc\u30c8" + }, "description": "WebSocket\u30dd\u30fc\u30c8(Kodi\u3067\u306fTCP\u30dd\u30fc\u30c8\u3068\u547c\u3070\u308c\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059)\u3002WebSocket\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308b \"\u30d7\u30ed\u30b0\u30e9\u30e0\u306bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002WebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30dd\u30fc\u30c8\u3092\u524a\u9664\u3057\u3066\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index aa0c2ef2303..6122be0009d 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -1,8 +1,19 @@ { "config": { "step": { + "confirm": { + "description": "\u30e2\u30c7\u30eb: {model}\n ID: {id}\n\u30db\u30b9\u30c8: {host}\n\u30dd\u30fc\u30c8: {port} \n\nKonnected Alarm Panel\u306e\u8a2d\u5b9a\u3067\u3001IO\u3068\u30d1\u30cd\u30eb\u306e\u52d5\u4f5c\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002" + }, "import_confirm": { - "description": "ID {id} \u306eKonnected\u30a2\u30e9\u30fc\u30e0\u30d1\u30cd\u30eb\u304c\u8a2d\u5b9a\u3067\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u30fc\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u304d\u307e\u3059\u3002" + "description": "ID {id} \u306eKonnected\u30a2\u30e9\u30fc\u30e0\u30d1\u30cd\u30eb\u304c\u8a2d\u5b9a\u3067\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u30fc\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u304d\u307e\u3059\u3002", + "title": "Konnected Device\u306e\u30a4\u30f3\u30dd\u30fc\u30c8" + }, + "user": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "Konnected Panel\u306e\u30db\u30b9\u30c8\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json index a42202307f2..9f68231f0d2 100644 --- a/homeassistant/components/kostal_plenticore/translations/ja.json +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index a70c2e14efd..15462cb8945 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -5,6 +5,14 @@ }, "error": { "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" + } } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index 0a0c82ea072..91cf6162551 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -1,4 +1,17 @@ { + "config": { + "error": { + "open_failed": "\u6307\u5b9a\u3055\u308c\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + }, + "description": "LiteJet\u306eRS232-2\u30dd\u30fc\u30c8\u3092\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3057\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n LiteJet MCP\u306f\u300119.2 K\u30dc\u30fc\u30018\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u30011\u30b9\u30c8\u30c3\u30d7\u30d3\u30c3\u30c8\u3001\u30d1\u30ea\u30c6\u30a3\u306a\u3057\u3001\u5404\u5fdc\u7b54\u306e\u5f8c\u306b 'CR' \u3092\u9001\u4fe1\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ja.json b/homeassistant/components/logi_circle/translations/ja.json index ac4af42d1a3..daeec7516af 100644 --- a/homeassistant/components/logi_circle/translations/ja.json +++ b/homeassistant/components/logi_circle/translations/ja.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "external_setup": "Logi Circle\u306f\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002" + }, + "error": { + "follow_link": "\u9001\u4fe1(submit) \u3092\u62bc\u3059\u524d\u306b\u3001\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "auth": { "title": "Logi Circle\u3067\u8a8d\u8a3c\u3059\u308b" }, "user": { + "data": { + "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" + }, "title": "\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" } } diff --git a/homeassistant/components/mazda/translations/ja.json b/homeassistant/components/mazda/translations/ja.json new file mode 100644 index 00000000000..8562ef078cc --- /dev/null +++ b/homeassistant/components/mazda/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "MyMazda\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u969b\u306b\u4f7f\u7528\u3059\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/melcloud/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index e4511a43cb7..38816c4399b 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -2,6 +2,11 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, "title": "Mikrotik\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/mill/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json index c6ee304e29d..ea563b3eab6 100644 --- a/homeassistant/components/minecraft_server/translations/ja.json +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -1,10 +1,15 @@ { "config": { "error": { - "invalid_ip": "IP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059(MAC\u30a2\u30c9\u30ec\u30b9\u3092\u7279\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f)\u3002\u4fee\u6b63\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u30b5\u30fc\u30d0\u30fc\u3067Minecraft\u306e\u30d0\u30fc\u30b8\u30e7\u30f31.7\u4ee5\u4e0a\u306e\u3082\u306e\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3082\u3042\u308f\u305b\u3066\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_ip": "IP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059(MAC\u30a2\u30c9\u30ec\u30b9\u3092\u7279\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f)\u3002\u4fee\u6b63\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_port": "\u30dd\u30fc\u30c8\u306f1024\u301c65535\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4fee\u6b63\u3057\u3066\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001Minecraft Server\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index d93d7aa4f0e..6b766ef76db 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -9,6 +9,9 @@ "title": "Phone\u30e2\u30c7\u30e0" }, "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + }, "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", "title": "Phone\u30e2\u30c7\u30e0" } diff --git a/homeassistant/components/monoprice/translations/ja.json b/homeassistant/components/monoprice/translations/ja.json index 45356b82d7a..f1b4a2e3617 100644 --- a/homeassistant/components/monoprice/translations/ja.json +++ b/homeassistant/components/monoprice/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "port": "\u30dd\u30fc\u30c8", "source_1": "\u30bd\u30fc\u30b9#1\u306e\u540d\u524d", "source_2": "\u30bd\u30fc\u30b9#2\u306e\u540d\u524d", "source_3": "\u30bd\u30fc\u30b9#3\u306e\u540d\u524d", diff --git a/homeassistant/components/moon/translations/sensor.ja.json b/homeassistant/components/moon/translations/sensor.ja.json index 88bd2e4e95a..9df6ceab2af 100644 --- a/homeassistant/components/moon/translations/sensor.ja.json +++ b/homeassistant/components/moon/translations/sensor.ja.json @@ -1,6 +1,7 @@ { "state": { "moon__phase": { + "first_quarter": "\u4e0a\u5f26\u306e\u6708", "full_moon": "\u6e80\u6708", "last_quarter": "\u4e0b\u5f26\u306e\u6708", "new_moon": "\u65b0\u6708", diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json index 9248531a751..35e254ef136 100644 --- a/homeassistant/components/motion_blinds/translations/id.json +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -33,5 +33,12 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "description": "Tentukan pengaturan opsional" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/id.json b/homeassistant/components/motioneye/translations/id.json index ac4aa89c8ef..7e3964dd4db 100644 --- a/homeassistant/components/motioneye/translations/id.json +++ b/homeassistant/components/motioneye/translations/id.json @@ -11,6 +11,9 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "hassio_confirm": { + "title": "motionEye melalui add-on Home Assistant" + }, "user": { "data": { "admin_password": "Kata Sandi Admin", @@ -21,5 +24,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "stream_url_template": "Templat URL streaming", + "webhook_set": "Konfigurasikan webhook motionEye untuk melaporkan peristiwa ke Home Assistant", + "webhook_set_overwrite": "Timpa webhook yang tidak dikenal" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 54e8a6315bb..08096987df4 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -3,6 +3,12 @@ "step": { "hassio_confirm": { "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306emotionEye" + }, + "user": { + "data": { + "admin_password": "\u7ba1\u7406\u8005(Admin)\u30d1\u30b9\u30ef\u30fc\u30c9", + "surveillance_password": "\u76e3\u8996(Surveillance)\u30d1\u30b9\u30ef\u30fc\u30c9" + } } } }, diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 338ca6fa6fe..d223dfb74df 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -61,13 +61,16 @@ "port": "Port", "username": "Nama Pengguna" }, - "description": "Masukkan informasi koneksi broker MQTT Anda." + "description": "Masukkan informasi koneksi broker MQTT Anda.", + "title": "Opsi broker" }, "options": { "data": { - "discovery": "Aktifkan penemuan" + "discovery": "Aktifkan penemuan", + "will_enable": "Aktifkan pesan 'will'" }, - "description": "Penemuan - Jika penemuan diaktifkan (disarankan), Home Assistant akan secara otomatis menemukan perangkat dan entitas yang mempublikasikan konfigurasinya di broker MQTT. Jika penemuan dinonaktifkan, semua konfigurasi harus dilakukan secara manual.\nPesan birth - Pesan birth akan dikirim setiap kali Home Assistant terhubung (kembali) ke broker MQTT.\nPesan will akan dikirim setiap kali Home Assistant kehilangan koneksi ke broker, baik dalam kasus bersih (misalnya Home Assistant dimatikan) dan dalam kasus (misalnya Home Assistant mogok atau kehilangan koneksi jaringan) terputus yang tidak bersih." + "description": "Penemuan - Jika penemuan diaktifkan (disarankan), Home Assistant akan secara otomatis menemukan perangkat dan entitas yang mempublikasikan konfigurasinya di broker MQTT. Jika penemuan dinonaktifkan, semua konfigurasi harus dilakukan secara manual.\nPesan birth - Pesan birth akan dikirim setiap kali Home Assistant terhubung (kembali) ke broker MQTT.\nPesan will akan dikirim setiap kali Home Assistant kehilangan koneksi ke broker, baik dalam kasus bersih (misalnya Home Assistant dimatikan) dan dalam kasus (misalnya Home Assistant mogok atau kehilangan koneksi jaringan) terputus yang tidak bersih.", + "title": "Opsi MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 01f77dc1eef..b8ff2edfca4 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "broker": { "data": { @@ -23,6 +26,10 @@ "options": { "step": { "broker": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Broker\u30aa\u30d7\u30b7\u30e7\u30f3" }, diff --git a/homeassistant/components/myq/translations/id.json b/homeassistant/components/myq/translations/id.json index 4972803f37d..1a5368dd611 100644 --- a/homeassistant/components/myq/translations/id.json +++ b/homeassistant/components/myq/translations/id.json @@ -13,7 +13,9 @@ "reauth_confirm": { "data": { "password": "Kata Sandi" - } + }, + "description": "Kata sandi untuk {username} tidak lagi berlaku.", + "title": "Autentikasi ulang Akun MyQ Anda" }, "user": { "data": { diff --git a/homeassistant/components/myq/translations/ja.json b/homeassistant/components/myq/translations/ja.json new file mode 100644 index 00000000000..07d401c0bc8 --- /dev/null +++ b/homeassistant/components/myq/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index e982250b09c..4db3c491be4 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -30,6 +30,7 @@ "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", + "mqtt_required": "Integrasi MQTT belum disiapkan", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index af277704c10..d426787eb0c 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9" + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", + "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "error": { - "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9" + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", + "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "step": { "gw_tcp": { "data": { - "device": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306eIP\u30a2\u30c9\u30ec\u30b9" + "device": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306eIP\u30a2\u30c9\u30ec\u30b9", + "tcp_port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json index bc4ca456f4e..9b2a11e83fe 100644 --- a/homeassistant/components/nam/translations/ca.json +++ b/homeassistant/components/nam/translations/ca.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "device_unsupported": "El dispositiu no \u00e9s compatible." + "device_unsupported": "El dispositiu no \u00e9s compatible.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, elimina la integraci\u00f3 i torna-la a configurar." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Vols configurar Nettigo Air Monitor a {host}?" }, + "credentials": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix el nom d'usuari i la contrasenya." + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix el nom d'usuari i contrasenya correctes de l'amfitri\u00f3: {host}" + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index e3c7159a0d6..7c30b441378 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "device_unsupported": "Das Ger\u00e4t wird nicht unterst\u00fctzt." + "device_unsupported": "Das Ger\u00e4t wird nicht unterst\u00fctzt.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte entferne die Integration und richte sie erneut ein." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "M\u00f6chtest du Nettigo Air Monitor unter {host} einrichten?" }, + "credentials": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte gib den Benutzernamen und das Passwort ein." + }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte gib den richtigen Benutzernamen und das richtige Passwort f\u00fcr den Host ein: {host}" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json index 0ea0c7ae6c1..4378f8d6c51 100644 --- a/homeassistant/components/nam/translations/en.json +++ b/homeassistant/components/nam/translations/en.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Device is already configured", - "device_unsupported": "The device is unsupported." + "device_unsupported": "The device is unsupported.", + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Do you want to set up Nettigo Air Monitor at {host}?" }, + "credentials": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please enter the username and password." + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please enter the correct username and password for host: {host}" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json index e94cd3a46b6..c8b3040ecbb 100644 --- a/homeassistant/components/nam/translations/et.json +++ b/homeassistant/components/nam/translations/et.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "device_unsupported": "Seadet ei toetata." + "device_unsupported": "Seadet ei toetata.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_unsuccessful": "Taastuvastamine nurjus, eemalda sidumine ja seadista see uuesti." }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Kas seadistada Nettigo Air Monitori asukohas {host}?" }, + "credentials": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta kasutajanimi jasalas\u00f5na." + }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta hosti jaoks \u00f5ige kasutajanimi ja salas\u00f5na: {host}" + }, "user": { "data": { "host": "host" diff --git a/homeassistant/components/nam/translations/id.json b/homeassistant/components/nam/translations/id.json index e289d14dd37..7b31e71e6d1 100644 --- a/homeassistant/components/nam/translations/id.json +++ b/homeassistant/components/nam/translations/id.json @@ -2,14 +2,34 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "device_unsupported": "Perangkat tidak didukung." + "device_unsupported": "Perangkat tidak didukung.", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_unsuccessful": "Proses autentikasi ulang tidak berhasil. Hapus integrasi dan siapkan kembali." }, "error": { "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { + "confirm_discovery": { + "description": "Ingin menyiapkan Nettigo Air Monitor di {host}?" + }, + "credentials": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan nama pengguna dan kata sandi." + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan nama pengguna dan kata sandi yang benar untuk: {host}." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 5e95c9baffe..373d86e2b56 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -1,10 +1,26 @@ { "config": { "abort": { - "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "{host} \u306bNettigo Air Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "credentials": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30db\u30b9\u30c8: {host} \u306e\u6b63\u3057\u3044\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json index 923efe4937b..f66a6b148e3 100644 --- a/homeassistant/components/nam/translations/no.json +++ b/homeassistant/components/nam/translations/no.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "device_unsupported": "Enheten st\u00f8ttes ikke." + "device_unsupported": "Enheten st\u00f8ttes ikke.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." }, "error": { "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Vil du konfigurere Nettigo Air Monitor p\u00e5 {host} ?" }, + "credentials": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn brukernavn og passord." + }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn riktig brukernavn og passord for verten: {host}" + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index bdf5014428d..94124da2e32 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -2,10 +2,12 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "{name}", @@ -13,6 +15,20 @@ "confirm_discovery": { "description": "Czy chcesz skonfigurowa\u0107 Nettigo Air Monitor {host}?" }, + "credentials": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Prosz\u0119 wpisa\u0107 nazw\u0119 u\u017cytkownika i has\u0142o." + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Prosz\u0119 wpisa\u0107 prawid\u0142ow\u0105 nazw\u0119 u\u017cytkownika i has\u0142o dla hosta: {host}." + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json index 1e94cb848f3..d73f9aa0ee5 100644 --- a/homeassistant/components/nam/translations/ru.json +++ b/homeassistant/components/nam/translations/ru.json @@ -2,17 +2,34 @@ "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.", - "device_unsupported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + "device_unsupported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "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.", + "reauth_unsuccessful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0423\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." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nettigo Air Monitor ({host})?" }, + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430: {host}" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json index 5d0b3f179af..f3c343f515c 100644 --- a/homeassistant/components/nam/translations/zh-Hant.json +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "device_unsupported": "\u88dd\u7f6e\u4e0d\u652f\u63f4\u3002" + "device_unsupported": "\u88dd\u7f6e\u4e0d\u652f\u63f4\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u79fb\u9664\u88dd\u7f6e\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u5740\u70ba {host} \u7684 Nettigo Air Monitor\uff1f" }, + "credentials": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u548c\u5bc6\u78bc" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165 {host} \u4e3b\u6a5f\u7684\u6b63\u78ba\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json index aaded77c720..298e681cadf 100644 --- a/homeassistant/components/nanoleaf/translations/id.json +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -9,10 +9,14 @@ }, "error": { "cannot_connect": "Gagal terhubung", + "not_allowing_new_tokens": "Nanoleaf tidak mengizinkan token baru, ikuti petunjuk di atas.", "unknown": "Kesalahan yang tidak diharapkan" }, "flow_title": "{name}", "step": { + "link": { + "title": "Tautkan Nanoleaf" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index d57d1949630..b1dcabf9bfd 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -6,7 +6,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Nanoleaf\u306e\u96fb\u6e90\u30dc\u30bf\u30f3\u3092\u30dc\u30bf\u30f3\u306eLED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u30675\u79d2\u9593\u62bc\u3057\u7d9a\u3051\u3066\u304b\u3089\u300130\u79d2\u4ee5\u5185\u306b **\u9001\u4fe1** \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "Nanoleaf\u306e\u96fb\u6e90\u30dc\u30bf\u30f3\u3092\u30dc\u30bf\u30f3\u306eLED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u30675\u79d2\u9593\u62bc\u3057\u7d9a\u3051\u3066\u304b\u3089\u300130\u79d2\u4ee5\u5185\u306b **\u9001\u4fe1(submit)** \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Nanoleaf\u3092\u30ea\u30f3\u30af\u3059\u308b" }, "user": { diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 948e3a894f7..b78b1764343 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -21,7 +21,9 @@ "auth": { "data": { "code": "Token Akses" - } + }, + "description": "Untuk menautkan akun Google Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel Token Auth yang disediakan di bawah ini.", + "title": "Tautkan Akun Google" }, "init": { "data": { diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json index c681a7be2e2..cc7a70d7e30 100644 --- a/homeassistant/components/nexia/translations/ja.json +++ b/homeassistant/components/nexia/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "brand": "\u30d6\u30e9\u30f3\u30c9" + "brand": "\u30d6\u30e9\u30f3\u30c9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index 324e2e4d71b..8f4cdabb4fd 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -5,7 +5,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", "title": "Android TV / Fire TV\u306e\u901a\u77e5" } } diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index a28acc92335..522df8978e3 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -5,9 +5,15 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } } diff --git a/homeassistant/components/nuheat/translations/ja.json b/homeassistant/components/nuheat/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/nuheat/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 79fa93725e1..5da4617a283 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -14,7 +14,9 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index a42202307f2..cffb4f55b44 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index 62f5db806c1..6d3233a4e22 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 58af1684052..1188b08dac5 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -3,7 +3,8 @@ "step": { "owserver": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } }, "user": { diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 4ac3f9c01e4..1a10c5e6dba 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -3,13 +3,16 @@ "step": { "configure": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" }, "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, "manual_input": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } }, "user": { @@ -18,5 +21,14 @@ } } } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "rtsp_transport": "RTSP\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30e1\u30ab\u30cb\u30ba\u30e0" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json new file mode 100644 index 00000000000..d0ff21c4db7 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + }, + "step": { + "init": { + "data": { + "device": "\u30d1\u30b9\u307e\u305f\u306fURL", + "id": "ID" + }, + "title": "OpenTherm\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 5075ec2c965..8851a532b99 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -17,5 +17,12 @@ "title": "Isi informasi Anda" } } + }, + "options": { + "step": { + "init": { + "title": "Konfigurasikan OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json index bda76d76231..2b55e3aa88d 100644 --- a/homeassistant/components/ovo_energy/translations/ja.json +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -3,9 +3,15 @@ "flow_title": "{username}", "step": { "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "OVO Energy\u306e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u73fe\u5728\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "OVO Energy\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u8a2d\u5b9a\u3057\u3066\u3001\u30a8\u30cd\u30eb\u30ae\u30fc\u4f7f\u7528\u91cf\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u3002", "title": "OVO Energy\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3059\u308b" } diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index e9d91d3238b..d3a0316114f 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "OwnTracks\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", "title": "OwnTracks\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/p1_monitor/translations/id.json b/homeassistant/components/p1_monitor/translations/id.json index 8c96f3ee6cb..a1241576b2e 100644 --- a/homeassistant/components/p1_monitor/translations/id.json +++ b/homeassistant/components/p1_monitor/translations/id.json @@ -9,7 +9,8 @@ "data": { "host": "Host", "name": "Nama" - } + }, + "description": "Siapkan Monitor P1 untuk diintegrasikan dengan Home Assistant." } } } diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json index b9a1b948a91..8bff8159841 100644 --- a/homeassistant/components/philips_js/translations/id.json +++ b/homeassistant/components/philips_js/translations/id.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Perangkat diminta untuk dinyalakan" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Izinkan penggunaan layanan notifikasi data." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/picnic/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index c2a6e29432a..444c0b517e2 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -4,6 +4,7 @@ "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Plaato Airlock\u3067Webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({doevice_type} \u306e\u540d\u524d\u304c **{device_name}** \u3067 was_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u3067\u3046\u307e\u304f\u3044\u304f\u30cf\u30ba\u3067\u3059\uff01\n\u539f\u6587: See [the documentation]({doevice_type} with name **{device_name}** was_url}) for successfurtherlly dsetails.up!" }, "error": { + "invalid_webhook_device": "Webhook\u3078\u306e\u30c7\u30fc\u30bf\u9001\u4fe1\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002Airlock\u3067\u306e\u307f\u5229\u7528\u53ef\u80fd\u3067\u3059", "no_api_method": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8ffd\u52a0\u3059\u308b\u304b\u3001webhook\u3092\u9078\u629e\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "step": { diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 6dda0588efc..13ab62c364e 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -4,9 +4,12 @@ "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "ssl_error": "SSL\u8a3c\u660e\u66f8\u306e\u554f\u984c" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8", "token": "\u30c8\u30fc\u30af\u30f3(\u30aa\u30d7\u30b7\u30e7\u30f3)" } }, @@ -17,5 +20,15 @@ "title": "Plex\u30b5\u30fc\u30d0\u30fc\u3092\u9078\u629e" } } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "use_episode_art": "\u30a8\u30d4\u30bd\u30fc\u30c9\u30a2\u30fc\u30c8\u3092\u4f7f\u7528" + }, + "description": "Plex Media Player\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json new file mode 100644 index 00000000000..453c7607fd8 --- /dev/null +++ b/homeassistant/components/plugwise/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user_gateway": { + "data": { + "password": "Smile ID", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/ja.json b/homeassistant/components/plum_lightpad/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 36653567d6a..b5b5bd84b50 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002" + }, "error": { - "follow_link": "\u9001\u4fe1 \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "follow_link": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "auth": { "title": "\u8a8d\u8a3c\u30dd\u30a4\u30f3\u30c8" + }, + "user": { + "data": { + "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" + } } } } diff --git a/homeassistant/components/poolsense/translations/ja.json b/homeassistant/components/poolsense/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/poolsense/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index 2a79188ae5b..c0bde46f9ba 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -1,5 +1,12 @@ { "config": { - "flow_title": "{ip_address}" + "flow_title": "{ip_address}", + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ja.json b/homeassistant/components/progettihwsw/translations/ja.json index bcfc9c3ce4f..dc9a663ff69 100644 --- a/homeassistant/components/progettihwsw/translations/ja.json +++ b/homeassistant/components/progettihwsw/translations/ja.json @@ -6,7 +6,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" }, "title": "\u30dc\u30fc\u30c9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/prosegur/translations/id.json b/homeassistant/components/prosegur/translations/id.json index 7b4c569c516..29fecb900da 100644 --- a/homeassistant/components/prosegur/translations/id.json +++ b/homeassistant/components/prosegur/translations/id.json @@ -12,6 +12,7 @@ "step": { "reauth_confirm": { "data": { + "description": "Autentikasi ulang dengan akun Prosegur.", "password": "Kata Sandi", "username": "Nama Pengguna" } diff --git a/homeassistant/components/prosegur/translations/ja.json b/homeassistant/components/prosegur/translations/ja.json index 76367501945..4fb86c92cf5 100644 --- a/homeassistant/components/prosegur/translations/ja.json +++ b/homeassistant/components/prosegur/translations/ja.json @@ -3,12 +3,14 @@ "step": { "reauth_confirm": { "data": { - "description": "Prosegur\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" + "description": "Prosegur\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, "user": { "data": { - "country": "\u56fd" + "country": "\u56fd", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 8d0bb1dd228..5c504dc2670 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -10,6 +10,7 @@ }, "step": { "creds": { + "description": "\u8a8d\u8a3c\u60c5\u5831\u304c\u5fc5\u8981\u3067\u3059\u3002'\u9001\u4fe1(submit)' \u3092\u62bc\u3057\u3066\u3001PS4\u306e2nd Screen App\u3067\u30c7\u30d0\u30a4\u30b9\u3092\u66f4\u65b0\u3057\u3001'Home-Assistant' \u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u7d9a\u884c\u3057\u307e\u3059\u3002", "title": "Play Station 4" }, "link": { diff --git a/homeassistant/components/rainmachine/translations/ja.json b/homeassistant/components/rainmachine/translations/ja.json index a86d3b073f3..4dc1974d819 100644 --- a/homeassistant/components/rainmachine/translations/ja.json +++ b/homeassistant/components/rainmachine/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "ip_address": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + "ip_address": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" }, "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index c4ba0aad4e4..16f6ef73e2c 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "setup_network": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + } + } + }, "device_automation": { "action_type": { "send_command": "\u30b3\u30de\u30f3\u30c9\u3092\u9001\u4fe1: {subtype}", diff --git a/homeassistant/components/ring/translations/ja.json b/homeassistant/components/ring/translations/ja.json index dc544bbcb75..c9bb1f3a111 100644 --- a/homeassistant/components/ring/translations/ja.json +++ b/homeassistant/components/ring/translations/ja.json @@ -6,6 +6,11 @@ "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, "title": "2\u8981\u7d20\u8a8d\u8a3c" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } } } } diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/risco/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/ja.json b/homeassistant/components/rituals_perfume_genie/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/roku/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index ac7cef536d7..04f64cdf3ca 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -7,13 +7,25 @@ "host": "\u30db\u30b9\u30c8" } }, + "link": { + "title": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u53d6\u5f97" + }, "link_manual": { - "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u81ea\u52d5\u7684\u306b\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b21\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u308b\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044: {auth_help_url}" + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u81ea\u52d5\u7684\u306b\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b21\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u308b\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044: {auth_help_url}", + "title": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b" }, "manual": { "data": { "host": "\u30db\u30b9\u30c8" } + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } } } } diff --git a/homeassistant/components/ruckus_unleashed/translations/ja.json b/homeassistant/components/ruckus_unleashed/translations/ja.json index a42202307f2..9f68231f0d2 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ja.json +++ b/homeassistant/components/ruckus_unleashed/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 60f77922a81..0033eab6f90 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -2,6 +2,13 @@ "config": { "abort": { "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index a48fe4e5ef6..a6f3c7c8023 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -3,6 +3,9 @@ "flow_title": "{name}", "step": { "gateway_entry": { + "data": { + "port": "\u30dd\u30fc\u30c8" + }, "description": "ScreenLogic\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "ScreenLogic" }, diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 190f6374790..6099051cbae 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "title": "Sense Energy Monitor\u306b\u63a5\u7d9a\u3059\u308b" diff --git a/homeassistant/components/sharkiq/translations/ja.json b/homeassistant/components/sharkiq/translations/ja.json new file mode 100644 index 00000000000..0c16328bbfd --- /dev/null +++ b/homeassistant/components/sharkiq/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 509eb2ec443..859cdc96cb9 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -1,6 +1,14 @@ { "config": { "step": { + "confirm_discovery": { + "description": "{model} {host} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f \n\n\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u4fdd\u8b77\u3055\u308c\u3066\u3044\u308b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u7d9a\u3051\u308b\u524d\u306b\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u4fdd\u8b77\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3057\u305f\u3068\u304d\u306b\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30dc\u30bf\u30f3\u3092\u4f7f\u7528\u3057\u3066\u624b\u52d5\u3067\u30c7\u30d0\u30a4\u30b9\u3092\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3055\u305b\u308b\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u6b21\u306e\u30c7\u30fc\u30bf\u66f4\u65b0\u3092\u5f85\u3061\u307e\u3059\u3002" + }, + "credentials": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index 39e0558acd1..57e6384d276 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -1,12 +1,16 @@ { "config": { "step": { + "additional_account": { + "title": "\u73fe\u5728\u306e\u30dd\u30fc\u30c8\u306b\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + }, "user": { "data": { "account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", "additional_account": "\u8ffd\u52a0\u306e\u30a2\u30ab\u30a6\u30f3\u30c8", "encryption_key": "\u6697\u53f7\u5316\u30ad\u30fc", "ping_interval": "ping\u9593\u9694(\u5206)", + "port": "\u30dd\u30fc\u30c8", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index d645425297e..30405f4a319 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -17,8 +17,17 @@ "mfa": { "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { - "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066 Submit \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json index 8145ff4cbaa..59ce3959a5f 100644 --- a/homeassistant/components/sma/translations/ja.json +++ b/homeassistant/components/sma/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "SMA Solar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/smart_meter_texas/translations/ja.json b/homeassistant/components/smart_meter_texas/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/smarthab/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index c32e7a7364c..89358ad113f 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -1,7 +1,10 @@ { "config": { "error": { + "app_setup_error": "SmartApp\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "token_forbidden": "\u30c8\u30fc\u30af\u30f3\u306b\u5fc5\u8981\u306aOAuth\u30b9\u30b3\u30fc\u30d7(OAuth scopes)\u304c\u3042\u308a\u307e\u305b\u3093\u3002", + "token_invalid_format": "\u30c8\u30fc\u30af\u30f3\u306f\u3001UID/GUID\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "token_unauthorized": "\u30c8\u30fc\u30af\u30f3\u304c\u7121\u52b9\u3001\u3082\u3057\u304f\u306f\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "webhook_error": "SmartThings\u304cWebhook URL\u3092\u691c\u8a3c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002Webhook URL\u304c\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u53ef\u80fd\u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index 7ade94ecd29..1efd17a5cfa 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -2,6 +2,10 @@ "config": { "step": { "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "SmartTub\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30ed\u30b0\u30a4\u30f3" } } diff --git a/homeassistant/components/solaredge/translations/ja.json b/homeassistant/components/solaredge/translations/ja.json index a81d9a23b71..b8dbac2050f 100644 --- a/homeassistant/components/solaredge/translations/ja.json +++ b/homeassistant/components/solaredge/translations/ja.json @@ -3,8 +3,10 @@ "step": { "user": { "data": { - "name": "\u3053\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u540d\u524d" - } + "name": "\u3053\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u540d\u524d", + "site_id": "SolarEdge\u306e\u30b5\u30a4\u30c8ID" + }, + "title": "\u3053\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306eAPI\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u306e\u5b9a\u7fa9" } } } diff --git a/homeassistant/components/solarlog/translations/ja.json b/homeassistant/components/solarlog/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/solarlog/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 8822fbc92ea..3797126ff71 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -2,6 +2,14 @@ "config": { "abort": { "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 38b1c2e9606..70205a43e6e 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -4,7 +4,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/songpal/translations/ja.json b/homeassistant/components/songpal/translations/ja.json new file mode 100644 index 00000000000..2021184a339 --- /dev/null +++ b/homeassistant/components/songpal/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "init": { + "description": "{name} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/spider/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json index 2b5dffa0ba7..ea8623de542 100644 --- a/homeassistant/components/squeezebox/translations/ja.json +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -1,9 +1,12 @@ { "config": { + "flow_title": "{host}", "step": { "edit": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" } }, "user": { diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 400505d7385..a4fccf05982 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -1,3 +1,12 @@ { + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + }, "title": "SRP\u30a8\u30cd\u30eb\u30ae\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json index c3e0d5ecbc6..79405cd2d4a 100644 --- a/homeassistant/components/starline/translations/ja.json +++ b/homeassistant/components/starline/translations/ja.json @@ -1,8 +1,37 @@ { "config": { + "error": { + "error_auth_app": "\u4e0d\u6b63\u306a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID\u307e\u305f\u306f\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", + "error_auth_mfa": "\u4e0d\u9069\u5207\u306a\u30b3\u30fc\u30c9", + "error_auth_user": "\u30e6\u30fc\u30b6\u30fc\u30cd\u30fc\u30e0\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9055\u3044\u307e\u3059" + }, "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" + }, + "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u753b\u50cf\u304b\u3089\u306e\u30b3\u30fc\u30c9" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, "auth_mfa": { + "data": { + "mfa_code": "SMS\u30b3\u30fc\u30c9" + }, "title": "2\u8981\u7d20\u8a8d\u8a3c" + }, + "auth_user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "StarLine\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9", + "title": "\u30e6\u30fc\u30b6\u30fc\u306e\u8cc7\u683c\u60c5\u5831" } } } diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/subaru/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/ja.json b/homeassistant/components/surepetcare/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/surepetcare/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 8e977d57200..e39d2355caa 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -8,7 +8,8 @@ "step": { "user": { "data": { - "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9" + "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "Switchbot\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/syncthru/translations/ja.json b/homeassistant/components/syncthru/translations/ja.json new file mode 100644 index 00000000000..cc965c9ca6f --- /dev/null +++ b/homeassistant/components/syncthru/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "syncthru_not_supported": "\u30c7\u30d0\u30a4\u30b9\u306fSyncThru\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 0e8464ccf72..2cfa77c3004 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -4,7 +4,16 @@ "reconfigure_successful": "\u518d\u8a2d\u5b9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "step": { + "link": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + }, "reauth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "\u7406\u7531: {details}", "title": "Synology DSM\u518d\u8a8d\u8a3c\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" }, @@ -16,7 +25,9 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index d651ca67d2f..538a97825a2 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -4,7 +4,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/tile/translations/ja.json b/homeassistant/components/tile/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/tile/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index 213e642271a..62c565c4f57 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -2,6 +2,13 @@ "config": { "abort": { "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json index 40c0e02eca1..df071d8e5f7 100644 --- a/homeassistant/components/tradfri/translations/ja.json +++ b/homeassistant/components/tradfri/translations/ja.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_authenticate": "\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306f\u3001Homekit\u306a\u3069\u306e\u4ed6\u306e\u30b5\u30fc\u30d0\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "invalid_key": "\u63d0\u4f9b\u3055\u308c\u305f\u30ad\u30fc\u3067\u306e\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u304c\u5f15\u304d\u7d9a\u304d\u767a\u751f\u3059\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3067\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" }, @@ -11,6 +12,7 @@ "host": "\u30db\u30b9\u30c8", "security_code": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9" }, + "description": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9\u306f\u3001\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u80cc\u9762\u306b\u3042\u308a\u307e\u3059\u3002", "title": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index a42202307f2..5af5263a537 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -3,8 +3,11 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" - } + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + }, + "title": "Transmission Client\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 8c73deb27a3..a2ee5e9a300 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -22,6 +22,7 @@ "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7" }, diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index aec02062f2f..4dd3954ca96 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{site} ({host})", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", "site": "\u30b5\u30a4\u30c8ID" }, "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/upb/translations/ja.json b/homeassistant/components/upb/translations/ja.json new file mode 100644 index 00000000000..0d7aaf26840 --- /dev/null +++ b/homeassistant/components/upb/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_upb_file": "UPB UPStart\u306e\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u306a\u3044\u304b\u7121\u52b9\u3067\u3059\u3002\u30d5\u30a1\u30a4\u30eb\u540d\u3068\u30d1\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "user": { + "data": { + "file_path": "UPStart UPB\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u30d1\u30b9\u3068\u540d\u524d\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/ja.json b/homeassistant/components/upcloud/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/upcloud/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/ja.json b/homeassistant/components/velbus/translations/ja.json new file mode 100644 index 00000000000..9c79d49ccba --- /dev/null +++ b/homeassistant/components/velbus/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u63a5\u7d9a\u6587\u5b57\u5217" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json new file mode 100644 index 00000000000..2cfa9ea621d --- /dev/null +++ b/homeassistant/components/verisure/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 85500dcafff..b152338b86a 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json index 71c0bbbb177..2a002535882 100644 --- a/homeassistant/components/vilfo/translations/ja.json +++ b/homeassistant/components/vilfo/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 9a8572b2ee1..9867f2354a7 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -2,6 +2,16 @@ "config": { "error": { "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "pairing_complete_import": { + "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json index a42202307f2..5dc41a91227 100644 --- a/homeassistant/components/volumio/translations/ja.json +++ b/homeassistant/components/volumio/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" } } } diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index 65203cc8c7e..b954dbedb6a 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -1,8 +1,17 @@ { "config": { + "error": { + "reauth_invalid": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u30aa\u30ea\u30b8\u30ca\u30eb\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093" + }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, "user": { "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7" } } diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index d0b14d727aa..42d1631efdd 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -11,9 +11,15 @@ "description": "\u76e3\u8996\u3059\u308b\u5834\u6240\u3092\u9078\u629e:" }, "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" }, "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } } diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json index cfa7169b323..4e567596b31 100644 --- a/homeassistant/components/wemo/translations/ja.json +++ b/homeassistant/components/wemo/translations/ja.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "confirm": { + "description": "Wemo\u3092\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + } + } + }, "device_automation": { "trigger_type": { "long_press": "Wemo\u30dc\u30bf\u30f3\u304c2\u79d2\u9593\u62bc\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/ja.json b/homeassistant/components/wiffi/translations/ja.json new file mode 100644 index 00000000000..8fd248072c8 --- /dev/null +++ b/homeassistant/components/wiffi/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "addr_in_use": "\u30b5\u30fc\u30d0\u30fc\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, + "step": { + "user": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index bca8f6606ca..e2e96e7b592 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -2,6 +2,14 @@ "config": { "create_entry": { "default": "\u9078\u629e\u3057\u305fWithings\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f\u3002" + }, + "step": { + "profile": { + "data": { + "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d" + }, + "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index d49599e7eb5..c40559f766c 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, "description": "WLED\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/wolflink/translations/ja.json b/homeassistant/components/wolflink/translations/ja.json new file mode 100644 index 00000000000..896966aee6c --- /dev/null +++ b/homeassistant/components/wolflink/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index d98e34886de..07bd2c60ede 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "incomplete_info": "\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u305f\u3081\u306e\u60c5\u5831\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30db\u30b9\u30c8\u307e\u305f\u306f\u30c8\u30fc\u30af\u30f3\u304c\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "not_xiaomi_miio": "\u30c7\u30d0\u30a4\u30b9\u306f(\u307e\u3060) Xiaomi Miio\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + }, "error": { + "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" }, "step": { @@ -15,7 +20,8 @@ "connect": { "data": { "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" - } + }, + "description": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u304b\u3089\u30c7\u30d0\u30a4\u30b9 \u30e2\u30c7\u30eb\u3092\u624b\u52d5\u3067\u9078\u629e\u3057\u307e\u3059\u3002" }, "device": { "data": { @@ -36,6 +42,9 @@ } }, "options": { + "error": { + "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "init": { "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json index 8325b797013..2c07ef701ff 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ja.json +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -1,9 +1,15 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, "user": { "data": { - "area_id": "\u30a8\u30ea\u30a2ID" + "area_id": "\u30a8\u30ea\u30a2ID", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index bfbd3e225ee..5b9670a9960 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -2,6 +2,9 @@ "config": { "flow_title": "{model} {id} ({host})", "step": { + "discovery_confirm": { + "description": "{model} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 599d27b807b..e577f6e1f2a 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -8,7 +8,41 @@ "step": { "confirm": { "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + }, + "port_config": { + "data": { + "baudrate": "\u30dd\u30fc\u30c8\u901f\u5ea6", + "flow_control": "\u30c7\u30fc\u30bf\u30d5\u30ed\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb", + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "\u30dd\u30fc\u30c8\u56fa\u6709\u306e\u8a2d\u5b9a\u3092\u5165\u529b", + "title": "\u8a2d\u5b9a" + }, + "user": { + "description": "Zigbee radio\u7528\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e", + "title": "ZHA" } } + }, + "device_automation": { + "trigger_subtype": { + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "close": "\u9589\u3058\u308b", + "dim_down": "\u8584\u6697\u304f\u3059\u308b", + "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", + "left": "\u5de6", + "open": "\u958b\u304f", + "right": "\u53f3", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" + }, + "trigger_type": { + "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", + "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b" + } } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json index fd2d82c0544..52e0afbe059 100644 --- a/homeassistant/components/zoneminder/translations/ja.json +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -1,16 +1,22 @@ { "config": { "abort": { + "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { "default": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u304c\u8ffd\u52a0\u3055\u308c\u307e\u3057\u305f\u3002" }, + "error": { + "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", + "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + }, "flow_title": "ZoneMinder", "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8(\u4f8b: 10.10.0.4:8010)" + "host": "\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8(\u4f8b: 10.10.0.4:8010)", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" } From 45983533158eabed09e0943520bb573cb1722d03 Mon Sep 17 00:00:00 2001 From: Robin Dupont Date: Fri, 19 Nov 2021 01:43:08 +0100 Subject: [PATCH 0615/1452] Add apparent_power for ZHA ElectricalMeasurement (#59857) * Add apparent_power for ZHA ElectricalMeasurement * Add apparent_power to REPORT_CONFIG * update device list with apparent_power attribute * update test decorators for apparent_power * remove comments * Add test for apparent_power in test_sensor --- .../zha/core/channels/homeautomation.py | 1 + homeassistant/components/zha/sensor.py | 18 +++++ tests/components/zha/test_channels.py | 1 + tests/components/zha/test_sensor.py | 51 +++++++++++--- tests/components/zha/zha_devices_list.py | 67 +++++++++++++++++++ 5 files changed, 129 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 89a9d51395e..5c3ef3a0f6c 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -65,6 +65,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): REPORT_CONFIG = ( {"attr": "active_power", "config": REPORT_CONFIG_OP}, {"attr": "active_power_max", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "apparent_power", "config": REPORT_CONFIG_OP}, {"attr": "rms_current", "config": REPORT_CONFIG_OP}, {"attr": "rms_current_max", "config": REPORT_CONFIG_DEFAULT}, {"attr": "rms_voltage", "config": REPORT_CONFIG_OP}, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 365a7d8084f..d5df223ffb0 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, @@ -313,6 +314,23 @@ class ElectricalMeasurement(Sensor): await super().async_update() +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementApparentPower( + ElectricalMeasurement, id_suffix="apparent_power" +): + """Apparent power measurement.""" + + SENSOR_ATTR = "apparent_power" + _device_class = DEVICE_CLASS_POWER + _unit = POWER_VOLT_AMPERE + _div_mul_prefix = "ac_power" + + @property + def should_poll(self) -> bool: + """Poll indirectly by ElectricalMeasurementSensor.""" + return False + + @MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"): """RMS current measurement.""" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index e2543181a1a..f4ec40fcb15 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -158,6 +158,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): { "active_power", "active_power_max", + "apparent_power", "rms_current", "rms_current_max", "rms_voltage", diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 918876fe448..af4bb082b03 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, PRESSURE_HPA, STATE_UNAVAILABLE, @@ -174,6 +175,24 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): assert hass.states.get(entity_id).attributes["active_power_max"] == "8.8" +async def async_test_em_apparent_power(hass, cluster, entity_id): + """Test electrical measurement Apparent Power sensor.""" + # update divisor cached value + await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) + await send_attributes_report(hass, cluster, {0: 1, 0x050F: 100, 10: 1000}) + assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + + await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 1000}) + assert_state(hass, entity_id, "99", POWER_VOLT_AMPERE) + + await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) + await send_attributes_report(hass, cluster, {0: 1, 0x050F: 1000, 10: 5000}) + assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + + await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 5000}) + assert_state(hass, entity_id, "9.9", POWER_VOLT_AMPERE) + + async def async_test_em_rms_current(hass, cluster, entity_id): """Test electrical measurement RMS Current sensor.""" @@ -290,25 +309,33 @@ async def async_test_powerconfiguration(hass, cluster, entity_id): homeautomation.ElectricalMeasurement.cluster_id, "electrical_measurement", async_test_electrical_measurement, - 6, + 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, - {"rms_current", "rms_voltage"}, + {"apparent_power", "rms_current", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + "electrical_measurement_apparent_power", + async_test_em_apparent_power, + 7, + {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, + {"active_power", "rms_current", "rms_voltage"}, ), ( homeautomation.ElectricalMeasurement.cluster_id, "electrical_measurement_rms_current", async_test_em_rms_current, - 6, + 7, {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, - {"active_power", "rms_voltage"}, + {"active_power", "apparent_power", "rms_voltage"}, ), ( homeautomation.ElectricalMeasurement.cluster_id, "electrical_measurement_rms_voltage", async_test_em_rms_voltage, - 6, + 7, {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1}, - {"active_power", "rms_current"}, + {"active_power", "apparent_power", "rms_current"}, ), ( general.PowerConfiguration.cluster_id, @@ -561,18 +588,22 @@ async def test_electrical_measurement_init( ( ( homeautomation.ElectricalMeasurement.cluster_id, - {"rms_voltage", "rms_current"}, + {"apparent_power", "rms_voltage", "rms_current"}, {"electrical_measurement"}, { + "electrical_measurement_apparent_power", "electrical_measurement_rms_voltage", "electrical_measurement_rms_current", }, ), ( homeautomation.ElectricalMeasurement.cluster_id, - {"rms_current"}, + {"apparent_power", "rms_current"}, {"electrical_measurement_rms_voltage", "electrical_measurement"}, - {"electrical_measurement_rms_current"}, + { + "electrical_measurement_apparent_power", + "electrical_measurement_rms_current", + }, ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -580,6 +611,7 @@ async def test_electrical_measurement_init( { "electrical_measurement_rms_voltage", "electrical_measurement", + "electrical_measurement_apparent_power", "electrical_measurement_rms_current", }, set(), @@ -853,6 +885,7 @@ async def test_elec_measurement_skip_unsupported_attribute( all_attrs = { "active_power", "active_power_max", + "apparent_power", "rms_current", "rms_current_max", "rms_voltage", diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 2ef2e578dce..41d4b0d1bee 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -117,6 +117,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.centralite_3210_l_77665544_electrical_measurement", + "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", + "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", "sensor.centralite_3210_l_77665544_smartenergy_metering", @@ -144,6 +146,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -1448,6 +1455,7 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_77665544_analog_input", "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", "switch.lumi_lumi_plug_maus01_77665544_on_off", @@ -1473,6 +1481,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -1517,6 +1530,7 @@ DEVICES = [ "light.lumi_lumi_relay_c2acn01_77665544_on_off", "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", ], @@ -1531,6 +1545,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -2602,6 +2621,7 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", ], @@ -2616,6 +2636,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -2646,6 +2671,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.osram_plug_01_77665544_electrical_measurement", + "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", "switch.osram_plug_01_77665544_on_off", @@ -2661,6 +2687,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -2930,6 +2961,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", "switch.securifi_ltd_unk_model_77665544_on_off", @@ -2945,6 +2977,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -3020,6 +3057,7 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.sercomm_corp_sz_esw01_77665544_on_off", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", @@ -3046,6 +3084,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -3119,6 +3162,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", "switch.sinope_technologies_rm3250zb_77665544_on_off", @@ -3134,6 +3178,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -3171,6 +3220,7 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "climate.sinope_technologies_th1123zb_77665544_thermostat", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", @@ -3192,6 +3242,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -3234,6 +3289,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1124zb_77665544_temperature", @@ -3261,6 +3317,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", @@ -3291,6 +3352,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.smartthings_outletv4_77665544_electrical_measurement", + "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", "switch.smartthings_outletv4_77665544_on_off", @@ -3306,6 +3368,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", From 347c4ea1377ba7c80873fc8b9c5717b668f93995 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Nov 2021 22:23:20 -0600 Subject: [PATCH 0616/1452] Bump zeroconf to 0.37.0 (#59932) --- .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bosch_shc/conftest.py | 8 ++ tests/components/default_config/conftest.py | 8 ++ .../devolo_home_control/conftest.py | 5 ++ .../devolo_home_network/conftest.py | 5 ++ tests/components/esphome/conftest.py | 8 ++ tests/components/homekit/test_config_flow.py | 10 ++- tests/components/homekit/test_homekit.py | 81 ++++++++++--------- .../components/homekit_controller/conftest.py | 4 +- tests/components/zeroconf/conftest.py | 12 --- tests/conftest.py | 17 +++- 14 files changed, 106 insertions(+), 60 deletions(-) create mode 100644 tests/components/bosch_shc/conftest.py create mode 100644 tests/components/default_config/conftest.py create mode 100644 tests/components/esphome/conftest.py diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 95f9407661b..338456ca576 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.36.13"], + "requirements": ["zeroconf==0.37.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c80b4a0416c..b4c6356fbe4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.26 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.13 +zeroconf==0.37.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 7b060b33723..41d8ac0201d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.13 +zeroconf==0.37.0 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d399d56e424..ef8605a6530 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.13 +zeroconf==0.37.0 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py new file mode 100644 index 00000000000..6a3797ad094 --- /dev/null +++ b/tests/components/bosch_shc/conftest.py @@ -0,0 +1,8 @@ +"""bosch_shc session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def bosch_shc_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/default_config/conftest.py b/tests/components/default_config/conftest.py new file mode 100644 index 00000000000..4714102eff9 --- /dev/null +++ b/tests/components/default_config/conftest.py @@ -0,0 +1,8 @@ +"""default_config session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def default_config_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py index 487831b0fa4..65e8b9b1c64 100644 --- a/tests/components/devolo_home_control/conftest.py +++ b/tests/components/devolo_home_control/conftest.py @@ -31,3 +31,8 @@ def patch_mydevolo(request): return_value=["1400000000000001", "1400000000000002"], ): yield + + +@pytest.fixture(autouse=True) +def devolo_home_control_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index ab798cd5cfd..255a9ec1f7c 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -39,3 +39,8 @@ def mock_validate_input(): return_value=info, ): yield info + + +@pytest.fixture(autouse=True) +def devolo_home_network_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py new file mode 100644 index 00000000000..91368044723 --- /dev/null +++ b/tests/components/esphome/conftest.py @@ -0,0 +1,8 @@ +"""esphome session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def esphome_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index f1564f9e3ae..fb65895604e 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -362,7 +362,13 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): async def test_options_flow_devices( - mock_hap, hass, demo_cleanup, device_reg, entity_reg, mock_get_source_ip + mock_hap, + hass, + demo_cleanup, + device_reg, + entity_reg, + mock_get_source_ip, + mock_async_zeroconf, ): """Test devices can be bridged.""" config_entry = MockConfigEntry( @@ -441,7 +447,7 @@ async def test_options_flow_devices( async def test_options_flow_devices_preserved_when_advanced_off( - mock_hap, hass, mock_get_source_ip + mock_hap, hass, mock_get_source_ip, mock_async_zeroconf ): """Test devices are preserved if they were added in advanced mode but it was turned off.""" config_entry = MockConfigEntry( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 58aa9741fa3..44ed71afaca 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -142,7 +142,7 @@ def _mock_pyhap_bridge(): ) -async def test_setup_min(hass, mock_zeroconf): +async def test_setup_min(hass, mock_async_zeroconf): """Test async_setup with min config options.""" entry = MockConfigEntry( @@ -181,7 +181,7 @@ async def test_setup_min(hass, mock_zeroconf): assert mock_homekit().async_start.called is True -async def test_homekit_setup(hass, hk_driver, mock_zeroconf): +async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf): """Test setup of bridge and driver.""" entry = MockConfigEntry( domain=DOMAIN, @@ -226,7 +226,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): assert homekit.driver.safe_mode is False -async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): +async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf): """Test setup with given IP address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -247,11 +247,10 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): entry_title=entry.title, ) - mock_zeroconf = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) uuid = await hass.helpers.instance_id.async_get() with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - await hass.async_add_executor_job(homekit.setup, mock_zeroconf, uuid) + await hass.async_add_executor_job(homekit.setup, mock_async_zeroconf, uuid) mock_driver.assert_called_with( hass, entry.entry_id, @@ -262,12 +261,12 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - async_zeroconf_instance=mock_zeroconf, + async_zeroconf_instance=mock_async_zeroconf, zeroconf_server=f"{uuid}-hap.local.", ) -async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): +async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf): """Test setup with given IP address to advertise.""" entry = MockConfigEntry( domain=DOMAIN, @@ -308,7 +307,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): ) -async def test_homekit_add_accessory(hass, mock_zeroconf): +async def test_homekit_add_accessory(hass, mock_async_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" entry = MockConfigEntry( @@ -345,7 +344,7 @@ async def test_homekit_add_accessory(hass, mock_zeroconf): @pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) async def test_homekit_warn_add_accessory_bridge( - hass, acc_category, mock_zeroconf, caplog + hass, acc_category, mock_async_zeroconf, caplog ): """Test we warn when adding cameras or tvs to a bridge.""" @@ -373,7 +372,7 @@ async def test_homekit_warn_add_accessory_bridge( assert "accessory mode" in caplog.text -async def test_homekit_remove_accessory(hass, mock_zeroconf): +async def test_homekit_remove_accessory(hass, mock_async_zeroconf): """Remove accessory from bridge.""" entry = await async_init_integration(hass) @@ -391,7 +390,7 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): assert len(homekit.bridge.accessories) == 0 -async def test_homekit_entity_filter(hass, mock_zeroconf): +async def test_homekit_entity_filter(hass, mock_async_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -410,7 +409,7 @@ async def test_homekit_entity_filter(hass, mock_zeroconf): assert hass.states.get("light.demo") not in filtered_states -async def test_homekit_entity_glob_filter(hass, mock_zeroconf): +async def test_homekit_entity_glob_filter(hass, mock_async_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -434,7 +433,7 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf): assert hass.states.get("light.included_test") in filtered_states -async def test_homekit_start(hass, hk_driver, mock_zeroconf, device_reg): +async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -509,7 +508,9 @@ async def test_homekit_start(hass, hk_driver, mock_zeroconf, device_reg): assert homekit.driver.state.config_version == 1 -async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroconf): +async def test_homekit_start_with_a_broken_accessory( + hass, hk_driver, mock_async_zeroconf +): """Test HomeKit start method.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} @@ -549,7 +550,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc async def test_homekit_start_with_a_device( - hass, hk_driver, mock_zeroconf, demo_cleanup, device_reg, entity_reg + hass, hk_driver, mock_async_zeroconf, demo_cleanup, device_reg, entity_reg ): """Test HomeKit start method with a device.""" @@ -611,7 +612,7 @@ async def test_homekit_stop(hass): assert homekit.driver.async_stop.called is True -async def test_homekit_reset_accessories(hass, mock_zeroconf): +async def test_homekit_reset_accessories(hass, mock_async_zeroconf): """Test resetting HomeKit accessories.""" entry = MockConfigEntry( @@ -656,7 +657,7 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): homekit.status = STATUS_READY -async def test_homekit_unpair(hass, device_reg, mock_zeroconf): +async def test_homekit_unpair(hass, device_reg, mock_async_zeroconf): """Test unpairing HomeKit accessories.""" entry = MockConfigEntry( @@ -698,7 +699,7 @@ async def test_homekit_unpair(hass, device_reg, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_zeroconf): +async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_async_zeroconf): """Test unpairing HomeKit accessories with invalid device id.""" entry = MockConfigEntry( @@ -736,7 +737,7 @@ async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_zeroconf) homekit.status = STATUS_STOPPED -async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_zeroconf): +async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_async_zeroconf): """Test unpairing HomeKit accessories with a non-homekit device id.""" entry = MockConfigEntry( @@ -784,7 +785,7 @@ async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_zeroconf homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): +async def test_homekit_reset_accessories_not_supported(hass, mock_async_zeroconf): """Test resetting HomeKit accessories with an unsupported entity.""" entry = MockConfigEntry( @@ -828,7 +829,7 @@ async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_state_missing(hass, mock_zeroconf): +async def test_homekit_reset_accessories_state_missing(hass, mock_async_zeroconf): """Test resetting HomeKit accessories when the state goes missing.""" entry = MockConfigEntry( @@ -870,7 +871,7 @@ async def test_homekit_reset_accessories_state_missing(hass, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_not_bridged(hass, mock_zeroconf): +async def test_homekit_reset_accessories_not_bridged(hass, mock_async_zeroconf): """Test resetting HomeKit accessories when the state is not bridged.""" entry = MockConfigEntry( @@ -912,7 +913,7 @@ async def test_homekit_reset_accessories_not_bridged(hass, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_reset_single_accessory(hass, mock_zeroconf): +async def test_homekit_reset_single_accessory(hass, mock_async_zeroconf): """Test resetting HomeKit single accessory.""" entry = MockConfigEntry( @@ -951,7 +952,7 @@ async def test_homekit_reset_single_accessory(hass, mock_zeroconf): homekit.status = STATUS_READY -async def test_homekit_reset_single_accessory_unsupported(hass, mock_zeroconf): +async def test_homekit_reset_single_accessory_unsupported(hass, mock_async_zeroconf): """Test resetting HomeKit single accessory with an unsupported entity.""" entry = MockConfigEntry( @@ -988,7 +989,7 @@ async def test_homekit_reset_single_accessory_unsupported(hass, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_reset_single_accessory_state_missing(hass, mock_zeroconf): +async def test_homekit_reset_single_accessory_state_missing(hass, mock_async_zeroconf): """Test resetting HomeKit single accessory when the state goes missing.""" entry = MockConfigEntry( @@ -1024,7 +1025,7 @@ async def test_homekit_reset_single_accessory_state_missing(hass, mock_zeroconf) homekit.status = STATUS_STOPPED -async def test_homekit_reset_single_accessory_no_match(hass, mock_zeroconf): +async def test_homekit_reset_single_accessory_no_match(hass, mock_async_zeroconf): """Test resetting HomeKit single accessory when the entity id does not match.""" entry = MockConfigEntry( @@ -1060,7 +1061,9 @@ async def test_homekit_reset_single_accessory_no_match(hass, mock_zeroconf): homekit.status = STATUS_STOPPED -async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroconf): +async def test_homekit_too_many_accessories( + hass, hk_driver, caplog, mock_async_zeroconf +): """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) @@ -1090,7 +1093,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco async def test_homekit_finds_linked_batteries( - hass, hk_driver, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1161,7 +1164,7 @@ async def test_homekit_finds_linked_batteries( async def test_homekit_async_get_integration_fails( - hass, hk_driver, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) @@ -1230,7 +1233,7 @@ async def test_homekit_async_get_integration_fails( ) -async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): +async def test_yaml_updates_update_config_entry_for_name(hass, mock_async_zeroconf): """Test async_setup with imported config.""" entry = MockConfigEntry( @@ -1274,7 +1277,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): mock_homekit().async_start.assert_called() -async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): +async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1306,7 +1309,7 @@ def _write_data(path: str, data: dict) -> None: async def test_homekit_ignored_missing_devices( - hass, hk_driver, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" @@ -1376,7 +1379,7 @@ async def test_homekit_ignored_missing_devices( async def test_homekit_finds_linked_motion_sensors( - hass, hk_driver, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1438,7 +1441,7 @@ async def test_homekit_finds_linked_motion_sensors( async def test_homekit_finds_linked_humidity_sensors( - hass, hk_driver, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1502,7 +1505,7 @@ async def test_homekit_finds_linked_humidity_sensors( ) -async def test_reload(hass, mock_zeroconf): +async def test_reload(hass, mock_async_zeroconf): """Test we can reload from yaml.""" entry = MockConfigEntry( @@ -1574,7 +1577,7 @@ async def test_reload(hass, mock_zeroconf): async def test_homekit_start_in_accessory_mode( - hass, hk_driver, mock_zeroconf, device_reg + hass, hk_driver, mock_async_zeroconf, device_reg ): """Test HomeKit start method in accessory mode.""" entry = await async_init_integration(hass) @@ -1605,7 +1608,7 @@ async def test_homekit_start_in_accessory_mode( async def test_homekit_start_in_accessory_mode_unsupported_entity( - hass, hk_driver, mock_zeroconf, device_reg, caplog + hass, hk_driver, mock_async_zeroconf, device_reg, caplog ): """Test HomeKit start method in accessory mode with an unsupported entity.""" entry = await async_init_integration(hass) @@ -1635,7 +1638,7 @@ async def test_homekit_start_in_accessory_mode_unsupported_entity( async def test_homekit_start_in_accessory_mode_missing_entity( - hass, hk_driver, mock_zeroconf, device_reg, caplog + hass, hk_driver, mock_async_zeroconf, device_reg, caplog ): """Test HomeKit start method in accessory mode when entity is not available.""" entry = await async_init_integration(hass) @@ -1659,7 +1662,7 @@ async def test_homekit_start_in_accessory_mode_missing_entity( assert "entity not available" in caplog.text -async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog): +async def test_wait_for_port_to_free(hass, hk_driver, mock_async_zeroconf, caplog): """Test we wait for the port to free before declaring unload success.""" entry = MockConfigEntry( diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index dc27162bc57..174fc4f7b8d 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -30,5 +30,5 @@ def controller(hass): @pytest.fixture(autouse=True) -def homekit_mock_zeroconf(mock_zeroconf): - """Mock zeroconf in all homekit tests.""" +def hk_mock_async_zeroconf(mock_async_zeroconf): + """Auto mock zeroconf.""" diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py index cbe2ec8dc26..d52f8234922 100644 --- a/tests/components/zeroconf/conftest.py +++ b/tests/components/zeroconf/conftest.py @@ -1,5 +1,4 @@ """Tests for the Zeroconf component.""" -from unittest.mock import AsyncMock, patch import pytest @@ -8,14 +7,3 @@ import pytest def zc_mock_get_source_ip(mock_get_source_ip): """Enable the mock_get_source_ip fixture for all zeroconf tests.""" return mock_get_source_ip - - -@pytest.fixture -def mock_async_zeroconf(mock_zeroconf): - """Mock AsyncZeroconf.""" - with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc: - zc = mock_aiozc.return_value - zc.async_register_service = AsyncMock() - zc.zeroconf.async_wait_for_start = AsyncMock() - zc.ha_async_close = AsyncMock() - yield zc diff --git a/tests/conftest.py b/tests/conftest.py index f6713596e56..10a9dd1627b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import logging import socket import ssl import threading -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp.test_utils import make_mocked_request import freezegun @@ -617,6 +617,21 @@ def mock_zeroconf(): yield +@pytest.fixture +def mock_async_zeroconf(mock_zeroconf): + """Mock AsyncZeroconf.""" + with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc: + zc = mock_aiozc.return_value + zc.async_unregister_service = AsyncMock() + zc.async_register_service = AsyncMock() + zc.async_update_service = AsyncMock() + zc.zeroconf.async_wait_for_start = AsyncMock() + zc.zeroconf.done = False + zc.async_close = AsyncMock() + zc.ha_async_close = AsyncMock() + yield zc + + @pytest.fixture def legacy_patchable_time(): """Allow time to be patchable by using event listeners instead of asyncio loop.""" From ff21453f5830235755b2c9eafbf4c350773ecbbe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 18 Nov 2021 22:24:12 -0700 Subject: [PATCH 0617/1452] Migrate appropriate Ridwell sensors to use datetime state objects (#59944) * Migrate appropriate Ridwell sensors to use datetime state objects * Linting * Whoops --- homeassistant/components/ridwell/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 68a8c066bd2..f80ddc45176 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from datetime import date, datetime from typing import Any from aioridwell.client import RidwellAccount, RidwellPickupEvent @@ -76,7 +77,7 @@ class RidwellSensor(CoordinatorEntity, SensorEntity): return attrs @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | date | datetime: """Return the value reported by the sensor.""" event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - return event.pickup_date.isoformat() + return event.pickup_date From 406cbcfe2dc6adc31ef47408111f37009fecef2f Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 18 Nov 2021 22:29:38 -0700 Subject: [PATCH 0618/1452] Use an assumed switch state until refresh callback is complete (#59805) --- .../components/litterrobot/entity.py | 29 ++++++++++++++----- homeassistant/components/litterrobot/hub.py | 2 +- .../components/litterrobot/manifest.json | 10 +++++-- .../components/litterrobot/switch.py | 12 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/test_switch.py | 28 +++++++++++++----- 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 064c9bcf8f1..206358201c3 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -67,19 +67,21 @@ class LitterRobotControlEntity(LitterRobotEntity): self, action: MethodType, *args: Any, **kwargs: Any ) -> bool: """Perform an action and initiates a refresh of the robot data after a few seconds.""" + success = False try: - await action(*args, **kwargs) + success = await action(*args, **kwargs) except InvalidCommandException as ex: # pragma: no cover # this exception should only occur if the underlying API for commands changes _LOGGER.error(ex) - return False + success = False - self.async_cancel_refresh_callback() - self._refresh_callback = async_call_later( - self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback - ) - return True + if success: + self.async_cancel_refresh_callback() + self._refresh_callback = async_call_later( + self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback + ) + return success async def async_call_later_callback(self, *_) -> None: """Perform refresh request on callback.""" @@ -118,3 +120,16 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): """A Litter-Robot entity that can control configuration of the unit.""" _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + """Init a Litter-Robot control entity.""" + super().__init__(robot=robot, entity_type=entity_type, hub=hub) + self._assumed_state: Any = None + + async def perform_action_and_assume_state( + self, action: MethodType, assumed_state: Any + ) -> bool: + """Perform an action and assume the state passed in if call is successful.""" + if await self.perform_action_and_refresh(action, assumed_state): + self._assumed_state = assumed_state + self.async_write_ha_state() diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 6a9155b9eaf..3aa86dcc93a 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -13,7 +13,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -UPDATE_INTERVAL_SECONDS = 10 +UPDATE_INTERVAL_SECONDS = 20 class LitterRobotHub: diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 7b864948569..0d76e8df09c 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,11 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.10.1"], - "codeowners": ["@natekspencer"], + "requirements": [ + "pylitterbot==2021.11.0" + ], + "codeowners": [ + "@natekspencer" + ], "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 9a08a008d96..4d302a0d4ae 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -19,6 +19,8 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): @property def is_on(self) -> bool: """Return true if switch is on.""" + if self._refresh_callback is not None: + return self._assumed_state return self.robot.night_light_mode_enabled @property @@ -28,11 +30,11 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.perform_action_and_refresh(self.robot.set_night_light, True) + await self.perform_action_and_assume_state(self.robot.set_night_light, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.perform_action_and_refresh(self.robot.set_night_light, False) + await self.perform_action_and_assume_state(self.robot.set_night_light, False) class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): @@ -41,6 +43,8 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): @property def is_on(self) -> bool: """Return true if switch is on.""" + if self._refresh_callback is not None: + return self._assumed_state return self.robot.panel_lock_enabled @property @@ -50,11 +54,11 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.perform_action_and_refresh(self.robot.set_panel_lockout, True) + await self.perform_action_and_assume_state(self.robot.set_panel_lockout, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.perform_action_and_refresh(self.robot.set_panel_lockout, False) + await self.perform_action_and_assume_state(self.robot.set_panel_lockout, False) ROBOT_SWITCHES: list[tuple[type[LitterRobotConfigEntity], str]] = [ diff --git a/requirements_all.txt b/requirements_all.txt index 41d8ac0201d..24162ca05bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1604,7 +1604,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.10.1 +pylitterbot==2021.11.0 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef8605a6530..c0b4a99dfdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.10.1 +pylitterbot==2021.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index d651b09fadc..99c34e4273f 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -1,5 +1,6 @@ """Test the Litter-Robot switch entity.""" from datetime import timedelta +from unittest.mock import MagicMock import pytest @@ -9,7 +10,13 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_CATEGORY_CONFIG, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_CATEGORY_CONFIG, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.util.dt import utcnow @@ -21,13 +28,13 @@ NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode" PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout" -async def test_switch(hass, mock_account): +async def test_switch(hass: HomeAssistant, mock_account: MagicMock): """Tests the switch entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID) - assert switch - assert switch.state == STATE_ON + state = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID) + assert state + assert state.state == STATE_ON ent_reg = entity_registry.async_get(hass) entity_entry = ent_reg.async_get(NIGHT_LIGHT_MODE_ENTITY_ID) @@ -42,12 +49,14 @@ async def test_switch(hass, mock_account): (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"), ], ) -async def test_on_off_commands(hass, mock_account, entity_id, robot_command): +async def test_on_off_commands( + hass: HomeAssistant, mock_account: MagicMock, entity_id: str, robot_command: str +): """Test sending commands to the switch.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - switch = hass.states.get(entity_id) - assert switch + state = hass.states.get(entity_id) + assert state data = {ATTR_ENTITY_ID: entity_id} @@ -65,3 +74,6 @@ async def test_on_off_commands(hass, mock_account, entity_id, robot_command): future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) async_fire_time_changed(hass, future) assert getattr(mock_account.robots[0], robot_command).call_count == count + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON if service == SERVICE_TURN_ON else STATE_OFF From a3d5aec7780ff2b04baadf967f8f992e40d73234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 19 Nov 2021 06:44:45 +0100 Subject: [PATCH 0619/1452] Mill local access (#59549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mill local Signed-off-by: Daniel Hjelseth Høyer * Mill local Signed-off-by: Daniel Hjelseth Høyer * Mill local Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/mill/config_flow.py Co-authored-by: Allen Porter * Update homeassistant/components/mill/config_flow.py Co-authored-by: Allen Porter * Update homeassistant/components/mill/config_flow.py Co-authored-by: Allen Porter * Fix review comments Signed-off-by: Daniel Hjelseth Høyer * coveragerc Signed-off-by: Daniel Hjelseth Høyer * Fix review comments Signed-off-by: Daniel Hjelseth Høyer * Fix review comments Signed-off-by: Daniel Hjelseth Høyer * Fix review comments Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Allen Porter --- .coveragerc | 1 - homeassistant/components/mill/__init__.py | 44 +++- homeassistant/components/mill/climate.py | 65 +++++- homeassistant/components/mill/config_flow.py | 81 ++++++- homeassistant/components/mill/const.py | 3 + homeassistant/components/mill/manifest.json | 4 +- homeassistant/components/mill/sensor.py | 8 +- homeassistant/components/mill/strings.json | 12 + .../components/mill/translations/en.json | 14 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mill/test_config_flow.py | 215 +++++++++++++++--- tests/components/mill/test_init.py | 121 ++++++++++ 13 files changed, 511 insertions(+), 63 deletions(-) create mode 100644 tests/components/mill/test_init.py diff --git a/.coveragerc b/.coveragerc index 1adede05ca4..e1ef57b9817 100644 --- a/.coveragerc +++ b/.coveragerc @@ -641,7 +641,6 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/device_tracker.py - homeassistant/components/mill/__init__.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 73480563c29..c087fe0d853 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,16 +1,19 @@ """The mill component.""" +from __future__ import annotations + from datetime import timedelta import logging from mill import Mill +from mill_local import Mill as MillLocal -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_IP_ADDRESS, 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 +from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL _LOGGER = logging.getLogger(__name__) @@ -23,8 +26,9 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + update_interval: timedelta | None = None, *, - mill_data_connection: Mill, + mill_data_connection: Mill | MillLocal, ) -> None: """Initialize global Mill data updater.""" self.mill_data_connection = mill_data_connection @@ -34,26 +38,42 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_method=mill_data_connection.fetch_heater_and_sensor_data, - update_interval=timedelta(seconds=30), + update_interval=update_interval, ) async def async_setup_entry(hass, entry): """Set up the Mill heater.""" - mill_data_connection = Mill( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - websession=async_get_clientsession(hass), - ) + hass.data.setdefault(DOMAIN, {LOCAL: {}, CLOUD: {}}) + + if entry.data.get(CONNECTION_TYPE) == LOCAL: + mill_data_connection = MillLocal( + entry.data[CONF_IP_ADDRESS], + websession=async_get_clientsession(hass), + ) + update_interval = timedelta(seconds=15) + key = entry.data[CONF_IP_ADDRESS] + conn_type = LOCAL + else: + mill_data_connection = Mill( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + websession=async_get_clientsession(hass), + ) + update_interval = timedelta(seconds=30) + key = entry.data[CONF_USERNAME] + conn_type = CLOUD + if not await mill_data_connection.connect(): raise ConfigEntryNotReady - - hass.data[DOMAIN] = MillDataUpdateCoordinator( + data_coordinator = MillDataUpdateCoordinator( hass, mill_data_connection=mill_data_connection, + update_interval=update_interval, ) - await hass.data[DOMAIN].async_config_entry_first_refresh() + hass.data[DOMAIN][conn_type][key] = data_coordinator + await data_coordinator.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 ad50bf437bb..80611cf9f96 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -12,7 +12,13 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_IP_ADDRESS, + CONF_USERNAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -23,15 +29,16 @@ from .const import ( ATTR_COMFORT_TEMP, ATTR_ROOM_NAME, ATTR_SLEEP_TEMP, + CLOUD, + CONNECTION_TYPE, DOMAIN, + LOCAL, MANUFACTURER, MAX_TEMP, MIN_TEMP, SERVICE_SET_ROOM_TEMP, ) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - SET_ROOM_TEMP_SCHEMA = vol.Schema( { vol.Required(ATTR_ROOM_NAME): cv.string, @@ -44,8 +51,12 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" + if entry.data.get(CONNECTION_TYPE) == LOCAL: + mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]] + async_add_entities([LocalMillHeater(mill_data_coordinator)]) + return - mill_data_coordinator = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]] entities = [ MillHeater(mill_data_coordinator, mill_device) @@ -75,7 +86,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - _attr_supported_features = SUPPORT_FLAGS + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS @@ -169,3 +180,47 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_hvac_mode = HVAC_MODE_HEAT else: self._attr_hvac_mode = HVAC_MODE_OFF + + +class LocalMillHeater(CoordinatorEntity, ClimateEntity): + """Representation of a Mill Thermostat device.""" + + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_hvac_modes = [HVAC_MODE_HEAT] + _attr_max_temp = MAX_TEMP + _attr_min_temp = MIN_TEMP + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, coordinator): + """Initialize the thermostat.""" + super().__init__(coordinator) + self._attr_name = coordinator.mill_data_connection.name + self._update_attr() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + await self.coordinator.mill_data_connection.set_target_temperature( + int(temperature) + ) + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() + + @callback + def _update_attr(self) -> None: + data = self.coordinator.data + self._attr_target_temperature = data["set_temperature"] + self._attr_current_temperature = data["ambient_temperature"] + + if data["current_power"] > 0: + self._attr_hvac_action = CURRENT_HVAC_HEAT + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py index 7970e2946f2..9f7dd5d5cdb 100644 --- a/homeassistant/components/mill/config_flow.py +++ b/homeassistant/components/mill/config_flow.py @@ -1,16 +1,13 @@ """Adds config flow for Mill integration.""" from mill import Mill +from mill_local import Mill as MillLocal import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN - -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} -) +from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -20,10 +17,68 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" + data_schema = vol.Schema( + { + vol.Required(CONNECTION_TYPE, default=CLOUD): vol.In( + ( + CLOUD, + LOCAL, + ) + ) + } + ) + if user_input is None: return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=data_schema, + ) + + if user_input[CONNECTION_TYPE] == LOCAL: + return await self.async_step_local() + return await self.async_step_cloud() + + async def async_step_local(self, user_input=None): + """Handle the local step.""" + data_schema = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) + if user_input is None: + return self.async_show_form( + step_id="local", + data_schema=data_schema, + ) + + mill_data_connection = MillLocal( + user_input[CONF_IP_ADDRESS], + websession=async_get_clientsession(self.hass), + ) + + await self.async_set_unique_id(mill_data_connection.device_ip) + self._abort_if_unique_id_configured() + + if not await mill_data_connection.connect(): + return self.async_show_form( + step_id="local", + data_schema=data_schema, + errors={"base": "cannot_connect"}, + ) + + return self.async_create_entry( + title=user_input[CONF_IP_ADDRESS], + data={ + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONNECTION_TYPE: LOCAL, + }, + ) + + async def async_step_cloud(self, user_input=None): + """Handle the cloud step.""" + data_schema = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ) + if user_input is None: + return self.async_show_form( + step_id="cloud", + data_schema=data_schema, errors={}, ) @@ -39,10 +94,10 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if not await mill_data_connection.connect(): - errors["cannot_connect"] = "cannot_connect" + errors["base"] = "cannot_connect" return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, + step_id="cloud", + data_schema=data_schema, errors=errors, ) @@ -53,5 +108,9 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=unique_id, - data={CONF_USERNAME: username, CONF_PASSWORD: password}, + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONNECTION_TYPE: CLOUD, + }, ) diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py index 25a57117a65..c42747920bf 100644 --- a/homeassistant/components/mill/const.py +++ b/homeassistant/components/mill/const.py @@ -5,11 +5,14 @@ ATTR_COMFORT_TEMP = "comfort_temp" ATTR_ROOM_NAME = "room_name" ATTR_SLEEP_TEMP = "sleep_temp" BATTERY = "battery" +CLOUD = "Cloud" +CONNECTION_TYPE = "connection_type" CONSUMPTION_TODAY = "day_consumption" CONSUMPTION_YEAR = "year_consumption" DOMAIN = "mill" ECO2 = "eco2" HUMIDITY = "humidity" +LOCAL = "Local" MANUFACTURER = "Mill" MAX_TEMP = 35 MIN_TEMP = 5 diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 7347cf16daa..a2507251524 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,8 +2,8 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.8.0"], + "requirements": ["millheater==0.8.0", "mill-local==0.1.0"], "codeowners": ["@danielhiversen"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "local_polling" } diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 1b9e84eafa8..64db26c371c 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -28,11 +29,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( BATTERY, + CLOUD, + CONNECTION_TYPE, CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, ECO2, HUMIDITY, + LOCAL, MANUFACTURER, TEMPERATURE, TVOC, @@ -95,8 +99,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" + if entry.data.get(CONNECTION_TYPE) == LOCAL: + return - mill_data_coordinator = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]] entities = [ MillSensor( diff --git a/homeassistant/components/mill/strings.json b/homeassistant/components/mill/strings.json index ab09f3f59b6..5f4cec1336e 100644 --- a/homeassistant/components/mill/strings.json +++ b/homeassistant/components/mill/strings.json @@ -8,10 +8,22 @@ }, "step": { "user": { + "data": { + "connection_type": "Select connection type" + }, + "description": "Select connection type. Local requires generation 3 heaters" + }, + "cloud": { "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "local": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "description": "Local IP address of the device." } } } diff --git a/homeassistant/components/mill/translations/en.json b/homeassistant/components/mill/translations/en.json index bb7d67f03b4..ee66706832e 100644 --- a/homeassistant/components/mill/translations/en.json +++ b/homeassistant/components/mill/translations/en.json @@ -7,11 +7,23 @@ "cannot_connect": "Failed to connect" }, "step": { - "user": { + "cloud": { "data": { "password": "Password", "username": "Username" } + }, + "local": { + "data": { + "ip_address": "IP Address" + }, + "description": "Local IP address of the device." + }, + "user": { + "data": { + "connection_type": "Select connection type" + }, + "description": "Select connection type. Local requires generation 3 heaters" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 24162ca05bb..28a79154886 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1010,6 +1010,9 @@ micloud==0.4 # homeassistant.components.miflora miflora==0.7.0 +# homeassistant.components.mill +mill-local==0.1.0 + # homeassistant.components.mill millheater==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0b4a99dfdf..c65cf8fcc7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -611,6 +611,9 @@ mficlient==0.3.0 # homeassistant.components.xiaomi_miio micloud==0.4 +# homeassistant.components.mill +mill-local==0.1.0 + # homeassistant.components.mill millheater==0.8.0 diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ce35b3d9708..ff2f7393c82 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -1,47 +1,57 @@ """Tests for Mill config flow.""" from unittest.mock import patch -import pytest - from homeassistant import config_entries -from homeassistant.components.mill.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import RESULT_TYPE_FORM from tests.common import MockConfigEntry -@pytest.fixture(name="mill_setup", autouse=True) -def mill_setup_fixture(): - """Patch mill setup entry.""" - with patch("homeassistant.components.mill.async_setup_entry", return_value=True): - yield - - async def test_show_config_form(hass): """Test show configuration form.""" 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["step_id"] == "user" async def test_create_entry(hass): """Test create entry from user input.""" - test_data = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pswd", - } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM with patch("mill.Mill.connect", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + }, ) + await hass.async_block_till_done() assert result["type"] == "create_entry" - assert result["title"] == test_data[CONF_USERNAME] - assert result["data"] == test_data + assert result["title"] == "user" + assert result["data"] == { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + CONNECTION_TYPE: CLOUD, + } async def test_flow_entry_already_exists(hass): @@ -59,10 +69,26 @@ async def test_flow_entry_already_exists(hass): ) first_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + with patch("mill.Mill.connect", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -70,23 +96,152 @@ async def test_flow_entry_already_exists(hass): async def test_connection_error(hass): """Test connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch("mill.Mill.connect", return_value=False): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_local_create_entry(hass): + """Test create entry from user input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM test_data = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pswd", + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + test_data[CONNECTION_TYPE] = LOCAL + assert result["type"] == "create_entry" + assert result["title"] == test_data[CONF_IP_ADDRESS] + assert result["data"] == test_data + + +async def test_local_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", } first_entry = MockConfigEntry( domain="mill", data=test_data, - unique_id=test_data[CONF_USERNAME], + unique_id=test_data[CONF_IP_ADDRESS], ) first_entry.add_to_hass(hass) - with patch("mill.Mill.connect", return_value=False): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, ) - assert result["type"] == "form" - assert result["errors"]["cannot_connect"] == "cannot_connect" + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_local_connection_error(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py new file mode 100644 index 00000000000..f92b4689ebf --- /dev/null +++ b/tests/components/mill/test_init.py @@ -0,0 +1,121 @@ +"""Tests for Mill init.""" + +from unittest.mock import patch + +from homeassistant.components import mill +from homeassistant.config_entries import ConfigEntryState +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_coro + + +async def test_setup_with_cloud_config(hass): + """Test setup of cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + with patch( + "mill.Mill.fetch_heater_and_sensor_data", return_value={} + ) as mock_fetch, patch("mill.Mill.connect", return_value=True) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + assert len(mock_fetch.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 + + +async def test_setup_with_cloud_config_fails(hass): + """Test setup of cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + with patch("mill.Mill.connect", return_value=False): + assert await async_setup_component(hass, "mill", entry) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_with_old_cloud_config(hass): + """Test setup of old cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + }, + ) + entry.add_to_hass(hass) + with patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}), patch( + "mill.Mill.connect", return_value=True + ) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + + assert len(mock_connect.mock_calls) == 1 + + +async def test_setup_with_local_config(hass): + """Test setup of local config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_IP_ADDRESS: "192.168.1.59", + mill.CONNECTION_TYPE: mill.LOCAL, + }, + ) + entry.add_to_hass(hass) + with patch( + "mill_local.Mill.fetch_heater_and_sensor_data", + return_value={ + "ambient_temperature": 20, + "set_temperature": 22, + "current_power": 0, + }, + ) as mock_fetch, patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + + assert len(mock_fetch.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 + + +async def test_unload_entry(hass): + """Test removing mill client.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry, patch( + "mill.Mill.fetch_heater_and_sensor_data", return_value={} + ), patch( + "mill.Mill.connect", return_value=True + ): + assert await async_setup_component(hass, "mill", entry) + + assert await hass.config_entries.async_unload(entry.entry_id) + + assert unload_entry.call_count == 2 + assert entry.entry_id not in hass.data[mill.DOMAIN] From f7b7786d0dc06f10991ebd86a97de57c2de19d19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Nov 2021 07:35:09 +0100 Subject: [PATCH 0620/1452] Use native datetime value in UniFi sensors (#59926) --- homeassistant/components/unifi/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index a9e89876459..c1181b029a5 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -172,8 +172,8 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): 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() - return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat() + return dt_util.now() - timedelta(seconds=self.client.uptime) + return dt_util.utc_from_timestamp(float(self.client.uptime)) async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" From 073bf6d6fdd95eadf98a76b8ea50dd04b9d638f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Nov 2021 07:36:28 +0100 Subject: [PATCH 0621/1452] Use native datetime value inMobile App sensors (#59945) --- homeassistant/components/mobile_app/sensor.py | 24 +++++++- tests/components/mobile_app/test_sensor.py | 59 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index ff4ca491411..f8b71da7c29 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -2,10 +2,17 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_WEBHOOK_ID, + DEVICE_CLASS_DATE, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import dt as dt_util from .const import ( ATTR_DEVICE_NAME, @@ -81,7 +88,20 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._config[ATTR_SENSOR_STATE] + if ( + (state := self._config[ATTR_SENSOR_STATE]) is not None + and self.device_class + in ( + DEVICE_CLASS_DATE, + DEVICE_CLASS_TIMESTAMP, + ) + and (timestamp := dt_util.parse_datetime(state)) is not None + ): + if self.device_class == DEVICE_CLASS_DATE: + return timestamp.date() + return timestamp + + return state @property def native_unit_of_measurement(self): diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 032870ffb8c..2fea00a692a 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,6 +1,9 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +import pytest + +from homeassistant.components.sensor import DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP from homeassistant.const import PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -276,3 +279,59 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client updated_entity = hass.states.get("sensor.test_1_battery_state") assert updated_entity.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "device_class,native_value,state_value", + [ + (DEVICE_CLASS_DATE, "2021-11-18", "2021-11-18"), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-11-18T20:25:00", + "2021-11-18T20:25:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-11-18 20:25:00", + "2021-11-18T20:25:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-11-18 20:25:00+01:00", + "2021-11-18T20:25:00+01:00", + ), + ], +) +async def test_sensor_datetime( + hass, create_registrations, webhook_client, device_class, native_value, state_value +): + """Test that sensors can be registered and updated.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "device_class": device_class, + "name": "Datetime sensor test", + "state": native_value, + "type": "sensor", + "unique_id": "super_unique", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("sensor.test_1_datetime_sensor_test") + assert entity is not None + + assert entity.attributes["device_class"] == device_class + assert entity.domain == "sensor" + assert entity.state == state_value From 2f00f8d3de9338af6dd25b951f6e07f5c3bf1979 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Nov 2021 00:39:49 -0600 Subject: [PATCH 0622/1452] Prevent executor overload when starting many homekit instances (#59950) --- homeassistant/components/homekit/__init__.py | 40 ++++++++++---------- homeassistant/components/homekit/const.py | 1 + 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 3c45ba8de39..ee8a3add610 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,4 +1,6 @@ """Support for Apple HomeKit.""" +from __future__ import annotations + import asyncio import ipaddress import logging @@ -91,6 +93,7 @@ from .const import ( HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, MANUFACTURER, + PERSIST_LOCK, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_UNPAIR, @@ -171,6 +174,15 @@ UNPAIR_SERVICE_SCHEMA = vol.All( ) +def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: + """All active HomeKit instances.""" + return [ + data[HOMEKIT] + for data in hass.data[DOMAIN].values() + if isinstance(data, dict) and HOMEKIT in data + ] + + def _async_get_entries_by_name(current_entries): """Return a dict of the entries by name.""" @@ -181,7 +193,7 @@ def _async_get_entries_by_name(current_entries): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomeKit from yaml.""" - hass.data.setdefault(DOMAIN, {}) + hass.data.setdefault(DOMAIN, {})[PERSIST_LOCK] = asyncio.Lock() _async_register_events_and_services(hass) @@ -360,10 +372,7 @@ def _async_register_events_and_services(hass: HomeAssistant): async def async_handle_homekit_reset_accessory(service): """Handle reset accessory HomeKit service call.""" - for entry_id in hass.data[DOMAIN]: - if HOMEKIT not in hass.data[DOMAIN][entry_id]: - continue - homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + for homekit in _async_all_homekit_instances(hass): if homekit.status != STATUS_RUNNING: _LOGGER.warning( "HomeKit is not running. Either it is waiting to be " @@ -393,16 +402,11 @@ def _async_register_events_and_services(hass: HomeAssistant): for ctype, cval in dev_reg_ent.connections if ctype == device_registry.CONNECTION_NETWORK_MAC ] - domain_data = hass.data[DOMAIN] matching_instances = [ - domain_data[entry_id][HOMEKIT] - for entry_id in domain_data - if HOMEKIT in domain_data[entry_id] - and domain_data[entry_id][HOMEKIT].driver - and device_registry.format_mac( - domain_data[entry_id][HOMEKIT].driver.state.mac - ) - in macs + homekit + for homekit in _async_all_homekit_instances(hass) + if homekit.driver + and device_registry.format_mac(homekit.driver.state.mac) in macs ] if not matching_instances: raise HomeAssistantError( @@ -421,10 +425,7 @@ def _async_register_events_and_services(hass: HomeAssistant): async def async_handle_homekit_service_start(service): """Handle start HomeKit service call.""" tasks = [] - for entry_id in hass.data[DOMAIN]: - if HOMEKIT not in hass.data[DOMAIN][entry_id]: - continue - homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + for homekit in _async_all_homekit_instances(hass): if homekit.status == STATUS_RUNNING: _LOGGER.debug("HomeKit is already running") continue @@ -707,7 +708,8 @@ class HomeKit: self._async_register_bridge() _LOGGER.debug("Driver start for %s", self._name) await self.driver.async_start() - self.driver.async_persist() + async with self.hass.data[DOMAIN][PERSIST_LOCK]: + await self.hass.async_add_executor_job(self.driver.persist) self.status = STATUS_RUNNING if self.driver.state.paired: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index c77efb705da..8494327bb68 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -12,6 +12,7 @@ HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" HOMEKIT = "homekit" SHUTDOWN_TIMEOUT = 30 CONF_ENTRY_INDEX = "index" +PERSIST_LOCK = "persist_lock" # ### Codecs #### VIDEO_CODEC_COPY = "copy" From b8ec0825d3ccf031b6b10b7b5d9ffd5816f04145 Mon Sep 17 00:00:00 2001 From: deftdawg Date: Fri, 19 Nov 2021 03:16:08 -0500 Subject: [PATCH 0623/1452] Add energy support to Neurio_Energy (#54445) * - Patch Neurio_Energy to support new HA energy Enables the Neurio Energy Meter as a Consumption device for Home Assistant Energy * Only return last_reset value for DEVICE_CLASS_ENERGY * Update homeassistant/components/neurio_energy/sensor.py Co-authored-by: Martin Hjelmare * Update with recommendations from CI/Black * Support new style typing * Attempt setting the state_class statically * Make state class static * Changing state class to STATE_CLASS_TOTAL_INCREASING and removing last_reset seems to work ok * Remove unused datetime import that was previously in last_reset * Apply suggestions from code review apply emontnemery's recommended changes Co-authored-by: Erik Montnemery Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery --- homeassistant/components/neurio_energy/sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 37113dde8b7..b316281faa1 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -1,4 +1,6 @@ """Support for monitoring a Neurio energy sensor.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -9,9 +11,16 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) -from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.const import ( + CONF_API_KEY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -139,9 +148,12 @@ class NeurioEnergy(SensorEntity): if sensor_type == ACTIVE_TYPE: self._unit_of_measurement = POWER_WATT + self._attr_device_class = DEVICE_CLASS_POWER self._attr_state_class = STATE_CLASS_MEASUREMENT elif sensor_type == DAILY_TYPE: self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @property def name(self): From ecf00a1eaec3dcbd5667650408042567047e173b Mon Sep 17 00:00:00 2001 From: rianadon Date: Fri, 19 Nov 2021 00:18:44 -0800 Subject: [PATCH 0624/1452] Add accumulated precipitation to unit system (#59657) * Add accumulated precipitation to unit system * Fix template test * Fix typo of testing pressure instead of precipitation * Add extra arguments so unit system test passes --- homeassistant/const.py | 1 + homeassistant/util/unit_system.py | 21 ++++++++++ tests/helpers/test_template.py | 2 + tests/util/test_unit_system.py | 66 +++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index da48646beed..dd8bbc5e555 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -688,6 +688,7 @@ TEMPERATURE: Final = "temperature" SPEED: Final = "speed" WIND_SPEED: Final = "wind_speed" ILLUMINANCE: Final = "illuminance" +ACCUMULATED_PRECIPITATION: Final = "accumulated_precipitation" WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 1956faea84d..bdb637a9149 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -4,11 +4,14 @@ from __future__ import annotations from numbers import Number from homeassistant.const import ( + ACCUMULATED_PRECIPITATION, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, LENGTH, + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, MASS, MASS_GRAMS, MASS_KILOGRAMS, @@ -55,6 +58,8 @@ def is_valid_unit(unit: str, unit_type: str) -> bool: """Check if the unit is valid for it's type.""" if unit_type == LENGTH: units = LENGTH_UNITS + elif unit_type == ACCUMULATED_PRECIPITATION: + units = LENGTH_UNITS elif unit_type == WIND_SPEED: units = WIND_SPEED_UNITS elif unit_type == TEMPERATURE: @@ -83,11 +88,13 @@ class UnitSystem: volume: str, mass: str, pressure: str, + accumulated_precipitation: str, ) -> None: """Initialize the unit system object.""" errors: str = ", ".join( UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type) for unit, unit_type in ( + (accumulated_precipitation, ACCUMULATED_PRECIPITATION), (temperature, TEMPERATURE), (length, LENGTH), (wind_speed, WIND_SPEED), @@ -102,6 +109,7 @@ class UnitSystem: raise ValueError(errors) self.name = name + self.accumulated_precipitation_unit = accumulated_precipitation self.temperature_unit = temperature self.length_unit = length self.mass_unit = mass @@ -131,6 +139,16 @@ class UnitSystem: length, from_unit, self.length_unit ) + def accumulated_precipitation(self, precip: float | None, from_unit: str) -> float: + """Convert the given length to this unit system.""" + if not isinstance(precip, Number): + raise TypeError(f"{precip!s} is not a numeric value.") + + # type ignore: https://github.com/python/mypy/issues/7207 + return distance_util.convert( # type: ignore + precip, from_unit, self.accumulated_precipitation_unit + ) + def pressure(self, pressure: float | None, from_unit: str) -> float: """Convert the given pressure to this unit system.""" if not isinstance(pressure, Number): @@ -161,6 +179,7 @@ class UnitSystem: """Convert the unit system to a dictionary.""" return { LENGTH: self.length_unit, + ACCUMULATED_PRECIPITATION: self.accumulated_precipitation_unit, MASS: self.mass_unit, PRESSURE: self.pressure_unit, TEMPERATURE: self.temperature_unit, @@ -177,6 +196,7 @@ METRIC_SYSTEM = UnitSystem( VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) IMPERIAL_SYSTEM = UnitSystem( @@ -187,4 +207,5 @@ IMPERIAL_SYSTEM = UnitSystem( VOLUME_GALLONS, MASS_POUNDS, PRESSURE_PSI, + LENGTH_INCHES, ) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index f871fc88d3a..0e1d96b8dac 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -12,6 +12,7 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, LENGTH_METERS, + LENGTH_MILLIMETERS, MASS_GRAMS, PRESSURE_PA, SPEED_KILOMETERS_PER_HOUR, @@ -42,6 +43,7 @@ def _set_up_units(hass): VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 5c7ac660f9b..954df07fc9d 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -2,9 +2,11 @@ import pytest from homeassistant.const import ( + ACCUMULATED_PRECIPITATION, LENGTH, LENGTH_KILOMETERS, LENGTH_METERS, + LENGTH_MILLIMETERS, MASS, MASS_GRAMS, PRESSURE, @@ -33,6 +35,7 @@ def test_invalid_units(): VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) with pytest.raises(ValueError): @@ -44,6 +47,7 @@ def test_invalid_units(): VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) with pytest.raises(ValueError): @@ -55,6 +59,7 @@ def test_invalid_units(): VOLUME_LITERS, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) with pytest.raises(ValueError): @@ -66,6 +71,7 @@ def test_invalid_units(): INVALID_UNIT, MASS_GRAMS, PRESSURE_PA, + LENGTH_MILLIMETERS, ) with pytest.raises(ValueError): @@ -77,6 +83,7 @@ def test_invalid_units(): VOLUME_LITERS, INVALID_UNIT, PRESSURE_PA, + LENGTH_MILLIMETERS, ) with pytest.raises(ValueError): @@ -88,6 +95,19 @@ def test_invalid_units(): VOLUME_LITERS, MASS_GRAMS, INVALID_UNIT, + LENGTH_MILLIMETERS, + ) + + with pytest.raises(ValueError): + UnitSystem( + SYSTEM_NAME, + TEMP_CELSIUS, + LENGTH_METERS, + SPEED_METERS_PER_SECOND, + VOLUME_LITERS, + MASS_GRAMS, + PRESSURE_PA, + INVALID_UNIT, ) @@ -103,6 +123,8 @@ def test_invalid_value(): METRIC_SYSTEM.volume("50L", VOLUME_LITERS) with pytest.raises(TypeError): METRIC_SYSTEM.pressure("50Pa", PRESSURE_PA) + with pytest.raises(TypeError): + METRIC_SYSTEM.accumulated_precipitation("50mm", LENGTH_MILLIMETERS) def test_as_dict(): @@ -114,6 +136,7 @@ def test_as_dict(): VOLUME: VOLUME_LITERS, MASS: MASS_GRAMS, PRESSURE: PRESSURE_PA, + ACCUMULATED_PRECIPITATION: LENGTH_MILLIMETERS, } assert expected == METRIC_SYSTEM.as_dict() @@ -213,6 +236,48 @@ def test_pressure_to_imperial(): ) == pytest.approx(14.7, abs=1e-4) +def test_accumulated_precipitation_same_unit(): + """Test no conversion happens if to unit is same as from unit.""" + assert ( + METRIC_SYSTEM.accumulated_precipitation( + 5, METRIC_SYSTEM.accumulated_precipitation_unit + ) + == 5 + ) + + +def test_accumulated_precipitation_unknown_unit(): + """Test no conversion happens if unknown unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.accumulated_precipitation(5, "K") + + +def test_accumulated_precipitation_to_metric(): + """Test accumulated_precipitation conversion to metric system.""" + assert ( + METRIC_SYSTEM.accumulated_precipitation( + 25, METRIC_SYSTEM.accumulated_precipitation_unit + ) + == 25 + ) + assert METRIC_SYSTEM.accumulated_precipitation( + 10, IMPERIAL_SYSTEM.accumulated_precipitation_unit + ) == pytest.approx(254, abs=1e-4) + + +def test_accumulated_precipitation_to_imperial(): + """Test accumulated_precipitation conversion to imperial system.""" + assert ( + IMPERIAL_SYSTEM.accumulated_precipitation( + 10, IMPERIAL_SYSTEM.accumulated_precipitation_unit + ) + == 10 + ) + assert IMPERIAL_SYSTEM.accumulated_precipitation( + 254, METRIC_SYSTEM.accumulated_precipitation_unit + ) == pytest.approx(10, abs=1e-4) + + def test_properties(): """Test the unit properties are returned as expected.""" assert METRIC_SYSTEM.length_unit == LENGTH_KILOMETERS @@ -221,6 +286,7 @@ def test_properties(): assert METRIC_SYSTEM.mass_unit == MASS_GRAMS assert METRIC_SYSTEM.volume_unit == VOLUME_LITERS assert METRIC_SYSTEM.pressure_unit == PRESSURE_PA + assert METRIC_SYSTEM.accumulated_precipitation_unit == LENGTH_MILLIMETERS def test_is_metric(): From e23cc3ecbf227471a9fdae48cc37e8d0128324e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:17:49 +0100 Subject: [PATCH 0625/1452] Optimise use of ZeroconfServiceInfo (#59966) Co-authored-by: epenet --- .../components/forked_daapd/config_flow.py | 17 ++++++----------- homeassistant/components/freebox/config_flow.py | 5 +++-- .../components/forked_daapd/test_config_flow.py | 4 +++- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 86bc39c05ed..28177bef97d 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -160,20 +160,15 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Prepare configuration for a discovered forked-daapd device.""" version_num = 0 - if discovery_info.get(zeroconf.ATTR_PROPERTIES) and discovery_info[ - zeroconf.ATTR_PROPERTIES - ].get("Machine Name"): + zeroconf_properties = discovery_info[zeroconf.ATTR_PROPERTIES] + if zeroconf_properties.get("Machine Name"): with suppress(ValueError): version_num = int( - discovery_info[zeroconf.ATTR_PROPERTIES] - .get("mtd-version", "0") - .split(".")[0] + zeroconf_properties.get("mtd-version", "0").split(".")[0] ) if version_num < 27: return self.async_abort(reason="not_forked_daapd") - await self.async_set_unique_id( - discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"] - ) + await self.async_set_unique_id(zeroconf_properties["Machine Name"]) self._abort_if_unique_id_configured() # Update title and abort if we already have an entry for this host @@ -182,14 +177,14 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): continue self.hass.config_entries.async_update_entry( entry, - title=discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"], + title=zeroconf_properties["Machine Name"], ) return self.async_abort(reason="already_configured") zeroconf_data = { CONF_HOST: discovery_info[zeroconf.ATTR_HOST], CONF_PORT: discovery_info[zeroconf.ATTR_PORT], - CONF_NAME: discovery_info[zeroconf.ATTR_PROPERTIES]["Machine Name"], + CONF_NAME: zeroconf_properties["Machine Name"], } self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data)) self.context.update({"title_placeholders": zeroconf_data}) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 77041d803ee..76b99dc31d2 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -111,6 +111,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Initialize flow from zeroconf.""" - host = discovery_info[zeroconf.ATTR_PROPERTIES]["api_domain"] - port = discovery_info[zeroconf.ATTR_PROPERTIES]["https_port"] + zeroconf_properties = discovery_info[zeroconf.ATTR_PROPERTIES] + host = zeroconf_properties["api_domain"] + port = zeroconf_properties["https_port"] return await self.async_step_user({CONF_HOST: host, CONF_PORT: port}) diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index c8b82c7a2e9..5c7bf8db97f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -130,7 +130,9 @@ async def test_config_flow_no_websocket(hass, config_entry): async def test_config_flow_zeroconf_invalid(hass): """Test that an invalid zeroconf entry doesn't work.""" # test with no discovery properties - discovery_info = zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=23) + discovery_info = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", port=23, properties={} + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort From 01fea8bbdd1667e6280794fbf0873c2e8e428e7a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:18:32 +0100 Subject: [PATCH 0626/1452] Use DhcpServiceInfo in guardian tests (#59970) Co-authored-by: epenet --- tests/components/guardian/test_config_flow.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index b8d23508c77..f3e0adeec0f 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -4,8 +4,7 @@ from unittest.mock import patch from aioguardian.errors import GuardianError from homeassistant import data_entry_flow -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( async_get_pin_from_discovery_hostname, @@ -137,11 +136,11 @@ async def test_step_zeroconf_already_in_progress(hass): async def test_step_dhcp(hass, ping_client): """Test the dhcp step.""" - dhcp_data = { - IP_ADDRESS: "192.168.1.100", - HOSTNAME: "GVC1-ABCD.local.", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - } + dhcp_data = dhcp.DhcpServiceInfo( + ip="192.168.1.100", + hostname="GVC1-ABCD.local.", + macaddress="aa:bb:cc:dd:ee:ff", + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data @@ -163,11 +162,11 @@ async def test_step_dhcp(hass, ping_client): async def test_step_dhcp_already_in_progress(hass): """Test the zeroconf step aborting because it's already in progress.""" - dhcp_data = { - IP_ADDRESS: "192.168.1.100", - HOSTNAME: "GVC1-ABCD.local.", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - } + dhcp_data = dhcp.DhcpServiceInfo( + ip="192.168.1.100", + hostname="GVC1-ABCD.local.", + macaddress="aa:bb:cc:dd:ee:ff", + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data From e7013f468ca875282566bb7d8f41a3643a7cf3c9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:18:56 +0100 Subject: [PATCH 0627/1452] Use DhcpServiceInfo in goalzero tests (#59969) Co-authored-by: epenet --- tests/components/goalzero/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 890cd75fcd4..8ffbbc11825 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,7 +1,7 @@ """Tests for the Goal Zero Yeti integration.""" from unittest.mock import AsyncMock, patch -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.goalzero import DOMAIN from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.const import CONF_HOST, CONF_NAME @@ -20,11 +20,11 @@ CONF_DATA = { CONF_NAME: DEFAULT_NAME, } -CONF_DHCP_FLOW = { - IP_ADDRESS: HOST, - MAC_ADDRESS: format_mac("AA:BB:CC:DD:EE:FF"), - HOSTNAME: "yeti", -} +CONF_DHCP_FLOW = dhcp.DhcpServiceInfo( + ip=HOST, + macaddress=format_mac("AA:BB:CC:DD:EE:FF"), + hostname="yeti", +) def create_entry(hass: HomeAssistant): From 2aa8c2cf74e2d2c2a41c87ed1a37739e469d9366 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:19:37 +0100 Subject: [PATCH 0628/1452] Use DhcpServiceInfo in gogogate2 (#59968) Co-authored-by: epenet --- homeassistant/components/gogogate2/config_flow.py | 11 ++++++----- tests/components/gogogate2/test_config_flow.py | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 3695703b509..d3df62c3b6e 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -7,8 +7,7 @@ from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_DEVICE, @@ -43,10 +42,12 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["id"]) return await self._async_discovery_handler(discovery_info[zeroconf.ATTR_HOST]) - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle dhcp discovery.""" - await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) - return await self._async_discovery_handler(discovery_info[IP_ADDRESS]) + await self.async_set_unique_id(discovery_info[dhcp.MAC_ADDRESS]) + return await self._async_discovery_handler(discovery_info[dhcp.IP_ADDRESS]) async def _async_discovery_handler(self, ip_address): """Start the user flow from any discovery.""" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 74997e827aa..202e7840456 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -6,7 +6,7 @@ from ismartgate.common import ApiError from ismartgate.const import GogoGate2ApiErrorCode from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, @@ -191,7 +191,7 @@ async def test_discovered_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + data=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress=MOCK_MAC_ADDR), ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} @@ -246,7 +246,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + data=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress=MOCK_MAC_ADDR), ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" @@ -254,7 +254,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): 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=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress="00:00:00:00:00:00"), ) assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "already_in_progress" From 59547289b4d5190b2473e840d11789f57496bd5c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:29:20 +0100 Subject: [PATCH 0629/1452] Use DhcpServiceInfo in broadlink (#59961) Co-authored-by: epenet --- .../components/broadlink/config_flow.py | 10 +-- .../components/broadlink/test_config_flow.py | 72 +++++++++---------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 2ab21372fd9..07fce107812 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -13,7 +13,7 @@ from broadlink.exceptions import ( import voluptuous as vol from homeassistant import config_entries, data_entry_flow -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv @@ -53,10 +53,12 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle dhcp discovery.""" - host = discovery_info[IP_ADDRESS] - unique_id = discovery_info[MAC_ADDRESS].lower().replace(":", "") + host = discovery_info[dhcp.IP_ADDRESS] + unique_id = discovery_info[dhcp.MAC_ADDRESS].lower().replace(":", "") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index ed27d497d23..8dbd28e0081 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -7,8 +7,8 @@ import broadlink.exceptions as blke import pytest from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.broadlink.const import DOMAIN -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.helpers import device_registry from . import get_device @@ -834,11 +834,11 @@ async def test_dhcp_can_finish(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: device_registry.format_mac(device.mac), - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="1.2.3.4", + macaddress=device_registry.format_mac(device.mac), + ), ) await hass.async_block_till_done() @@ -868,11 +868,11 @@ async def test_dhcp_fails_to_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="1.2.3.4", + macaddress="34:ea:34:b4:3b:5a", + ), ) await hass.async_block_till_done() @@ -887,11 +887,11 @@ async def test_dhcp_unreachable(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="1.2.3.4", + macaddress="34:ea:34:b4:3b:5a", + ), ) await hass.async_block_till_done() @@ -906,11 +906,11 @@ async def test_dhcp_connect_unknown_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="1.2.3.4", + macaddress="34:ea:34:b4:3b:5a", + ), ) await hass.async_block_till_done() @@ -928,11 +928,11 @@ async def test_dhcp_device_not_supported(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: device.host, - MAC_ADDRESS: device_registry.format_mac(device.mac), - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip=device.host, + macaddress=device_registry.format_mac(device.mac), + ), ) assert result["type"] == "abort" @@ -952,11 +952,11 @@ async def test_dhcp_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="1.2.3.4", + macaddress="34:ea:34:b4:3b:5a", + ), ) await hass.async_block_till_done() @@ -977,11 +977,11 @@ async def test_dhcp_updates_host(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "broadlink", - IP_ADDRESS: "4.5.6.7", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", - }, + data=dhcp.DhcpServiceInfo( + hostname="broadlink", + ip="4.5.6.7", + macaddress="34:ea:34:b4:3b:5a", + ), ) await hass.async_block_till_done() From a51f2a433f1275a438394deeed1312fd9eb0bfbf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Nov 2021 12:36:25 +0100 Subject: [PATCH 0630/1452] Upgrade pyatmo to 6.2.0 (#59975) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f162abbaad5..501d5142bcc 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.1.0" + "pyatmo==6.2.0" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 28a79154886..15e80661719 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,7 +1363,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.1.0 +pyatmo==6.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c65cf8fcc7a..42162f3b655 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -829,7 +829,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.1.0 +pyatmo==6.2.0 # homeassistant.components.apple_tv pyatv==0.8.2 From cb306236f2cc6c13bcb1440480dd9288db4ed7bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:37:59 +0100 Subject: [PATCH 0631/1452] Use DhcpServiceInfo in flux_led (#59967) Co-authored-by: epenet --- tests/components/flux_led/__init__.py | 16 ++++++---------- tests/components/flux_led/test_config_flow.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 08aefabe17d..19e11e6085c 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -13,11 +13,7 @@ from flux_led.const import ( ) from flux_led.protocol import LEDENETRawState -from homeassistant.components.dhcp import ( - HOSTNAME as DHCP_HOSTNAME, - IP_ADDRESS as DHCP_IP_ADDRESS, - MAC_ADDRESS as DHCP_MAC_ADDRESS, -) +from homeassistant.components import dhcp from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL from homeassistant.core import HomeAssistant @@ -30,11 +26,11 @@ FLUX_MAC_ADDRESS = "aabbccddeeff" DEFAULT_ENTRY_TITLE = f"{MODEL} {FLUX_MAC_ADDRESS}" -DHCP_DISCOVERY = { - DHCP_HOSTNAME: MODEL, - DHCP_IP_ADDRESS: IP_ADDRESS, - DHCP_MAC_ADDRESS: MAC_ADDRESS, -} +DHCP_DISCOVERY = dhcp.DhcpServiceInfo( + hostname=MODEL, + ip=IP_ADDRESS, + macaddress=MAC_ADDRESS, +) FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS} diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 582823439f6..b9518e35cc0 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, @@ -29,9 +30,6 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( DEFAULT_ENTRY_TITLE, DHCP_DISCOVERY, - DHCP_HOSTNAME, - DHCP_IP_ADDRESS, - DHCP_MAC_ADDRESS, FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, @@ -341,11 +339,11 @@ async def test_discovered_by_discovery_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - DHCP_HOSTNAME: "any", - DHCP_IP_ADDRESS: IP_ADDRESS, - DHCP_MAC_ADDRESS: "00:00:00:00:00:00", - }, + data=dhcp.DhcpServiceInfo( + hostname="any", + ip=IP_ADDRESS, + macaddress="00:00:00:00:00:00", + ), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT From f17d58a049979d1f7f2b4345deca160b0d5f206d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:48:51 +0100 Subject: [PATCH 0632/1452] Use DhcpServiceInfo in emonitor (#59965) Co-authored-by: epenet --- .../components/emonitor/config_flow.py | 11 ++++--- tests/components/emonitor/test_config_flow.py | 32 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py index cc63e707013..a745f652be4 100644 --- a/homeassistant/components/emonitor/config_flow.py +++ b/homeassistant/components/emonitor/config_flow.py @@ -6,8 +6,9 @@ import aiohttp import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import format_mac @@ -62,12 +63,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.discovered_ip = discovery_info[IP_ADDRESS] - await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) + self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] + await self.async_set_unique_id(format_mac(discovery_info[dhcp.MAC_ADDRESS])) self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip}) - name = name_short_mac(short_mac(discovery_info[MAC_ADDRESS])) + name = name_short_mac(short_mac(discovery_info[dhcp.MAC_ADDRESS])) self.context["title_placeholders"] = {"name": name} try: self.discovered_info = await fetch_mac_and_title( diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index a030f242d8d..0314ce7420e 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -5,7 +5,7 @@ from aioemonitor.monitor import EmonitorNetwork, EmonitorStatus import aiohttp from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.emonitor.const import DOMAIN from homeassistant.const import CONF_HOST @@ -102,11 +102,11 @@ async def test_dhcp_can_confirm(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "emonitor", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, + data=dhcp.DhcpServiceInfo( + hostname="emonitor", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), ) await hass.async_block_till_done() @@ -145,11 +145,11 @@ async def test_dhcp_fails_to_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "emonitor", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, + data=dhcp.DhcpServiceInfo( + hostname="emonitor", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), ) await hass.async_block_till_done() @@ -174,11 +174,11 @@ async def test_dhcp_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "emonitor", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, + data=dhcp.DhcpServiceInfo( + hostname="emonitor", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), ) await hass.async_block_till_done() From 45d41e584f80baf1373542ad6ac314e780f7af77 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:49:28 +0100 Subject: [PATCH 0633/1452] Use DhcpServiceInfo in dhcp tests (#59962) Co-authored-by: epenet --- tests/components/dhcp/test_init.py | 110 ++++++++++++++--------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index dc50edbeb10..40164375d66 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -171,11 +171,11 @@ async def test_dhcp_match_hostname_and_macaddress(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_dhcp_renewal_match_hostname_and_macaddress(hass): @@ -199,11 +199,11 @@ async def test_dhcp_renewal_match_hostname_and_macaddress(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.1.120", - dhcp.HOSTNAME: "irobot-ae9ec12dd3b04885bcbfa36afb01e1cc", - dhcp.MAC_ADDRESS: "50147903852c", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.1.120", + hostname="irobot-ae9ec12dd3b04885bcbfa36afb01e1cc", + macaddress="50147903852c", + ) async def test_dhcp_match_hostname(hass): @@ -223,11 +223,11 @@ async def test_dhcp_match_hostname(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_dhcp_match_macaddress(hass): @@ -247,11 +247,11 @@ async def test_dhcp_match_macaddress(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_dhcp_match_macaddress_without_hostname(hass): @@ -271,11 +271,11 @@ async def test_dhcp_match_macaddress_without_hostname(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.107.151", - dhcp.HOSTNAME: "", - dhcp.MAC_ADDRESS: "606bbd59e4b4", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.107.151", + hostname="", + macaddress="606bbd59e4b4", + ) async def test_dhcp_nomatch(hass): @@ -548,11 +548,11 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_device_tracker_hostname_and_macaddress_after_start(hass): @@ -585,11 +585,11 @@ async def test_device_tracker_hostname_and_macaddress_after_start(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_device_tracker_hostname_and_macaddress_after_start_not_home(hass): @@ -731,11 +731,11 @@ async def test_aiodiscover_finds_new_hosts(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) async def test_aiodiscover_does_not_call_again_on_shorter_hostname(hass): @@ -786,20 +786,20 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "irobot-abc", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="irobot-abc", + macaddress="b8b7f16db533", + ) assert mock_init.mock_calls[1][1][0] == "mock-domain" assert mock_init.mock_calls[1][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[1][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "irobot-abcdef", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[1][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="irobot-abcdef", + macaddress="b8b7f16db533", + ) async def test_aiodiscover_finds_new_hosts_after_interval(hass): @@ -838,8 +838,8 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass): assert mock_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_DHCP } - assert mock_init.mock_calls[0][2]["data"] == { - dhcp.IP_ADDRESS: "192.168.210.56", - dhcp.HOSTNAME: "connect", - dhcp.MAC_ADDRESS: "b8b7f16db533", - } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) From c557da028a201cc1ceb997ef92f4731953f07043 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Nov 2021 14:25:34 +0100 Subject: [PATCH 0634/1452] Bump entity_registry store version to 1.2 (#59912) * Bump entity_registry store version to 1.2 * Migrate also when importing yaml * Adjust tests * Satisfy pylint * Fix typing --- homeassistant/helpers/entity_registry.py | 88 ++++++++++++++++++------ homeassistant/helpers/instance_id.py | 2 +- homeassistant/helpers/storage.py | 20 +++--- tests/helpers/test_entity_registry.py | 6 +- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 9575e6722ed..27a4eaa98de 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -36,7 +36,7 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import MaxLengthExceeded -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, storage from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass from homeassistant.util import slugify @@ -58,7 +58,8 @@ DISABLED_HASS = "hass" DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" -STORAGE_VERSION = 1 +STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 2 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -147,6 +148,16 @@ class RegistryEntry: hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) +class EntityRegistryStore(storage.Store): + """Store entity registry data.""" + + async def _async_migrate_func( + self, old_major_version: int, old_minor_version: int, old_data: dict + ) -> dict: + """Migrate to the new version.""" + return await _async_migrate(old_major_version, old_minor_version, old_data) + + class EntityRegistry: """Class to hold a registry of entities.""" @@ -155,8 +166,12 @@ class EntityRegistry: self.hass = hass self.entities: dict[str, RegistryEntry] self._index: dict[tuple[str, str, str], str] = {} - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + self._store = EntityRegistryStore( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, ) self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified @@ -509,11 +524,12 @@ class EntityRegistry: """Load the entity registry.""" async_setup_entity_restore(self.hass, self) - data = await self.hass.helpers.storage.async_migrator( + data = await storage.async_migrator( + self.hass, self.hass.config.path(PATH_REGISTRY), self._store, old_conf_load_func=load_yaml, - old_conf_migrate_func=_async_migrate, + old_conf_migrate_func=_async_migrate_yaml_to_json, ) entities: dict[str, RegistryEntry] = OrderedDict() @@ -526,22 +542,22 @@ class EntityRegistry: continue entities[entity["entity_id"]] = RegistryEntry( - area_id=entity.get("area_id"), - capabilities=entity.get("capabilities") or {}, - config_entry_id=entity.get("config_entry_id"), - device_class=entity.get("device_class"), - device_id=entity.get("device_id"), - disabled_by=entity.get("disabled_by"), - entity_category=entity.get("entity_category"), + area_id=entity["area_id"], + capabilities=entity["capabilities"], + config_entry_id=entity["config_entry_id"], + device_class=entity["device_class"], + device_id=entity["device_id"], + disabled_by=entity["disabled_by"], + entity_category=entity["entity_category"], entity_id=entity["entity_id"], - icon=entity.get("icon"), - name=entity.get("name"), - original_icon=entity.get("original_icon"), - original_name=entity.get("original_name"), + icon=entity["icon"], + name=entity["name"], + original_icon=entity["original_icon"], + original_name=entity["original_name"], platform=entity["platform"], - supported_features=entity.get("supported_features", 0), + supported_features=entity["supported_features"], unique_id=entity["unique_id"], - unit_of_measurement=entity.get("unit_of_measurement"), + unit_of_measurement=entity["unit_of_measurement"], ) self.entities = entities @@ -703,13 +719,43 @@ def async_config_entry_disabled_by_changed( ) -async def _async_migrate(entities: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: +async def _async_migrate( + old_major_version: int, old_minor_version: int, data: dict +) -> dict: + """Migrate to the new version.""" + if old_major_version < 2 and old_minor_version < 2: + # From version 1.1 + for entity in data["entities"]: + # Populate all keys + entity["area_id"] = entity.get("area_id") + entity["capabilities"] = entity.get("capabilities") or {} + entity["config_entry_id"] = entity.get("config_entry_id") + entity["device_class"] = entity.get("device_class") + entity["device_id"] = entity.get("device_id") + entity["disabled_by"] = entity.get("disabled_by") + entity["entity_category"] = entity.get("entity_category") + entity["icon"] = entity.get("icon") + entity["name"] = entity.get("name") + entity["original_icon"] = entity.get("original_icon") + entity["original_name"] = entity.get("original_name") + entity["platform"] = entity["platform"] + entity["supported_features"] = entity.get("supported_features", 0) + entity["unit_of_measurement"] = entity.get("unit_of_measurement") + if old_major_version > 1: + raise NotImplementedError + return data + + +async def _async_migrate_yaml_to_json( + entities: dict[str, Any] +) -> dict[str, list[dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" - return { + entities_1_1 = { "entities": [ {"entity_id": entity_id, **info} for entity_id, info in entities.items() ] } + return await _async_migrate(1, 1, entities_1_1) @callback diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 5b6f645c55a..59a4cf39498 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -18,7 +18,7 @@ async def async_get(hass: HomeAssistant) -> str: """Get unique ID for the hass instance.""" store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) - data: dict[str, str] | None = await storage.async_migrator( # type: ignore + data: dict[str, str] | None = await storage.async_migrator( hass, hass.config.path(LEGACY_UUID_FILE), store, diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 87d8044c044..323737d5b98 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -13,7 +13,6 @@ from typing import Any from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback -from homeassistant.helpers.event import async_call_later from homeassistant.loader import MAX_LOAD_CONCURRENTLY, bind_hass from homeassistant.util import json as json_util @@ -28,13 +27,13 @@ STORAGE_SEMAPHORE = "storage_semaphore" @bind_hass async def async_migrator( - hass, - old_path, - store, + hass: HomeAssistant, + old_path: str, + store: Store, *, - old_conf_load_func=None, - old_conf_migrate_func=None, -): + old_conf_load_func: Callable | None = None, + old_conf_migrate_func: Callable | None = None, +) -> Any: """Migrate old data to a store and then load data. async def old_conf_migrate_func(old_data) @@ -157,10 +156,12 @@ class Store: stored = data["data"] else: _LOGGER.info( - "Migrating %s storage from %s to %s", + "Migrating %s storage from %s.%s to %s.%s", self.key, data["version"], + data["minor_version"], self.version, + self.minor_version, ) if len(inspect.signature(self._async_migrate_func).parameters) == 2: # pylint: disable-next=no-value-for-parameter @@ -195,6 +196,9 @@ class Store: @callback def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None: """Save data with an optional delay.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.helpers.event import async_call_later + self._data = { "version": self.version, "minor_version": self.minor_version, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index fe445e32c96..b07c4131795 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -223,7 +223,8 @@ def test_is_registered(registry): async def test_loading_extra_values(hass, hass_storage): """Test we load extra data from the registry.""" hass_storage[er.STORAGE_KEY] = { - "version": er.STORAGE_VERSION, + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": 1, "data": { "entities": [ { @@ -387,7 +388,8 @@ async def test_migration(hass): async def test_loading_invalid_entity_id(hass, hass_storage): """Test we autofix invalid entity IDs.""" hass_storage[er.STORAGE_KEY] = { - "version": er.STORAGE_VERSION, + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": er.STORAGE_VERSION_MINOR, "data": { "entities": [ { From d6c5aaa0cb8904903b956c0cce3eea7164a356f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 15:12:31 +0100 Subject: [PATCH 0635/1452] Use ServiceInfo in hunterdouglas_powerview (#59981) Co-authored-by: epenet --- .../hunterdouglas_powerview/config_flow.py | 25 +++++++++++-------- .../test_config_flow.py | 23 +++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 8dbe21eb10e..d2c78227fef 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -8,8 +8,9 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.components import dhcp, zeroconf 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 . import async_get_device_info @@ -85,25 +86,29 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return info, None - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle DHCP discovery.""" - self.discovered_ip = discovery_info[IP_ADDRESS] - self.discovered_name = discovery_info[HOSTNAME] + self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] + self.discovered_name = discovery_info[dhcp.HOSTNAME] return await self.async_step_discovery_confirm() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - self.discovered_ip = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] + self.discovered_ip = discovery_info[zeroconf.ATTR_HOST] + name = discovery_info[zeroconf.ATTR_NAME] if name.endswith(POWERVIEW_SUFFIX): name = name[: -len(POWERVIEW_SUFFIX)] self.discovered_name = name return await self.async_step_discovery_confirm() - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle HomeKit discovery.""" - self.discovered_ip = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] + self.discovered_ip = discovery_info[zeroconf.ATTR_HOST] + name = discovery_info[zeroconf.ATTR_NAME] if name.endswith(HAP_SUFFIX): name = name[: -len(HAP_SUFFIX)] self.discovered_name = name diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index e3cb9aa4c8b..1631cd2cb60 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -6,22 +6,25 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant import config_entries +from homeassistant.components import dhcp, zeroconf from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from tests.common import MockConfigEntry, load_fixture -HOMEKIT_DISCOVERY_INFO = { - "name": "Hunter Douglas Powerview Hub._hap._tcp.local.", - "host": "1.2.3.4", - "properties": {"id": "AA::BB::CC::DD::EE::FF"}, -} +HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + name="Hunter Douglas Powerview Hub._hap._tcp.local.", + host="1.2.3.4", + properties={"id": "AA::BB::CC::DD::EE::FF"}, +) -ZEROCONF_DISCOVERY_INFO = { - "name": "Hunter Douglas Powerview Hub._powerview._tcp.local.", - "host": "1.2.3.4", -} +ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + name="Hunter Douglas Powerview Hub._powerview._tcp.local.", + host="1.2.3.4", +) -DHCP_DISCOVERY_INFO = {"hostname": "Hunter Douglas Powerview Hub", "ip": "1.2.3.4"} +DHCP_DISCOVERY_INFO = dhcp.DhcpServiceInfo( + hostname="Hunter Douglas Powerview Hub", ip="1.2.3.4" +) DISCOVERY_DATA = [ ( From 982f2065c848308af73713dd533889ad5e9817d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 15:13:56 +0100 Subject: [PATCH 0636/1452] Use ZeroconfServiceInfo in homekit_controller (#59979) Co-authored-by: epenet --- .../homekit_controller/config_flow.py | 30 +++++++------- .../homekit_controller/test_config_flow.py | 39 ++++++++++--------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index c8118f61b87..905aec83fe2 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, async_get_registry as async_get_device_registry, @@ -157,13 +158,13 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): continue record = device.info return await self.async_step_zeroconf( - { - "host": record["address"], - "port": record["port"], - "hostname": record["name"], - "type": "_hap._tcp.local.", - "name": record["name"], - "properties": { + zeroconf.ZeroconfServiceInfo( + host=record["address"], + port=record["port"], + hostname=record["name"], + type="_hap._tcp.local.", + name=record["name"], + properties={ "md": record["md"], "pv": record["pv"], "id": unique_id, @@ -174,7 +175,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "sf": record["sf"], "sh": "", }, - } + ) ) return self.async_abort(reason="no_devices") @@ -196,7 +197,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return False - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a discovered HomeKit accessory. This flow is triggered by the discovery component. @@ -205,7 +208,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # homekit_python has code to do this, but not in a form we can # easily use, so do the bare minimum ourselves here instead. properties = { - key.lower(): value for (key, value) in discovery_info["properties"].items() + key.lower(): value + for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() } if "id" not in properties: @@ -221,7 +225,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # It changes if a device is factory reset. hkid = properties["id"] model = properties["md"] - name = discovery_info["name"].replace("._hap._tcp.local.", "") + name = discovery_info[zeroconf.ATTR_NAME].replace("._hap._tcp.local.", "") status_flags = int(properties["sf"]) paired = not status_flags & 0x01 @@ -239,8 +243,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Set unique-id and error out if it's already configured existing_entry = await self.async_set_unique_id(normalize_hkid(hkid)) updated_ip_port = { - "AccessoryIP": discovery_info["host"], - "AccessoryPort": discovery_info["port"], + "AccessoryIP": discovery_info[zeroconf.ATTR_HOST], + "AccessoryPort": discovery_info[zeroconf.ATTR_PORT], } # If the device is already paired and known to us we should monitor c# diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index b08659bf77b..7ed4b7e5422 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -11,6 +11,7 @@ from aiohomekit.model.services import ServicesTypes import pytest from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.helpers import device_registry @@ -133,16 +134,18 @@ def get_flow_context(hass, result): return flow["context"] -def get_device_discovery_info(device, upper_case_props=False, missing_csharp=False): +def get_device_discovery_info( + device, upper_case_props=False, missing_csharp=False +) -> zeroconf.ZeroconfServiceInfo: """Turn a aiohomekit format zeroconf entry into a homeassistant one.""" record = device.info - result = { - "host": record["address"], - "port": record["port"], - "hostname": record["name"], - "type": "_hap._tcp.local.", - "name": record["name"], - "properties": { + result = zeroconf.ZeroconfServiceInfo( + host=record["address"], + port=record["port"], + hostname=record["name"], + type="_hap._tcp.local.", + name=record["name"], + properties={ "md": record["md"], "pv": record["pv"], "id": device.device_id, @@ -153,7 +156,7 @@ def get_device_discovery_info(device, upper_case_props=False, missing_csharp=Fal "sf": 0x01, # record["sf"], "sh": "", }, - } + ) if missing_csharp: del result["properties"]["c#"] @@ -286,8 +289,8 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) - discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" - discovery_info["properties"]["md"] = "HHKBridge1,1" + discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES]["md"] = "HHKBridge1,1" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -314,7 +317,7 @@ async def test_discovery_ignored_hk_bridge(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -341,7 +344,7 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -373,7 +376,7 @@ async def test_discovery_broken_pairing_flag(hass, controller): discovery_info = get_device_discovery_info(device) # Make sure that we are pairable - assert discovery_info["properties"]["sf"] != 0x0 + assert discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] != 0x0 # Device is discovered result = await hass.config_entries.flow.async_init( @@ -445,7 +448,7 @@ async def test_discovery_already_configured(hass, controller): discovery_info = get_device_discovery_info(device) # Set device as already paired - discovery_info["properties"]["sf"] = 0x00 + discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] = 0x00 # Device is discovered result = await hass.config_entries.flow.async_init( @@ -481,9 +484,9 @@ async def test_discovery_already_configured_update_csharp(hass, controller): discovery_info = get_device_discovery_info(device) # Set device as already paired - discovery_info["properties"]["sf"] = 0x00 - discovery_info["properties"]["c#"] = 99999 - discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] = 0x00 + discovery_info[zeroconf.ATTR_PROPERTIES]["c#"] = 99999 + discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( From 14d4a9a69d73bba018f5ffe0f56824f6f98cdef9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 15:14:25 +0100 Subject: [PATCH 0637/1452] Use DhcpServiceInfo in isy994 (#59982) Co-authored-by: epenet --- .../components/isy994/config_flow.py | 15 +++++----- tests/components/isy994/test_config_flow.py | 30 +++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 413d0689b6e..60e69f73cef 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -10,8 +10,7 @@ from pyisy.connection import Connection import voluptuous as vol from homeassistant import config_entries, core, data_entry_flow, exceptions -from homeassistant.components import ssdp -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, ssdp from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -184,16 +183,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) raise data_entry_flow.AbortFlow("already_configured") - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle a discovered isy994 via dhcp.""" - friendly_name = discovery_info[HOSTNAME] - url = f"http://{discovery_info[IP_ADDRESS]}" - mac = discovery_info[MAC_ADDRESS] + friendly_name = discovery_info[dhcp.HOSTNAME] + url = f"http://{discovery_info[dhcp.IP_ADDRESS]}" + mac = discovery_info[dhcp.MAC_ADDRESS] isy_mac = ( f"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}" ) await self._async_set_unique_id_or_update( - isy_mac, discovery_info[IP_ADDRESS], None + isy_mac, discovery_info[dhcp.IP_ADDRESS], None ) self.discovered_conf = { diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 62a779293be..c2853454327 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -485,11 +485,11 @@ async def test_form_dhcp(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data={ - dhcp.IP_ADDRESS: "1.2.3.4", - dhcp.HOSTNAME: "isy994-ems", - dhcp.MAC_ADDRESS: MOCK_MAC, - }, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="isy994-ems", + macaddress=MOCK_MAC, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -529,11 +529,11 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data={ - dhcp.IP_ADDRESS: "1.2.3.4", - dhcp.HOSTNAME: "isy994-ems", - dhcp.MAC_ADDRESS: MOCK_MAC, - }, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="isy994-ems", + macaddress=MOCK_MAC, + ), ) await hass.async_block_till_done() @@ -559,11 +559,11 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data={ - dhcp.IP_ADDRESS: "1.2.3.4", - dhcp.HOSTNAME: "isy994-ems", - dhcp.MAC_ADDRESS: MOCK_MAC, - }, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="isy994-ems", + macaddress=MOCK_MAC, + ), ) await hass.async_block_till_done() From 2b7bcd6aeb9e111ca2f74b50fe23d3d1e41aa86d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 19 Nov 2021 16:02:07 +0100 Subject: [PATCH 0638/1452] Yeelight disable polling (#59885) Co-authored-by: J. Nick Koston --- homeassistant/components/yeelight/light.py | 5 +++-- tests/components/yeelight/test_init.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index ca2752a7a7d..838e56f5d99 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -322,7 +322,7 @@ async def async_setup_entry( device.name, ) - async_add_entities(lights, True) + async_add_entities(lights) _async_setup_services(hass) @@ -411,6 +411,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): _attr_color_mode = COLOR_MODE_BRIGHTNESS _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + _attr_should_poll = False def __init__(self, device, entry, custom_effects=None): """Initialize the Yeelight light.""" @@ -591,7 +592,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_update(self): """Update light properties.""" - await self.device.async_update() + await self.device.async_update(True) async def async_set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 7e9958a09d2..39e31aa91cf 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, + STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant @@ -530,12 +531,14 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant): assert len(mocked_bulb.async_get_properties.mock_calls) == 1 mocked_bulb._async_callback({KEY_CONNECTED: False}) await hass.async_block_till_done() + assert hass.states.get("light.test_name").state == STATE_UNAVAILABLE assert len(mocked_bulb.async_get_properties.mock_calls) == 1 mocked_bulb._async_callback({KEY_CONNECTED: True}) async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=STATE_CHANGE_TIME) ) await hass.async_block_till_done() + assert hass.states.get("light.test_name").state == STATE_ON assert len(mocked_bulb.async_get_properties.mock_calls) == 2 From 9aa41be8b78dc56e82646ac6277047fde64ff011 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 16:36:18 +0100 Subject: [PATCH 0639/1452] Use ZeroconfServiceInfo in lutron_caseta (#59988) Co-authored-by: epenet --- .../components/lutron_caseta/config_flow.py | 14 +++++--- .../lutron_caseta/test_config_flow.py | 33 ++++++++++--------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 9d028e97c87..dac0d6c5f94 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -10,8 +10,10 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import ( ABORT_REASON_CANNOT_CONNECT, @@ -61,16 +63,18 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - hostname = discovery_info["hostname"] + hostname = discovery_info[zeroconf.ATTR_HOSTNAME] if hostname is None or not hostname.startswith("lutron-"): return self.async_abort(reason="not_lutron_device") self.lutron_id = hostname.split("-")[1].replace(".local.", "") await self.async_set_unique_id(self.lutron_id) - host = discovery_info[CONF_HOST] + host = discovery_info[zeroconf.ATTR_HOST] self._abort_if_unique_id_configured({CONF_HOST: host}) self.data[CONF_HOST] = host @@ -80,7 +84,9 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_link() - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a flow initialized by homekit discovery.""" return await self.async_step_zeroconf(discovery_info) diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 2697bb363f6..ee9403bad50 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -8,6 +8,7 @@ from pylutron_caseta.smartbridge import Smartbridge import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.lutron_caseta import DOMAIN import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow from homeassistant.components.lutron_caseta.const import ( @@ -424,10 +425,10 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: "1.1.1.1", - ATTR_HOSTNAME: "lutron-abc.local.", - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + hostname="lutron-abc.local.", + ), ) await hass.async_block_till_done() @@ -447,10 +448,10 @@ async def test_zeroconf_lutron_id_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: "1.1.1.1", - ATTR_HOSTNAME: "lutron-abc.local.", - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + hostname="lutron-abc.local.", + ), ) await hass.async_block_till_done() @@ -465,10 +466,10 @@ async def test_zeroconf_not_lutron_device(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: "1.1.1.1", - ATTR_HOSTNAME: "notlutron-abc.local.", - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + hostname="notlutron-abc.local.", + ), ) await hass.async_block_till_done() @@ -489,10 +490,10 @@ async def test_zeroconf(hass, source, tmpdir): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={ - CONF_HOST: "1.1.1.1", - ATTR_HOSTNAME: "lutron-abc.local.", - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + hostname="lutron-abc.local.", + ), ) await hass.async_block_till_done() From 81e02a39df2ebcece259c5dfcabc77b66b958846 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 19 Nov 2021 10:14:12 -0600 Subject: [PATCH 0640/1452] Ignore non-Sonos SSDP devices with Sonos-like identifiers (#59809) --- homeassistant/components/sonos/__init__.py | 8 +++++--- tests/components/sonos/conftest.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 5e02832b05a..72e5a33ca28 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -267,11 +267,13 @@ class SonosDiscoveryManager: if change == ssdp.SsdpChange.BYEBYE: return + uid = info.get(ssdp.ATTR_UPNP_UDN) + if not uid.startswith("uuid:RINCON_"): + return + + uid = uid[5:] discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname boot_seqnum = info.get("X-RINCON-BOOTSEQ") - uid = info.get(ssdp.ATTR_UPNP_UDN) - if uid.startswith("uuid:"): - uid = uid[5:] self.async_discovered_player( "SSDP", info, discovered_ip, uid, boot_seqnum, info.get("modelName"), None ) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f650c6e8fef..8a3a6571faa 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -93,7 +93,7 @@ def discover_fixture(soco): async def do_callback(hass, callback, *args, **kwargs): await callback( { - ssdp.ATTR_UPNP_UDN: soco.uid, + ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", ssdp.ATTR_SSDP_LOCATION: f"http://{soco.ip_address}/", }, ssdp.SsdpChange.ALIVE, From 8a4d3b2a2e4ae7ecf14f3caee148a115f6d2e91f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 17:16:07 +0100 Subject: [PATCH 0641/1452] Use ZeroconfServiceInfo in lookin (#59987) Co-authored-by: epenet --- homeassistant/components/lookin/config_flow.py | 4 ++-- tests/components/lookin/__init__.py | 16 ++++++++-------- tests/components/lookin/test_config_flow.py | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index e41aad0406b..af42febbb3f 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -31,8 +31,8 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Start a discovery flow from zeroconf.""" - uid: str = discovery_info["hostname"][: -len(".local.")] - host: str = discovery_info["host"] + uid: str = discovery_info[zeroconf.ATTR_HOSTNAME][: -len(".local.")] + host: str = discovery_info[zeroconf.ATTR_HOST] await self.async_set_unique_id(uid.upper()) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index c55b5228b47..911e984a57e 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -17,14 +17,14 @@ DEFAULT_ENTRY_TITLE = DEVICE_NAME ZC_NAME = f"LOOKin_{DEVICE_ID}" ZC_TYPE = "_lookin._tcp." -ZEROCONF_DATA: ZeroconfServiceInfo = { - "host": IP_ADDRESS, - "hostname": f"{ZC_NAME.lower()}.local.", - "port": 80, - "type": ZC_TYPE, - "name": ZC_NAME, - "properties": {}, -} +ZEROCONF_DATA = ZeroconfServiceInfo( + host=IP_ADDRESS, + hostname=f"{ZC_NAME.lower()}.local.", + port=80, + type=ZC_TYPE, + name=ZC_NAME, + properties={}, +) def _mocked_climate() -> Climate: diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index 92f6e500045..2050e80392d 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch from aiolookin import NoUsableService from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.lookin.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -138,7 +139,7 @@ async def test_discovered_zeroconf(hass): entry = hass.config_entries.async_entries(DOMAIN)[0] zc_data_new_ip = ZEROCONF_DATA.copy() - zc_data_new_ip["host"] = "127.0.0.2" + zc_data_new_ip[zeroconf.ATTR_HOST] = "127.0.0.2" with _patch_get_info(), patch( f"{MODULE}.async_setup_entry", return_value=True From e3ee19d0c432fcb96876a297f7f3050ff8616fd6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 17:16:28 +0100 Subject: [PATCH 0642/1452] Use ZeroconfServiceInfo in kodi (#59984) Co-authored-by: epenet --- homeassistant/components/kodi/config_flow.py | 17 +++++----- tests/components/kodi/util.py | 33 ++++++++++---------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 404540d47aa..6e7ff6bb2a4 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -7,6 +7,7 @@ from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connecti import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,8 +18,8 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_WS_PORT, @@ -99,15 +100,17 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._ssl: bool | None = DEFAULT_SSL self._discovery_name: str | None = None - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - self._host = discovery_info["host"] - self._port = int(discovery_info["port"]) - self._name = discovery_info["hostname"][: -len(".local.")] - if not (uuid := discovery_info["properties"].get("uuid")): + self._host = discovery_info[zeroconf.ATTR_HOST] + self._port = int(discovery_info[zeroconf.ATTR_PORT]) + self._name = discovery_info[zeroconf.ATTR_HOSTNAME][: -len(".local.")] + if not (uuid := discovery_info[zeroconf.ATTR_PROPERTIES].get("uuid")): return self.async_abort(reason="no_uuid") - self._discovery_name = discovery_info["name"] + self._discovery_name = discovery_info[zeroconf.ATTR_NAME] await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured( diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index edd6950d76e..c3aaca16d5a 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -1,4 +1,5 @@ """Test the Kodi config flow.""" +from homeassistant.components import zeroconf from homeassistant.components.kodi.const import DEFAULT_SSL TEST_HOST = { @@ -14,24 +15,24 @@ TEST_CREDENTIALS = {"username": "username", "password": "password"} TEST_WS_PORT = {"ws_port": 9090} UUID = "11111111-1111-1111-1111-111111111111" -TEST_DISCOVERY = { - "host": "1.1.1.1", - "port": 8080, - "hostname": "hostname.local.", - "type": "_xbmc-jsonrpc-h._tcp.local.", - "name": "hostname._xbmc-jsonrpc-h._tcp.local.", - "properties": {"uuid": UUID}, -} +TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + port=8080, + hostname="hostname.local.", + type="_xbmc-jsonrpc-h._tcp.local.", + name="hostname._xbmc-jsonrpc-h._tcp.local.", + properties={"uuid": UUID}, +) -TEST_DISCOVERY_WO_UUID = { - "host": "1.1.1.1", - "port": 8080, - "hostname": "hostname.local.", - "type": "_xbmc-jsonrpc-h._tcp.local.", - "name": "hostname._xbmc-jsonrpc-h._tcp.local.", - "properties": {}, -} +TEST_DISCOVERY_WO_UUID = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + port=8080, + hostname="hostname.local.", + type="_xbmc-jsonrpc-h._tcp.local.", + name="hostname._xbmc-jsonrpc-h._tcp.local.", + properties={}, +) TEST_IMPORT = { From 386520b8839a72e418f61e449f98275b40879121 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Nov 2021 17:16:34 +0100 Subject: [PATCH 0643/1452] Use ZeroconfServiceInfo in ipp (#59983) Co-authored-by: epenet --- homeassistant/components/ipp/config_flow.py | 17 +++++---- tests/components/ipp/__init__.py | 42 +++++++++------------ tests/components/ipp/test_config_flow.py | 8 +++- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index b760fccb598..543592cd4f0 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -15,6 +15,7 @@ from pyipp import ( ) import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -99,25 +100,27 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_zeroconf(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - port = discovery_info[CONF_PORT] - zctype = discovery_info["type"] - name = discovery_info[CONF_NAME].replace(f".{zctype}", "") + port = discovery_info[zeroconf.ATTR_PORT] + zctype = discovery_info[zeroconf.ATTR_TYPE] + name = discovery_info[zeroconf.ATTR_NAME].replace(f".{zctype}", "") tls = zctype == "_ipps._tcp.local." - base_path = discovery_info["properties"].get("rp", "ipp/print") + base_path = discovery_info[zeroconf.ATTR_PROPERTIES].get("rp", "ipp/print") self.context.update({"title_placeholders": {"name": name}}) self.discovery_info.update( { - CONF_HOST: discovery_info[CONF_HOST], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], CONF_PORT: port, CONF_SSL: tls, CONF_VERIFY_SSL: False, CONF_BASE_PATH: f"/{base_path}", CONF_NAME: name, - CONF_UUID: discovery_info["properties"].get("UUID"), + CONF_UUID: discovery_info[zeroconf.ATTR_PROPERTIES].get("UUID"), } ) diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index ec1001b5772..0e88fb21baf 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -2,15 +2,9 @@ import aiohttp from pyipp import IPPConnectionUpgradeRequired, IPPError +from homeassistant.components import zeroconf from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SSL, - CONF_TYPE, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, get_fixture_path @@ -40,23 +34,23 @@ MOCK_USER_INPUT = { CONF_BASE_PATH: BASE_PATH, } -MOCK_ZEROCONF_IPP_SERVICE_INFO = { - CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE, - CONF_NAME: f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}", - CONF_HOST: ZEROCONF_HOST, - ATTR_HOSTNAME: ZEROCONF_HOSTNAME, - CONF_PORT: ZEROCONF_PORT, - ATTR_PROPERTIES: {"rp": ZEROCONF_RP}, -} +MOCK_ZEROCONF_IPP_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( + type=IPP_ZEROCONF_SERVICE_TYPE, + name=f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}", + host=ZEROCONF_HOST, + hostname=ZEROCONF_HOSTNAME, + port=ZEROCONF_PORT, + properties={"rp": ZEROCONF_RP}, +) -MOCK_ZEROCONF_IPPS_SERVICE_INFO = { - CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE, - CONF_NAME: f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}", - CONF_HOST: ZEROCONF_HOST, - ATTR_HOSTNAME: ZEROCONF_HOSTNAME, - CONF_PORT: ZEROCONF_PORT, - ATTR_PROPERTIES: {"rp": ZEROCONF_RP}, -} +MOCK_ZEROCONF_IPPS_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( + type=IPPS_ZEROCONF_SERVICE_TYPE, + name=f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}", + host=ZEROCONF_HOST, + hostname=ZEROCONF_HOSTNAME, + port=ZEROCONF_PORT, + properties={"rp": ZEROCONF_RP}, +) def load_fixture_binary(filename): diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 23670ff0d1a..b08150ba9bd 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for the IPP config flow.""" from unittest.mock import patch +from homeassistant.components import zeroconf from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL @@ -281,7 +282,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( discovery_info = { **MOCK_ZEROCONF_IPP_SERVICE_INFO, "properties": { - **MOCK_ZEROCONF_IPP_SERVICE_INFO["properties"], + **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", }, } @@ -303,7 +304,10 @@ async def test_zeroconf_empty_unique_id( discovery_info = { **MOCK_ZEROCONF_IPP_SERVICE_INFO, - "properties": {**MOCK_ZEROCONF_IPP_SERVICE_INFO["properties"], "UUID": ""}, + "properties": { + **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], + "UUID": "", + }, } result = await hass.config_entries.flow.async_init( DOMAIN, From da68cfa82145fe059935a746d1e155f2ea431907 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 19 Nov 2021 19:31:28 +0100 Subject: [PATCH 0644/1452] Bump devolo_plc_api to 0.6.3 (#59991) --- homeassistant/components/devolo_home_network/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 987211ca631..ef3f1f0c82a 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -3,7 +3,7 @@ "name": "devolo Home Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", - "requirements": ["devolo-plc-api==0.6.2"], + "requirements": ["devolo-plc-api==0.6.3"], "zeroconf": ["_dvl-deviceapi._tcp.local."], "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 15e80661719..b37f890f11f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ denonavr==0.10.9 devolo-home-control-api==0.17.4 # homeassistant.components.devolo_home_network -devolo-plc-api==0.6.2 +devolo-plc-api==0.6.3 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42162f3b655..4a3c2c848b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -342,7 +342,7 @@ denonavr==0.10.9 devolo-home-control-api==0.17.4 # homeassistant.components.devolo_home_network -devolo-plc-api==0.6.2 +devolo-plc-api==0.6.3 # homeassistant.components.directv directv==0.4.0 From 6f091d235fe78457be5f24204d9bf8c3ad15b0d6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 19 Nov 2021 21:16:21 +0100 Subject: [PATCH 0645/1452] Fix late comment correcting kwargs should always be Any in deCONZ covers (#59997) * Fix late comments correcting kwargs should always be Any --- homeassistant/components/deconz/cover.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 4c40e89b9f3..324452c4aa6 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import ValuesView -from typing import Any +from typing import Any, cast from pydeconz.light import Cover @@ -108,9 +108,9 @@ class DeconzCover(DeconzDevice, CoverEntity): """Return if the cover is closed.""" return not self._device.is_open - async def async_set_cover_position(self, **kwargs: int) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - position = 100 - kwargs[ATTR_POSITION] + position = 100 - cast(int, kwargs[ATTR_POSITION]) await self._device.set_position(lift=position) async def async_open_cover(self, **kwargs: Any) -> None: @@ -132,9 +132,9 @@ class DeconzCover(DeconzDevice, CoverEntity): return 100 - self._device.tilt # type: ignore[no-any-return] return None - async def async_set_cover_tilt_position(self, **kwargs: int) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the cover to a specific position.""" - position = 100 - kwargs[ATTR_TILT_POSITION] + position = 100 - cast(int, kwargs[ATTR_TILT_POSITION]) await self._device.set_position(tilt=position) async def async_open_cover_tilt(self, **kwargs: Any) -> None: From dedc4a8285eeece18d45cc7b5d00e13a83d4ce75 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Nov 2021 13:53:47 -0700 Subject: [PATCH 0646/1452] Prevent OpenUV from blocking other integrations' startup (#59956) * Prevent OpenUV from blocking other integrations' startup * Comment --- homeassistant/components/openuv/__init__.py | 30 ++++++++++++------- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 20ef5211c23..77755fdca21 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -51,23 +51,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" _verify_domain_control = verify_domain_control(hass, DOMAIN) + websession = aiohttp_client.async_get_clientsession(hass) + openuv = OpenUV( + entry, + Client( + entry.data[CONF_API_KEY], + entry.data.get(CONF_LATITUDE, hass.config.latitude), + entry.data.get(CONF_LONGITUDE, hass.config.longitude), + altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), + session=websession, + ), + ) + + # We disable the client's request retry abilities here to avoid a lengthy (and + # blocking) startup: + openuv.client.disable_request_retries() + try: - websession = aiohttp_client.async_get_clientsession(hass) - openuv = OpenUV( - entry, - Client( - entry.data[CONF_API_KEY], - entry.data.get(CONF_LATITUDE, hass.config.latitude), - entry.data.get(CONF_LONGITUDE, hass.config.longitude), - altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), - session=websession, - ), - ) await openuv.async_update() except OpenUvError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err + # Once we've successfully authenticated, we re-enable client request retries: + openuv.client.enable_request_retries() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = openuv diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 207bd307d21..6132cda2710 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==2.2.1"], + "requirements": ["pyopenuv==2021.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b37f890f11f..7b08015b8f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1691,7 +1691,7 @@ pyoctoprintapi==0.1.6 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2.2.1 +pyopenuv==2021.11.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a3c2c848b5..b1e4b52a2fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1034,7 +1034,7 @@ pynzbgetapi==0.2.0 pyoctoprintapi==0.1.6 # homeassistant.components.openuv -pyopenuv==2.2.1 +pyopenuv==2021.11.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From 8f6796f428e7464d0c6ebad2535dddc573e723fd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Nov 2021 14:01:26 -0700 Subject: [PATCH 0647/1452] Prevent IQVIA from blocking other integrations' startup (#60001) --- homeassistant/components/iqvia/__init__.py | 7 +++++++ homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 38d29aeffb2..b0f7086cad7 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -51,6 +51,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = aiohttp_client.async_get_clientsession(hass) client = Client(entry.data[CONF_ZIP_CODE], session=websession) + # We disable the client's request retry abilities here to avoid a lengthy (and + # blocking) startup: + client.disable_request_retries() + async def async_get_data_from_api( api_coro: Callable[..., Awaitable] ) -> dict[str, Any]: @@ -90,6 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # API calls fail: raise ConfigEntryNotReady() + # Once we've successfully authenticated, we re-enable client request retries: + client.enable_request_retries() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 3ac1ffd31f9..f78ca1e258c 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.4", "pyiqvia==1.1.0"], + "requirements": ["numpy==1.21.4", "pyiqvia==2021.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7b08015b8f7..8327ac75675 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1556,7 +1556,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==1.1.0 +pyiqvia==2021.11.0 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1e4b52a2fa..bccd9f18359 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -944,7 +944,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==1.1.0 +pyiqvia==2021.11.0 # homeassistant.components.isy994 pyisy==3.0.0 From 4f89ce4fb8c0a610094a5ea1a34ae45869f72b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 19 Nov 2021 23:18:52 +0100 Subject: [PATCH 0648/1452] Bump Mill library (#59995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index a2507251524..9cb220b06dc 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.8.0", "mill-local==0.1.0"], + "requirements": ["millheater==0.9.0", "mill-local==0.1.0"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 8327ac75675..b303e91a28a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ miflora==0.7.0 mill-local==0.1.0 # homeassistant.components.mill -millheater==0.8.0 +millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bccd9f18359..079aee4c98c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -615,7 +615,7 @@ micloud==0.4 mill-local==0.1.0 # homeassistant.components.mill -millheater==0.8.0 +millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 From 5f8646600fae63b8afca3ecd7986ac5fcc257ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 19 Nov 2021 23:19:56 +0100 Subject: [PATCH 0649/1452] Correct supported_features and model for Mill (#59996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Correct supported_features and model for Mill Signed-off-by: Daniel Hjelseth Høyer * Correct supported_features and model for Mill Signed-off-by: Daniel Hjelseth Høyer * revert Signed-off-by: Daniel Hjelseth Høyer * Mill Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/climate.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 80611cf9f96..91ba10618f0 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -86,7 +86,6 @@ class MillHeater(CoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS @@ -103,13 +102,21 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.device_id)}, manufacturer=MANUFACTURER, - model=f"generation {1 if heater.is_gen1 else 2}", + model=f"Generation {heater.generation}", name=self.name, ) if heater.is_gen1 or heater.is_gen3: self._attr_hvac_modes = [HVAC_MODE_HEAT] else: self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + + if heater.generation < 3: + self._attr_supported_features = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + ) + else: + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._update_attr(heater) async def async_set_temperature(self, **kwargs): @@ -162,7 +169,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): "open_window": heater.open_window, "heating": heater.is_heating, "controlled_by_tibber": heater.tibber_control, - "heater_generation": 1 if heater.is_gen1 else 2, + "heater_generation": heater.generation, } if heater.room: self._attr_extra_state_attributes["room"] = heater.room.name From e8970d6390cee760b78c941317c246f58c88c163 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 19 Nov 2021 22:46:45 +0000 Subject: [PATCH 0650/1452] Bump roombapy to 1.6.4 (#60008) --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 2aaa1f6762e..907026fd77e 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.3"], + "requirements": ["roombapy==1.6.4"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index b303e91a28a..0d3c85970b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2073,7 +2073,7 @@ rocketchat-API==0.6.1 rokuecp==0.8.1 # homeassistant.components.roomba -roombapy==1.6.3 +roombapy==1.6.4 # homeassistant.components.roon roonapi==0.0.38 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 079aee4c98c..d87c868f2b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,7 +1226,7 @@ ring_doorbell==0.7.1 rokuecp==0.8.1 # homeassistant.components.roomba -roombapy==1.6.3 +roombapy==1.6.4 # homeassistant.components.roon roonapi==0.0.38 From 72b0eb719e646cd8983f30ff797e7cd1cbe911e9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 20 Nov 2021 00:12:27 +0000 Subject: [PATCH 0651/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 6 ++- .../accuweather/translations/ja.json | 4 ++ .../components/adguard/translations/ja.json | 6 ++- .../advantage_air/translations/ja.json | 1 + .../components/aemet/translations/ja.json | 1 + .../components/airly/translations/ja.json | 4 ++ .../components/airnow/translations/ja.json | 10 ++++- .../components/airvisual/translations/ja.json | 22 ++++++++++ .../alarm_control_panel/translations/ja.json | 5 +++ .../alarmdecoder/translations/ja.json | 42 +++++++++++++++++-- .../components/almond/translations/ja.json | 9 ++++ .../components/ambee/translations/ja.json | 5 +++ .../ambiclimate/translations/ja.json | 8 ++++ .../ambient_station/translations/ja.json | 1 + .../components/apple_tv/translations/ja.json | 37 +++++++++++++++- .../components/asuswrt/translations/ja.json | 18 +++++--- .../components/atag/translations/ja.json | 6 ++- .../components/august/translations/ja.json | 12 ++++-- .../components/axis/translations/ja.json | 3 +- .../binary_sensor/translations/ja.json | 1 + .../components/blebox/translations/ja.json | 1 + .../components/blink/translations/ja.json | 3 +- .../bmw_connected_drive/translations/ja.json | 3 +- .../components/brunt/translations/ca.json | 29 +++++++++++++ .../components/brunt/translations/de.json | 29 +++++++++++++ .../components/brunt/translations/et.json | 29 +++++++++++++ .../components/brunt/translations/ja.json | 20 +++++++++ .../components/brunt/translations/lt.json | 17 ++++++++ .../components/brunt/translations/no.json | 29 +++++++++++++ .../components/brunt/translations/ru.json | 29 +++++++++++++ .../brunt/translations/zh-Hant.json | 29 +++++++++++++ .../components/bsblan/translations/ja.json | 3 +- .../components/button/translations/lt.json | 3 ++ .../components/canary/translations/ja.json | 3 +- .../components/climacell/translations/ja.json | 11 +++++ .../components/climate/translations/ja.json | 3 +- .../components/control4/translations/ja.json | 1 + .../coolmaster/translations/ja.json | 3 +- .../components/daikin/translations/ja.json | 4 +- .../components/deconz/translations/ja.json | 4 ++ .../components/denonavr/translations/ja.json | 3 ++ .../device_tracker/translations/ja.json | 3 +- .../devolo_home_control/translations/ja.json | 5 +++ .../components/dexcom/translations/ja.json | 3 +- .../components/directv/translations/ja.json | 1 + .../components/doorbird/translations/ja.json | 3 +- .../components/elkm1/translations/ja.json | 4 +- .../components/epson/translations/ja.json | 3 +- .../components/fan/translations/ja.json | 3 +- .../fireservicerota/translations/ja.json | 3 +- .../components/flo/translations/ja.json | 3 +- .../components/flume/translations/ja.json | 8 +++- .../forecast_solar/translations/ja.json | 3 +- .../components/foscam/translations/ja.json | 3 +- .../components/freebox/translations/ja.json | 1 + .../components/fritz/translations/ja.json | 18 ++++++-- .../components/fritzbox/translations/ja.json | 9 +++- .../fritzbox_callmonitor/translations/ja.json | 4 +- .../garages_amsterdam/translations/ja.json | 6 ++- .../components/goalzero/translations/ja.json | 6 ++- .../components/gogogate2/translations/ja.json | 4 +- .../google_travel_time/translations/ja.json | 10 +++++ .../growatt_server/translations/ja.json | 3 ++ .../components/guardian/translations/ja.json | 3 ++ .../components/habitica/translations/ja.json | 3 ++ .../components/hlk_sw16/translations/ja.json | 3 +- .../homeassistant/translations/ja.json | 1 + .../components/homekit/translations/ja.json | 13 +++++- .../huawei_lte/translations/ja.json | 6 ++- .../hvv_departures/translations/ja.json | 3 +- .../components/hyperion/translations/ja.json | 3 ++ .../components/iaqualink/translations/ja.json | 3 +- .../components/icloud/translations/ja.json | 3 ++ .../input_boolean/translations/ja.json | 3 +- .../input_datetime/translations/ja.json | 3 ++ .../input_number/translations/ja.json | 3 ++ .../input_select/translations/ja.json | 3 ++ .../input_text/translations/ja.json | 3 ++ .../components/insteon/translations/ja.json | 9 +++- .../components/ipp/translations/ja.json | 4 +- .../components/isy994/translations/ja.json | 8 ++++ .../components/jellyfin/translations/ja.json | 4 +- .../keenetic_ndms2/translations/ja.json | 4 +- .../components/kodi/translations/ja.json | 3 +- .../components/konnected/translations/ja.json | 7 +++- .../components/life360/translations/ja.json | 3 +- .../components/lock/translations/ja.json | 7 ++++ .../logi_circle/translations/ja.json | 1 + .../lutron_caseta/translations/ja.json | 8 ++++ .../components/mikrotik/translations/ja.json | 12 +++++- .../components/mill/translations/ca.json | 16 ++++++- .../components/mill/translations/de.json | 16 ++++++- .../components/mill/translations/en.json | 4 +- .../components/mill/translations/et.json | 16 ++++++- .../components/mill/translations/ja.json | 16 ++++++- .../components/mill/translations/no.json | 16 ++++++- .../components/mill/translations/ru.json | 16 ++++++- .../components/mill/translations/zh-Hant.json | 16 ++++++- .../minecraft_server/translations/ja.json | 3 +- .../motion_blinds/translations/ja.json | 12 +++++- .../components/mqtt/translations/ja.json | 14 ++++++- .../components/myq/translations/ja.json | 6 ++- .../components/mysensors/translations/ja.json | 1 + .../components/nam/translations/ja.json | 6 ++- .../components/nexia/translations/ja.json | 3 +- .../nfandroidtv/translations/ja.json | 3 +- .../nightscout/translations/ja.json | 11 +++++ .../components/notion/translations/ja.json | 3 +- .../components/nuheat/translations/ja.json | 3 +- .../components/nzbget/translations/ja.json | 3 +- .../components/omnilogic/translations/ja.json | 3 +- .../components/onvif/translations/ja.json | 10 ++++- .../components/pi_hole/translations/ja.json | 7 ++++ .../components/plant/translations/ja.json | 3 +- .../components/plugwise/translations/ja.json | 1 + .../components/point/translations/ja.json | 1 + .../components/proximity/translations/ja.json | 3 ++ .../components/ps4/translations/ja.json | 1 + .../components/rachio/translations/ja.json | 11 +++++ .../components/remote/translations/ja.json | 3 +- .../components/roomba/translations/ja.json | 9 +++- .../components/roon/translations/ja.json | 1 + .../components/samsungtv/translations/ja.json | 8 +++- .../components/scene/translations/ja.json | 3 ++ .../components/smappee/translations/ja.json | 1 + .../somfy_mylink/translations/ja.json | 3 +- .../components/sonarr/translations/ja.json | 9 ++++ .../components/songpal/translations/ja.json | 3 ++ .../components/spotify/translations/ja.json | 3 ++ .../components/syncthru/translations/ja.json | 7 ++++ .../synology_dsm/translations/ja.json | 3 +- .../tellduslive/translations/ja.json | 1 + .../transmission/translations/ja.json | 1 + .../components/tuya/translations/ja.json | 4 +- .../components/unifi/translations/ja.json | 6 ++- .../components/upnp/translations/ja.json | 1 + .../components/vacuum/translations/ja.json | 10 ++++- .../components/vera/translations/ja.json | 7 ++++ .../components/volumio/translations/ja.json | 4 ++ .../components/wallbox/translations/ja.json | 3 +- .../components/weather/translations/ja.json | 4 +- .../components/wiffi/translations/ja.json | 3 +- .../components/wled/translations/ja.json | 1 + .../wolflink/translations/sensor.ja.json | 4 +- .../xiaomi_aqara/translations/ja.json | 9 +++- .../xiaomi_miio/translations/ja.json | 5 +++ .../yale_smart_alarm/translations/ja.json | 3 ++ .../components/zha/translations/ja.json | 1 + .../components/zwave/translations/ja.json | 3 +- 149 files changed, 931 insertions(+), 105 deletions(-) create mode 100644 homeassistant/components/almond/translations/ja.json create mode 100644 homeassistant/components/brunt/translations/ca.json create mode 100644 homeassistant/components/brunt/translations/de.json create mode 100644 homeassistant/components/brunt/translations/et.json create mode 100644 homeassistant/components/brunt/translations/ja.json create mode 100644 homeassistant/components/brunt/translations/lt.json create mode 100644 homeassistant/components/brunt/translations/no.json create mode 100644 homeassistant/components/brunt/translations/ru.json create mode 100644 homeassistant/components/brunt/translations/zh-Hant.json create mode 100644 homeassistant/components/button/translations/lt.json create mode 100644 homeassistant/components/climacell/translations/ja.json create mode 100644 homeassistant/components/input_datetime/translations/ja.json create mode 100644 homeassistant/components/input_number/translations/ja.json create mode 100644 homeassistant/components/input_select/translations/ja.json create mode 100644 homeassistant/components/input_text/translations/ja.json create mode 100644 homeassistant/components/lock/translations/ja.json create mode 100644 homeassistant/components/nightscout/translations/ja.json create mode 100644 homeassistant/components/proximity/translations/ja.json create mode 100644 homeassistant/components/rachio/translations/ja.json create mode 100644 homeassistant/components/scene/translations/ja.json create mode 100644 homeassistant/components/vera/translations/ja.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index 439df99f9fc..33b2cf9caea 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -12,13 +12,15 @@ }, "reauth_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "Email" }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "Email" }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index a79159a236e..6bc1875187f 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -5,6 +5,10 @@ }, "step": { "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d" + }, "description": "\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/accuweather/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306e\u8a2d\u5b9a\u5f8c\u306b\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", "title": "AccuWeather" } diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index 5b8d5def9c4..05380f8a830 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -4,11 +4,15 @@ "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" }, "step": { + "hassio_confirm": { + "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAdGuard Home" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u76e3\u8996\u3068\u5236\u5fa1\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001AdGuardHome\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/advantage_air/translations/ja.json b/homeassistant/components/advantage_air/translations/ja.json index 50cfb370444..5c8751e4ced 100644 --- a/homeassistant/components/advantage_air/translations/ja.json +++ b/homeassistant/components/advantage_air/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" }, "description": "Advantage Air wall mounted tablet\u306eAPI\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index f8035e1ef47..8d296476c15 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 8d48269cc7b..20b2a5c92dc 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -5,6 +5,10 @@ }, "step": { "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d" + }, "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Airly" } diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index 80779208ca9..ce1f2f06287 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -5,8 +5,14 @@ }, "step": { "user": { - "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "data": { + "api_key": "API\u30ad\u30fc", + "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" + }, + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "AirNow" } } - } + }, + "title": "AirNow" } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index c43e09ae3d7..8db9bff9956 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -5,11 +5,15 @@ }, "step": { "geography_by_coords": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u7def\u5ea6/\u7d4c\u5ea6\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", "title": "Geography\u306e\u8a2d\u5b9a" }, "geography_by_name": { "data": { + "api_key": "API\u30ad\u30fc", "city": "\u90fd\u5e02", "country": "\u56fd", "state": "\u5dde" @@ -18,11 +22,29 @@ "title": "Geography\u306e\u8a2d\u5b9a" }, "node_pro": { + "data": { + "ip_address": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, "description": "\u500b\u4eba\u306eAirVisual\u30e6\u30cb\u30c3\u30c8\u3092\u76e3\u8996\u3057\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u672c\u4f53\u306eUI\u304b\u3089\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002", "title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a" }, "reauth_confirm": { "title": "AirVisual\u3092\u518d\u8a8d\u8a3c" + }, + "user": { + "description": "\u76e3\u8996\u3057\u305f\u3044\u3001AirVisual\u306e\u30c7\u30fc\u30bf\u306e\u7a2e\u985e\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "AirVisual\u306e\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u76e3\u8996\u5bfe\u8c61\u306e\u5730\u7406\u3092\u5730\u56f3\u306b\u8868\u793a" + }, + "title": "AirVisual\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 01dfcc9f8b8..c9cfd274bb5 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -1,4 +1,9 @@ { + "device_automation": { + "action_type": { + "trigger": "\u30c8\u30ea\u30ac\u30fc {entity_name}" + } + }, "state": { "_": { "armed": "\u8b66\u6212", diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index 7c75ed677ba..544d097afd5 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -6,24 +6,60 @@ "step": { "protocol": { "data": { + "device_baudrate": "\u30c7\u30d0\u30a4\u30b9\u306e\u30dc\u30fc\u30ec\u30fc\u30c8", + "device_path": "\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9", "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "\u63a5\u7d9a\u8a2d\u5b9a\u306e\u69cb\u6210" + }, + "user": { + "data": { + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" + }, + "title": "AlarmDecoder\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9078\u629e" } } }, "options": { + "error": { + "int": "\u4ee5\u4e0b\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306f\u6574\u6570\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "loop_range": "RF\u30eb\u30fc\u30d7\u306f1\u304b\u30894\u307e\u3067\u306e\u6574\u6570\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002", + "loop_rfid": "RF\u30eb\u30fc\u30d7\u306fRF\u30b7\u30ea\u30a2\u30eb\u306a\u3057\u3067\u306f\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002", + "relay_inclusive": "\u30ea\u30ec\u30fc\u30a2\u30c9\u30ec\u30b9\u3068\u30ea\u30ec\u30fc\u30c1\u30e3\u30cd\u30eb\u306f\u76f8\u4e92\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u4e00\u7dd2\u306b\u542b\u3081\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u4ee3\u66ff\u30ca\u30a4\u30c8\u30e2\u30fc\u30c9" + }, + "title": "AlarmDecoder\u306e\u8a2d\u5b9a" + }, + "init": { + "data": { + "edit_select": "\u7de8\u96c6" + }, + "description": "\u4f55\u3092\u7de8\u96c6\u3057\u307e\u3059\u304b\uff1f", + "title": "AlarmDecoder\u306e\u8a2d\u5b9a" + }, "zone_details": { "data": { + "zone_loop": "RF\u30eb\u30fc\u30d7", "zone_name": "\u30be\u30fc\u30f3\u540d", + "zone_relayaddr": "\u30ea\u30ec\u30fc\u30a2\u30c9\u30ec\u30b9", + "zone_relaychan": "\u30ea\u30ec\u30fc\u30c1\u30e3\u30cd\u30eb", + "zone_rfid": "RF\u30b7\u30ea\u30a2\u30eb", "zone_type": "\u30be\u30fc\u30f3\u306e\u7a2e\u985e" - } + }, + "description": "{zone_number} \u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u307e\u3059\u3002 {zone_number} \u3092\u524a\u9664\u3059\u308b\u306b\u306f\u3001\u30be\u30fc\u30f3\u540d\u3092\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002", + "title": "AlarmDecoder\u306e\u8a2d\u5b9a" }, "zone_select": { "data": { "zone_number": "\u30be\u30fc\u30f3\u756a\u53f7" - } + }, + "description": "\u8ffd\u52a0\u3001\u7de8\u96c6\u3001\u307e\u305f\u306f\u524a\u9664\u3059\u308b\u30be\u30fc\u30f3\u756a\u53f7\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "AlarmDecoder\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json new file mode 100644 index 00000000000..64c0a6b8bb7 --- /dev/null +++ b/homeassistant/components/almond/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAlmond" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index 54754b49372..f382bfe91de 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -3,10 +3,15 @@ "step": { "reauth_confirm": { "data": { + "api_key": "API\u30ad\u30fc", "description": "Ambee\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" } }, "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d" + }, "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/ambiclimate/translations/ja.json b/homeassistant/components/ambiclimate/translations/ja.json index 7bf2e014e5e..e1b79448f55 100644 --- a/homeassistant/components/ambiclimate/translations/ja.json +++ b/homeassistant/components/ambiclimate/translations/ja.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "follow_link": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_token": "Ambiclimate\u3067\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, "step": { "auth": { + "description": "\u3053\u306e[\u30ea\u30f3\u30af]({authorization_url}) \u306b\u5f93\u3044\u3001Ambiclimate\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092 **\u8a31\u53ef(Allow)** \u3057\u3066\u304b\u3089\u3001\u623b\u3063\u3066\u304d\u3066\u4ee5\u4e0b\u306e **\u9001\u4fe1(submit)** \u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n(\u6307\u5b9a\u3055\u308c\u305f\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL\u304c {cb_url} \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044)", "title": "Ambiclimate\u3092\u8a8d\u8a3c\u3059\u308b" } } diff --git a/homeassistant/components/ambient_station/translations/ja.json b/homeassistant/components/ambient_station/translations/ja.json index f7cddd6c5a0..2a92b237822 100644 --- a/homeassistant/components/ambient_station/translations/ja.json +++ b/homeassistant/components/ambient_station/translations/ja.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "app_key": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30ad\u30fc" }, "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index c32e4c566b3..f2c2a46ab3a 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -1,13 +1,46 @@ { "config": { + "abort": { + "backoff": "\u73fe\u5728\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u8981\u6c42\u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u305b\u3093(\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9\u3092\u4f55\u5ea6\u3082\u5165\u529b\u3057\u305f\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059)\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "device_did_not_pair": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u3092\u7d42\u4e86\u3059\u308b\u8a66\u307f\u306f\u884c\u308f\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + }, "error": { "no_usable_service": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u78ba\u7acb\u3059\u308b\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u5f15\u304d\u7d9a\u304d\u8868\u793a\u3055\u308c\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3059\u308b\u304b\u3001Apple TV\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", + "title": "Apple TV\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" + }, + "pair_no_pin": { + "description": "`{protocol}` \u30b5\u30fc\u30d3\u30b9\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u5fc5\u8981\u3067\u3059\u3002\u7d9a\u884c\u3059\u308b\u306b\u306f\u3001Apple TV\u3067\u3001PIN {pin}\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30da\u30a2\u30ea\u30f3\u30b0" + }, + "pair_with_pin": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + }, + "description": "`{protocol}` \u30d7\u30ed\u30c8\u30b3\u30eb\u306b\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u5fc5\u8981\u3067\u3059\u3002\u753b\u9762\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u306a\u304a\u5148\u982d\u306e\u30bc\u30ed\u306f\u7701\u7565\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3064\u307e\u308a\u3001\u8868\u793a\u3055\u308c\u308b\u30b3\u30fc\u30c9\u304c0123\u306e\u5834\u5408\u306f123\u3068\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "\u30da\u30a2\u30ea\u30f3\u30b0" + }, + "reconfigure": { + "description": "\u3053\u306eApple TV\u306b\u306f\u63a5\u7d9a\u969c\u5bb3\u304c\u767a\u751f\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u518d\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u518d\u69cb\u6210" + }, + "service_problem": { + "description": "\u30d7\u30ed\u30c8\u30b3\u30eb `{protocol}`\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u4e2d\u306b\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002", + "title": "\u30b5\u30fc\u30d3\u30b9\u306e\u8ffd\u52a0\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, "user": { - "description": "\u307e\u305a\u3001\u8ffd\u52a0\u3057\u305f\u3044Apple TV\u306e\u30c7\u30d0\u30a4\u30b9\u540d(Kitchen \u3084 Bedroom\u306a\u3069)\u304bIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u898b\u3064\u304b\u3063\u305f\u5834\u5408\u306f\u3001\u4ee5\u4e0b\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n\n\u30c7\u30d0\u30a4\u30b9\u304c\u8868\u793a\u3055\u308c\u306a\u3044\u5834\u5408\u3084\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n\n{devices}" + "data": { + "device_input": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u307e\u305a\u3001\u8ffd\u52a0\u3057\u305f\u3044Apple TV\u306e\u30c7\u30d0\u30a4\u30b9\u540d(Kitchen \u3084 Bedroom\u306a\u3069)\u304bIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u898b\u3064\u304b\u3063\u305f\u5834\u5408\u306f\u3001\u4ee5\u4e0b\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n\n\u30c7\u30d0\u30a4\u30b9\u304c\u8868\u793a\u3055\u308c\u306a\u3044\u5834\u5408\u3084\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n\n{devices}", + "title": "\u65b0\u3057\u3044Apple TV\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } - } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 5ec55700a0e..5c82bf452df 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -1,17 +1,23 @@ { "config": { "error": { - "pwd_and_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u306e\u307f\u63d0\u4f9b" + "pwd_and_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u306e\u307f\u63d0\u4f9b", + "pwd_or_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3092\u63d0\u4f9b\u3057\u3066\u304f\u3060\u3055\u3044", + "ssh_not_file": "SSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "ssh_key": "SSH\u30ad\u30fc \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9 (\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u4ee3\u308f\u308a)" + "protocol": "\u4f7f\u7528\u3059\u308b\u901a\u4fe1\u30d7\u30ed\u30c8\u30b3\u30eb", + "ssh_key": "SSH\u30ad\u30fc \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9 (\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u4ee3\u308f\u308a)", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059" + "description": "\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059", + "title": "AsusWRT" } } }, @@ -20,8 +26,10 @@ "init": { "data": { "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", - "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)" - } + "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)", + "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)" + }, + "title": "AsusWRT\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/atag/translations/ja.json b/homeassistant/components/atag/translations/ja.json index 5dc41a91227..ed7241cafc5 100644 --- a/homeassistant/components/atag/translations/ja.json +++ b/homeassistant/components/atag/translations/ja.json @@ -1,11 +1,15 @@ { "config": { + "error": { + "unauthorized": "\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u3067\u8a8d\u8a3c\u8981\u6c42\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index 86d32ce8aff..5b2d0535a85 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -5,16 +5,22 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "August\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u518d\u8a8d\u8a3c" }, "user_validate": { "data": { "login_method": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u5b50\u30e1\u30fc\u30eb\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u8a71\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f \"+NNNNNNNNN\" \u5f62\u5f0f\u306e\u96fb\u8a71\u756a\u53f7\u3067\u3059\u3002" + "description": "\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u5b50\u30e1\u30fc\u30eb\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002\u30ed\u30b0\u30a4\u30f3\u65b9\u6cd5\u304c \"\u96fb\u8a71\" \u306e\u5834\u5408\u3001\u30e6\u30fc\u30b6\u30fc\u540d\u306f \"+NNNNNNNNN\" \u5f62\u5f0f\u306e\u96fb\u8a71\u756a\u53f7\u3067\u3059\u3002", + "title": "August account\u306e\u8a2d\u5b9a" }, "validation": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, "title": "2\u8981\u7d20\u8a8d\u8a3c" } } diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index b95bf43490b..62c62befcb0 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -9,7 +9,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Axis\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index cdb7445d7dc..79b523a8bc5 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -52,6 +52,7 @@ "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "no_update": "{entity_name} \u304c\u6700\u65b0\u306b\u306a\u308a\u307e\u3057\u305f", "no_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u306a\u304f\u306a\u3063\u305f", + "not_cold": "{entity_name} \u306f\u51b7\u3048\u3066\u3044\u307e\u305b\u3093", "not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", "not_hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u305b\u3093", "not_locked": "{entity_name} \u306e\u30ed\u30c3\u30af\u304c\u89e3\u9664\u3055\u308c\u307e\u3057\u305f", diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index 5ccec9230d6..8d1401bde6d 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -4,6 +4,7 @@ "step": { "user": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" }, "description": "BleBox\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index c9bb1f3a111..f030f1c48b4 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -9,7 +9,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/brunt/translations/ca.json b/homeassistant/components/brunt/translations/ca.json new file mode 100644 index 00000000000..9a1c657af7c --- /dev/null +++ b/homeassistant/components/brunt/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Torna a introduir la contrasenya de: {username}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 de la integraci\u00f3 Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/de.json b/homeassistant/components/brunt/translations/de.json new file mode 100644 index 00000000000..efbc9b437e1 --- /dev/null +++ b/homeassistant/components/brunt/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein:", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Richte deine Brunt-Integration ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/et.json b/homeassistant/components/brunt/translations/et.json new file mode 100644 index 00000000000..0d283b8a664 --- /dev/null +++ b/homeassistant/components/brunt/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta salas\u00f5na uuesti: {username}", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "title": "Seadista oma Brunti sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json new file mode 100644 index 00000000000..48b2c0d6259 --- /dev/null +++ b/homeassistant/components/brunt/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {username}", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "title": "Brunt\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/lt.json b/homeassistant/components/brunt/translations/lt.json new file mode 100644 index 00000000000..98e6719deb2 --- /dev/null +++ b/homeassistant/components/brunt/translations/lt.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Slapta\u017eodis" + } + }, + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "Slapyvardis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/no.json b/homeassistant/components/brunt/translations/no.json new file mode 100644 index 00000000000..b77151ac92a --- /dev/null +++ b/homeassistant/components/brunt/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen 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": { + "password": "Passord" + }, + "description": "Skriv inn passordet p\u00e5 nytt for: {username}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Konfigurer Brunt-integrasjonen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/ru.json b/homeassistant/components/brunt/translations/ru.json new file mode 100644 index 00000000000..1adcd8906d3 --- /dev/null +++ b/homeassistant/components/brunt/translations/ru.json @@ -0,0 +1,29 @@ +{ + "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_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", + "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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/zh-Hant.json b/homeassistant/components/brunt/translations/zh-Hant.json new file mode 100644 index 00000000000..960fea4967b --- /dev/null +++ b/homeassistant/components/brunt/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165\u5bc6\u78bc\uff1a{username}", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Brunt \u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index 427d83bfb94..91ec977f598 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -5,7 +5,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "BSB-Lan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/button/translations/lt.json b/homeassistant/components/button/translations/lt.json new file mode 100644 index 00000000000..14c1935abdc --- /dev/null +++ b/homeassistant/components/button/translations/lt.json @@ -0,0 +1,3 @@ +{ + "title": "Mygtukas" +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json new file mode 100644 index 00000000000..c2ff6bbb145 --- /dev/null +++ b/homeassistant/components/climacell/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/ja.json b/homeassistant/components/climate/translations/ja.json index 2d660b8dd54..853bc73ae81 100644 --- a/homeassistant/components/climate/translations/ja.json +++ b/homeassistant/components/climate/translations/ja.json @@ -8,5 +8,6 @@ "heat": "\u6696\u623f", "off": "\u30aa\u30d5" } - } + }, + "title": "\u6c17\u5019" } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json index a7d0d5dc3e8..73f6391a238 100644 --- a/homeassistant/components/control4/translations/ja.json +++ b/homeassistant/components/control4/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "Control4\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8a73\u7d30\u3068\u3001\u30ed\u30fc\u30ab\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json index 58173440320..c66c45c785a 100644 --- a/homeassistant/components/coolmaster/translations/ja.json +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "off": "\u30aa\u30d5\u306b\u3067\u304d\u307e\u3059" } } } diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index bf3fb1bdc19..74b683cc36b 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "api_key": "API\u30ad\u30fc", + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API Key\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306e\u8a2d\u5b9a" diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 20ba31ec60f..d503a3658e4 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -27,6 +27,10 @@ "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_7": "7\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_8": "8\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "close": "\u9589\u3058\u308b", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 1e4d4d2cd8d..92b40eeb2b0 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -18,6 +18,9 @@ } }, "user": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + }, "description": "\u53d7\u4fe1\u6a5f\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" } diff --git a/homeassistant/components/device_tracker/translations/ja.json b/homeassistant/components/device_tracker/translations/ja.json index 6679d6cca06..40e8295480a 100644 --- a/homeassistant/components/device_tracker/translations/ja.json +++ b/homeassistant/components/device_tracker/translations/ja.json @@ -4,5 +4,6 @@ "home": "\u5728\u5b85", "not_home": "\u5916\u51fa" } - } + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json index 6c371a08ae7..c6de7b0261c 100644 --- a/homeassistant/components/devolo_home_control/translations/ja.json +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -4,6 +4,11 @@ "reauth_failed": "\u4ee5\u524d\u306e\u3068\u540c\u3058mydevolo\u30e6\u30fc\u30b6\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, "zeroconf_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index 327868e0b9c..c64d4d1b192 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/directv/translations/ja.json b/homeassistant/components/directv/translations/ja.json index a42202307f2..ad4cf1544ef 100644 --- a/homeassistant/components/directv/translations/ja.json +++ b/homeassistant/components/directv/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 57fdc081a18..37144b4dca0 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -7,7 +7,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 8e155e2dd56..62ca50f0af0 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -5,7 +5,9 @@ "data": { "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002" + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", + "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/epson/translations/ja.json b/homeassistant/components/epson/translations/ja.json index 4d81a0612b5..d4d0d85fd04 100644 --- a/homeassistant/components/epson/translations/ja.json +++ b/homeassistant/components/epson/translations/ja.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" } } } diff --git a/homeassistant/components/fan/translations/ja.json b/homeassistant/components/fan/translations/ja.json index d3546ca6721..c7311885159 100644 --- a/homeassistant/components/fan/translations/ja.json +++ b/homeassistant/components/fan/translations/ja.json @@ -10,5 +10,6 @@ "off": "\u30aa\u30d5", "on": "\u30aa\u30f3" } - } + }, + "title": "\u30d5\u30a1\u30f3" } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ja.json b/homeassistant/components/fireservicerota/translations/ja.json index d453028ca2a..11baaff1a52 100644 --- a/homeassistant/components/fireservicerota/translations/ja.json +++ b/homeassistant/components/fireservicerota/translations/ja.json @@ -9,7 +9,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/flo/translations/ja.json b/homeassistant/components/flo/translations/ja.json index 9f68231f0d2..2981d97e3c6 100644 --- a/homeassistant/components/flo/translations/ja.json +++ b/homeassistant/components/flo/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json index 07d401c0bc8..8c51c540482 100644 --- a/homeassistant/components/flume/translations/ja.json +++ b/homeassistant/components/flume/translations/ja.json @@ -5,11 +5,15 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", + "title": "Flume\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID", + "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json index 881d2bfeb80..7a38264a2df 100644 --- a/homeassistant/components/forecast_solar/translations/ja.json +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -5,7 +5,8 @@ "data": { "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)", - "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b" + "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b", + "name": "\u540d\u524d" }, "description": "\u30bd\u30fc\u30e9\u30fc\u30d1\u30cd\u30eb\u306e\u30c7\u30fc\u30bf\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e0d\u660e\u306a\u5834\u5408\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 402b0f2c897..0f2f95f65b8 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -10,7 +10,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "rtsp_port": "RTSP\u30dd\u30fc\u30c8", - "stream": "\u30b9\u30c8\u30ea\u30fc\u30e0" + "stream": "\u30b9\u30c8\u30ea\u30fc\u30e0", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json index de9812fb598..2691a4a91e7 100644 --- a/homeassistant/components/freebox/translations/ja.json +++ b/homeassistant/components/freebox/translations/ja.json @@ -2,6 +2,7 @@ "config": { "step": { "link": { + "description": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u30eb\u30fc\u30bf\u30fc\u306e\u53f3\u77e2\u5370\u3092\u30bf\u30c3\u30c1\u3057\u3066\u3001Freebox\u3092Home Assistant\u306b\u767b\u9332\u3057\u307e\u3059\u3002 \n\n\uff01[\u30eb\u30fc\u30bf\u30fc\u306e\u30dc\u30bf\u30f3\u306e\u5834\u6240](/static/images/config_freebox.png)", "title": "Freebox router\u306b\u30ea\u30f3\u30af" }, "user": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 7c362d1c8e7..f8168b58649 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -5,7 +5,8 @@ "confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "reauth_confirm": { "data": { @@ -18,7 +19,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8" }, - "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" + "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002", + "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7 - \u5fc5\u9808" }, "user": { "data": { @@ -26,7 +28,17 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8" }, - "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" + "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002", + "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "'\u30db\u30fc\u30e0' \u3067\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u8a0e\u3059\u308b\u79d2\u6570" + } } } } diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index b1790f8547c..dd5bbb44a71 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -1,9 +1,16 @@ { "config": { "step": { + "confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "reauth_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index 2ca204485ec..0c55d7d61a8 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -4,8 +4,10 @@ "step": { "user": { "data": { + "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/ja.json b/homeassistant/components/garages_amsterdam/translations/ja.json index 2bbaa94cd90..64d3e6fac92 100644 --- a/homeassistant/components/garages_amsterdam/translations/ja.json +++ b/homeassistant/components/garages_amsterdam/translations/ja.json @@ -4,8 +4,10 @@ "user": { "data": { "garage_name": "\u30ac\u30ec\u30fc\u30b8\u540d" - } + }, + "title": "\u76e3\u8996\u3059\u308b\u30ac\u30ec\u30fc\u30b8\u3092\u9078\u629e" } } - } + }, + "title": "Garages Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index 4f3b4f0b2bf..0192d489ed7 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -2,11 +2,13 @@ "config": { "step": { "confirm_discovery": { - "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Goal Zero Yeti" }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" }, "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/gogogate2/translations/ja.json b/homeassistant/components/gogogate2/translations/ja.json index 896966aee6c..8a9cace2eeb 100644 --- a/homeassistant/components/gogogate2/translations/ja.json +++ b/homeassistant/components/gogogate2/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index daafb1a4c47..3f854c0d96f 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "origin": "\u30aa\u30ea\u30b8\u30f3" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json index 0eba932ebaf..a464db24f8c 100644 --- a/homeassistant/components/growatt_server/translations/ja.json +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_plants": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u690d\u7269(plants)\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" + }, "step": { "plant": { "data": { diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 3a61126ea94..a8716ef19d3 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -1,6 +1,9 @@ { "config": { "step": { + "discovery_confirm": { + "description": "\u3053\u306eGuardian\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "port": "\u30dd\u30fc\u30c8" diff --git a/homeassistant/components/habitica/translations/ja.json b/homeassistant/components/habitica/translations/ja.json index 9b979b7c31f..e8697990afb 100644 --- a/homeassistant/components/habitica/translations/ja.json +++ b/homeassistant/components/habitica/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "description": "Habitica profile\u306b\u63a5\u7d9a\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3068\u30bf\u30b9\u30af\u3092\u76e3\u8996\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002 \u6ce8\u610f: api_id\u3068api_key\u306f\u3001https://habitica.com/user/settings/api \u304b\u3089\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } diff --git a/homeassistant/components/hlk_sw16/translations/ja.json b/homeassistant/components/hlk_sw16/translations/ja.json index 9f68231f0d2..2981d97e3c6 100644 --- a/homeassistant/components/hlk_sw16/translations/ja.json +++ b/homeassistant/components/hlk_sw16/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 0504d2f2ad5..d3849132538 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "docker": "Docker", "user": "\u30e6\u30fc\u30b6\u30fc" } } diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 717ac52192f..15e62346320 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -1,16 +1,25 @@ { + "config": { + "step": { + "pairing": { + "description": "\u201cHomeKit Pairing\u201d\u306e\"\u901a\u77e5\"\u306e\u6307\u793a\u306b\u5f93\u3063\u3066\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002" + } + } + }, "options": { "step": { "advanced": { "data": { "devices": "\u30c7\u30d0\u30a4\u30b9(\u30c8\u30ea\u30ac\u30fc)" - } + }, + "description": "\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9\u3054\u3068\u306b\u3001\u30d7\u30ed\u30b0\u30e9\u30e0\u53ef\u80fd\u306a\u30b9\u30a4\u30c3\u30c1\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u304c\u767a\u751f\u3059\u308b\u3068\u3001HomeKit\u306f\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3084\u30b7\u30fc\u30f3\u3092\u5b9f\u884c\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002" }, "cameras": { "data": { "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9", "camera_copy": "H.264\u306e\u30cd\u30a4\u30c6\u30a3\u30d6\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308b\u30ab\u30e1\u30e9" - } + }, + "title": "\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a" }, "yaml": { "description": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u306fYAML\u3092\u4ecb\u3057\u3066\u5236\u5fa1\u3055\u308c\u307e\u3059" diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index ef1c6588f11..4ce32f5266f 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -13,8 +13,11 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Huawei LTE\u306e\u8a2d\u5b9a" } } @@ -25,6 +28,7 @@ "data": { "recipient": "SMS\u901a\u77e5\u306e\u53d7\u4fe1\u8005", "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1", + "track_wired_clients": "\u6709\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "unauthenticated_mode": "\u8a8d\u8a3c\u306a\u3057\u306e\u30e2\u30fc\u30c9(\u5909\u66f4\u306b\u306f\u30ea\u30ed\u30fc\u30c9\u304c\u5fc5\u8981)" } } diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json index a42202307f2..048870eee7d 100644 --- a/homeassistant/components/hvv_departures/translations/ja.json +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index eb1da4ce43a..dd4ac2cd8bf 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -7,6 +7,9 @@ "confirm": { "description": "\u3053\u306eHyperion Ambilight\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f \n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}" }, + "create_token": { + "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 84f6c52ac04..5d2fea9c8e8 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "iAqualink\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "iAqualink\u306b\u63a5\u7d9a" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 4aad8f31345..076ab1294b6 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "validate_verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u78ba\u8a8d\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u518d\u5ea6\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/input_boolean/translations/ja.json b/homeassistant/components/input_boolean/translations/ja.json index 15dd3796187..5af782e4e46 100644 --- a/homeassistant/components/input_boolean/translations/ja.json +++ b/homeassistant/components/input_boolean/translations/ja.json @@ -4,5 +4,6 @@ "off": "\u30aa\u30d5", "on": "\u30aa\u30f3" } - } + }, + "title": "\u771f\u507d\u5024\u5165\u529b(booleans)" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/ja.json b/homeassistant/components/input_datetime/translations/ja.json new file mode 100644 index 00000000000..aef27609568 --- /dev/null +++ b/homeassistant/components/input_datetime/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u65e5\u6642\u3092\u5165\u529b" +} \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/ja.json b/homeassistant/components/input_number/translations/ja.json new file mode 100644 index 00000000000..3104ee351fc --- /dev/null +++ b/homeassistant/components/input_number/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u756a\u53f7\u5165\u529b" +} \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/ja.json b/homeassistant/components/input_select/translations/ja.json new file mode 100644 index 00000000000..c84c50cd7bc --- /dev/null +++ b/homeassistant/components/input_select/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u9078\u629e\u80a2\u5165\u529b" +} \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/ja.json b/homeassistant/components/input_text/translations/ja.json new file mode 100644 index 00000000000..1d16098fa72 --- /dev/null +++ b/homeassistant/components/input_text/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30c6\u30ad\u30b9\u30c8\u5165\u529b" +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 7b206985e6d..a9b0ae57fe8 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -3,13 +3,16 @@ "step": { "hubv1": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" } }, "hubv2": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } @@ -24,8 +27,10 @@ }, "change_hub_config": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index a1e9314c1c5..34f73e9770b 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -8,13 +8,15 @@ "error": { "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "flow_title": "{name}", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "description": "\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u5370\u5237\u30d7\u30ed\u30c8\u30b3\u30eb(IPP)\u3092\u4ecb\u3057\u3066\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + "description": "\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u5370\u5237\u30d7\u30ed\u30c8\u30b3\u30eb(IPP)\u3092\u4ecb\u3057\u3066\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", + "title": "\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30ea\u30f3\u30af\u3059\u308b" } } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index e50b4676261..f9605b589ce 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -14,8 +14,16 @@ } } }, + "options": { + "step": { + "init": { + "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } + }, "system_health": { "info": { + "device_connected": "ISY\u63a5\u7d9a\u6e08", "host_reachable": "\u30db\u30b9\u30c8\u5230\u9054\u53ef\u80fd", "websocket_status": "\u30a4\u30d9\u30f3\u30c8\u30bd\u30b1\u30c3\u30c8 \u30b9\u30c6\u30fc\u30bf\u30b9" } diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 896966aee6c..38e13174ac8 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index 11f6dea5500..f858f676cbf 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -9,7 +9,9 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Keenetic NDMS2\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 75e42ddec49..7e1005545cf 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -6,7 +6,8 @@ "step": { "credentials": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "Kodi\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 6122be0009d..3fb8d834035 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -27,7 +27,8 @@ "4": "\u30be\u30fc\u30f34", "5": "\u30be\u30fc\u30f35", "6": "\u30be\u30fc\u30f36", - "7": "\u30be\u30fc\u30f37" + "7": "\u30be\u30fc\u30f37", + "out": "OUT" } }, "options_io_ext": { @@ -36,7 +37,9 @@ "11": "\u30be\u30fc\u30f311", "12": "\u30be\u30fc\u30f312", "8": "\u30be\u30fc\u30f38", - "9": "\u30be\u30fc\u30f39" + "9": "\u30be\u30fc\u30f39", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" } }, "options_misc": { diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 15462cb8945..ae0b4576715 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } diff --git a/homeassistant/components/lock/translations/ja.json b/homeassistant/components/lock/translations/ja.json new file mode 100644 index 00000000000..23d24b05066 --- /dev/null +++ b/homeassistant/components/lock/translations/ja.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condition_type": { + "is_unlocked": "{entity_name} \u306e\u30ed\u30c3\u30af\u306f\u89e3\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ja.json b/homeassistant/components/logi_circle/translations/ja.json index daeec7516af..9b1708ba528 100644 --- a/homeassistant/components/logi_circle/translations/ja.json +++ b/homeassistant/components/logi_circle/translations/ja.json @@ -8,6 +8,7 @@ }, "step": { "auth": { + "description": "\u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001Logi Circle\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092 **\u627f\u8a8d(Accept)** \u3057\u3066\u304b\u3089\u3001\u623b\u3063\u3066\u304d\u3066\u4ee5\u4e0b\u306e **\u9001\u4fe1(submit)** \u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n[\u30ea\u30f3\u30af]({authorization_url})", "title": "Logi Circle\u3067\u8a8d\u8a3c\u3059\u308b" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 2de21e912a2..85cf75a1105 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -2,6 +2,9 @@ "config": { "flow_title": "{name} ({host})", "step": { + "link": { + "description": "{name} ({host}) \u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3059\u308b\u306b\u306f\u3001\u3053\u306e\u30d5\u30a9\u30fc\u30e0\u3092\u9001\u4fe1(submit)\u3057\u305f\u5f8c\u3001\u30d6\u30ea\u30c3\u30b8\u306e\u80cc\u9762\u306b\u3042\u308b\u9ed2\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u307e\u3059\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" @@ -9,5 +12,10 @@ "description": "\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } + }, + "device_automation": { + "trigger_subtype": { + "on": "\u30aa\u30f3" + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index 38816c4399b..201bda4fa96 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -5,10 +5,20 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Mikrotik\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "force_dhcp": "DHCP\u3092\u4f7f\u7528\u3057\u305f\u5f37\u5236\u30b9\u30ad\u30e3\u30f3" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json index 309e5ccc41c..ffb567fc218 100644 --- a/homeassistant/components/mill/translations/ca.json +++ b/homeassistant/components/mill/translations/ca.json @@ -7,11 +7,25 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { - "user": { + "cloud": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" } + }, + "local": { + "data": { + "ip_address": "Adre\u00e7a IP" + }, + "description": "Adre\u00e7a IP local del dispositiu." + }, + "user": { + "data": { + "connection_type": "Selecciona el tipus de connexi\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Selecciona el tipus de connexi\u00f3. La local necessita escalfadors de generaci\u00f3 3" } } } diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json index 44d9c2448e6..1688dddb321 100644 --- a/homeassistant/components/mill/translations/de.json +++ b/homeassistant/components/mill/translations/de.json @@ -7,11 +7,25 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { - "user": { + "cloud": { "data": { "password": "Passwort", "username": "Benutzername" } + }, + "local": { + "data": { + "ip_address": "IP-Adresse" + }, + "description": "Lokale IP-Adresse des Ger\u00e4ts." + }, + "user": { + "data": { + "connection_type": "Verbindungstyp ausw\u00e4hlen", + "password": "Passwort", + "username": "Benutzername" + }, + "description": "W\u00e4hle die Anschlussart. Lokal erfordert Heizger\u00e4te der Generation 3" } } } diff --git a/homeassistant/components/mill/translations/en.json b/homeassistant/components/mill/translations/en.json index ee66706832e..20291847893 100644 --- a/homeassistant/components/mill/translations/en.json +++ b/homeassistant/components/mill/translations/en.json @@ -21,7 +21,9 @@ }, "user": { "data": { - "connection_type": "Select connection type" + "connection_type": "Select connection type", + "password": "Password", + "username": "Username" }, "description": "Select connection type. Local requires generation 3 heaters" } diff --git a/homeassistant/components/mill/translations/et.json b/homeassistant/components/mill/translations/et.json index c9f9d5dbd98..dad66420441 100644 --- a/homeassistant/components/mill/translations/et.json +++ b/homeassistant/components/mill/translations/et.json @@ -7,11 +7,25 @@ "cannot_connect": "\u00dchendus nurjus" }, "step": { - "user": { + "cloud": { "data": { "password": "Salas\u00f5na", "username": "Kasutajanimi" } + }, + "local": { + "data": { + "ip_address": "IP aadress" + }, + "description": "Seadme kohalik IP-aadress." + }, + "user": { + "data": { + "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Vali \u00fchenduse t\u00fc\u00fcp. Kohalik vajab 3. p\u00f5lvkonna k\u00fctteseadmeid" } } } diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json index 896966aee6c..07f88e63c7c 100644 --- a/homeassistant/components/mill/translations/ja.json +++ b/homeassistant/components/mill/translations/ja.json @@ -1,10 +1,24 @@ { "config": { "step": { + "cloud": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, + "local": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30fc\u30ab\u30ebIP\u30a2\u30c9\u30ec\u30b9\u3002" + }, "user": { "data": { + "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30ed\u30fc\u30ab\u30eb\u306b\u306f\u7b2c3\u4e16\u4ee3\u306e\u30d2\u30fc\u30bf\u30fc\u304c\u5fc5\u8981\u3067\u3059" } } } diff --git a/homeassistant/components/mill/translations/no.json b/homeassistant/components/mill/translations/no.json index ef481a449ae..d5306fd090d 100644 --- a/homeassistant/components/mill/translations/no.json +++ b/homeassistant/components/mill/translations/no.json @@ -7,11 +7,25 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { - "user": { + "cloud": { "data": { "password": "Passord", "username": "Brukernavn" } + }, + "local": { + "data": { + "ip_address": "IP adresse" + }, + "description": "Lokal IP-adresse til enheten." + }, + "user": { + "data": { + "connection_type": "Velg tilkoblingstype", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Velg tilkoblingstype. Lokal krever generasjon 3 varmeovner" } } } diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json index eac6c63c559..ab09ec1fdaa 100644 --- a/homeassistant/components/mill/translations/ru.json +++ b/homeassistant/components/mill/translations/ru.json @@ -7,11 +7,25 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { - "user": { + "cloud": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } + }, + "local": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, + "user": { + "data": { + "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u043f\u0446\u0438\u0438 Local \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u0442\u0440\u0435\u0442\u044c\u0435\u0433\u043e \u043f\u043e\u043a\u043e\u043b\u0435\u043d\u0438\u044f." } } } diff --git a/homeassistant/components/mill/translations/zh-Hant.json b/homeassistant/components/mill/translations/zh-Hant.json index 179100726da..a77203a0495 100644 --- a/homeassistant/components/mill/translations/zh-Hant.json +++ b/homeassistant/components/mill/translations/zh-Hant.json @@ -7,11 +7,25 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { - "user": { + "cloud": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } + }, + "local": { + "data": { + "ip_address": "IP \u4f4d\u5740" + }, + "description": "\u88dd\u7f6e IP \u4f4d\u5740\u3002" + }, + "user": { + "data": { + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json index ea563b3eab6..ae3838f3cc1 100644 --- a/homeassistant/components/minecraft_server/translations/ja.json +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -8,7 +8,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" }, "description": "\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001Minecraft Server\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index 21621989af3..b8f5a423124 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -7,10 +7,20 @@ "connect": { "data": { "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" + }, + "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" + }, + "select": { + "data": { + "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" } }, "user": { - "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059" + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + }, + "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", + "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" } } }, diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index b8ff2edfca4..4b4977a298b 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -23,17 +23,29 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + } + }, "options": { "step": { "broker": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "MQTT broker\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Broker\u30aa\u30d7\u30b7\u30e7\u30f3" }, "options": { + "data": { + "birth_enable": "\u30d0\u30fc\u30b9(birth)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", + "birth_payload": "\u30d0\u30fc\u30b9(Birth)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9", + "will_enable": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", + "will_payload": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9" + }, "description": "\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(Discovery(\u691c\u51fa)) - \u691c\u51fa\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408(\u63a8\u5968)\u3001Home Assistant\u306f\u3001MQTT broker\u306b\u8a2d\u5b9a\u3092\u516c\u958b\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3084\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3057\u307e\u3059\u3002\u691c\u51fa\u3092\u7121\u52b9\u306b\u3057\u305f\u5834\u5408\u306f\u3001\u3059\u3079\u3066\u306e\u8a2d\u5b9a\u3092\u624b\u52d5\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d0\u30fc\u30b9(Birth(\u8a95\u751f))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u8a95\u751f\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304cMQTT broker\u306b\u3001(\u518d)\u63a5\u7d9a\u3059\u308b\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\n\u30a6\u30a3\u30eb(Will(\u610f\u601d))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u30a6\u30a3\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304c\u30d6\u30ed\u30fc\u30ab\u30fc(broker)\u3078\u306e\u63a5\u7d9a\u3092\u5931\u3046\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f\u3001\u30af\u30ea\u30fc\u30f3\u306a\u63a5\u7d9a(Home Assistant\u306e\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3\u306a\u3069)\u306e\u5834\u5408\u3068\u3001\u30af\u30ea\u30fc\u30f3\u3067\u306f\u306a\u3044\u63a5\u7d9a(Home Assistant\u306e\u30af\u30e9\u30c3\u30b7\u30e5\u3084\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u5931\u3063\u305f\u5834\u5408)\u306e\u3069\u3061\u3089\u306e\u5834\u5408\u3067\u3042\u3063\u3066\u3082\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002", "title": "MQTT\u30aa\u30d7\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/myq/translations/ja.json b/homeassistant/components/myq/translations/ja.json index 07d401c0bc8..7e44514d6fe 100644 --- a/homeassistant/components/myq/translations/ja.json +++ b/homeassistant/components/myq/translations/ja.json @@ -5,11 +5,13 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", + "title": "MyQ\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index d426787eb0c..bbe592fc697 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -10,6 +10,7 @@ "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "step": { diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 373d86e2b56..d92b9320051 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -11,13 +11,15 @@ }, "credentials": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30db\u30b9\u30c8: {host} \u306e\u6b63\u3057\u3044\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json index cc7a70d7e30..d7413b7de7c 100644 --- a/homeassistant/components/nexia/translations/ja.json +++ b/homeassistant/components/nexia/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "brand": "\u30d6\u30e9\u30f3\u30c9", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index 8f4cdabb4fd..dd12e0e7086 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" }, "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", "title": "Android TV / Fire TV\u306e\u901a\u77e5" diff --git a/homeassistant/components/nightscout/translations/ja.json b/homeassistant/components/nightscout/translations/ja.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/nightscout/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index 522df8978e3..403ac3e7b90 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -12,7 +12,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } diff --git a/homeassistant/components/nuheat/translations/ja.json b/homeassistant/components/nuheat/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/nuheat/translations/ja.json +++ b/homeassistant/components/nuheat/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index cffb4f55b44..362e7d9549c 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -5,7 +5,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index 6d3233a4e22..c8836fb4115 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 1a10c5e6dba..5d0b820c74a 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -1,6 +1,12 @@ { "config": { "step": { + "auth": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "configure": { "data": { "host": "\u30db\u30b9\u30c8", @@ -12,13 +18,15 @@ "manual_input": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" } }, "user": { "data": { "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" - } + }, + "description": "\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3001\u30d7\u30ed\u30d5\u30a1\u30a4\u30ebS\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308bONVIF\u30c7\u30d0\u30a4\u30b9\u3092\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067\u691c\u7d22\u3057\u307e\u3059\u3002\n\n\u4e00\u90e8\u306e\u30e1\u30fc\u30ab\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u8a2d\u5b9a\u304b\u3089\u3001ONVIF\u3092\u7121\u52b9\u306b\u3057\u59cb\u3081\u3066\u3044\u307e\u3059\u3002\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a\u3067ONVIF\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index 5dc41a91227..12a527f3c55 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -1,9 +1,16 @@ { "config": { "step": { + "api_key": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, "user": { "data": { + "api_key": "API\u30ad\u30fc", "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" } } diff --git a/homeassistant/components/plant/translations/ja.json b/homeassistant/components/plant/translations/ja.json index 01708fffd87..1e6829895b2 100644 --- a/homeassistant/components/plant/translations/ja.json +++ b/homeassistant/components/plant/translations/ja.json @@ -3,5 +3,6 @@ "_": { "ok": "OK" } - } + }, + "title": "\u30d7\u30e9\u30f3\u30c8\u30e2\u30cb\u30bf\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 453c7607fd8..410320d4d52 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user_gateway": { "data": { diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index b5b5bd84b50..c2a9f098c33 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -8,6 +8,7 @@ }, "step": { "auth": { + "description": "\u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001Minut\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092 **\u627f\u8a8d(Accept)** \u3057\u3066\u304b\u3089\u3001\u623b\u3063\u3066\u304d\u3066\u4ee5\u4e0b\u306e **\u9001\u4fe1(submit)** \u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n[\u30ea\u30f3\u30af]({authorization_url})", "title": "\u8a8d\u8a3c\u30dd\u30a4\u30f3\u30c8" }, "user": { diff --git a/homeassistant/components/proximity/translations/ja.json b/homeassistant/components/proximity/translations/ja.json new file mode 100644 index 00000000000..6b085416cc5 --- /dev/null +++ b/homeassistant/components/proximity/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u8fd1\u63a5" +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 5c504dc2670..3bd61b1634b 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -6,6 +6,7 @@ "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { + "credential_timeout": "\u8cc7\u683c\u60c5\u5831\u30b5\u30fc\u30d3\u30b9\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002\u9001\u4fe1(submit)\u3092\u62bc\u3057\u3066\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN Code\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { diff --git a/homeassistant/components/rachio/translations/ja.json b/homeassistant/components/rachio/translations/ja.json new file mode 100644 index 00000000000..c2ff6bbb145 --- /dev/null +++ b/homeassistant/components/rachio/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ja.json b/homeassistant/components/remote/translations/ja.json index 15dd3796187..3b55598aaba 100644 --- a/homeassistant/components/remote/translations/ja.json +++ b/homeassistant/components/remote/translations/ja.json @@ -4,5 +4,6 @@ "off": "\u30aa\u30d5", "on": "\u30aa\u30f3" } - } + }, + "title": "\u30ea\u30e2\u30fc\u30c8" } \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index 04f64cdf3ca..7307245a9a8 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -5,9 +5,11 @@ "init": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" }, "link": { + "description": "{name} \u306e\u30db\u30fc\u30e0\u30dc\u30bf\u30f3\u3092\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u97f3\u3092\u51fa\u3059\u307e\u3067(\u7d042\u79d2)\u62bc\u3057\u7d9a\u3051\u300130\u79d2\u4ee5\u5185\u306b\u9001\u4fe1(submit)\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u53d6\u5f97" }, "link_manual": { @@ -24,8 +26,11 @@ }, "user": { "data": { + "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "\u30eb\u30f3\u30d0\u307e\u305f\u306f\u30d6\u30e9\u30fc\u30d0\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" } } } diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 859d3d16017..d3aaca838b2 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -2,6 +2,7 @@ "config": { "step": { "link": { + "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 0033eab6f90..985c590d831 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -1,9 +1,15 @@ { "config": { "abort": { - "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", + "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002", + "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, + "flow_title": "{device}", "step": { + "reauth_confirm": { + "description": "\u9001\u4fe1(submit)\u3001\u8a8d\u8a3c\u3092\u8981\u6c42\u3059\u308b {device} \u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u300130\u79d2\u4ee5\u5185\u306b\u53d7\u3051\u5165\u308c\u307e\u3059\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/scene/translations/ja.json b/homeassistant/components/scene/translations/ja.json new file mode 100644 index 00000000000..7ea3c93bca0 --- /dev/null +++ b/homeassistant/components/scene/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30b7\u30fc\u30f3" +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index aa0d07e8551..6fc08d2e793 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "environment": { "description": "Smappee\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 70205a43e6e..b0c2f760a12 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -15,7 +15,8 @@ "entity_config": { "data": { "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" - } + }, + "title": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u8a2d\u5b9a" }, "target_config": { "data": { diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 5dc41a91227..88d1f10b275 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -8,5 +8,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "wanted_max_items": "\u8868\u793a\u3057\u305f\u3044\u30a2\u30a4\u30c6\u30e0\u306e\u6700\u5927\u6570" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/ja.json b/homeassistant/components/songpal/translations/ja.json index 2021184a339..ebac0e32cc9 100644 --- a/homeassistant/components/songpal/translations/ja.json +++ b/homeassistant/components/songpal/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "not_songpal_device": "Songpal\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, "flow_title": "{name} ({host})", "step": { "init": { diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index ec197ac35cb..f81413cf319 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" + }, "create_entry": { "default": "Spotify\u306e\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" } diff --git a/homeassistant/components/syncthru/translations/ja.json b/homeassistant/components/syncthru/translations/ja.json index cc965c9ca6f..dcb36d3d21c 100644 --- a/homeassistant/components/syncthru/translations/ja.json +++ b/homeassistant/components/syncthru/translations/ja.json @@ -2,6 +2,13 @@ "config": { "error": { "syncthru_not_supported": "\u30c7\u30d0\u30a4\u30b9\u306fSyncThru\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" + }, + "step": { + "confirm": { + "data": { + "name": "\u540d\u524d" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 2cfa77c3004..02b44142786 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -7,7 +7,8 @@ "link": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "reauth": { diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index fb920c2969c..794e300a2ed 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -2,6 +2,7 @@ "config": { "step": { "auth": { + "description": "TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f:\n1. \u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n2. Telldus Live\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\n3. **{app_name}** \u3092\u627f\u8a8d\u3057\u307e\u3059(**Yes(\u306f\u3044)**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059)\n4. \u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u3001**\u9001\u4fe1(submit)** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n [TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30ea\u30f3\u30af]({auth_url})", "title": "TelldusLive\u306b\u5bfe\u3057\u3066\u8a8d\u8a3c\u3059\u308b" }, "user": { diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 5af5263a537..8648b9e8da2 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8" }, diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index a2ee5e9a300..3f6692e28b1 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -23,8 +23,10 @@ "access_secret": "Tuya IoT Access Secret", "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "platform": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u30a2\u30d7\u30ea", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", - "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7" + "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7", + "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" }, "description": "Tuya\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Tuya\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 4dd3954ca96..f229b4db85e 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -10,7 +10,8 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "site": "\u30b5\u30a4\u30c8ID" + "site": "\u30b5\u30a4\u30c8ID", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } @@ -23,6 +24,9 @@ "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8" } }, + "simple_options": { + "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + }, "statistics_sensors": { "description": "\u7d71\u8a08\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a" } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index a930173b0bf..0c81caf16aa 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/vacuum/translations/ja.json b/homeassistant/components/vacuum/translations/ja.json index ba421a8767c..2d45d5772e2 100644 --- a/homeassistant/components/vacuum/translations/ja.json +++ b/homeassistant/components/vacuum/translations/ja.json @@ -1,7 +1,13 @@ { "state": { "_": { - "docked": "\u30c9\u30c3\u30ad\u30f3\u30b0" + "cleaning": "\u30af\u30ea\u30fc\u30cb\u30f3\u30b0", + "docked": "\u30c9\u30c3\u30ad\u30f3\u30b0", + "error": "\u30a8\u30e9\u30fc", + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3", + "returning": "\u30c9\u30c3\u30af\u306b\u623b\u308b" } - } + }, + "title": "\u771f\u7a7a(Vacuum)" } \ No newline at end of file diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json new file mode 100644 index 00000000000..58165f3240e --- /dev/null +++ b/homeassistant/components/vera/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "URL {base_url} \u3092\u6301\u3064\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json index 5dc41a91227..31f1eeb3228 100644 --- a/homeassistant/components/volumio/translations/ja.json +++ b/homeassistant/components/volumio/translations/ja.json @@ -1,6 +1,10 @@ { "config": { "step": { + "discovery_confirm": { + "description": "Volumio (`{name}`) \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Volumio\u3092\u767a\u898b" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index b954dbedb6a..f2012e0cb33 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -6,7 +6,8 @@ "step": { "reauth_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { diff --git a/homeassistant/components/weather/translations/ja.json b/homeassistant/components/weather/translations/ja.json index 8b2d8a46d74..787791be8cd 100644 --- a/homeassistant/components/weather/translations/ja.json +++ b/homeassistant/components/weather/translations/ja.json @@ -3,6 +3,7 @@ "_": { "clear-night": "\u6674\u308c\u305f\u591c", "cloudy": "\u66c7\u308a", + "exceptional": "\u534a\u7aef\u3058\u3083\u306a\u3044", "fog": "\u9727", "hail": "\u96f9", "lightning": "\u96f7", @@ -13,7 +14,8 @@ "snowy": "\u96ea", "snowy-rainy": "\u307f\u305e\u308c", "sunny": "\u6674\u308c", - "windy": "\u5f37\u98a8" + "windy": "\u5f37\u98a8", + "windy-variant": "\u5f37\u98a8" } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/ja.json b/homeassistant/components/wiffi/translations/ja.json index 8fd248072c8..1102213699d 100644 --- a/homeassistant/components/wiffi/translations/ja.json +++ b/homeassistant/components/wiffi/translations/ja.json @@ -7,7 +7,8 @@ "user": { "data": { "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "WIFFI\u30c7\u30d0\u30a4\u30b9\u7528\u306eTCP\u30b5\u30fc\u30d0\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index c40559f766c..16593c09912 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 64f7286e08b..f9b6778da08 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -1,7 +1,9 @@ { "state": { "wolflink__state": { - "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0" + "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", + "permanent": "\u6c38\u7d9a", + "stabilisierung": "\u5b89\u5b9a\u5316" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index 59ea458ea2a..463949a796e 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -1,9 +1,16 @@ { "config": { + "flow_title": "{name}", "step": { + "select": { + "data": { + "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "user": { "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "host": "IP\u30a2\u30c9\u30ec\u30b9(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "mac": "Mac\u30a2\u30c9\u30ec\u30b9 (\u30aa\u30d7\u30b7\u30e7\u30f3)" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 07bd2c60ede..1a4ae3c5bff 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -28,6 +28,11 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9" } }, + "gateway": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "manual": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9" diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json index 2c07ef701ff..f537c6a63a5 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ja.json +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -3,12 +3,15 @@ "step": { "reauth_confirm": { "data": { + "area_id": "\u30a8\u30ea\u30a2ID", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, "user": { "data": { "area_id": "\u30a8\u30ea\u30a2ID", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index e577f6e1f2a..528fab416d5 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -26,6 +26,7 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index 3398f27a713..dab546b6f2a 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -7,7 +7,8 @@ "user": { "data": { "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)" - } + }, + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306f\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002\u65b0\u898f\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3059\u308b\u5834\u5408\u306f\u3001\u4ee3\u308f\u308a\u306bZ-Wave JS\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u69cb\u6210\u5909\u6570\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/docs/z-wave/installation/ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, From 40570b572d4e8dd03822e42ef0594190d92ffb0b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Nov 2021 18:06:23 -0700 Subject: [PATCH 0652/1452] Migrate appropriate ReCollect Waste sensors to use datetime state objects (#59943) --- homeassistant/components/recollect_waste/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index eb4cacb38e2..b7291df86ba 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -97,4 +97,4 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_DATE: next_pickup_event.date.isoformat(), } ) - self._attr_native_value = pickup_event.date.isoformat() + self._attr_native_value = pickup_event.date From 394ccae8a1217fdfde32353814ce9ff582afcd79 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Nov 2021 18:06:39 -0700 Subject: [PATCH 0653/1452] Migrate appropriate Ambient PWS sensors to use datetime state objects (#59942) --- homeassistant/components/ambient_station/sensor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 66bccb30b55..58ac081efbf 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,6 +1,8 @@ """Support for Ambient Weather Station sensors.""" from __future__ import annotations +from datetime import datetime + from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -643,6 +645,11 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): @callback def update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - self._attr_native_value = self._ambient.stations[self._mac_address][ - ATTR_LAST_DATA - ][self.entity_description.key] + raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ + self.entity_description.key + ] + + if self.entity_description.key == TYPE_LASTRAIN: + self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z") + else: + self._attr_native_value = raw From 02423d6edc5cba58a7aa6c598daa9f7c64292416 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 20 Nov 2021 04:52:33 +0100 Subject: [PATCH 0654/1452] Add long-term statistics support for devolo Home Control (#57612) * Add long-term statistics support * Fix messed up rebase --- .../components/devolo_home_control/sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index c0ce78dfd72..c342f691b00 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) @@ -33,6 +34,16 @@ DEVICE_CLASS_MAPPING = { "voltage": DEVICE_CLASS_VOLTAGE, } +STATE_CLASS_MAPPING = { + "battery": STATE_CLASS_MEASUREMENT, + "temperature": STATE_CLASS_MEASUREMENT, + "light": STATE_CLASS_MEASUREMENT, + "humidity": STATE_CLASS_MEASUREMENT, + "current": STATE_CLASS_MEASUREMENT, + "total": STATE_CLASS_TOTAL_INCREASING, + "voltage": STATE_CLASS_MEASUREMENT, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -106,6 +117,9 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): self._attr_device_class = DEVICE_CLASS_MAPPING.get( self._multi_level_sensor_property.sensor_type ) + self._attr_state_class = STATE_CLASS_MAPPING.get( + self._multi_level_sensor_property.sensor_type + ) self._attr_native_unit_of_measurement = self._multi_level_sensor_property.unit self._value = self._multi_level_sensor_property.value @@ -132,6 +146,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): ) self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") + self._attr_state_class = STATE_CLASS_MAPPING.get("battery") self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC self._attr_native_unit_of_measurement = PERCENTAGE @@ -158,6 +173,7 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._sensor_type = consumption self._attr_device_class = DEVICE_CLASS_MAPPING.get(consumption) + self._attr_state_class = STATE_CLASS_MAPPING.get(consumption) self._attr_native_unit_of_measurement = getattr( device_instance.consumption_property[element_uid], f"{consumption}_unit" ) From 59f10373ada8a784c2bd3cf0e8bfa91821baca80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Nov 2021 21:55:51 -0600 Subject: [PATCH 0655/1452] Add configuration url to august (#60013) --- homeassistant/components/august/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 8da7fe3d418..a0fe44838c2 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -25,6 +25,7 @@ class AugustEntityMixin(Entity): name=device.device_name, sw_version=self._detail.firmware_version, suggested_area=_remove_device_types(device.device_name, DEVICE_TYPES), + configuration_url="https://account.august.com", ) @property From d0ff8a9b765e116f3c6fff73b462eeb2eb65247e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Nov 2021 11:07:59 +0100 Subject: [PATCH 0656/1452] Add id constant for homekit discovery (#59986) --- .../components/gogogate2/config_flow.py | 4 +++- .../homekit_controller/config_flow.py | 6 +++--- homeassistant/components/zeroconf/__init__.py | 3 +++ .../components/gogogate2/test_config_flow.py | 10 +++++----- .../homekit_controller/test_config_flow.py | 20 +++++++++++++------ 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index d3df62c3b6e..e71046f7974 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -39,7 +39,9 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle homekit discovery.""" - await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["id"]) + await self.async_set_unique_id( + discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + ) return await self._async_discovery_handler(discovery_info[zeroconf.ATTR_HOST]) async def async_step_dhcp( diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 905aec83fe2..557fc3894c7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -167,7 +167,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): properties={ "md": record["md"], "pv": record["pv"], - "id": unique_id, + zeroconf.ATTR_PROPERTIES_ID: unique_id, "c#": record["c#"], "s#": record["s#"], "ff": record["ff"], @@ -212,7 +212,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() } - if "id" not in properties: + if zeroconf.ATTR_PROPERTIES_ID not in properties: # This can happen if the TXT record is received after the PTR record # we will wait for the next update in this case _LOGGER.debug( @@ -223,7 +223,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. - hkid = properties["id"] + hkid = properties[zeroconf.ATTR_PROPERTIES_ID] model = properties["md"] name = discovery_info[zeroconf.ATTR_NAME].replace("._hap._tcp.local.", "") status_flags = int(properties["sf"]) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 01b289f6d18..8735dc47b83 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -68,6 +68,9 @@ ATTR_PORT: Final = "port" ATTR_PROPERTIES: Final = "properties" ATTR_TYPE: Final = "type" +# Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES] +ATTR_PROPERTIES_ID: Final = "id" + CONFIG_SCHEMA = vol.Schema( { diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 202e7840456..a70959cfd09 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -108,7 +108,7 @@ async def test_form_homekit_unique_id_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} ), ) assert result["type"] == RESULT_TYPE_FORM @@ -130,7 +130,7 @@ async def test_form_homekit_unique_id_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -149,7 +149,7 @@ async def test_form_homekit_ip_address_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -162,7 +162,7 @@ async def test_form_homekit_ip_address(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} ), ) assert result["type"] == RESULT_TYPE_FORM @@ -237,7 +237,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={"id": MOCK_MAC_ADDR} + host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} ), ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 7ed4b7e5422..c375dc3dd4d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -148,7 +148,7 @@ def get_device_discovery_info( properties={ "md": record["md"], "pv": record["pv"], - "id": device.device_id, + zeroconf.ATTR_PROPERTIES_ID: device.device_id, "c#": record["c#"], "s#": record["s#"], "ff": record["ff"], @@ -273,7 +273,7 @@ async def test_id_missing(hass, controller): discovery_info = get_device_discovery_info(device) # Remove id from device - del discovery_info["properties"]["id"] + del discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] # Device is discovered result = await hass.config_entries.flow.async_init( @@ -289,7 +289,9 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) - discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES][ + zeroconf.ATTR_PROPERTIES_ID + ] = "AA:BB:CC:DD:EE:FF" discovery_info[zeroconf.ATTR_PROPERTIES]["md"] = "HHKBridge1,1" # Device is discovered @@ -317,7 +319,9 @@ async def test_discovery_ignored_hk_bridge(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES][ + zeroconf.ATTR_PROPERTIES_ID + ] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -344,7 +348,9 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES][ + zeroconf.ATTR_PROPERTIES_ID + ] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -486,7 +492,9 @@ async def test_discovery_already_configured_update_csharp(hass, controller): # Set device as already paired discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] = 0x00 discovery_info[zeroconf.ATTR_PROPERTIES]["c#"] = 99999 - discovery_info[zeroconf.ATTR_PROPERTIES]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info[zeroconf.ATTR_PROPERTIES][ + zeroconf.ATTR_PROPERTIES_ID + ] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( From 40104de0bf4b2bf6adfdcc159ec4b55124b35ca2 Mon Sep 17 00:00:00 2001 From: Ullrich Neiss Date: Sat, 20 Nov 2021 11:16:53 +0100 Subject: [PATCH 0657/1452] Address late review of kostal plenticore (#59998) --- .coveragerc | 2 +- .../components/kostal_plenticore/__init__.py | 2 +- .../components/kostal_plenticore/const.py | 20 ++++++-- .../components/kostal_plenticore/helper.py | 4 +- .../components/kostal_plenticore/select.py | 48 +++++++++---------- .../components/kostal_plenticore/switch.py | 18 ++----- 6 files changed, 45 insertions(+), 49 deletions(-) diff --git a/.coveragerc b/.coveragerc index e1ef57b9817..25ef70250cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -550,9 +550,9 @@ omit = homeassistant/components/kostal_plenticore/__init__.py homeassistant/components/kostal_plenticore/const.py homeassistant/components/kostal_plenticore/helper.py + homeassistant/components/kostal_plenticore/select.py homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kostal_plenticore/switch.py - homeassistant/components/kostal_plenticore/select.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/* diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 8e2beb73cc2..f5c973cc499 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -11,7 +11,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "switch", "select"] +PLATFORMS = ["select", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index b025738a7b8..ebed1ddcb74 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,5 +1,4 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from collections import namedtuple from typing import NamedTuple from homeassistant.components.sensor import ( @@ -692,6 +691,20 @@ SENSOR_SETTINGS_DATA = [ ), ] + +class SwitchData(NamedTuple): + """Representation of a SelectData tuple.""" + + module_id: str + data_id: str + name: str + is_on: str + on_value: str + on_label: str + off_value: str + off_label: str + + # Defines all entities for switches. # # Each entry is defined with a tuple of these values: @@ -702,11 +715,8 @@ SENSOR_SETTINGS_DATA = [ # - on Label (str) # - off Value (str) # - off Label (str) -SWITCH = namedtuple( - "SWITCH", "module_id data_id name is_on on_value on_label off_value off_label" -) SWITCH_SETTINGS_DATA = [ - SWITCH( + SwitchData( "devices:local", "Battery:Strategy", "Battery Strategy:", diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 264d7e90efb..fd367230c6c 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -278,11 +278,11 @@ class SelectDataUpdateCoordinator( _LOGGER.debug("Fetching select %s for %s", self.name, self._fetch) - fetched_data = await self.async_get_currentoption(self._fetch) + fetched_data = await self._async_get_current_option(self._fetch) return fetched_data - async def async_get_currentoption( + async def _async_get_current_option( self, module_id: str | dict[str, Iterable[str]], ) -> dict[str, dict[str, str]]: diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 1f9d11dc334..4e594095f7e 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, SELECT_SETTINGS_DATA @@ -19,15 +20,21 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add kostal plenticore Select widget.""" plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id] + select_data_update_coordinator = SelectDataUpdateCoordinator( + hass, + _LOGGER, + "Settings Data", + timedelta(seconds=30), + plenticore, + ) async_add_entities( PlenticoreDataSelect( - hass=hass, - plenticore=plenticore, + select_data_update_coordinator, entry_id=entry.entry_id, platform_name=entry.title, device_class="kostal_plenticore__battery", @@ -45,12 +52,11 @@ async def async_setup_entry( class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): - """Representation of a Plenticore Switch.""" + """Representation of a Plenticore Select.""" def __init__( self, - hass: HomeAssistant, - plenticore: Plenticore, + coordinator, entry_id: str, platform_name: str, device_class: str | None, @@ -63,17 +69,8 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): device_info: DeviceInfo, unique_id: str, ) -> None: - """Create a new switch Entity for Plenticore process data.""" - super().__init__( - coordinator=SelectDataUpdateCoordinator( - hass, - _LOGGER, - "Select Data", - timedelta(seconds=30), - plenticore, - ) - ) - self.plenticore = plenticore + """Create a new Select Entity for Plenticore process data.""" + super().__init__(coordinator) self.entry_id = entry_id self.platform_name = platform_name self._attr_device_class = device_class @@ -90,20 +87,13 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): @property def available(self) -> bool: """Return if entity is available.""" - is_available = ( + return ( super().available and self.coordinator.data is not None and self.module_id in self.coordinator.data and self.data_id in self.coordinator.data[self.module_id] ) - if is_available: - self._attr_current_option = self.coordinator.data[self.module_id][ - self.data_id - ] - - return is_available - async def async_added_to_hass(self) -> None: """Register this entity on the Update Coordinator.""" await super().async_added_to_hass() @@ -127,3 +117,11 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): if option != "None": await self.coordinator.async_write_data(self.module_id, {option: "1"}) self.async_write_ha_state() + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self.available: + return self.coordinator.data[self.module_id][self.data_id] + + return None diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 9598e12aac0..b3b1ba29e84 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -4,12 +4,12 @@ from __future__ import annotations from abc import ABC from datetime import timedelta import logging -from typing import Any from homeassistant.components.switch import SwitchEntity 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 homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, SWITCH_SETTINGS_DATA @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Add kostal plenticore Switch.""" plenticore = hass.data[DOMAIN][entry.entry_id] @@ -87,13 +87,12 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): attr_name: str, attr_unique_id: str, ): - """Create a new switch Entity for Plenticore process data.""" + """Create a new Switch Entity for Plenticore process data.""" super().__init__(coordinator) self.entry_id = entry_id self.platform_name = platform_name self.module_id = module_id self.data_id = data_id - self._last_run_success: bool | None = None self._name = name self._is_on = is_on self._attr_name = attr_name @@ -130,24 +129,18 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.on_value} ): - self._last_run_success = True self.coordinator.name = f"{self.platform_name} {self._name} {self.on_label}" await self.coordinator.async_request_refresh() - else: - self._last_run_success = False async def async_turn_off(self, **kwargs) -> None: """Turn device off.""" if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.off_value} ): - self._last_run_success = True self.coordinator.name = ( f"{self.platform_name} {self._name} {self.off_label}" ) await self.coordinator.async_request_refresh() - else: - self._last_run_success = False @property def device_info(self) -> DeviceInfo: @@ -164,8 +157,3 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): f"{self.platform_name} {self._name} {self.off_label}" ) return bool(self.coordinator.data[self.module_id][self.data_id] == self._is_on) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - return {"last_run_success": self._last_run_success} From e5c33474e389eb6b72710cbef233fecfb60a6331 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 20 Nov 2021 11:30:41 +0100 Subject: [PATCH 0658/1452] Add config and options flow to KNX integration (#59377) --- .coveragerc | 13 +- homeassistant/components/knx/__init__.py | 224 ++++--- homeassistant/components/knx/binary_sensor.py | 26 +- homeassistant/components/knx/button.py | 26 +- homeassistant/components/knx/climate.py | 30 +- homeassistant/components/knx/config_flow.py | 409 +++++++++++++ homeassistant/components/knx/const.py | 5 + homeassistant/components/knx/cover.py | 22 +- homeassistant/components/knx/fan.py | 18 +- homeassistant/components/knx/light.py | 30 +- homeassistant/components/knx/manifest.json | 13 +- homeassistant/components/knx/notify.py | 4 +- homeassistant/components/knx/number.py | 29 +- homeassistant/components/knx/scene.py | 22 +- homeassistant/components/knx/schema.py | 15 +- homeassistant/components/knx/select.py | 22 +- homeassistant/components/knx/sensor.py | 20 +- homeassistant/components/knx/services.yaml | 3 - homeassistant/components/knx/strings.json | 63 ++ homeassistant/components/knx/switch.py | 26 +- .../components/knx/translations/en.json | 63 ++ homeassistant/components/knx/weather.py | 22 +- homeassistant/generated/config_flows.py | 1 + tests/components/knx/conftest.py | 34 +- tests/components/knx/test_config_flow.py | 573 ++++++++++++++++++ 25 files changed, 1469 insertions(+), 244 deletions(-) create mode 100644 homeassistant/components/knx/config_flow.py create mode 100644 homeassistant/components/knx/strings.json create mode 100644 homeassistant/components/knx/translations/en.json create mode 100644 tests/components/knx/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 25ef70250cc..8a9fdf070c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -540,7 +540,18 @@ omit = homeassistant/components/keyboard_remote/* homeassistant/components/kira/* homeassistant/components/kiwi/lock.py - homeassistant/components/knx/* + homeassistant/components/knx/__init__.py + homeassistant/components/knx/climate.py + homeassistant/components/knx/const.py + homeassistant/components/knx/cover.py + homeassistant/components/knx/expose.py + homeassistant/components/knx/knx_entity.py + homeassistant/components/knx/light.py + homeassistant/components/knx/notify.py + homeassistant/components/knx/scene.py + homeassistant/components/knx/schema.py + homeassistant/components/knx/switch.py + homeassistant/components/knx/weather.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/const.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 0cdd92abe64..d98dd6663a3 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -21,28 +21,30 @@ from xknx.telegram.address import ( ) from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EVENT, CONF_HOST, CONF_PORT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, - SERVICE_RELOAD, ) from homeassistant.core import Event, HomeAssistant, ServiceCall -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_KNX_CONNECTION_TYPE, CONF_KNX_EXPOSE, CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_ROUTING, CONF_KNX_TUNNELING, + DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms, @@ -87,6 +89,13 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( # deprecated since 2021.12 + cv.deprecated(ConnectionSchema.CONF_KNX_STATE_UPDATER), + cv.deprecated(ConnectionSchema.CONF_KNX_RATE_LIMIT), + cv.deprecated(CONF_KNX_ROUTING), + cv.deprecated(CONF_KNX_TUNNELING), + cv.deprecated(CONF_KNX_INDIVIDUAL_ADDRESS), + cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_GRP), + cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_PORT), cv.deprecated(CONF_KNX_EVENT_FILTER), # deprecated since 2021.4 cv.deprecated("config_file"), @@ -185,35 +194,73 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the KNX integration.""" - try: - knx_module = KNXModule(hass, config) - hass.data[DOMAIN] = knx_module - await knx_module.start() - except XKNXException as ex: - _LOGGER.warning("Could not connect to KNX interface: %s", ex) - hass.components.persistent_notification.async_create( - f"Could not connect to KNX interface:
{ex}", title="KNX" + """Start the KNX integration.""" + conf: ConfigType | None = config.get(DOMAIN) + + if conf is None: + # If we have a config entry, setup is done by that config entry. + # If there is no config entry, this should fail. + return bool(hass.config_entries.async_entries(DOMAIN)) + + conf = dict(conf) + + hass.data[DATA_KNX_CONFIG] = conf + + # Only import if we haven't before. + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) ) - if CONF_KNX_EXPOSE in config[DOMAIN]: - for expose_config in config[DOMAIN][CONF_KNX_EXPOSE]: + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + conf = hass.data.get(DATA_KNX_CONFIG) + + # When reloading + if conf is None: + conf = await async_integration_yaml_config(hass, DOMAIN) + if not conf or DOMAIN not in conf: + return False + + conf = conf[DOMAIN] + + # If user didn't have configuration.yaml config, generate defaults + if conf is None: + conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + + config = {**conf, **entry.data} + + try: + knx_module = KNXModule(hass, config, entry) + await knx_module.start() + except XKNXException as ex: + raise ConfigEntryNotReady from ex + + hass.data[DATA_KNX_CONFIG] = conf + hass.data[DOMAIN] = knx_module + + if CONF_KNX_EXPOSE in config: + for expose_config in config[CONF_KNX_EXPOSE]: knx_module.exposures.append( create_knx_exposure(hass, knx_module.xknx, expose_config) ) - for platform in SupportedPlatforms: - if platform.value not in config[DOMAIN]: - continue + hass.config_entries.async_setup_platforms( + entry, + [platform.value for platform in SupportedPlatforms if platform.value in config], + ) + + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + if NotifySchema.PLATFORM_NAME in conf: hass.async_create_task( discovery.async_load_platform( - hass, - platform.value, - DOMAIN, - { - "platform_config": config[DOMAIN][platform.value], - }, - config, + hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM_NAME], config ) ) @@ -247,39 +294,53 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, ) - async def reload_service_handler(service_call: ServiceCall) -> None: - """Remove all KNX components and load new ones from config.""" - - # First check for config file. If for some reason it is no longer there - # or knx is no longer mentioned, stop the reload. - config = await async_integration_yaml_config(hass, DOMAIN) - if not config or DOMAIN not in config: - return - - await asyncio.gather( - *(platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)) - ) - await knx_module.xknx.stop() - - await async_setup(hass, config) - - async_register_admin_service( - hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) - ) - return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unloading the KNX platforms.""" + # if not loaded directly return + if not hass.data.get(DOMAIN): + return True + + knx_module: KNXModule = hass.data[DOMAIN] + for exposure in knx_module.exposures: + exposure.shutdown() + + unload_ok = await hass.config_entries.async_unload_platforms( + entry, + [ + platform.value + for platform in SupportedPlatforms + if platform.value in hass.data[DATA_KNX_CONFIG] + ], + ) + if unload_ok: + await knx_module.stop() + hass.data.pop(DOMAIN) + hass.data.pop(DATA_KNX_CONFIG) + + return unload_ok + + +async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Update a given config entry.""" + return await hass.config_entries.async_reload(entry.entry_id) + + class KNXModule: """Representation of KNX Object.""" - def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: + def __init__( + self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry + ) -> None: """Initialize KNX module.""" self.hass = hass self.config = config self.connected = False self.exposures: list[KNXExposeSensor | KNXExposeTime] = [] self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {} + self.entry = entry self.init_xknx() self.xknx.connection_manager.register_connection_state_changed_cb( @@ -292,64 +353,49 @@ class KNXModule: self.register_event_callback() ) + self.entry.async_on_unload( + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) + ) + + self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry)) + def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( - own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], - rate_limit=self.config[DOMAIN][ConnectionSchema.CONF_KNX_RATE_LIMIT], - multicast_group=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_GRP], - multicast_port=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_PORT], + own_address=self.config[CONF_KNX_INDIVIDUAL_ADDRESS], + rate_limit=self.config[ConnectionSchema.CONF_KNX_RATE_LIMIT], + multicast_group=self.config[ConnectionSchema.CONF_KNX_MCAST_GRP], + multicast_port=self.config[ConnectionSchema.CONF_KNX_MCAST_PORT], connection_config=self.connection_config(), - state_updater=self.config[DOMAIN][ConnectionSchema.CONF_KNX_STATE_UPDATER], + state_updater=self.config[ConnectionSchema.CONF_KNX_STATE_UPDATER], ) async def start(self) -> None: """Start XKNX object. Connect to tunneling or Routing device.""" await self.xknx.start() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - async def stop(self, event: Event) -> None: + async def stop(self, event: Event | None = None) -> None: """Stop XKNX object. Disconnect from tunneling or Routing device.""" await self.xknx.stop() def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" - if CONF_KNX_TUNNELING in self.config[DOMAIN]: - return self.connection_config_tunneling() - if CONF_KNX_ROUTING in self.config[DOMAIN]: - return self.connection_config_routing() - return ConnectionConfig(auto_reconnect=True) - - def connection_config_routing(self) -> ConnectionConfig: - """Return the connection_config if routing is configured.""" - local_ip = None - # all configuration values are optional - if self.config[DOMAIN][CONF_KNX_ROUTING] is not None: - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get( - ConnectionSchema.CONF_KNX_LOCAL_IP + _conn_type: str = self.config[CONF_KNX_CONNECTION_TYPE] + if _conn_type == CONF_KNX_ROUTING: + return ConnectionConfig( + connection_type=ConnectionType.ROUTING, + auto_reconnect=True, + ) + if _conn_type == CONF_KNX_TUNNELING: + return ConnectionConfig( + connection_type=ConnectionType.TUNNELING, + gateway_ip=self.config[CONF_HOST], + gateway_port=self.config[CONF_PORT], + route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), + auto_reconnect=True, ) - return ConnectionConfig( - connection_type=ConnectionType.ROUTING, local_ip=local_ip - ) - def connection_config_tunneling(self) -> ConnectionConfig: - """Return the connection_config if tunneling is configured.""" - gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST] - gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT] - local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ) - route_back = self.config[DOMAIN][CONF_KNX_TUNNELING][ - ConnectionSchema.CONF_KNX_ROUTE_BACK - ] - return ConnectionConfig( - connection_type=ConnectionType.TUNNELING, - gateway_ip=gateway_ip, - gateway_port=gateway_port, - local_ip=local_ip, - route_back=route_back, - auto_reconnect=True, - ) + return ConnectionConfig(auto_reconnect=True) async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: """Call invoked after a KNX connection state change was received.""" @@ -409,10 +455,8 @@ class KNXModule: """Register callback for knx_event within XKNX TelegramQueue.""" # backwards compatibility for deprecated CONF_KNX_EVENT_FILTER # use `address_filters = []` when this is not needed anymore - address_filters = list( - map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) - ) - for filter_set in self.config[DOMAIN][CONF_EVENT]: + address_filters = list(map(AddressFilter, self.config[CONF_KNX_EVENT_FILTER])) + for filter_set in self.config[CONF_EVENT]: _filters = list(map(AddressFilter, filter_set[KNX_ADDRESS])) address_filters.extend(_filters) if (dpt := filter_set.get(CONF_TYPE)) and ( diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index b4e3354fc37..b3dbdc0db12 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -6,6 +6,7 @@ from typing import Any from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor +from homeassistant import config_entries from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -18,28 +19,31 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import ATTR_COUNTER, ATTR_SOURCE, DOMAIN +from .const import ( + ATTR_COUNTER, + ATTR_SOURCE, + DATA_KNX_CONFIG, + DOMAIN, + SupportedPlatforms, +) from .knx_entity import KnxEntity from .schema import BinarySensorSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up binary sensor(s) for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - - platform_config = discovery_info["platform_config"] + """Set up the KNX binary sensor platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXBinarySensor(xknx, entity_config) for entity_config in platform_config + KNXBinarySensor(xknx, entity_config) + for entity_config in config[SupportedPlatforms.BINARY_SENSOR.value] ) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 72bb807abb0..457e1d6d43d 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -4,30 +4,36 @@ from __future__ import annotations from xknx import XKNX from xknx.devices import RawValue as XknxRawValue +from homeassistant import config_entries from homeassistant.components.button import ButtonEntity from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS +from .const import ( + CONF_PAYLOAD, + CONF_PAYLOAD_LENGTH, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, + SupportedPlatforms, +) from .knx_entity import KnxEntity -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up buttons for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up the KNX binary sensor platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) for entity_config in platform_config + KNXButton(xknx, entity_config) + for entity_config in config[SupportedPlatforms.BUTTON.value] ) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 6d3194731f1..791e897f4d3 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -8,6 +8,7 @@ from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from xknx.telegram.address import parse_device_group_address +from homeassistant import config_entries from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_IDLE, @@ -26,9 +27,16 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DOMAIN, PRESET_MODES +from .const import ( + CONTROLLER_MODES, + CURRENT_HVAC_ACTIONS, + DATA_KNX_CONFIG, + DOMAIN, + PRESET_MODES, + SupportedPlatforms, +) from .knx_entity import KnxEntity from .schema import ClimateSchema @@ -37,23 +45,19 @@ CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up climate(s) for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.CLIMATE.value + ] - _async_migrate_unique_id(hass, platform_config) - async_add_entities( - KNXClimate(xknx, entity_config) for entity_config in platform_config - ) + _async_migrate_unique_id(hass, config) + async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) @callback diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py new file mode 100644 index 00000000000..3fcf4069624 --- /dev/null +++ b/homeassistant/components/knx/config_flow.py @@ -0,0 +1,409 @@ +"""Config flow for KNX.""" +from __future__ import annotations + +from typing import Any, Final + +import voluptuous as vol +from xknx import XKNX +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT +from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_INITIAL_CONNECTION_TYPES, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, + DOMAIN, +) +from .schema import ConnectionSchema + +CONF_KNX_GATEWAY: Final = "gateway" +CONF_MAX_RATE_LIMIT: Final = 60 + +DEFAULT_ENTRY_DATA: Final = { + ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER, + ConnectionSchema.CONF_KNX_RATE_LIMIT: ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, +} + + +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a KNX config flow.""" + + VERSION = 1 + + _tunnels: list + _gateway_ip: str = "" + _gateway_port: int = DEFAULT_MCAST_PORT + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlowHandler: + """Get the options flow for this handler.""" + return KNXOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + self._tunnels = [] + return await self.async_step_type() + + async def async_step_type(self, user_input: dict | None = None) -> FlowResult: + """Handle connection type configuration.""" + errors: dict = {} + supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy() + fields = {} + + if user_input is None: + gateways = await scan_for_gateways() + + if gateways: + supported_connection_types.insert(0, CONF_KNX_AUTOMATIC) + self._tunnels = [ + gateway for gateway in gateways if gateway.supports_tunnelling + ] + + fields = { + vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In( + supported_connection_types + ) + } + + if user_input is not None: + connection_type = user_input[CONF_KNX_CONNECTION_TYPE] + if connection_type == CONF_KNX_AUTOMATIC: + return self.async_create_entry( + title=CONF_KNX_AUTOMATIC.capitalize(), + data={**DEFAULT_ENTRY_DATA, **user_input}, + ) + + if connection_type == CONF_KNX_ROUTING: + return await self.async_step_routing() + + if connection_type == CONF_KNX_TUNNELING and self._tunnels: + return await self.async_step_tunnel() + + return await self.async_step_manual_tunnel() + + return self.async_show_form( + step_id="type", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_manual_tunnel( + self, user_input: dict | None = None + ) -> FlowResult: + """General setup.""" + errors: dict = {} + + if user_input is not None: + return self.async_create_entry( + title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}", + data={ + **DEFAULT_ENTRY_DATA, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ + CONF_KNX_INDIVIDUAL_ADDRESS + ], + ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[ + ConnectionSchema.CONF_KNX_ROUTE_BACK + ], + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + + fields = { + vol.Required(CONF_HOST, default=self._gateway_ip): str, + vol.Required(CONF_PORT, default=self._gateway_port): vol.Coerce(int), + vol.Required( + CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS + ): str, + vol.Required( + ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False + ): vol.Coerce(bool), + } + + return self.async_show_form( + step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: + """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found.""" + errors: dict = {} + + if user_input is not None: + gateway: GatewayDescriptor = next( + gateway + for gateway in self._tunnels + if user_input[CONF_KNX_GATEWAY] == str(gateway) + ) + + self._gateway_ip = gateway.ip_addr + self._gateway_port = gateway.port + + return await self.async_step_manual_tunnel() + + tunnel_repr = { + str(tunnel) for tunnel in self._tunnels if tunnel.supports_tunnelling + } + + # skip this step if the user has only one unique gateway. + if len(tunnel_repr) == 1: + _gateway: GatewayDescriptor = self._tunnels[0] + self._gateway_ip = _gateway.ip_addr + self._gateway_port = _gateway.port + return await self.async_step_manual_tunnel() + + fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_repr)} + + return self.async_show_form( + step_id="tunnel", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_routing(self, user_input: dict | None = None) -> FlowResult: + """Routing setup.""" + errors: dict = {} + + if user_input is not None: + return self.async_create_entry( + title=CONF_KNX_ROUTING.capitalize(), + data={ + **DEFAULT_ENTRY_DATA, + ConnectionSchema.CONF_KNX_MCAST_GRP: user_input[ + ConnectionSchema.CONF_KNX_MCAST_GRP + ], + ConnectionSchema.CONF_KNX_MCAST_PORT: user_input[ + ConnectionSchema.CONF_KNX_MCAST_PORT + ], + CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ + CONF_KNX_INDIVIDUAL_ADDRESS + ], + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + + fields = { + vol.Required( + CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS + ): str, + vol.Required( + ConnectionSchema.CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP + ): str, + vol.Required( + ConnectionSchema.CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT + ): cv.port, + } + + return self.async_show_form( + step_id="routing", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_import(self, config: dict | None = None) -> FlowResult: + """Import a config entry. + + Performs a one time import of the YAML configuration and creates a config entry based on it + if not already done before. + """ + if self._async_current_entries() or not config: + return self.async_abort(reason="single_instance_allowed") + + data = { + ConnectionSchema.CONF_KNX_RATE_LIMIT: min( + config[ConnectionSchema.CONF_KNX_RATE_LIMIT], CONF_MAX_RATE_LIMIT + ), + ConnectionSchema.CONF_KNX_STATE_UPDATER: config[ + ConnectionSchema.CONF_KNX_STATE_UPDATER + ], + ConnectionSchema.CONF_KNX_MCAST_GRP: config[ + ConnectionSchema.CONF_KNX_MCAST_GRP + ], + ConnectionSchema.CONF_KNX_MCAST_PORT: config[ + ConnectionSchema.CONF_KNX_MCAST_PORT + ], + CONF_KNX_INDIVIDUAL_ADDRESS: config[CONF_KNX_INDIVIDUAL_ADDRESS], + } + + if CONF_KNX_TUNNELING in config: + return self.async_create_entry( + title=f"{CONF_KNX_TUNNELING.capitalize()} @ {config[CONF_KNX_TUNNELING][CONF_HOST]}", + data={ + **DEFAULT_ENTRY_DATA, + CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST], + CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT], + ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][ + ConnectionSchema.CONF_KNX_ROUTE_BACK + ], + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + **data, + }, + ) + + if CONF_KNX_ROUTING in config: + return self.async_create_entry( + title=CONF_KNX_ROUTING.capitalize(), + data={ + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + **data, + }, + ) + + return self.async_create_entry( + title=CONF_KNX_AUTOMATIC.capitalize(), + data={ + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + **data, + }, + ) + + +class KNXOptionsFlowHandler(OptionsFlow): + """Handle KNX options.""" + + general_settings: dict + current_config: dict + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize KNX options flow.""" + self.config_entry = config_entry + + async def async_step_tunnel( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage KNX tunneling options.""" + if ( + self.general_settings.get(CONF_KNX_CONNECTION_TYPE) == CONF_KNX_TUNNELING + and user_input is None + ): + return self.async_show_form( + step_id="tunnel", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=self.current_config.get(CONF_HOST) + ): str, + vol.Required( + CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) + ): cv.port, + vol.Required( + ConnectionSchema.CONF_KNX_ROUTE_BACK, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_ROUTE_BACK, False + ), + ): vol.Coerce(bool), + } + ), + last_step=True, + ) + + entry_data = { + **DEFAULT_ENTRY_DATA, + **self.general_settings, + CONF_HOST: self.current_config.get(CONF_HOST, ""), + } + + if user_input is not None: + entry_data = { + **entry_data, + **user_input, + } + + entry_title = entry_data[CONF_KNX_CONNECTION_TYPE].capitalize() + if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING: + entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}" + + self.hass.config_entries.async_update_entry( + self.config_entry, + data=entry_data, + title=entry_title, + ) + + return self.async_create_entry(title="", data={}) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage KNX options.""" + if user_input is not None: + self.general_settings = user_input + return await self.async_step_tunnel() + + supported_connection_types = [ + CONF_KNX_AUTOMATIC, + CONF_KNX_TUNNELING, + CONF_KNX_ROUTING, + ] + self.current_config = self.config_entry.data # type: ignore + + data_schema = { + vol.Required( + CONF_KNX_CONNECTION_TYPE, + default=self.current_config.get(CONF_KNX_CONNECTION_TYPE), + ): vol.In(supported_connection_types), + vol.Required( + CONF_KNX_INDIVIDUAL_ADDRESS, + default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], + ): str, + vol.Required( + ConnectionSchema.CONF_KNX_MCAST_GRP, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP + ), + ): str, + vol.Required( + ConnectionSchema.CONF_KNX_MCAST_PORT, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT + ), + ): cv.port, + } + + if self.show_advanced_options: + data_schema[ + vol.Required( + ConnectionSchema.CONF_KNX_STATE_UPDATER, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_STATE_UPDATER, + ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER, + ), + ) + ] = bool + data_schema[ + vol.Required( + ConnectionSchema.CONF_KNX_RATE_LIMIT, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_RATE_LIMIT, + ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT, + ), + ) + ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT)) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(data_schema), + last_step=self.current_config.get(CONF_KNX_CONNECTION_TYPE) + != CONF_KNX_TUNNELING, + ) + + +async def scan_for_gateways(stop_on_found: int = 0) -> list: + """Scan for gateways within the network.""" + xknx = XKNX() + gatewayscanner = GatewayScanner( + xknx, stop_on_found=stop_on_found, timeout_in_seconds=2 + ) + return await gatewayscanner.scan() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index f3468fc8dc2..e12d9594795 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -29,6 +29,8 @@ KNX_ADDRESS: Final = "address" CONF_INVERT: Final = "invert" CONF_KNX_EXPOSE: Final = "expose" CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" +CONF_KNX_CONNECTION_TYPE: Final = "connection_type" +CONF_KNX_AUTOMATIC: Final = "automatic" CONF_KNX_ROUTING: Final = "routing" CONF_KNX_TUNNELING: Final = "tunneling" CONF_PAYLOAD: Final = "payload" @@ -37,6 +39,9 @@ CONF_RESET_AFTER: Final = "reset_after" CONF_RESPOND_TO_READ: Final = "respond_to_read" CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" +CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING] + +DATA_KNX_CONFIG: Final = "knx_config" ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 8eb906d1ba0..62fd3a1ba08 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -9,6 +9,7 @@ from xknx import XKNX from xknx.devices import Cover as XknxCover, Device as XknxDevice from xknx.telegram.address import parse_device_group_address +from homeassistant import config_entries from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -28,29 +29,26 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms from .knx_entity import KnxEntity from .schema import CoverSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up cover(s) for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.COVER.value + ] - _async_migrate_unique_id(hass, platform_config) - async_add_entities( - KNXCover(xknx, entity_config) for entity_config in platform_config - ) + _async_migrate_unique_id(hass, config) + async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) @callback diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index a000cdec973..bdb0bbf9dcc 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -7,37 +7,35 @@ from typing import Any, Final from xknx import XKNX from xknx.devices import Fan as XknxFan +from homeassistant import config_entries from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) -from .const import DOMAIN, KNX_ADDRESS +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms from .knx_entity import KnxEntity from .schema import FanSchema DEFAULT_PERCENTAGE: Final = 50 -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up fans for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up fan(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][SupportedPlatforms.FAN.value] - async_add_entities(KNXFan(xknx, entity_config) for entity_config in platform_config) + async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) class KNXFan(KnxEntity, FanEntity): diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 4841b056ebc..f9223d69d74 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -7,6 +7,7 @@ from xknx import XKNX from xknx.devices.light import Light as XknxLight, XYYColor from xknx.telegram.address import parse_device_group_address +from homeassistant import config_entries from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -27,30 +28,33 @@ from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .const import DOMAIN, KNX_ADDRESS, ColorTempModes +from .const import ( + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, + ColorTempModes, + SupportedPlatforms, +) from .knx_entity import KnxEntity from .schema import LightSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up lights for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up light(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.LIGHT.value + ] - _async_migrate_unique_id(hass, platform_config) - async_add_entities( - KNXLight(xknx, entity_config) for entity_config in platform_config - ) + _async_migrate_unique_id(hass, config) + async_add_entities(KNXLight(xknx, entity_config) for entity_config in config) @callback diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 30dafe7d7f9..b793c667353 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -1,9 +1,16 @@ { "domain": "knx", "name": "KNX", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.13"], - "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], + "requirements": [ + "xknx==0.18.13" + ], + "codeowners": [ + "@Julius2342", + "@farmio", + "@marvin-w" + ], "quality_scale": "silver", "iot_class": "local_push" -} +} \ No newline at end of file diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 6f549d9cfac..61bee14e5e2 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -20,10 +20,10 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> KNXNotificationService | None: """Get the KNX notification service.""" - if not discovery_info or not discovery_info["platform_config"]: + if not discovery_info: return None - platform_config = discovery_info["platform_config"] + platform_config: dict = discovery_info xknx: XKNX = hass.data[DOMAIN].xknx notification_devices = [] diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 659c334fd4a..7d4ea8a717b 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -6,6 +6,7 @@ from typing import cast from xknx import XKNX from xknx.devices import NumericValue +from homeassistant import config_entries from homeassistant.components.number import NumberEntity from homeassistant.const import ( CONF_ENTITY_CATEGORY, @@ -18,28 +19,32 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, DOMAIN, KNX_ADDRESS +from .const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, + SupportedPlatforms, +) from .knx_entity import KnxEntity from .schema import NumberSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up number entities for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up number(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.NUMBER.value + ] - async_add_entities( - KNXNumber(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index b09dc678be3..658c6d6d298 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -6,32 +6,30 @@ from typing import Any from xknx import XKNX from xknx.devices import Scene as XknxScene +from homeassistant import config_entries from homeassistant.components.scene import Scene from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, KNX_ADDRESS +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms from .knx_entity import KnxEntity from .schema import SceneSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the scenes for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up scene(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.SCENE.value + ] - async_add_entities( - KNXScene(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) class KNXScene(KnxEntity, Scene): diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index d84531bbcaf..02e4803b163 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -201,7 +201,11 @@ sync_state_validator = vol.Any( class ConnectionSchema: - """Voluptuous schema for KNX connection.""" + """ + Voluptuous schema for KNX connection. + + DEPRECATED: Migrated to config and options flow. Will be removed in a future version of Home Assistant. + """ CONF_KNX_LOCAL_IP = "local_ip" CONF_KNX_MCAST_GRP = "multicast_group" @@ -210,6 +214,9 @@ class ConnectionSchema: CONF_KNX_ROUTE_BACK = "route_back" CONF_KNX_STATE_UPDATER = "state_updater" + CONF_KNX_DEFAULT_STATE_UPDATER = True + CONF_KNX_DEFAULT_RATE_LIMIT = 20 + TUNNELING_SCHEMA = vol.Schema( { vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port, @@ -229,8 +236,10 @@ class ConnectionSchema: ): ia_validator, vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, - vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, - vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( + vol.Optional( + CONF_KNX_STATE_UPDATER, default=CONF_KNX_DEFAULT_STATE_UPDATER + ): cv.boolean, + vol.Optional(CONF_KNX_RATE_LIMIT, default=CONF_KNX_DEFAULT_RATE_LIMIT): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), } diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index f002bad37ce..aefa4749e88 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -4,6 +4,7 @@ from __future__ import annotations from xknx import XKNX from xknx.devices import Device as XknxDevice, RawValue +from homeassistant import config_entries from homeassistant.components.select import SelectEntity from homeassistant.const import ( CONF_ENTITY_CATEGORY, @@ -14,7 +15,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_PAYLOAD, @@ -22,28 +23,27 @@ from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, + DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, + SupportedPlatforms, ) from .knx_entity import KnxEntity from .schema import SelectSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up select entities for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up select(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.SELECT.value + ] - async_add_entities( - KNXSelect(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 84f535fdc8f..a9a1feca9e3 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -6,6 +6,7 @@ from typing import Any from xknx import XKNX from xknx.devices import Sensor as XknxSensor +from homeassistant import config_entries from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES, @@ -14,28 +15,25 @@ from homeassistant.components.sensor import ( from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, StateType -from .const import ATTR_SOURCE, DOMAIN +from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms from .knx_entity import KnxEntity from .schema import SensorSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up sensor(s) for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.SENSOR.value + ] - async_add_entities( - KNXSensor(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXSensor(xknx, entity_config) for entity_config in config) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index fca5f4fe07d..11519be48f3 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -100,6 +100,3 @@ exposure_register: default: false selector: boolean: -reload: - name: "Reload KNX configuration" - description: "Reload the KNX configuration from YAML." diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json new file mode 100644 index 00000000000..ff191f7a4ce --- /dev/null +++ b/homeassistant/components/knx/strings.json @@ -0,0 +1,63 @@ +{ + "config": { + "step": { + "type": { + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", + "data": { + "connection_type": "KNX Connection Type" + } + }, + "tunnel": { + "description": "Please select a gateway from the list.", + "data": { + "gateway": "KNX Tunnel Connection" + } + }, + "manual_tunnel": { + "description": "Please enter the connection information of your tunneling device.", + "data": { + "port": "[%key:common::config_flow::data::port%]", + "host": "[%key:common::config_flow::data::host%]", + "individual_address": "Individual address for the connection", + "route_back": "Route Back / NAT Mode" + } + }, + "routing": { + "description": "Please configure the routing options.", + "data": { + "individual_address": "Individual address for the routing connection", + "multicast_group": "The multicast group used for routing", + "multicast_port": "The multicast port used for routing" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX Connection Type", + "individual_address": "Default individual address", + "multicast_group": "Multicast group used for routing and discovery", + "multicast_port": "Multicast port used for routing and discovery", + "state_updater": "Globally enable reading states from the KNX Bus", + "rate_limit": "Maximum outgoing telegrams per second" + } + }, + "tunnel": { + "data": { + "port": "[%key:common::config_flow::data::port%]", + "host": "[%key:common::config_flow::data::host%]", + "route_back": "Route Back / NAT Mode" + } + } + } + } +} diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index c775ce70d32..3bbb419a22b 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -6,6 +6,7 @@ from typing import Any from xknx import XKNX from xknx.devices import Switch as XknxSwitch +from homeassistant import config_entries from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( CONF_ENTITY_CATEGORY, @@ -17,28 +18,31 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import CONF_RESPOND_TO_READ, DOMAIN, KNX_ADDRESS +from .const import ( + CONF_RESPOND_TO_READ, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, + SupportedPlatforms, +) from .knx_entity import KnxEntity from .schema import SwitchSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up switch(es) for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.SWITCH.value + ] - async_add_entities( - KNXSwitch(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXSwitch(xknx, entity_config) for entity_config in config) class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json new file mode 100644 index 00000000000..231cc790db8 --- /dev/null +++ b/homeassistant/components/knx/translations/en.json @@ -0,0 +1,63 @@ +{ + "config": { + "step": { + "type": { + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", + "data": { + "connection_type": "KNX Connection Type" + } + }, + "tunnel": { + "description": "Please select a gateway from the list.", + "data": { + "gateway": "KNX Tunnel Connection" + } + }, + "manual_tunnel": { + "description": "Please enter the connection information of your tunneling device.", + "data": { + "host": "IP Address of KNX gateway", + "port": "Port of KNX gateway", + "individual_address": "Individual address for the connection", + "route_back": "Route Back / NAT Mode" + } + }, + "routing": { + "description": "Please configure the routing options.", + "data": { + "individual_address": "Individual address for the routing connection", + "multicast_group": "The multicast group used for routing", + "multicast_port": "The multicast port used for routing" + } + } + }, + "abort": { + "already_configured": "Service is already configured", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect." + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX Connection Type", + "individual_address": "Default individual address", + "multicast_group": "Multicast group used for routing and discovery", + "multicast_port": "Multicast port used for routing and discovery", + "state_updater": "Globally enable reading states from the KNX Bus", + "rate_limit": "Maximum outgoing telegrams per second" + } + }, + "tunnel": { + "data": { + "port": "Port of KNX gateway", + "host": "IP Address of KNX gateway", + "route_back": "Route Back / NAT Mode" + } + } + } + } +} diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 13ebd2480e3..b52ee644b39 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -4,32 +4,30 @@ from __future__ import annotations from xknx import XKNX from xknx.devices import Weather as XknxWeather +from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms from .knx_entity import KnxEntity from .schema import WeatherSchema -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up weather entities for KNX platform.""" - if not discovery_info or not discovery_info["platform_config"]: - return - platform_config = discovery_info["platform_config"] + """Set up switch(es) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ + SupportedPlatforms.WEATHER.value + ] - async_add_entities( - KNXWeather(xknx, entity_config) for entity_config in platform_config - ) + async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5d865465e61..4d9cb7d7bc4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -152,6 +152,7 @@ FLOWS = [ "juicenet", "keenetic_ndms2", "kmtronic", + "knx", "kodi", "konnected", "kostal_plenticore", diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index ca6f81057e1..a692fa97814 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -8,23 +8,33 @@ import pytest from xknx import XKNX from xknx.core import XknxConnectionState from xknx.dpt import DPTArray, DPTBinary +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram import Telegram, TelegramDirection from xknx.telegram.address import GroupAddress, IndividualAddress from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite -from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN +from homeassistant.components.knx import ConnectionSchema +from homeassistant.components.knx.const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_INDIVIDUAL_ADDRESS, + DOMAIN as KNX_DOMAIN, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + class KNXTestKit: """Test helper for the KNX integration.""" INDIVIDUAL_ADDRESS = "1.2.3" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry): """Init KNX test helper class.""" self.hass: HomeAssistant = hass + self.mock_config_entry: MockConfigEntry = mock_config_entry self.xknx: XKNX # outgoing telegrams will be put in the Queue instead of sent to the interface # telegrams to an InternalGroupAddress won't be queued here @@ -60,6 +70,7 @@ class KNXTestKit: return_value=knx_ip_interface_mock(), side_effect=fish_xknx, ): + self.mock_config_entry.add_to_hass(self.hass) await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) await self.xknx.connection_manager.connection_state_changed( XknxConnectionState.CONNECTED @@ -191,8 +202,23 @@ class KNXTestKit: @pytest.fixture -async def knx(request, hass): +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="KNX", + domain=KNX_DOMAIN, + data={ + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + }, + ) + + +@pytest.fixture +async def knx(request, hass, mock_config_entry: MockConfigEntry): """Create a KNX TestKit instance.""" - knx_test_kit = KNXTestKit(hass) + knx_test_kit = KNXTestKit(hass, mock_config_entry) yield knx_test_kit await knx_test_kit.assert_no_telegram() diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py new file mode 100644 index 00000000000..2b792044fe5 --- /dev/null +++ b/tests/components/knx/test_config_flow.py @@ -0,0 +1,573 @@ +"""Test the KNX config flow.""" +from unittest.mock import patch + +from xknx import XKNX +from xknx.io import DEFAULT_MCAST_GRP +from xknx.io.gateway_scanner import GatewayDescriptor + +from homeassistant import config_entries +from homeassistant.components.knx import ConnectionSchema +from homeassistant.components.knx.config_flow import ( + CONF_KNX_GATEWAY, + DEFAULT_ENTRY_DATA, +) +from homeassistant.components.knx.const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor: + """Get mock gw descriptor.""" + return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True) + + +async def test_user_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_routing_setup(hass: HomeAssistant) -> None: + """Test routing setup.""" + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "routing" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == CONF_KNX_ROUTING.capitalize() + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tunneling_setup(hass: HomeAssistant) -> None: + """Test tunneling if only one gateway is found.""" + gateway = _gateway_descriptor("192.168.0.1", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "manual_tunnel" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.1" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) -> None: + """Test tunneling if only one gateway is found.""" + gateway = _gateway_descriptor("192.168.0.1", 3675) + gateway2 = _gateway_descriptor("192.168.1.100", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway, gateway2] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + tunnel_flow = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["step_id"] == "tunnel" + assert not tunnel_flow["errors"] + + manual_tunnel = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + {CONF_KNX_GATEWAY: str(gateway)}, + ) + await hass.async_block_till_done() + assert manual_tunnel["type"] == RESULT_TYPE_FORM + assert manual_tunnel["step_id"] == "manual_tunnel" + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + manual_tunnel_flow = await hass.config_entries.flow.async_configure( + manual_tunnel["flow_id"], + { + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + }, + ) + await hass.async_block_till_done() + assert manual_tunnel_flow["type"] == RESULT_TYPE_CREATE_ENTRY + assert manual_tunnel_flow["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: + """Test manual tunnel if no gateway is found and tunneling is selected.""" + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + tunnel_flow = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["step_id"] == "manual_tunnel" + assert not tunnel_flow["errors"] + + +async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> None: + """Test we get the form.""" + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [_gateway_descriptor("192.168.0.1", 3675)] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() + assert result2["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +## +# Import Tests +## +async def test_import_config_tunneling(hass: HomeAssistant) -> None: + """Test tunneling import from config.yaml.""" + config = { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config + CONF_KNX_TUNNELING: { + CONF_HOST: "192.168.1.1", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + }, + } + + with patch( + "homeassistant.components.knx.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 + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Tunneling @ 192.168.1.1" + assert result["data"] == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.1.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_config_routing(hass: HomeAssistant) -> None: + """Test routing import from config.yaml.""" + config = { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config + CONF_KNX_ROUTING: {}, # is required when using routing + } + + with patch( + "homeassistant.components.knx.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 + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONF_KNX_ROUTING.capitalize() + assert result["data"] == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_config_automatic(hass: HomeAssistant) -> None: + """Test automatic import from config.yaml.""" + config = { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config + } + + with patch( + "homeassistant.components.knx.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 + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() + assert result["data"] == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_rate_limit_out_of_range(hass: HomeAssistant) -> None: + """Test automatic import from config.yaml.""" + config = { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config + ConnectionSchema.CONF_KNX_RATE_LIMIT: 80, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config + } + + with patch( + "homeassistant.components.knx.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 + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() + assert result["data"] == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 60, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_options(hass: HomeAssistant) -> None: + """Test import from config.yaml with options.""" + config = { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 30, + } + + with patch( + "homeassistant.components.knx.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 + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() + assert result["data"] == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 30, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_if_entry_exists_already(hass: HomeAssistant) -> None: + """Test routing import from config.yaml.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test options config flow.""" + mock_config_entry.add_to_hass(hass) + + gateway = _gateway_descriptor("192.168.0.1", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + }, + ) + + await hass.async_block_till_done() + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert not result2.get("data") + + assert mock_config_entry.data == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", + CONF_HOST: "", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + } + + +async def test_tunneling_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test options flow for tunneling.""" + mock_config_entry.add_to_hass(hass) + + gateway = _gateway_descriptor("192.168.0.1", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert not result2.get("data") + assert "flow_id" in result2 + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={ + CONF_HOST: "192.168.1.1", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + }, + ) + + await hass.async_block_till_done() + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert not result3.get("data") + + assert mock_config_entry.data == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, + ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + CONF_HOST: "192.168.1.1", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + } + + +async def test_advanced_options( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test options config flow.""" + mock_config_entry.add_to_hass(hass) + + gateway = _gateway_descriptor("192.168.0.1", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.options.async_init( + mock_config_entry.entry_id, context={"show_advanced_options": True} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + }, + ) + + await hass.async_block_till_done() + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert not result2.get("data") + + assert mock_config_entry.data == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_HOST: "", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + } From f305d99af9e75d0a101cdba258edf72d5d631dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 20 Nov 2021 12:53:04 +0200 Subject: [PATCH 0659/1452] Add integration filtering and error if core options to helpers.frame.report (#60009) Reduces some existing and upcoming boilerplate. --- homeassistant/async_timeout_backcompat.py | 39 ++++------------------ homeassistant/components/zeroconf/usage.py | 33 +++--------------- homeassistant/helpers/frame.py | 17 ++++++---- 3 files changed, 22 insertions(+), 67 deletions(-) diff --git a/homeassistant/async_timeout_backcompat.py b/homeassistant/async_timeout_backcompat.py index 0a59b0d900e..189f64020bb 100644 --- a/homeassistant/async_timeout_backcompat.py +++ b/homeassistant/async_timeout_backcompat.py @@ -2,19 +2,11 @@ from __future__ import annotations import asyncio -import contextlib -import logging from typing import Any import async_timeout -from homeassistant.helpers.frame import ( - MissingIntegrationFrame, - get_integration_frame, - report_integration, -) - -_LOGGER = logging.getLogger(__name__) +from homeassistant.helpers.frame import report def timeout( @@ -24,8 +16,9 @@ def timeout( if loop is None: loop = asyncio.get_running_loop() else: - _report( - "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2" + report( + "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2", + error_if_core=False, ) if delay is not None: deadline: float | None = loop.time() + delay @@ -36,8 +29,9 @@ def timeout( def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None: """Backwards compatible current_task.""" - _report( - "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead" + report( + "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead", + error_if_core=False, ) return asyncio.current_task() @@ -46,22 +40,3 @@ def enable() -> None: """Enable backwards compat transitions.""" async_timeout.timeout = timeout async_timeout.current_task = current_task # type: ignore[attr-defined] - - -def _report(what: str) -> None: - """Report incorrect usage. - - Async friendly. - """ - integration_frame = None - - with contextlib.suppress(MissingIntegrationFrame): - integration_frame = get_integration_frame() - - if not integration_frame: - _LOGGER.warning( - "Detected code that %s; Please report this issue", what, stack_info=True - ) - return - - report_integration(what, integration_frame) diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index f7689ab63a4..ab0a0eaf9a7 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -1,28 +1,22 @@ """Zeroconf usage utility to warn about multiple instances.""" -from contextlib import suppress -import logging from typing import Any import zeroconf -from homeassistant.helpers.frame import ( - MissingIntegrationFrame, - get_integration_frame, - report_integration, -) +from homeassistant.helpers.frame import report from .models import HaZeroconf -_LOGGER = logging.getLogger(__name__) - def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: """Wrap the Zeroconf class to return the shared instance if multiple instances are detected.""" def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: - _report( + report( "attempted to create another Zeroconf instance. Please use the shared Zeroconf via await homeassistant.components.zeroconf.async_get_instance(hass)", + exclude_integrations={"zeroconf"}, + error_if_core=False, ) return hass_zc @@ -31,22 +25,3 @@ def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore - - -def _report(what: str) -> None: - """Report incorrect usage. - - Async friendly. - """ - integration_frame = None - - with suppress(MissingIntegrationFrame): - integration_frame = get_integration_frame(exclude_integrations={"zeroconf"}) - - if not integration_frame: - _LOGGER.warning( - "Detected code that %s; Please report this issue", what, stack_info=True - ) - return - - report_integration(what, integration_frame) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 7d29d78dc54..8105c4f6c0e 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -50,18 +50,23 @@ class MissingIntegrationFrame(HomeAssistantError): """Raised when no integration is found in the frame.""" -def report(what: str) -> None: +def report( + what: str, exclude_integrations: set | None = None, error_if_core: bool = True +) -> None: """Report incorrect usage. Async friendly. """ try: - integration_frame = get_integration_frame() + integration_frame = get_integration_frame( + exclude_integrations=exclude_integrations + ) except MissingIntegrationFrame as err: - # Did not source from an integration? Hard error. - raise RuntimeError( - f"Detected code that {what}. Please report this issue." - ) from err + msg = f"Detected code that {what}. Please report this issue." + if error_if_core: + raise RuntimeError(msg) from err + _LOGGER.warning(msg, stack_info=True) + return report_integration(what, integration_frame) From bf79db4226bd6b1c29f17c65f7bc94056bb935ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Nov 2021 08:34:09 -0600 Subject: [PATCH 0660/1452] Add harmony configuration url (#60014) --- homeassistant/components/harmony/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 706e06e881e..aa373d5813a 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -96,6 +96,7 @@ class HarmonyData(HarmonySubscriberMixin): sw_version=self._client.hub_config.info.get( "hubSwVersion", self._client.fw_version ), + configuration_url="https://www.logitech.com/en-us/my-account", ) async def connect(self) -> bool: From 25f491ad16d403c20b79d91f250bc9a96867ed03 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Nov 2021 15:57:47 +0100 Subject: [PATCH 0661/1452] Add WLED reverse effect switch (#59778) --- homeassistant/components/wled/switch.py | 91 +++++++++++++++++++++++-- tests/components/wled/fixtures/rgb.json | 2 +- tests/components/wled/test_light.py | 2 +- tests/components/wled/test_switch.py | 79 ++++++++++++++++++++- 4 files changed, 163 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 376132c18a7..a05a0ddaf08 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,12 +1,13 @@ """Support for WLED switches.""" from __future__ import annotations +from functools import partial from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -31,12 +32,22 @@ async def async_setup_entry( """Set up WLED switch based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - switches = [ - WLEDNightlightSwitch(coordinator), - WLEDSyncSendSwitch(coordinator), - WLEDSyncReceiveSwitch(coordinator), - ] - async_add_entities(switches) + async_add_entities( + [ + WLEDNightlightSwitch(coordinator), + WLEDSyncSendSwitch(coordinator), + WLEDSyncReceiveSwitch(coordinator), + ] + ) + + update_segments = partial( + async_update_segments, + coordinator, + set(), + async_add_entities, + ) + coordinator.async_add_listener(update_segments) + update_segments() class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): @@ -140,3 +151,69 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the WLED sync receive switch.""" await self.coordinator.wled.sync(receive=True) + + +class WLEDReverseSwitch(WLEDEntity, SwitchEntity): + """Defines a WLED reverse effect switch.""" + + _attr_icon = "mdi:swap-horizontal-bold" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _segment: int + + def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: + """Initialize WLED reverse effect switch.""" + super().__init__(coordinator=coordinator) + + # Segment 0 uses a simpler name, which is more natural for when using + # a single segment / using WLED with one big LED strip. + self._attr_name = f"{coordinator.data.info.name} Segment {segment} Reverse" + if segment == 0: + self._attr_name = f"{coordinator.data.info.name} Reverse" + + self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}" + self._segment = segment + + @property + def available(self) -> bool: + """Return True if entity is available.""" + try: + self.coordinator.data.state.segments[self._segment] + except IndexError: + return False + + return super().available + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.coordinator.data.state.segments[self._segment].reverse + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the WLED reverse effect switch.""" + await self.coordinator.wled.segment(segment_id=self._segment, reverse=False) + + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the WLED reverse effect switch.""" + await self.coordinator.wled.segment(segment_id=self._segment, reverse=True) + + +@callback +def async_update_segments( + coordinator: WLEDDataUpdateCoordinator, + current_ids: set[int], + async_add_entities, +) -> None: + """Update segments.""" + segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} + + new_entities = [] + + # Process new segments, add them to Home Assistant + for segment_id in segment_ids - current_ids: + current_ids.add(segment_id) + new_entities.append(WLEDReverseSwitch(coordinator, segment_id)) + + if new_entities: + async_add_entities(new_entities) diff --git a/tests/components/wled/fixtures/rgb.json b/tests/components/wled/fixtures/rgb.json index 20647c0f946..2f0d4d8fd12 100644 --- a/tests/components/wled/fixtures/rgb.json +++ b/tests/components/wled/fixtures/rgb.json @@ -41,7 +41,7 @@ "ix": 64, "pal": 1, "sel": true, - "rev": false, + "rev": true, "cln": -1 } ] diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 2d71126e0be..7826fe5521b 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -76,7 +76,7 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_INTENSITY) == 64 assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" assert state.attributes.get(ATTR_PRESET) is None - assert state.attributes.get(ATTR_REVERSE) is False + assert state.attributes.get(ATTR_REVERSE) is True assert state.attributes.get(ATTR_SPEED) == 16 assert state.state == STATE_ON diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 7ba86960d2b..c47d7012f6e 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,8 +1,9 @@ """Tests for the WLED switch platform.""" +import json from unittest.mock import MagicMock import pytest -from wled import WLEDConnectionError, WLEDError +from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -10,6 +11,7 @@ from homeassistant.components.wled.const import ( ATTR_FADE, ATTR_TARGET_BRIGHTNESS, ATTR_UDP_PORT, + SCAN_INTERVAL, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,8 +25,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture async def test_switch_state( @@ -68,6 +71,16 @@ async def test_switch_state( assert entry.unique_id == "aabbccddeeff_sync_receive" assert entry.entity_category == ENTITY_CATEGORY_CONFIG + state = hass.states.get("switch.wled_rgb_light_reverse") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:swap-horizontal-bold" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_reverse") + assert entry + assert entry.unique_id == "aabbccddeeff_reverse_0" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + async def test_switch_change_state( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock @@ -137,6 +150,26 @@ async def test_switch_change_state( assert mock_wled.sync.call_count == 4 mock_wled.sync.assert_called_with(receive=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=0, reverse=True) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 2 + mock_wled.segment.assert_called_with(segment_id=0, reverse=False) + async def test_switch_error( hass: HomeAssistant, @@ -182,3 +215,45 @@ async def test_switch_connection_error( assert state assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_switch_dynamically_handle_segments( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert not segment1 + + # Test adding a segment dynamically... + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb.json")) + ) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert segment1 + assert segment1.state == STATE_ON + + # Test remove segment again... + mock_wled.update.return_value = return_value + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert segment1 + assert segment1.state == STATE_UNAVAILABLE From 1d63ae8696269bb8ac739ea1aeeae60cef91ca4f Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 20 Nov 2021 16:15:33 +0100 Subject: [PATCH 0662/1452] Code quality improvements for KNX integration (#60024) --- .coveragerc | 4 ---- homeassistant/components/knx/__init__.py | 4 ++-- script/hassfest/coverage.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8a9fdf070c4..9de86199803 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,16 +542,12 @@ omit = homeassistant/components/kiwi/lock.py homeassistant/components/knx/__init__.py homeassistant/components/knx/climate.py - homeassistant/components/knx/const.py homeassistant/components/knx/cover.py homeassistant/components/knx/expose.py homeassistant/components/knx/knx_entity.py homeassistant/components/knx/light.py homeassistant/components/knx/notify.py - homeassistant/components/knx/scene.py homeassistant/components/knx/schema.py - homeassistant/components/knx/switch.py - homeassistant/components/knx/weather.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/const.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index d98dd6663a3..786c1264248 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -323,9 +323,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update a given config entry.""" - return await hass.config_entries.async_reload(entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) class KNXModule: diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 1e91edee90e..e4e8058c69b 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -34,7 +34,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("ifttt", "config_flow.py"), ("ios", "config_flow.py"), ("iqvia", "config_flow.py"), - ("knx", "scene.py"), ("konnected", "config_flow.py"), ("lcn", "scene.py"), ("life360", "config_flow.py"), From 70990ebf814132c8147b52b44d543c3cee249208 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Nov 2021 16:15:47 +0100 Subject: [PATCH 0663/1452] Add WLED Live Override controls (#59783) --- homeassistant/components/wled/const.py | 4 + homeassistant/components/wled/select.py | 38 +++++++- .../components/wled/strings.select.json | 9 ++ .../wled/translations/select.en.json | 9 ++ tests/components/wled/test_select.py | 88 +++++++++++++++++++ 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/wled/strings.select.json create mode 100644 homeassistant/components/wled/translations/select.en.json diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 1d76d6633dd..25c1fea8f9a 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -1,6 +1,7 @@ """Constants for the WLED integration.""" from datetime import timedelta import logging +from typing import Final # Integration domain DOMAIN = "wled" @@ -30,3 +31,6 @@ ATTR_UDP_PORT = "udp_port" # Services SERVICE_EFFECT = "effect" SERVICE_PRESET = "preset" + +# Device classes +DEVICE_CLASS_WLED_LIVE_OVERRIDE: Final = "wled__live_override" diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 47942359b0f..d82f12cffd7 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import partial -from wled import Playlist, Preset +from wled import Live, Playlist, Preset from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -11,7 +11,7 @@ from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DEVICE_CLASS_WLED_LIVE_OVERRIDE, DOMAIN from .coordinator import WLEDDataUpdateCoordinator from .helpers import wled_exception_handler from .models import WLEDEntity @@ -27,7 +27,13 @@ async def async_setup_entry( """Set up WLED select based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([WLEDPlaylistSelect(coordinator), WLEDPresetSelect(coordinator)]) + async_add_entities( + [ + WLEDLiveOverrideSelect(coordinator), + WLEDPlaylistSelect(coordinator), + WLEDPresetSelect(coordinator), + ] + ) update_segments = partial( async_update_segments, @@ -39,6 +45,32 @@ async def async_setup_entry( update_segments() +class WLEDLiveOverrideSelect(WLEDEntity, SelectEntity): + """Defined a WLED Live Override select.""" + + _attr_device_class = DEVICE_CLASS_WLED_LIVE_OVERRIDE + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_icon = "mdi:theater" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED .""" + super().__init__(coordinator=coordinator) + + self._attr_name = f"{coordinator.data.info.name} Live Override" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_live_override" + self._attr_options = [str(live.value) for live in Live] + + @property + def current_option(self) -> str: + """Return the current selected live override.""" + return str(self.coordinator.data.state.lor.value) + + @wled_exception_handler + async def async_select_option(self, option: str) -> None: + """Set WLED state to the selected live override state.""" + await self.coordinator.wled.live(live=Live(int(option))) + + class WLEDPresetSelect(WLEDEntity, SelectEntity): """Defined a WLED Preset select.""" diff --git a/homeassistant/components/wled/strings.select.json b/homeassistant/components/wled/strings.select.json new file mode 100644 index 00000000000..9f678e380b4 --- /dev/null +++ b/homeassistant/components/wled/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "[%key:common::state::off%]", + "1": "[%key:common::state::on%]", + "2": "Until device restarts" + } + } +} diff --git a/homeassistant/components/wled/translations/select.en.json b/homeassistant/components/wled/translations/select.en.json new file mode 100644 index 00000000000..0aafa31c0f5 --- /dev/null +++ b/homeassistant/components/wled/translations/select.en.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Off", + "1": "On", + "2": "Until device restart" + } + } +} \ No newline at end of file diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index 96ea07f52c4..345c0c632fe 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -451,3 +451,91 @@ async def test_playlist_select_connection_error( assert "Error communicating with API" in caplog.text assert mock_wled.playlist.call_count == 1 mock_wled.playlist.assert_called_with(playlist="Playlist 2") + + +async def test_live_override( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the creation and values of the WLED selects.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("select.wled_rgb_light_live_override") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:theater" + assert state.attributes.get(ATTR_OPTIONS) == ["0", "1", "2"] + assert state.state == "0" + + entry = entity_registry.async_get("select.wled_rgb_light_live_override") + assert entry + assert entry.unique_id == "aabbccddeeff_live_override" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", + ATTR_OPTION: "2", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.live.call_count == 1 + mock_wled.live.assert_called_with(live=2) + + +async def test_live_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.live.side_effect = WLEDError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", + ATTR_OPTION: "1", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgb_light_live_override") + assert state + assert state.state == "0" + assert "Invalid response from API" in caplog.text + assert mock_wled.live.call_count == 1 + mock_wled.live.assert_called_with(live=1) + + +async def test_live_select_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.live.side_effect = WLEDConnectionError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", + ATTR_OPTION: "2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgb_light_live_override") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.live.call_count == 1 + mock_wled.live.assert_called_with(live=2) From 6d4b74f8f251afc0cc8f75a96c36c40f69436113 Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Sat, 20 Nov 2021 08:22:10 -0700 Subject: [PATCH 0664/1452] Add haa vendor extensions (#59750) Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- .../components/homekit_controller/button.py | 92 +++++++++++++++++++ .../components/homekit_controller/const.py | 2 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_haa_fan.py | 28 ++++++ .../homekit_controller/test_button.py | 45 +++++++++ 8 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/homekit_controller/button.py create mode 100644 tests/components/homekit_controller/test_button.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3342f77cd8f..4382543675e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py new file mode 100644 index 00000000000..19dc69c50a8 --- /dev/null +++ b/homeassistant/components/homekit_controller/button.py @@ -0,0 +1,92 @@ +""" +Support for Homekit buttons. + +These are mostly used where a HomeKit accessory exposes additional non-standard +characteristics that don't map to a Home Assistant feature. +""" +from __future__ import annotations + +from dataclasses import dataclass + +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import callback + +from . import KNOWN_DEVICES, CharacteristicEntity + + +@dataclass +class HomeKitButtonEntityDescription(ButtonEntityDescription): + """Describes Homekit button.""" + + write_value: int | str | None = None + + +BUTTON_ENTITIES: dict[str, HomeKitButtonEntityDescription] = { + CharacteristicsTypes.Vendor.HAA_SETUP: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.Vendor.HAA_SETUP, + name="Setup", + icon="mdi:cog", + entity_category=ENTITY_CATEGORY_CONFIG, + write_value="#HAA@trcmd", + ), + CharacteristicsTypes.Vendor.HAA_UPDATE: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.Vendor.HAA_UPDATE, + name="Update", + icon="mdi:update", + entity_category=ENTITY_CATEGORY_CONFIG, + write_value="#HAA@trcmd", + ), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit buttons.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_characteristic(char: Characteristic): + if not (description := BUTTON_ENTITIES.get(char.type)): + return False + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([HomeKitButton(conn, info, char, description)], True) + return True + + conn.add_char_factory(async_add_characteristic) + + +class HomeKitButton(CharacteristicEntity, ButtonEntity): + """Representation of a Button control on a homekit accessory.""" + + entity_description = HomeKitButtonEntityDescription + + def __init__( + self, + conn, + info, + char, + description: HomeKitButtonEntityDescription, + ): + """Initialise a HomeKit button control.""" + self.entity_description = description + super().__init__(conn, info, char) + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] + + @property + def name(self) -> str: + """Return the name of the device if any.""" + if name := super().name: + return f"{name} - {self.entity_description.name}" + return f"{self.entity_description.name}" + + async def async_press(self) -> None: + """Press the button.""" + key = self.entity_description.key + val = self.entity_description.write_value + return await self.async_put_characteristics({key: val}) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 3c9372f96db..6eb507c7214 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -51,6 +51,8 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", + CharacteristicsTypes.Vendor.HAA_SETUP: "button", + CharacteristicsTypes.Vendor.HAA_UPDATE: "button", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3a07ae7ec8b..d9645d22a2d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.6.3"], + "requirements": ["aiohomekit==0.6.4"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 0d3c85970b0..ea0da595599 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.3 +aiohomekit==0.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d87c868f2b5..f784501a351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.3 +aiohomekit==0.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 9e04434d830..0339c61168f 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -46,3 +46,31 @@ async def test_haa_fan_setup(hass): state = await helper.poll_and_get_state() assert state.attributes["friendly_name"] == "HAA-C718B3" assert round(state.attributes["percentage_step"], 2) == 33.33 + + # Check that custom HAA Setup button is created + entry = entity_registry.async_get("button.haa_c718b3_setup") + assert entry.unique_id == "homekit-C718B3-1-aid:1-sid:1010-cid:1012" + + helper = Helper( + hass, + "button.haa_c718b3_setup", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3 - Setup" + + # Check that custom HAA Update button is created + entry = entity_registry.async_get("button.haa_c718b3_update") + assert entry.unique_id == "homekit-C718B3-1-aid:1-sid:1010-cid:1011" + + helper = Helper( + hass, + "button.haa_c718b3_update", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3 - Update" diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py new file mode 100644 index 00000000000..020f303ffaa --- /dev/null +++ b/tests/components/homekit_controller/test_button.py @@ -0,0 +1,45 @@ +"""Basic checks for HomeKit button.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from tests.components.homekit_controller.common import Helper, setup_test_component + + +def create_switch_with_setup_button(accessory): + """Define setup button characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + setup = service.add_char(CharacteristicsTypes.Vendor.HAA_SETUP) + + setup.value = "" + setup.format = "string" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + +async def test_press_button(hass): + """Test a switch service that has a button characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_setup_button) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the button. + energy_helper = Helper( + hass, + "button.testdevice_setup", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + setup = outlet[CharacteristicsTypes.Vendor.HAA_SETUP] + + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.testdevice_setup"}, + blocking=True, + ) + assert setup.value == "#HAA@trcmd" From 769661adc37576d5202eda9af3ee288edba9a85b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 20 Nov 2021 16:34:59 +0100 Subject: [PATCH 0665/1452] KNX christmas cleaning #2 - remove old migrations (#60026) * Remove old migrations from KNX integration * Remove cover migration --- homeassistant/components/knx/climate.py | 45 +--------------- homeassistant/components/knx/cover.py | 30 ----------- homeassistant/components/knx/light.py | 72 +------------------------ 3 files changed, 2 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 791e897f4d3..8517eed7ace 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -6,7 +6,6 @@ from typing import Any from xknx import XKNX from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode -from xknx.telegram.address import parse_device_group_address from homeassistant import config_entries from homeassistant.components.climate import ClimateEntity @@ -24,8 +23,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -56,50 +54,9 @@ async def async_setup_entry( SupportedPlatforms.CLIMATE.value ] - _async_migrate_unique_id(hass, config) async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) -@callback -def _async_migrate_unique_id( - hass: HomeAssistant, platform_config: list[ConfigType] -) -> None: - """Change unique_ids used in 2021.4 to include target_temperature GA.""" - entity_registry = er.async_get(hass) - for entity_config in platform_config: - # normalize group address strings - ga_temperature_state was the old uid - ga_temperature_state = parse_device_group_address( - entity_config[ClimateSchema.CONF_TEMPERATURE_ADDRESS][0] - ) - old_uid = str(ga_temperature_state) - - entity_id = entity_registry.async_get_entity_id("climate", DOMAIN, old_uid) - if entity_id is None: - continue - ga_target_temperature_state = parse_device_group_address( - entity_config[ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS][0] - ) - target_temp = entity_config.get(ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS) - ga_target_temperature = ( - parse_device_group_address(target_temp[0]) - if target_temp is not None - else None - ) - setpoint_shift = entity_config.get(ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS) - ga_setpoint_shift = ( - parse_device_group_address(setpoint_shift[0]) - if setpoint_shift is not None - else None - ) - new_uid = ( - f"{ga_temperature_state}_" - f"{ga_target_temperature_state}_" - f"{ga_target_temperature}_" - f"{ga_setpoint_shift}" - ) - entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) - - def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: """Return a KNX Climate device to be used within XKNX.""" climate_mode = XknxClimateMode( diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 62fd3a1ba08..8b00f6232f3 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -7,7 +7,6 @@ from typing import Any from xknx import XKNX from xknx.devices import Cover as XknxCover, Device as XknxDevice -from xknx.telegram.address import parse_device_group_address from homeassistant import config_entries from homeassistant.components.cover import ( @@ -26,7 +25,6 @@ from homeassistant.components.cover import ( ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.typing import ConfigType @@ -47,37 +45,9 @@ async def async_setup_entry( SupportedPlatforms.COVER.value ] - _async_migrate_unique_id(hass, config) async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) -@callback -def _async_migrate_unique_id( - hass: HomeAssistant, platform_config: list[ConfigType] -) -> None: - """Change unique_ids used in 2021.4 to include position_target GA.""" - entity_registry = er.async_get(hass) - for entity_config in platform_config: - # normalize group address strings - ga_updown was the old uid but is optional - updown_addresses = entity_config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS) - if updown_addresses is None: - continue - ga_updown = parse_device_group_address(updown_addresses[0]) - old_uid = str(ga_updown) - - entity_id = entity_registry.async_get_entity_id("cover", DOMAIN, old_uid) - if entity_id is None: - continue - position_target_addresses = entity_config.get(CoverSchema.CONF_POSITION_ADDRESS) - ga_position_target = ( - parse_device_group_address(position_target_addresses[0]) - if position_target_addresses is not None - else None - ) - new_uid = f"{ga_updown}_{ga_position_target}" - entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) - - class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index f9223d69d74..0403b658221 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -5,7 +5,6 @@ from typing import Any, Tuple, cast from xknx import XKNX from xknx.devices.light import Light as XknxLight, XYYColor -from xknx.telegram.address import parse_device_group_address from homeassistant import config_entries from homeassistant.components.light import ( @@ -25,8 +24,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util @@ -53,77 +51,9 @@ async def async_setup_entry( SupportedPlatforms.LIGHT.value ] - _async_migrate_unique_id(hass, config) async_add_entities(KNXLight(xknx, entity_config) for entity_config in config) -@callback -def _async_migrate_unique_id( - hass: HomeAssistant, platform_config: list[ConfigType] -) -> None: - """Change unique_ids used in 2021.4 to exchange individual color switch address for brightness address.""" - entity_registry = er.async_get(hass) - for entity_config in platform_config: - individual_colors_config = entity_config.get(LightSchema.CONF_INDIVIDUAL_COLORS) - if individual_colors_config is None: - continue - try: - ga_red_switch = individual_colors_config[LightSchema.CONF_RED][KNX_ADDRESS][ - 0 - ] - ga_green_switch = individual_colors_config[LightSchema.CONF_GREEN][ - KNX_ADDRESS - ][0] - ga_blue_switch = individual_colors_config[LightSchema.CONF_BLUE][ - KNX_ADDRESS - ][0] - except KeyError: - continue - # normalize group address strings - ga_red_switch = parse_device_group_address(ga_red_switch) - ga_green_switch = parse_device_group_address(ga_green_switch) - ga_blue_switch = parse_device_group_address(ga_blue_switch) - # white config is optional so it has to be checked for `None` extra - white_config = individual_colors_config.get(LightSchema.CONF_WHITE) - white_switch = ( - white_config.get(KNX_ADDRESS) if white_config is not None else None - ) - ga_white_switch = ( - parse_device_group_address(white_switch[0]) - if white_switch is not None - else None - ) - - old_uid = ( - f"{ga_red_switch}_" - f"{ga_green_switch}_" - f"{ga_blue_switch}_" - f"{ga_white_switch}" - ) - entity_id = entity_registry.async_get_entity_id("light", DOMAIN, old_uid) - if entity_id is None: - continue - - ga_red_brightness = parse_device_group_address( - individual_colors_config[LightSchema.CONF_RED][ - LightSchema.CONF_BRIGHTNESS_ADDRESS - ][0] - ) - ga_green_brightness = parse_device_group_address( - individual_colors_config[LightSchema.CONF_GREEN][ - LightSchema.CONF_BRIGHTNESS_ADDRESS - ][0] - ) - ga_blue_brightness = parse_device_group_address( - individual_colors_config[LightSchema.CONF_BLUE][ - LightSchema.CONF_BRIGHTNESS_ADDRESS - ][0] - ) - - new_uid = f"{ga_red_brightness}_{ga_green_brightness}_{ga_blue_brightness}" - entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) - - def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" From 2412afbacd6b0d03931ba7f7d4ac4cad71dd27bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Nov 2021 09:45:25 -0600 Subject: [PATCH 0666/1452] Add configuration_url to lutron_caseta (#60015) --- homeassistant/components/lutron_caseta/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 0d6c29047a9..e1a93385d31 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -340,6 +340,7 @@ class LutronCasetaDevice(Entity): name=self.name, suggested_area=self._device["name"].split("_")[0], via_device=(DOMAIN, self._bridge_device["serial"]), + configuration_url="https://device-login.lutron.com", ) @property From df3f3321f2e1a2500b70dc7718d579ee18c7ff4e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Nov 2021 23:39:14 +0100 Subject: [PATCH 0667/1452] Use ZeroconfServiceInfo in hue (#59980) Co-authored-by: epenet --- homeassistant/components/hue/config_flow.py | 5 ++- tests/components/hue/test_config_flow.py | 49 +++++++++++---------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0499031c4f2..fd743213d7f 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -232,7 +232,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host is already configured and delegate to the import step if not. """ bridge = await self._get_bridge( - discovery_info["host"], discovery_info["properties"]["bridgeid"] + discovery_info[zeroconf.ATTR_HOST], + discovery_info[zeroconf.ATTR_PROPERTIES]["bridgeid"], ) await self.async_set_unique_id(bridge.id) @@ -252,7 +253,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): as the unique identifier. Therefore, this method uses discovery without a unique ID. """ - self.bridge = await self._get_bridge(discovery_info[CONF_HOST]) + self.bridge = await self._get_bridge(discovery_info[zeroconf.ATTR_HOST]) await self._async_handle_discovery_without_unique_id() return await self.async_step_link() diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 31468e198da..3079875eff1 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -8,7 +8,7 @@ import pytest import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -515,12 +515,10 @@ async def test_bridge_homekit(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={ - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - "properties": {"id": "aa:bb:cc:dd:ee:ff"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="0.0.0.0", + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ) assert result["type"] == "form" @@ -560,7 +558,10 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + data=zeroconf.ZeroconfServiceInfo( + host="0.0.0.0", + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ) assert result["type"] == "abort" @@ -659,18 +660,18 @@ async def test_bridge_zeroconf(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.217", - "port": 443, - "hostname": "Philips-hue.local.", - "type": "_hue._tcp.local.", - "name": "Philips Hue - ABCABC._hue._tcp.local.", - "properties": { + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.217", + port=443, + hostname="Philips-hue.local.", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ "_raw": {"bridgeid": b"ecb5fafffeabcabc", "modelid": b"BSB002"}, "bridgeid": "ecb5fafffeabcabc", "modelid": "BSB002", }, - }, + ), ) assert result["type"] == "form" @@ -690,18 +691,18 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.217", - "port": 443, - "hostname": "Philips-hue.local.", - "type": "_hue._tcp.local.", - "name": "Philips Hue - ABCABC._hue._tcp.local.", - "properties": { + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.217", + port=443, + hostname="Philips-hue.local.", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ "_raw": {"bridgeid": b"ecb5faabcabc", "modelid": b"BSB002"}, "bridgeid": "ecb5faabcabc", "modelid": "BSB002", }, - }, + ), ) assert result["type"] == "abort" From 902da4daf8fa04b64ebd8f68aa15de1d537050cf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 20 Nov 2021 23:43:31 +0100 Subject: [PATCH 0668/1452] Add integration_entities template helper (#59841) --- homeassistant/helpers/template.py | 37 ++++++++++++++++++++++++++ tests/helpers/test_template.py | 44 +++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7f3937d41c1..4c6888cbf2f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -916,6 +916,38 @@ def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: return [entry.entity_id for entry in entries] +def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: + """ + Get entity ids for entities tied to an integration/domain. + + Provide entry_name as domain to get all entity id's for a integration/domain + or provide a config entry title for filtering between instances of the same integration. + """ + # first try if this is a config entry match + conf_entry = next( + ( + entry.entry_id + for entry in hass.config_entries.async_entries() + if entry.title == entry_name + ), + None, + ) + if conf_entry is not None: + ent_reg = entity_registry.async_get(hass) + entries = entity_registry.async_entries_for_config_entry(ent_reg, conf_entry) + return [entry.entity_id for entry in entries] + + # fallback to just returning all entities for a domain + # pylint: disable=import-outside-toplevel + from homeassistant.helpers.entity import entity_sources + + return [ + entity_id + for entity_id, info in entity_sources(hass).items() + if info["domain"] == entry_name + ] + + def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: """Get a device ID from an entity ID or device name.""" entity_reg = entity_registry.async_get(hass) @@ -1853,6 +1885,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["area_devices"] = hassfunction(area_devices) self.filters["area_devices"] = pass_context(self.globals["area_devices"]) + self.globals["integration_entities"] = hassfunction(integration_entities) + self.filters["integration_entities"] = pass_context( + self.globals["integration_entities"] + ) + if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0e1d96b8dac..f1dc740a068 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,5 +1,6 @@ """Test Home Assistant template helper methods.""" -from datetime import datetime +from datetime import datetime, timedelta +import logging import math import random from unittest.mock import patch @@ -20,7 +21,8 @@ from homeassistant.const import ( VOLUME_LITERS, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import device_registry as dr, template +from homeassistant.helpers import device_registry as dr, entity, template +from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem @@ -1849,6 +1851,44 @@ async def test_device_entities(hass): assert info.rate_limit is None +async def test_integration_entities(hass): + """Test integration_entities function.""" + entity_registry = mock_registry(hass) + + # test entities for given config entry title + config_entry = MockConfigEntry(domain="mock", title="Mock bridge 2") + config_entry.add_to_hass(hass) + entity_entry = entity_registry.async_get_or_create( + "sensor", "mock", "test", config_entry=config_entry + ) + info = render_to_info(hass, "{{ integration_entities('Mock bridge 2') }}") + assert_result_info(info, [entity_entry.entity_id]) + assert info.rate_limit is None + + # test integration entities not in entity registry + mock_entity = entity.Entity() + mock_entity.hass = hass + mock_entity.entity_id = "light.test_entity" + mock_entity.platform = EntityPlatform( + hass=hass, + logger=logging.getLogger(__name__), + domain="light", + platform_name="entryless_integration", + platform=None, + scan_interval=timedelta(seconds=30), + entity_namespace=None, + ) + await mock_entity.async_internal_added_to_hass() + info = render_to_info(hass, "{{ integration_entities('entryless_integration') }}") + assert_result_info(info, ["light.test_entity"]) + assert info.rate_limit is None + + # Test non existing integration/entry title + info = render_to_info(hass, "{{ integration_entities('abc123') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + async def test_device_id(hass): """Test device_id function.""" config_entry = MockConfigEntry(domain="light") From 4820acb897847fdb6d3faf3d2bff949daafc753b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 20 Nov 2021 23:45:51 +0100 Subject: [PATCH 0669/1452] Fix for deviceless entities in Hue integration (#59820) --- homeassistant/components/hue/v2/entity.py | 4 +--- tests/components/hue/test_light_v2.py | 2 ++ tests/components/hue/test_scene.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 1b646857e44..9bb81c16fa5 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -36,9 +36,7 @@ class HueBaseEntity(Entity): self.bridge = bridge self.controller = controller self.resource = resource - self.device = ( - controller.get_device(resource.id) or bridge.api.config.bridge_device - ) + self.device = controller.get_device(resource.id) self.logger = bridge.logger.getChild(resource.type.value) # Entity class attributes diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 0a06a87f7f2..e608ef00e26 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -240,6 +240,8 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert entity_entry assert entity_entry.disabled assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + # entity should not have a device assigned + assert entity_entry.device_id is None # enable the entity updated_entry = ent_reg.async_update_entity( diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 82595d3845d..15684eb7e56 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -1,6 +1,8 @@ """Philips Hue scene platform tests for V2 bridge/api.""" +from homeassistant.helpers import entity_registry as er + from .conftest import setup_platform from .const import FAKE_SCENE @@ -39,6 +41,16 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity.attributes["brightness"] == 100.0 assert test_entity.attributes["is_dynamic"] is False + # scene entities should not have a device assigned + ent_reg = er.async_get(hass) + for entity_id in ( + "scene.test_zone_dynamic_test_scene", + "scene.test_room_regular_test_scene", + ): + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry + assert entity_entry.device_id is None + async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn on service on a scene.""" From 7161a0bf2a5eeb4791e7d81d8cd968fbf1e0e72b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 20 Nov 2021 23:46:11 +0100 Subject: [PATCH 0670/1452] Add guard for already migrated Hue entity (#59930) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/migration.py | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index fb584a19100..ca41478b97c 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -147,7 +147,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N ent.unique_id, new_unique_id, ) - ent_reg.async_update_entity(ent.entity_id, new_unique_id=sensor.id) + try: + ent_reg.async_update_entity( + ent.entity_id, new_unique_id=sensor.id + ) + except ValueError: + # assume edge case where the entity was already migrated in a previous run + # which got aborted somehow and we do not want + # to crash the entire integration init + LOGGER.warning( + "Skip migration of %s because it already exists", + ent.entity_id, + ) break # migrate entities that are not connected to a device (groups) @@ -170,5 +181,14 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N ent.unique_id, new_unique_id, ) - ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) + try: + ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) + except ValueError: + # assume edge case where the entity was already migrated in a previous run + # which got aborted somehow and we do not want + # to crash the entire integration init + LOGGER.warning( + "Skip migration of %s because it already exists", + ent.entity_id, + ) LOGGER.info("Migration of devices and entities to support API schema 2 finished") From e98977fb493f167c932d7a52de84cf79d0adbb26 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 21 Nov 2021 00:13:51 +0000 Subject: [PATCH 0671/1452] [ci skip] Translation update --- .../components/airvisual/translations/ja.json | 3 + .../components/aurora/translations/ja.json | 21 ++++ .../components/awair/translations/ja.json | 16 +++ .../components/axis/translations/ja.json | 12 +- .../azure_devops/translations/ja.json | 15 ++- .../binary_sensor/translations/ja.json | 7 +- .../components/blink/translations/ja.json | 15 ++- .../bmw_connected_drive/translations/ja.json | 9 ++ .../components/braviatv/translations/ja.json | 9 +- .../components/broadlink/translations/ja.json | 24 +++- .../components/brother/translations/ja.json | 7 ++ .../components/brunt/translations/hu.json | 29 +++++ .../components/brunt/translations/tr.json | 29 +++++ .../components/bsblan/translations/ja.json | 2 + .../components/camera/translations/ja.json | 1 + .../components/canary/translations/ja.json | 4 +- .../components/cast/translations/ja.json | 3 + .../cloudflare/translations/ja.json | 1 + .../emulated_roku/translations/ja.json | 1 + .../forked_daapd/translations/ja.json | 3 +- .../components/fritz/translations/ja.json | 9 +- .../components/fritzbox/translations/ja.json | 6 + .../components/gios/translations/ja.json | 3 + .../components/glances/translations/ja.json | 2 + .../components/gogogate2/translations/ja.json | 1 + .../google_travel_time/translations/ja.json | 1 + .../components/harmony/translations/ja.json | 1 + .../homeassistant/translations/ja.json | 1 + .../components/homekit/translations/ja.json | 6 +- .../homekit_controller/translations/ja.json | 7 +- .../homematicip_cloud/translations/ja.json | 2 +- .../components/honeywell/translations/ja.json | 3 +- .../huawei_lte/translations/ja.json | 1 + .../components/hue/translations/tr.json | 12 +- .../translations/ja.json | 5 + .../components/ipma/translations/ja.json | 3 + .../components/knx/translations/ca.json | 63 ++++++++++ .../components/knx/translations/de.json | 63 ++++++++++ .../components/knx/translations/en.json | 116 +++++++++--------- .../components/knx/translations/et.json | 63 ++++++++++ .../components/knx/translations/hu.json | 63 ++++++++++ .../components/knx/translations/ja.json | 32 +++++ .../components/knx/translations/ru.json | 63 ++++++++++ .../components/knx/translations/tr.json | 63 ++++++++++ .../components/konnected/translations/ja.json | 21 ++++ .../components/met/translations/ja.json | 3 + .../components/metoffice/translations/ja.json | 11 ++ .../components/mikrotik/translations/ja.json | 2 + .../components/mill/translations/hu.json | 16 ++- .../components/mill/translations/tr.json | 16 ++- .../modem_callerid/translations/ja.json | 1 + .../components/motioneye/translations/ja.json | 6 +- .../components/nam/translations/hu.json | 19 ++- .../components/nam/translations/ja.json | 2 +- .../components/nam/translations/tr.json | 21 +++- .../components/nest/translations/ja.json | 1 + .../components/nut/translations/ja.json | 3 +- .../components/nws/translations/ja.json | 3 + .../components/nzbget/translations/ja.json | 2 + .../components/onvif/translations/ja.json | 4 +- .../opentherm_gw/translations/ja.json | 3 +- .../openweathermap/translations/ja.json | 1 + .../components/owntracks/translations/ja.json | 2 +- .../p1_monitor/translations/ja.json | 3 +- .../panasonic_viera/translations/ja.json | 11 ++ .../philips_js/translations/ja.json | 8 ++ .../components/picnic/translations/ja.json | 7 +- .../components/plaato/translations/ja.json | 2 +- .../components/plugwise/translations/ja.json | 3 + .../components/powerwall/translations/ja.json | 1 + .../components/prosegur/translations/ja.json | 6 +- .../components/ps4/translations/ja.json | 2 + .../pvpc_hourly_pricing/translations/ja.json | 3 +- .../rainmachine/translations/ja.json | 1 + .../components/ring/translations/ja.json | 3 +- .../components/risco/translations/ja.json | 3 +- .../components/samsungtv/translations/ja.json | 3 +- .../components/sensor/translations/tr.json | 2 + .../components/shelly/translations/ja.json | 1 + .../shopping_list/translations/ja.json | 10 ++ .../components/sia/translations/ja.json | 5 +- .../smartthings/translations/ja.json | 3 + .../components/smhi/translations/ja.json | 3 + .../components/solaredge/translations/ja.json | 1 + .../components/sonarr/translations/ja.json | 1 + .../components/starline/translations/ja.json | 3 +- .../components/subaru/translations/ja.json | 10 ++ .../surepetcare/translations/ja.json | 3 +- .../components/switchbot/translations/ja.json | 1 + .../components/syncthing/translations/ja.json | 10 ++ .../synology_dsm/translations/ja.json | 6 +- .../system_bridge/translations/ja.json | 9 +- .../components/tado/translations/ja.json | 12 ++ .../totalconnect/translations/tr.json | 1 + .../transmission/translations/ja.json | 3 +- .../components/unifi/translations/ja.json | 6 +- .../uptimerobot/translations/ja.json | 3 + .../components/vizio/translations/ja.json | 11 +- .../components/wallbox/translations/ja.json | 3 +- .../components/wallbox/translations/tr.json | 10 +- .../components/watttime/translations/ja.json | 3 +- .../waze_travel_time/translations/ja.json | 3 + .../components/withings/translations/ja.json | 1 + .../wled/translations/select.en.json | 2 +- .../xiaomi_miio/translations/ja.json | 1 + .../components/zha/translations/ja.json | 9 ++ .../components/zwave_js/translations/ja.json | 18 ++- 107 files changed, 1029 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/aurora/translations/ja.json create mode 100644 homeassistant/components/awair/translations/ja.json create mode 100644 homeassistant/components/brunt/translations/hu.json create mode 100644 homeassistant/components/brunt/translations/tr.json create mode 100644 homeassistant/components/knx/translations/ca.json create mode 100644 homeassistant/components/knx/translations/de.json create mode 100644 homeassistant/components/knx/translations/et.json create mode 100644 homeassistant/components/knx/translations/hu.json create mode 100644 homeassistant/components/knx/translations/ja.json create mode 100644 homeassistant/components/knx/translations/ru.json create mode 100644 homeassistant/components/knx/translations/tr.json create mode 100644 homeassistant/components/metoffice/translations/ja.json create mode 100644 homeassistant/components/panasonic_viera/translations/ja.json create mode 100644 homeassistant/components/shopping_list/translations/ja.json create mode 100644 homeassistant/components/tado/translations/ja.json diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index 8db9bff9956..925cf20c769 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -30,6 +30,9 @@ "title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a" }, "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "title": "AirVisual\u3092\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/aurora/translations/ja.json b/homeassistant/components/aurora/translations/ja.json new file mode 100644 index 00000000000..4d822454b31 --- /dev/null +++ b/homeassistant/components/aurora/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u524d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u3057\u304d\u3044\u5024(%)" + } + } + } + }, + "title": "NOAA\u30aa\u30fc\u30ed\u30e9\u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json new file mode 100644 index 00000000000..4e2226fd83d --- /dev/null +++ b/homeassistant/components/awair/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "email": "Email" + } + }, + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index 62c62befcb0..db2e5941f30 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", + "not_axis_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fAxis\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "flow_title": "{name} ({host})", "step": { @@ -15,5 +16,14 @@ "title": "Axis\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u4f7f\u7528\u3059\u308b\u30b9\u30c8\u30ea\u30fc\u30e0\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u9078\u629e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/ja.json b/homeassistant/components/azure_devops/translations/ja.json index 71f05935cb7..dac3aa396ce 100644 --- a/homeassistant/components/azure_devops/translations/ja.json +++ b/homeassistant/components/azure_devops/translations/ja.json @@ -1,16 +1,25 @@ { "config": { + "error": { + "project_error": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, + "flow_title": "{project_url}", "step": { "reauth": { "data": { "personal_access_token": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3(PAT)" - } + }, + "description": "{project_url} \u306e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u73fe\u5728\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u518d\u8a8d\u8a3c" }, "user": { "data": { - "personal_access_token": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3(PAT)" + "organization": "\u7d44\u7e54", + "personal_access_token": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3(PAT)", + "project": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8" }, - "description": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u305f\u3081\u306bAzureDevOps\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u30d1\u30fc\u30bd\u30ca\u30eb\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306f\u3001\u30d7\u30e9\u30a4\u30d9\u30fc\u30c8\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u5834\u5408\u306e\u307f\u5fc5\u8981\u3067\u3059\u3002" + "description": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u305f\u3081\u306bAzureDevOps\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u30d1\u30fc\u30bd\u30ca\u30eb\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306f\u3001\u30d7\u30e9\u30a4\u30d9\u30fc\u30c8\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u5834\u5408\u306e\u307f\u5fc5\u8981\u3067\u3059\u3002", + "title": "Azure DevOps\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u8ffd\u52a0" } } } diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 79b523a8bc5..e78d8b3d669 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -6,7 +6,7 @@ "is_connected": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", "is_gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", "is_hot": "{entity_name} \u71b1\u3044", - "is_light": "{\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u540d} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", + "is_light": "{entity_name} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "is_moist": "{entity_name} \u306f\u6e7f\u3063\u3066\u3044\u307e\u3059", "is_no_update": "{entity_name} \u306f\u6700\u65b0\u3067\u3059", @@ -76,7 +76,7 @@ "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", "unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u306a\u304f\u306a\u308a\u307e\u3057\u305f", - "update": "{\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u540d} \u306f\u3001\u5229\u7528\u53ef\u80fd\u306a\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u53d6\u5f97\u3057\u307e\u3057\u305f\u3002", + "update": "{entity_name} \u306f\u3001\u5229\u7528\u53ef\u80fd\u306a\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u53d6\u5f97\u3057\u307e\u3057\u305f\u3002", "vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u59cb\u3081\u307e\u3057\u305f" } }, @@ -151,7 +151,8 @@ "on": "\u5728\u5b85" }, "problem": { - "off": "OK" + "off": "OK", + "on": "\u554f\u984c" }, "running": { "off": "\u30e9\u30f3\u30cb\u30f3\u30b0\u3067\u306f\u306a\u3044", diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index f030f1c48b4..978befe8202 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -5,13 +5,26 @@ "data": { "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, + "description": "\u30e1\u30fc\u30eb\u3067\u9001\u3089\u308c\u3066\u304d\u305fPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "2\u8981\u7d20\u8a8d\u8a3c" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Blink\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" + }, + "description": "Blink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a", + "title": "Blink \u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 38abb3ce5b6..7fd445b29af 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -8,5 +8,14 @@ } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 116a3129a86..324db726109 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -12,7 +12,11 @@ "title": "Sony Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" }, "user": { - "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Sony Bravia TV" } } }, @@ -21,7 +25,8 @@ "user": { "data": { "ignored_sources": "\u7121\u8996\u3055\u308c\u305f\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8" - } + }, + "title": "Sony Bravia TV\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index 299cb227fe9..b490a2f58ce 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -1,14 +1,34 @@ { "config": { + "abort": { + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, "flow_title": "{name} ({model} at {host})", "step": { "auth": { "title": "\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u8a8d\u8a3c" }, + "finish": { + "data": { + "name": "\u540d\u524d" + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "reset": { + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664" + }, + "unlock": { + "data": { + "unlock": "\u306f\u3044\u3001\u3084\u308a\u307e\u3059\u3002" + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664(\u30aa\u30d7\u30b7\u30e7\u30f3)" + }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" - } + "host": "\u30db\u30b9\u30c8", + "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index 487c2bbf2f6..e468ecc114f 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -7,6 +7,7 @@ "snmp_error": "SNMP\u30b5\u30fc\u30d0\u30fc\u304c\u30aa\u30d5\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { @@ -14,6 +15,12 @@ "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" }, "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "zeroconf_confirm": { + "data": { + "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" + }, + "title": "\u30d6\u30e9\u30b6\u30fc\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u767a\u898b" } } } diff --git a/homeassistant/components/brunt/translations/hu.json b/homeassistant/components/brunt/translations/hu.json new file mode 100644 index 00000000000..3abb5cbf297 --- /dev/null +++ b/homeassistant/components/brunt/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {felhaszn\u00e1l\u00f3n\u00e9v}", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "A Brunt integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/tr.json b/homeassistant/components/brunt/translations/tr.json new file mode 100644 index 00000000000..95875b3de67 --- /dev/null +++ b/homeassistant/components/brunt/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen \u015fifreyi tekrar girin: {username}", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Brunt entegrasyonunuzu kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index 91ec977f598..fcdcef76149 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -1,9 +1,11 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "passkey": "\u30d1\u30b9\u30ad\u30fc\u6587\u5b57\u5217", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" diff --git a/homeassistant/components/camera/translations/ja.json b/homeassistant/components/camera/translations/ja.json index 27569fb66f3..9a893c41643 100644 --- a/homeassistant/components/camera/translations/ja.json +++ b/homeassistant/components/camera/translations/ja.json @@ -2,6 +2,7 @@ "state": { "_": { "idle": "\u30a2\u30a4\u30c9\u30eb", + "recording": "\u30ec\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0", "streaming": "\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0" } }, diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 38abb3ce5b6..8d0400a38ce 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -1,11 +1,13 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Canary\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 2e7141d1f03..7b71ae2b72e 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "config": { + "data": { + "known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8" + }, "description": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8 - Cast\u30c7\u30d0\u30a4\u30b9\u306e\u30db\u30b9\u30c8\u540d\u3001\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u306e\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8\u3002mDNS\u691c\u51fa\u304c\u6a5f\u80fd\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u4f7f\u7528\u3057\u307e\u3059\u3002", "title": "Google Cast\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 99d952cb71f..c6c9388af87 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/emulated_roku/translations/ja.json b/homeassistant/components/emulated_roku/translations/ja.json index 62f80176b2d..4e1b189552f 100644 --- a/homeassistant/components/emulated_roku/translations/ja.json +++ b/homeassistant/components/emulated_roku/translations/ja.json @@ -7,6 +7,7 @@ "advertise_port": "\u30a2\u30c9\u30d0\u30bf\u30a4\u30ba \u30dd\u30fc\u30c8", "host_ip": "\u30db\u30b9\u30c8\u306eIP\u30a2\u30c9\u30ec\u30b9", "listen_port": "\u30ea\u30c3\u30b9\u30f3 \u30dd\u30fc\u30c8", + "name": "\u540d\u524d", "upnp_bind_multicast": "\u30d0\u30a4\u30f3\u30c9 \u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8 (True/False)" }, "title": "\u30b5\u30fc\u30d0\u30fc\u69cb\u6210\u306e\u5b9a\u7fa9" diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 45f27956f02..bd2c92850ed 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -2,7 +2,8 @@ "config": { "error": { "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002" + "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", + "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index f8168b58649..c1f3f102204 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -10,14 +10,16 @@ }, "reauth_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "start_config": { "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002", "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7 - \u5fc5\u9808" @@ -26,7 +28,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002", "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index dd5bbb44a71..3dfaff6d15f 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -12,6 +13,11 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index abc8feb8a32..0616dab260d 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "name": "\u540d\u524d" + }, "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } diff --git a/homeassistant/components/glances/translations/ja.json b/homeassistant/components/glances/translations/ja.json index 6d220a900d1..a773b6499e0 100644 --- a/homeassistant/components/glances/translations/ja.json +++ b/homeassistant/components/glances/translations/ja.json @@ -7,8 +7,10 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "version": "Glances API\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3)" }, "title": "Glances\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/gogogate2/translations/ja.json b/homeassistant/components/gogogate2/translations/ja.json index 8a9cace2eeb..f37d3f9edb5 100644 --- a/homeassistant/components/gogogate2/translations/ja.json +++ b/homeassistant/components/gogogate2/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index 3f854c0d96f..135a2c2bdcc 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d", "origin": "\u30aa\u30ea\u30b8\u30f3" } } diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index 78f02028ea3..aad26b140c4 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "link": { "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index d3849132538..28cc613ca10 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -2,6 +2,7 @@ "system_health": { "info": { "docker": "Docker", + "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30d5\u30a1\u30df\u30ea\u30fc", "user": "\u30e6\u30fc\u30b6\u30fc" } } diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 15e62346320..09cf3797153 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -21,8 +21,12 @@ }, "title": "\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a" }, + "init": { + "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + }, "yaml": { - "description": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u306fYAML\u3092\u4ecb\u3057\u3066\u5236\u5fa1\u3055\u308c\u307e\u3059" + "description": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u306fYAML\u3092\u4ecb\u3057\u3066\u5236\u5fa1\u3055\u308c\u307e\u3059", + "title": "HomeKit\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } } diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 0aa22ee0b55..6681a5c9a0b 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -9,6 +9,7 @@ "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "flow_title": "{name}", "step": { "pair": { "data": { @@ -18,8 +19,10 @@ "user": { "data": { "device": "\u30c7\u30d0\u30a4\u30b9" - } + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" } } - } + }, + "title": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 0f6f12ebf00..42151b3e0cc 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -5,7 +5,7 @@ "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "error": { - "invalid_sgtin_or_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_sgtin_or_pin": "SGTIN\u3001\u307e\u305f\u306fPIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "timeout_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index 87ae2a44188..d4932c4c47e 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "mytotalconnectcomfort.com \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3059\u308b\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Honeywell Total Connect Comfort (US)" diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 4ce32f5266f..4c1d4282c3c 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -10,6 +10,7 @@ "invalid_url": "\u7121\u52b9\u306aURL", "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index 8f555c52c43..a883283048f 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "\u0130lk d\u00fc\u011fme", + "2": "\u0130kinci d\u00fc\u011fme", + "3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", "button_1": "\u0130lk d\u00fc\u011fme", "button_2": "\u0130kinci d\u00fc\u011fme", "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", @@ -47,11 +51,16 @@ "turn_on": "A\u00e7" }, "trigger_type": { + "double_short_release": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", + "initial_press": "Ba\u015flang\u0131\u00e7ta \" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", + "long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", - "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131" + "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", + "repeat": "\" {subtype} \" d\u00fc\u011fmesi bas\u0131l\u0131 tutuldu", + "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Hue gruplar\u0131na izin ver", + "allow_hue_scenes": "Hue sahnelerine izin ver", "allow_unreachable": "Ula\u015f\u0131lamayan ampullerin durumlar\u0131n\u0131 do\u011fru \u015fekilde bildirmesine izin verin" } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json index ae96a3282ce..800082b9e70 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ja.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -4,6 +4,11 @@ "step": { "link": { "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + }, + "user": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } } } } diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json index 3e0f61d33e1..a8c46878fe5 100644 --- a/homeassistant/components/ipma/translations/ja.json +++ b/homeassistant/components/ipma/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "name": "\u540d\u524d" + }, "description": "\u30dd\u30eb\u30c8\u30ac\u30eb\u6d77\u6d0b\u5927\u6c17\u7814\u7a76\u6240(Instituto Portugu\u00eas do Mar e Atmosfera)" } } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json new file mode 100644 index 00000000000..fef4093de94 --- /dev/null +++ b/homeassistant/components/knx/translations/ca.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Amfitri\u00f3", + "individual_address": "Adre\u00e7a individual de la connexi\u00f3", + "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT" + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." + }, + "routing": { + "data": { + "individual_address": "Adre\u00e7a individual de la connexi\u00f3 d'encaminament", + "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a l'encaminament", + "multicast_port": "Port de multidifusi\u00f3 utilitzat per a l'encaminament" + }, + "description": "Configura les opcions d'encaminament." + }, + "tunnel": { + "data": { + "gateway": "Connexi\u00f3 t\u00fanel KNX" + }, + "description": "Selecciona una passarel\u00b7la d'enlla\u00e7 de la llista." + }, + "type": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX" + }, + "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX", + "individual_address": "Adre\u00e7a individual predeterminada", + "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a encaminament i descobriment", + "multicast_port": "Port de multidifusi\u00f3 utilitzat per a encaminament i descobriment", + "rate_limit": "Telegrames de sortida m\u00e0xims per segon", + "state_updater": "Habilita la lectura global d'estats del bus KNX" + } + }, + "tunnel": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json new file mode 100644 index 00000000000..4d26412bdae --- /dev/null +++ b/homeassistant/components/knx/translations/de.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "individual_address": "Individuelle Adresse f\u00fcr die Verbindung", + "port": "Port", + "route_back": "Route Back / NAT-Modus" + }, + "description": "Bitte gib die Verbindungsinformationen deines Tunnelger\u00e4ts ein." + }, + "routing": { + "data": { + "individual_address": "Individuelle Adresse f\u00fcr die Routingverbindung", + "multicast_group": "Die f\u00fcr das Routing verwendete Multicast-Gruppe", + "multicast_port": "Der f\u00fcr das Routing verwendete Multicast-Port" + }, + "description": "Bitte konfiguriere die Routing-Optionen." + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Verbindung" + }, + "description": "Bitte w\u00e4hle ein Gateway aus der Liste aus." + }, + "type": { + "data": { + "connection_type": "KNX-Verbindungstyp" + }, + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX-Verbindungstyp", + "individual_address": "Individuelle Standardadresse", + "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery verwenden", + "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery verwenden", + "rate_limit": "Maximal ausgehende Telegrams pro Sekunde", + "state_updater": "Lesen von Zust\u00e4nden aus dem KNX Bus global freigeben" + } + }, + "tunnel": { + "data": { + "host": "Host", + "port": "Port", + "route_back": "Route Back / NAT-Modus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 231cc790db8..80890538fbc 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -1,63 +1,63 @@ { - "config": { - "step": { - "type": { - "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", - "data": { - "connection_type": "KNX Connection Type" + "config": { + "abort": { + "already_configured": "Service is already configured", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "individual_address": "Individual address for the connection", + "port": "Port", + "route_back": "Route Back / NAT Mode" + }, + "description": "Please enter the connection information of your tunneling device." + }, + "routing": { + "data": { + "individual_address": "Individual address for the routing connection", + "multicast_group": "The multicast group used for routing", + "multicast_port": "The multicast port used for routing" + }, + "description": "Please configure the routing options." + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Connection" + }, + "description": "Please select a gateway from the list." + }, + "type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." + } } - }, - "tunnel": { - "description": "Please select a gateway from the list.", - "data": { - "gateway": "KNX Tunnel Connection" - } - }, - "manual_tunnel": { - "description": "Please enter the connection information of your tunneling device.", - "data": { - "host": "IP Address of KNX gateway", - "port": "Port of KNX gateway", - "individual_address": "Individual address for the connection", - "route_back": "Route Back / NAT Mode" - } - }, - "routing": { - "description": "Please configure the routing options.", - "data": { - "individual_address": "Individual address for the routing connection", - "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" - } - } }, - "abort": { - "already_configured": "Service is already configured", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "cannot_connect": "Failed to connect." - } - }, - "options": { - "step": { - "init": { - "data": { - "connection_type": "KNX Connection Type", - "individual_address": "Default individual address", - "multicast_group": "Multicast group used for routing and discovery", - "multicast_port": "Multicast port used for routing and discovery", - "state_updater": "Globally enable reading states from the KNX Bus", - "rate_limit": "Maximum outgoing telegrams per second" + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX Connection Type", + "individual_address": "Default individual address", + "multicast_group": "Multicast group used for routing and discovery", + "multicast_port": "Multicast port used for routing and discovery", + "rate_limit": "Maximum outgoing telegrams per second", + "state_updater": "Globally enable reading states from the KNX Bus" + } + }, + "tunnel": { + "data": { + "host": "Host", + "port": "Port", + "route_back": "Route Back / NAT Mode" + } + } } - }, - "tunnel": { - "data": { - "port": "Port of KNX gateway", - "host": "IP Address of KNX gateway", - "route_back": "Route Back / NAT Mode" - } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json new file mode 100644 index 00000000000..d64e025f029 --- /dev/null +++ b/homeassistant/components/knx/translations/et.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud.", + "single_instance_allowed": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "individual_address": "\u00dchenduse individuaalne aadress", + "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" + }, + "description": "Sisesta tunneldamisseadme \u00fchenduse teave." + }, + "routing": { + "data": { + "individual_address": "Marsruutimis\u00fchenduse individuaalne aadress", + "multicast_group": "Marsruutimiseks kasutatav multisaater\u00fchm", + "multicast_port": "Marsruutimiseks kasutatav multisaateport" + }, + "description": "Konfigureeri marsruutimissuvandid." + }, + "tunnel": { + "data": { + "gateway": "KNX tunneli \u00fchendus" + }, + "description": "Vali loendist l\u00fc\u00fcs." + }, + "type": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" + }, + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp", + "individual_address": "Vaikimisi individuaalne aadress", + "multicast_group": "Marsruutimiseks ja avastamiseks kasutatav multisaategrupp", + "multicast_port": "Marsruutimiseks ja avastamiseks kasutatav multisaateport", + "rate_limit": "Maksimaalne v\u00e4ljaminevate teavituste arv sekundis", + "state_updater": "Luba globaalselt seisundi lugemine KNX-siinilt" + } + }, + "tunnel": { + "data": { + "host": "Host", + "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json new file mode 100644 index 00000000000..9d2e4d5f858 --- /dev/null +++ b/homeassistant/components/knx/translations/hu.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "C\u00edm", + "individual_address": "A kapcsolat egy\u00e9ni c\u00edme", + "port": "Port", + "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" + }, + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + }, + "routing": { + "data": { + "individual_address": "Az \u00fatv\u00e1laszt\u00e1si (routing) kapcsolat egy\u00e9ni c\u00edme", + "multicast_group": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast csoport", + "multicast_port": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast portsz\u00e1m" + }, + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." + }, + "tunnel": { + "data": { + "gateway": "KNX alag\u00fat (tunnel) kapcsolat" + }, + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." + }, + "type": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" + }, + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa", + "individual_address": "Alap\u00e9rtelmezett egy\u00e9ni c\u00edm", + "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast csoport", + "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast portsz\u00e1m\n", + "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet darabsz\u00e1m m\u00e1sodpercenk\u00e9nt", + "state_updater": "Glob\u00e1lisan enged\u00e9lyezi az \u00e1llapotok olvas\u00e1s\u00e1t a KNX buszr\u00f3l." + } + }, + "tunnel": { + "data": { + "host": "C\u00edm", + "port": "Port", + "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json new file mode 100644 index 00000000000..db2f15d53fc --- /dev/null +++ b/homeassistant/components/knx/translations/ja.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "\u30db\u30b9\u30c8" + }, + "step": { + "manual_tunnel": { + "data": { + "port": "\u30dd\u30fc\u30c8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7", + "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", + "state_updater": "KNX\u30d0\u30b9\u304b\u3089\u306e\u8aad\u307f\u53d6\u308a\u72b6\u614b\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u306b\u3059\u308b" + } + }, + "tunnel": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8", + "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json new file mode 100644 index 00000000000..93c15c33415 --- /dev/null +++ b/homeassistant/components/knx/translations/ru.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "manual_tunnel": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "port": "\u041f\u043e\u0440\u0442", + "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." + }, + "routing": { + "data": { + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f", + "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438", + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." + }, + "tunnel": { + "data": { + "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." + }, + "type": { + "data": { + "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX", + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", + "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443", + "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u0447\u0438\u0442\u044b\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441 \u0448\u0438\u043d\u044b KNX" + } + }, + "tunnel": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json new file mode 100644 index 00000000000..18efaa74586 --- /dev/null +++ b/homeassistant/components/knx/translations/tr.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Ana bilgisayar", + "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", + "port": "Port", + "route_back": "Geri Y\u00f6nlendirme / NAT Modu" + }, + "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin." + }, + "routing": { + "data": { + "individual_address": "Y\u00f6nlendirme ba\u011flant\u0131s\u0131 i\u00e7in bireysel adres", + "multicast_group": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", + "multicast_port": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131" + }, + "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n." + }, + "tunnel": { + "data": { + "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" + }, + "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in." + }, + "type": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" + }, + "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc", + "individual_address": "Varsay\u0131lan bireysel adres", + "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", + "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", + "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131", + "state_updater": "KNX Veri Yolu'ndan okuma durumlar\u0131n\u0131 genel olarak etkinle\u015ftirin" + } + }, + "tunnel": { + "data": { + "host": "Ana bilgisayar", + "port": "Port", + "route_back": "Geri Y\u00f6nlendirme / NAT Modu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 3fb8d834035..175136568f6 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -18,7 +18,22 @@ } }, "options": { + "error": { + "bad_host": "\u7121\u52b9\u306a\u4e0a\u66f8\u304d(Override)API\u30db\u30b9\u30c8URL" + }, "step": { + "options_binary": { + "data": { + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + }, + "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "options_digital": { + "data": { + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + }, + "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3" + }, "options_io": { "data": { "1": "\u30be\u30fc\u30f31", @@ -46,6 +61,12 @@ "data": { "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" } + }, + "options_switch": { + "data": { + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + }, + "description": "{zone}\u30aa\u30d7\u30b7\u30e7\u30f3 : \u72b6\u614b{state}" } } } diff --git a/homeassistant/components/met/translations/ja.json b/homeassistant/components/met/translations/ja.json index 686d04d1c11..9d1865e0115 100644 --- a/homeassistant/components/met/translations/ja.json +++ b/homeassistant/components/met/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "name": "\u540d\u524d" + }, "description": "Meteorologisk institutt" } } diff --git a/homeassistant/components/metoffice/translations/ja.json b/homeassistant/components/metoffice/translations/ja.json new file mode 100644 index 00000000000..c2ff6bbb145 --- /dev/null +++ b/homeassistant/components/metoffice/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index 201bda4fa96..6f07407a010 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" @@ -16,6 +17,7 @@ "step": { "device_tracker": { "data": { + "arp_ping": "ARP ping\u3092\u6709\u52b9\u306b\u3059\u308b", "force_dhcp": "DHCP\u3092\u4f7f\u7528\u3057\u305f\u5f37\u5236\u30b9\u30ad\u30e3\u30f3" } } diff --git a/homeassistant/components/mill/translations/hu.json b/homeassistant/components/mill/translations/hu.json index 74a6f9abbac..8a77a822fc8 100644 --- a/homeassistant/components/mill/translations/hu.json +++ b/homeassistant/components/mill/translations/hu.json @@ -7,11 +7,25 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { - "user": { + "cloud": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } + }, + "local": { + "data": { + "ip_address": "IP c\u00edm" + }, + "description": "Az eszk\u00f6z helyi IP-c\u00edme." + }, + "user": { + "data": { + "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t. A helyi kapcsolat 3. gener\u00e1ci\u00f3s f\u0171t\u0151berendez\u00e9seket ig\u00e9nyel" } } } diff --git a/homeassistant/components/mill/translations/tr.json b/homeassistant/components/mill/translations/tr.json index 0f14728873a..63ddc4ee7b1 100644 --- a/homeassistant/components/mill/translations/tr.json +++ b/homeassistant/components/mill/translations/tr.json @@ -7,11 +7,25 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { - "user": { + "cloud": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } + }, + "local": { + "data": { + "ip_address": "IP Adresi" + }, + "description": "Cihaz\u0131n yerel IP adresi." + }, + "user": { + "data": { + "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in. Yerel, 3. nesil \u0131s\u0131t\u0131c\u0131lar gerektirir" } } } diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index 6b766ef76db..8da72b9d45d 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -10,6 +10,7 @@ }, "user": { "data": { + "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 08096987df4..89f065c295e 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_url": "\u7121\u52b9\u306aURL" + }, "step": { "hassio_confirm": { "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306emotionEye" @@ -7,7 +10,8 @@ "user": { "data": { "admin_password": "\u7ba1\u7406\u8005(Admin)\u30d1\u30b9\u30ef\u30fc\u30c9", - "surveillance_password": "\u76e3\u8996(Surveillance)\u30d1\u30b9\u30ef\u30fc\u30c9" + "surveillance_password": "\u76e3\u8996(Surveillance)\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL" } } } diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index 0698b4d3e26..f0cf505163c 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -2,10 +2,13 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "device_unsupported": "Az eszk\u00f6z nem t\u00e1mogatott." + "device_unsupported": "Az eszk\u00f6z nem t\u00e1mogatott.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, k\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba" }, "flow_title": "{name}", @@ -13,6 +16,20 @@ "confirm_discovery": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Nettigo Air Monitor-ot a {host} c\u00edmen?" }, + "credentials": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t." + }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rem, adja meg a helyes felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t ide: {host}" + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index d92b9320051..8cd3da484e0 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -4,7 +4,7 @@ "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "{host} \u306bNettigo Air Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index 1c5cadbdf0b..e252cff4fcd 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "device_unsupported": "Cihaz desteklenmiyor." + "device_unsupported": "Cihaz desteklenmiyor.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "reauth_unsuccessful": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "{host} Nettigo Air Monitor'\u00fc kurmak istiyor musunuz?" }, + "credentials": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen kullan\u0131c\u0131 ad\u0131 ve \u015fifreyi giriniz." + }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen sunucu i\u00e7in do\u011fru kullan\u0131c\u0131 ad\u0131n\u0131 ve \u015fifreyi girin: {host}" + }, "user": { "data": { "host": "Ana bilgisayar" diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 50db0a874e8..cb0ecea34d1 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -2,6 +2,7 @@ "config": { "error": { "internal_error": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u4e2d\u306b\u5185\u90e8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", + "invalid_pin": "\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9", "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059" }, "step": { diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 5da4617a283..4b855e7ae53 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -16,7 +16,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json index 29be7bcc491..71e4d4d08f8 100644 --- a/homeassistant/components/nws/translations/ja.json +++ b/homeassistant/components/nws/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "title": "\u30a2\u30e1\u30ea\u30ab\u56fd\u7acb\u6c17\u8c61\u5c40\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index 362e7d9549c..0a2c394592d 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -1,9 +1,11 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 5d0b820c74a..e8de619eb6f 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -10,8 +10,10 @@ "configure": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index d0ff21c4db7..362eaac4cd0 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -7,7 +7,8 @@ "init": { "data": { "device": "\u30d1\u30b9\u307e\u305f\u306fURL", - "id": "ID" + "id": "ID", + "name": "\u540d\u524d" }, "title": "OpenTherm\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" } diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index d13b04e97d6..3b5880a862e 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "language": "\u8a00\u8a9e", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index d3a0316114f..1937781d0ae 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\niOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `'1A'`\n - Device ID: `'1B'`\n\niOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `'1A'`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index ffda5204c1c..30ed51abe5a 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" }, "description": "P1 Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/panasonic_viera/translations/ja.json b/homeassistant/components/panasonic_viera/translations/ja.json new file mode 100644 index 00000000000..c72d0c34cc2 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pairing": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index 375375a4787..761f9072c66 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "invalid_pin": "\u7121\u52b9\u306aPIN" + }, "step": { + "pair": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index 896966aee6c..dbfec34c4ed 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -3,9 +3,12 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } - } + }, + "title": "\u30d4\u30af\u30cb\u30c3\u30af" } \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 444c0b517e2..fef09a888b9 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Plaato Airlock\u3067Webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({doevice_type} \u306e\u540d\u524d\u304c **{device_name}** \u3067 was_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u3067\u3046\u307e\u304f\u3044\u304f\u30cf\u30ba\u3067\u3059\uff01\n\u539f\u6587: See [the documentation]({doevice_type} with name **{device_name}** was_url}) for successfurtherlly dsetails.up!" + "default": "Plaato {device_type} \u540d\u524d **{device_name}** \u304c\u6b63\u5e38\u306b\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "invalid_webhook_device": "Webhook\u3078\u306e\u30c7\u30fc\u30bf\u9001\u4fe1\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002Airlock\u3067\u306e\u307f\u5229\u7528\u53ef\u80fd\u3067\u3059", diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 410320d4d52..d25e01dc68e 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -2,6 +2,9 @@ "config": { "flow_title": "{name}", "step": { + "user": { + "description": "\u30d7\u30ed\u30c0\u30af\u30c8:" + }, "user_gateway": { "data": { "password": "Smile ID", diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index c0bde46f9ba..e0f13949a5d 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -4,6 +4,7 @@ "step": { "user": { "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/prosegur/translations/ja.json b/homeassistant/components/prosegur/translations/ja.json index 4fb86c92cf5..faa9fb6ea46 100644 --- a/homeassistant/components/prosegur/translations/ja.json +++ b/homeassistant/components/prosegur/translations/ja.json @@ -4,13 +4,15 @@ "reauth_confirm": { "data": { "description": "Prosegur\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { "data": { "country": "\u56fd", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 3bd61b1634b..c2bcc8971ec 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -16,6 +16,8 @@ }, "link": { "data": { + "code": "PIN\u30b3\u30fc\u30c9", + "name": "\u540d\u524d", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, "title": "Play Station 4" diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index db9a55235e7..6ef715d02c8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "power": "\u5951\u7d04\u96fb\u529b (kW)" - } + }, + "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/rainmachine/translations/ja.json b/homeassistant/components/rainmachine/translations/ja.json index 4dc1974d819..e5c92cd4a8f 100644 --- a/homeassistant/components/rainmachine/translations/ja.json +++ b/homeassistant/components/rainmachine/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/ring/translations/ja.json b/homeassistant/components/ring/translations/ja.json index c9bb1f3a111..f030f1c48b4 100644 --- a/homeassistant/components/ring/translations/ja.json +++ b/homeassistant/components/ring/translations/ja.json @@ -9,7 +9,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index 896966aee6c..03d5baf2071 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "pin": "PIN\u30b3\u30fc\u30c9" } } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 985c590d831..ff53e1b3cd1 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -12,7 +12,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" } } } diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 1b500a1f478..db5d774e453 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", "is_energy": "Mevcut {entity_name} enerjisi", + "is_frequency": "Ge\u00e7erli {entity_name} frekans\u0131", "is_gas": "Mevcut {entity_name} gaz\u0131", "is_humidity": "Ge\u00e7erli {entity_name} nem oran\u0131", "is_illuminance": "Mevcut {entity_name} ayd\u0131nlatma d\u00fczeyi", @@ -32,6 +33,7 @@ "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", + "frequency": "{entity_name} frekans de\u011fi\u015fiklikleri", "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 859cdc96cb9..3d020eb2865 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "confirm_discovery": { "description": "{model} {host} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f \n\n\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u4fdd\u8b77\u3055\u308c\u3066\u3044\u308b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u7d9a\u3051\u308b\u524d\u306b\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u4fdd\u8b77\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3057\u305f\u3068\u304d\u306b\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30dc\u30bf\u30f3\u3092\u4f7f\u7528\u3057\u3066\u624b\u52d5\u3067\u30c7\u30d0\u30a4\u30b9\u3092\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3055\u305b\u308b\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u6b21\u306e\u30c7\u30fc\u30bf\u66f4\u65b0\u3092\u5f85\u3061\u307e\u3059\u3002" diff --git a/homeassistant/components/shopping_list/translations/ja.json b/homeassistant/components/shopping_list/translations/ja.json new file mode 100644 index 00000000000..56b6469f63a --- /dev/null +++ b/homeassistant/components/shopping_list/translations/ja.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "\u30b7\u30e7\u30c3\u30d4\u30f3\u30b0\u30ea\u30b9\u30c8" + } + } + }, + "title": "\u30b7\u30e7\u30c3\u30d4\u30f3\u30b0\u30ea\u30b9\u30c8" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index 57e6384d276..a9988181049 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_ping": "ping\u9593\u9694\u306f1\u301c1440\u5206\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "step": { "additional_account": { "title": "\u73fe\u5728\u306e\u30dd\u30fc\u30c8\u306b\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" @@ -9,7 +12,7 @@ "account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", "additional_account": "\u8ffd\u52a0\u306e\u30a2\u30ab\u30a6\u30f3\u30c8", "encryption_key": "\u6697\u53f7\u5316\u30ad\u30fc", - "ping_interval": "ping\u9593\u9694(\u5206)", + "ping_interval": "Ping\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u5206)", "port": "\u30dd\u30fc\u30c8", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" } diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 89358ad113f..c1ef9364561 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -16,6 +16,9 @@ }, "select_location": { "title": "\u5834\u6240\u3092\u9078\u629e" + }, + "user": { + "title": "\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL\u306e\u78ba\u8a8d" } } } diff --git a/homeassistant/components/smhi/translations/ja.json b/homeassistant/components/smhi/translations/ja.json index bfddae6169d..ceabfe8d7a6 100644 --- a/homeassistant/components/smhi/translations/ja.json +++ b/homeassistant/components/smhi/translations/ja.json @@ -6,6 +6,9 @@ }, "step": { "user": { + "data": { + "name": "\u540d\u524d" + }, "title": "\u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u3067\u306e\u4f4d\u7f6e" } } diff --git a/homeassistant/components/solaredge/translations/ja.json b/homeassistant/components/solaredge/translations/ja.json index b8dbac2050f..70652c898fe 100644 --- a/homeassistant/components/solaredge/translations/ja.json +++ b/homeassistant/components/solaredge/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "name": "\u3053\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u540d\u524d", "site_id": "SolarEdge\u306e\u30b5\u30a4\u30c8ID" }, diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 88d1f10b275..3e1d0719df2 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json index 79405cd2d4a..8b95713541d 100644 --- a/homeassistant/components/starline/translations/ja.json +++ b/homeassistant/components/starline/translations/ja.json @@ -28,7 +28,8 @@ }, "auth_user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "StarLine\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9", "title": "\u30e6\u30fc\u30b6\u30fc\u306e\u8cc7\u683c\u60c5\u5831" diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index 896966aee6c..cd57c7bfddd 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -1,6 +1,16 @@ { "config": { + "error": { + "bad_pin_format": "PIN\u306f4\u6841\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "incorrect_pin": "PIN\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093" + }, "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "title": "Subaru Starlink\u306e\u8a2d\u5b9a" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/surepetcare/translations/ja.json b/homeassistant/components/surepetcare/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/surepetcare/translations/ja.json +++ b/homeassistant/components/surepetcare/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index e39d2355caa..f74956203c0 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -9,6 +9,7 @@ "user": { "data": { "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9", + "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "Switchbot\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index 8f63633fe04..c9bd081bdbe 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -1,3 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "token": "\u30c8\u30fc\u30af\u30f3" + } + } + } + }, "title": "Syncthing" } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 02b44142786..0609bbeeb52 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -13,7 +13,8 @@ }, "reauth": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u7406\u7531: {details}", "title": "Synology DSM\u518d\u8a8d\u8a3c\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" @@ -28,7 +29,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index 538a97825a2..fce5e777fd2 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -2,11 +2,18 @@ "config": { "flow_title": "{name}", "step": { + "authenticate": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, "user": { "data": { + "api_key": "API\u30ad\u30fc", "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "description": "\u63a5\u7d9a\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/tado/translations/ja.json b/homeassistant/components/tado/translations/ja.json new file mode 100644 index 00000000000..38abb3ce5b6 --- /dev/null +++ b/homeassistant/components/tado/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json index 3b3f8eabe5e..925353f05a4 100644 --- a/homeassistant/components/totalconnect/translations/tr.json +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_locations": "Bu kullan\u0131c\u0131 i\u00e7in uygun konum yok, TotalConnect ayarlar\u0131n\u0131 kontrol edin", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 8648b9e8da2..4e455012a4a 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -6,7 +6,8 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Transmission Client\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index f229b4db85e..a27dbb2c732 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -28,7 +28,11 @@ "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" }, "statistics_sensors": { - "description": "\u7d71\u8a08\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a" + "data": { + "allow_bandwidth_sensors": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u7528\u306e\u5e2f\u57df\u5e45\u4f7f\u7528\u30bb\u30f3\u30b5\u30fc" + }, + "description": "\u7d71\u8a08\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a", + "title": "UniFi\u30aa\u30d7\u30b7\u30e7\u30f33/3" } } } diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 8aeafef4efa..0ad91fd3bd7 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -11,6 +11,9 @@ "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "description": "UptimeRobot\u304b\u3089\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 9867f2354a7..a6ab7c13dd8 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -4,13 +4,20 @@ "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { + "pair_tv": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + } + }, "pairing_complete_import": { "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" - } + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" + }, + "title": "VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9" } } } diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index f2012e0cb33..7e26b2dadfd 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -13,7 +13,8 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7" + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/wallbox/translations/tr.json b/homeassistant/components/wallbox/translations/tr.json index a5efc052b2f..1bee24a696d 100644 --- a/homeassistant/components/wallbox/translations/tr.json +++ b/homeassistant/components/wallbox/translations/tr.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_invalid": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu; Seri Numaras\u0131 orijinalle e\u015fle\u015fmiyor", "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 42d1631efdd..24251e77a48 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -18,7 +18,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index cbca4b1091f..c42311694a0 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "name": "\u540d\u524d" + }, "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30ab\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" } } diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index e2e96e7b592..f35fecd232d 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -3,6 +3,7 @@ "create_entry": { "default": "\u9078\u629e\u3057\u305fWithings\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f\u3002" }, + "flow_title": "{profile}", "step": { "profile": { "data": { diff --git a/homeassistant/components/wled/translations/select.en.json b/homeassistant/components/wled/translations/select.en.json index 0aafa31c0f5..14a685345a6 100644 --- a/homeassistant/components/wled/translations/select.en.json +++ b/homeassistant/components/wled/translations/select.en.json @@ -3,7 +3,7 @@ "wled__live_override": { "0": "Off", "1": "On", - "2": "Until device restart" + "2": "Until device restarts" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 1a4ae3c5bff..3395d2eff44 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -8,6 +8,7 @@ "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" }, + "flow_title": "{name}", "step": { "cloud": { "data": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 528fab416d5..83b7fd64acb 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -24,6 +24,15 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_master_code": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30de\u30b9\u30bf\u30fc\u30b3\u30fc\u30c9", + "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "zha_options": { + "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" + } + }, "device_automation": { "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 2da13d21b64..deacc4c6e4b 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002" }, + "error": { + "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" + }, "flow_title": "{name}", "step": { "configure_addon": { @@ -17,6 +20,11 @@ "hassio_confirm": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, + "manual": { + "data": { + "url": "URL" + } + }, "usb_confirm": { "description": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } @@ -25,7 +33,7 @@ "device_automation": { "action_type": { "clear_lock_usercode": "{entity_name} \u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u30af\u30ea\u30a2", - "ping": "ping\u30c7\u30d0\u30a4\u30b9", + "ping": "Ping\u30c7\u30d0\u30a4\u30b9", "refresh_value": "{entity_name} \u306e\u5024\u3092\u66f4\u65b0", "reset_meter": "{subtype} \u306e\u30e1\u30fc\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8", "set_config_parameter": "\u8a2d\u5b9a\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u3092\u8a2d\u5b9a", @@ -65,6 +73,11 @@ "install_addon": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" }, + "manual": { + "data": { + "url": "URL" + } + }, "on_supervisor": { "data": { "use_addon": "\u30a2\u30c9\u30aa\u30f3\u300cZ-Wave JS Supervisor\u300d\u306e\u4f7f\u7528" @@ -76,5 +89,6 @@ "title": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u304c\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059\u3002" } } - } + }, + "title": "Z-Wave JS" } \ No newline at end of file From 23f37d0127e213976d1d539ef366142a6a306695 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 21 Nov 2021 01:59:45 +0100 Subject: [PATCH 0672/1452] Bump aioshelly to 1.0.5 (#60058) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 389d31ac195..7d4da653d51 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.4"], + "requirements": ["aioshelly==1.0.5"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ea0da595599..2430f459232 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aiorecollect==1.0.8 aioridwell==0.2.0 # homeassistant.components.shelly -aioshelly==1.0.4 +aioshelly==1.0.5 # homeassistant.components.switcher_kis aioswitcher==2.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f784501a351..bb75445a52c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aiorecollect==1.0.8 aioridwell==0.2.0 # homeassistant.components.shelly -aioshelly==1.0.4 +aioshelly==1.0.5 # homeassistant.components.switcher_kis aioswitcher==2.0.6 From e056f9aa0f0a8c09568a1b084a19df5b37beebd8 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 21 Nov 2021 17:31:45 +1100 Subject: [PATCH 0673/1452] Explicitly cast the SOMA API response to an integer (#60071) This resolves the `TypeError: unsupported operand type(s) for -: 'int' and 'str'` error. Fixes #60070. Signed-off-by: Avi Miller --- homeassistant/components/soma/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 1005bf32f20..43ea60d372e 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -87,5 +87,5 @@ class SomaCover(SomaEntity, CoverEntity): ) self.is_available = False return - self.current_position = 100 - response["position"] + self.current_position = 100 - int(response["position"]) self.is_available = True From 85b37a85323755625beaa464adf0664813822c3b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 08:08:45 +0100 Subject: [PATCH 0674/1452] Use ZeroconfServiceInfo in rainmachine (#60055) --- homeassistant/components/rainmachine/config_flow.py | 2 +- tests/components/rainmachine/test_config_flow.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 4dcbbe0423f..fc7c594f677 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -70,7 +70,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery via zeroconf.""" - ip_address = discovery_info["host"] + ip_address = discovery_info[zeroconf.ATTR_HOST] self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) # Handle IP change diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 1a015a5b181..d7925c2a0ae 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from regenmaschine.errors import RainMachineError from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL @@ -171,7 +172,7 @@ async def test_step_homekit_zeroconf_ip_already_exists(hass, source): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={"host": "192.168.1.100"}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -200,7 +201,7 @@ async def test_step_homekit_zeroconf_ip_change(hass, source): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={"host": "192.168.1.2"}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.2"), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -231,7 +232,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist(hass, source result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={"host": "192.168.1.100"}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -275,7 +276,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={"host": "192.168.1.100"}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -288,7 +289,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "192.168.1.100"}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT From 7e0ddd1d8c0b598f50fc66ad5def10eb380353c0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 20 Nov 2021 23:10:09 -0800 Subject: [PATCH 0675/1452] Bump google-nest-sdm to 0.4.0 (#60068) Full changelog: https://github.com/allenporter/python-google-nest-sdm/compare/0.3.9...0.4.0 All changes are in new code and is expected to be a no-op for the current code. This release introduces a new API for fetching events for upcoming features in Home Assistant, namely fetching camera clips for battery cameras. The new API is uniform across old and new cameras. --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 18ced7b2e8f..488aeb9d053 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.9"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2430f459232..1a81f10b22e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.9 +google-nest-sdm==0.4.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb75445a52c..d43dfb63f07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -458,7 +458,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.9 +google-nest-sdm==0.4.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 3cdca4a657e687f00c1b70b63bef390857e4d922 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 08:40:46 +0100 Subject: [PATCH 0676/1452] Use ZeroconfServiceInfo in netatmo tests (#60048) --- tests/components/netatmo/test_config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index fbc1c62c0b1..b38c59055b6 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import zeroconf from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( CONF_NEW_AREA, @@ -39,7 +40,10 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + data=zeroconf.ZeroconfServiceInfo( + host="0.0.0.0", + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" From f606ba3b2394bbda4566a1443026f375a14eceb9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 21 Nov 2021 10:25:18 +0100 Subject: [PATCH 0677/1452] Detail reason for ConfigEntryNotReady (#60062) --- homeassistant/components/shelly/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ca690e33361..92e13aad664 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -166,8 +166,12 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await device.initialize() - except (asyncio.TimeoutError, OSError) as err: - raise ConfigEntryNotReady from err + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady( + str(err) or "Timeout during device setup" + ) from err + except OSError as err: + raise ConfigEntryNotReady(str(err) or "Error during device setup") from err await async_block_device_setup(hass, entry, device) elif sleep_period is None or device_entry is None: @@ -219,8 +223,10 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool device = await RpcDevice.create( aiohttp_client.async_get_clientsession(hass), options ) - except (asyncio.TimeoutError, OSError) as err: - raise ConfigEntryNotReady from err + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady(str(err) or "Timeout during device setup") from err + except OSError as err: + raise ConfigEntryNotReady(str(err) or "Error during device setup") from err device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ RPC From ba93a384a73d712a2254de341815ecb4320523a5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 10:25:34 +0100 Subject: [PATCH 0678/1452] Use ZeroconfServiceInfo in nam (#60044) --- homeassistant/components/nam/config_flow.py | 2 +- tests/components/nam/test_config_flow.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 0e98e4f7ef5..00df0e0bf90 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -126,7 +126,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self.host = discovery_info[CONF_HOST] + self.host = discovery_info[zeroconf.ATTR_HOST] self.context["title_placeholders"] = {"host": self.host} # Do not probe the device if the host is already configured diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 3dbbe416831..5a3e46cedb9 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -6,12 +6,13 @@ from nettigo_air_monitor import ApiError, AuthFailed, CannotGetMac import pytest from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from tests.common import MockConfigEntry -DISCOVERY_INFO = {"host": "10.10.2.3"} +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(host="10.10.2.3") VALID_CONFIG = {"host": "10.10.2.3"} VALID_AUTH = {"username": "fake_username", "password": "fake_password"} From 9197512ed1b26e7ddd5c2f056b393b3f52701e12 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 21 Nov 2021 12:09:55 +0100 Subject: [PATCH 0679/1452] Fix Sensors for HmIP-DLD (#59804) --- homeassistant/components/homematic/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 78e00985c9f..427a4ccb7aa 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -131,6 +131,7 @@ HM_DEVICE_TYPES = { "IPMultiIOPCB", "ValveBoxW", "CO2SensorIP", + "IPLockDLD", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -180,7 +181,6 @@ HM_DEVICE_TYPES = { "IPRainSensor", "IPLanRouter", "IPMultiIOPCB", - "IPLockDLD", "IPWHS2", ], DISCOVER_COVER: [ From 0dece582e41ed5e5b682f5a00fa3656013daf4ac Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 21 Nov 2021 12:11:36 +0100 Subject: [PATCH 0680/1452] Improve coordinator for yale_smart_alarm (#54091) * Commit coordinator adjustments * Review changes --- .../yale_smart_alarm/coordinator.py | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 8b09507e956..3cef1876e3a 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -3,12 +3,17 @@ from __future__ import annotations from datetime import timedelta +import requests from yalesmartalarmclient.client import AuthenticationError, YaleSmartAlarmClient -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + ConfigEntryAuthFailed, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER @@ -40,14 +45,28 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): if device["type"] == "device_type.door_lock": lock_status_str = device["minigw_lock_status"] lock_status = int(str(lock_status_str or 0), 16) + jammed = (lock_status & 48) == 48 closed = (lock_status & 16) == 16 locked = (lock_status & 1) == 1 if not lock_status and "device_status.lock" in state: device["_state"] = "locked" + device["_state2"] = "unknown" locks.append(device) continue if not lock_status and "device_status.unlock" in state: device["_state"] = "unlocked" + device["_state2"] = "unknown" + locks.append(device) + continue + if ( + lock_status + and ( + "device_status.lock" in state or "device_status.unlock" in state + ) + and jammed + ): + device["_state"] = "jammed" + device["_state2"] = "closed" locks.append(device) continue if ( @@ -59,6 +78,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): and locked ): device["_state"] = "locked" + device["_state2"] = "closed" locks.append(device) continue if ( @@ -70,6 +90,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): and not locked ): device["_state"] = "unlocked" + device["_state2"] = "closed" locks.append(device) continue if ( @@ -80,6 +101,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): and not closed ): device["_state"] = "unlocked" + device["_state2"] = "open" locks.append(device) continue device["_state"] = "unavailable" @@ -110,9 +132,16 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): """Fetch data from Yale.""" if self.yale is None: - self.yale = YaleSmartAlarmClient( - self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] - ) + try: + self.yale = YaleSmartAlarmClient( + self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] + ) + except AuthenticationError as error: + raise ConfigEntryAuthFailed from error + except requests.HTTPError as error: + if error.response.status_code == 401: + raise ConfigEntryAuthFailed from error + raise UpdateFailed from error try: arm_status = self.yale.get_armed_status() @@ -121,14 +150,12 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): online = self.yale.get_online() except AuthenticationError as error: - LOGGER.error("Authentication failed. Check credentials %s", error) - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": self.entry.entry_id}, - data=self.entry.data, - ) - ) + raise ConfigEntryAuthFailed from error + except requests.HTTPError as error: + if error.response.status_code == 401: + raise ConfigEntryAuthFailed from error + raise UpdateFailed from error + except requests.RequestException as error: raise UpdateFailed from error return { From 56e93ff0ec33cba8ee801d25721cdc8b6ed6eaf8 Mon Sep 17 00:00:00 2001 From: Dan Klaffenbach Date: Sun, 21 Nov 2021 12:57:31 +0100 Subject: [PATCH 0681/1452] Add support for HEOS groups (#32568) * Add support for grouping HEOS media players * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Handle groups at controller level, refine tests. Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Fix linting issues * Update homeassistant/components/heos/media_player.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/heos/media_player.py Co-authored-by: Martin Hjelmare * Rename variables and improve resolving of entity_ids Co-authored-by: Martin Hjelmare * Don't patch internal methods Use the pytest fixtures which have already been defined for this. * Fix linting issues * Remove unused property * Ignore groups with unknown leader This makes sure that the group_members attribute won't contain a `None` value as a leader entity_id. * Don't call force_update_groups() from tests * Don't pass `None` player ids to HEOS API * Use signal for group manager communication * Use imports for async_dispatcher_send/async_dispatcher_connect * Raise exception when leader/player could not be resolved * Disconnect signal handlers, avoid calling async_update_groups too early * Update homeassistant/components/heos/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- homeassistant/components/heos/__init__.py | 166 +++++++++++++++++- homeassistant/components/heos/const.py | 3 + homeassistant/components/heos/media_player.py | 43 ++++- tests/components/heos/conftest.py | 88 ++++++---- tests/components/heos/test_media_player.py | 104 ++++++++++- 5 files changed, 362 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 610292f0389..2dbab5e9409 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -11,9 +11,13 @@ import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError 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.util import Throttle @@ -23,8 +27,11 @@ from .const import ( COMMAND_RETRY_ATTEMPTS, COMMAND_RETRY_DELAY, DATA_CONTROLLER_MANAGER, + DATA_ENTITY_ID_MAP, + DATA_GROUP_MANAGER, DATA_SOURCE_MANAGER, DOMAIN, + SIGNAL_HEOS_PLAYER_ADDED, SIGNAL_HEOS_UPDATED, ) @@ -117,15 +124,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: source_manager = SourceManager(favorites, inputs) source_manager.connect_update(hass, controller) + group_manager = GroupManager(hass, controller) + hass.data[DOMAIN] = { DATA_CONTROLLER_MANAGER: controller_manager, + DATA_GROUP_MANAGER: group_manager, DATA_SOURCE_MANAGER: source_manager, MEDIA_PLAYER_DOMAIN: players, + # Maps player_id to entity_id. Populated by the individual HeosMediaPlayer entities. + DATA_ENTITY_ID_MAP: {}, } services.register(hass, controller) hass.config_entries.async_setup_platforms(entry, PLATFORMS) + group_manager.connect_update() + entry.async_on_unload(group_manager.disconnect_update) return True @@ -184,7 +198,7 @@ class ControllerManager: if event == heos_const.EVENT_PLAYERS_CHANGED: self.update_ids(data[heos_const.DATA_MAPPED_IDS]) # Update players - self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) + async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED) async def _heos_event(self, event): """Handle connection event.""" @@ -196,7 +210,7 @@ class ControllerManager: except HeosError as ex: _LOGGER.error("Unable to refresh players: %s", ex) # Update players - self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) + async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED) def update_ids(self, mapped_ids: dict[int, int]): """Update the IDs in the device and entity registry.""" @@ -223,6 +237,148 @@ class ControllerManager: _LOGGER.debug("Updated entity %s unique id to %s", entity_id, new_id) +class GroupManager: + """Class that manages HEOS groups.""" + + def __init__(self, hass, controller): + """Init group manager.""" + self._hass = hass + self._group_membership = {} + self._disconnect_player_added = None + self._initialized = False + self.controller = controller + + def _get_entity_id_to_player_id_map(self) -> dict: + """Return a dictionary which maps all HeosMediaPlayer entity_ids to player_ids.""" + return {v: k for k, v in self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP].items()} + + async def async_get_group_membership(self): + """Return a dictionary which contains all group members for each player as entity_ids.""" + group_info_by_entity_id = { + player_entity_id: [] + for player_entity_id in self._get_entity_id_to_player_id_map() + } + + try: + groups = await self.controller.get_groups(refresh=True) + except HeosError as err: + _LOGGER.error("Unable to get HEOS group info: %s", err) + return group_info_by_entity_id + + player_id_to_entity_id_map = self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP] + for group in groups.values(): + leader_entity_id = player_id_to_entity_id_map.get(group.leader.player_id) + member_entity_ids = [ + player_id_to_entity_id_map[member.player_id] + for member in group.members + if member.player_id in player_id_to_entity_id_map + ] + # Make sure the group leader is always the first element + group_info = [leader_entity_id, *member_entity_ids] + if leader_entity_id: + group_info_by_entity_id[leader_entity_id] = group_info + for member_entity_id in member_entity_ids: + group_info_by_entity_id[member_entity_id] = group_info + + return group_info_by_entity_id + + async def async_join_players( + self, leader_entity_id: str, member_entity_ids: list[str] + ) -> None: + """Create a group with `leader_entity_id` as group leader and `member_entity_ids` as member players.""" + entity_id_to_player_id_map = self._get_entity_id_to_player_id_map() + leader_id = entity_id_to_player_id_map.get(leader_entity_id) + if not leader_id: + raise HomeAssistantError( + f"The group leader {leader_entity_id} could not be resolved to a HEOS player." + ) + member_ids = [ + entity_id_to_player_id_map[member] + for member in member_entity_ids + if member in entity_id_to_player_id_map + ] + + try: + await self.controller.create_group(leader_id, member_ids) + except HeosError as err: + _LOGGER.error( + "Failed to group %s with %s: %s", + leader_entity_id, + member_entity_ids, + err, + ) + + async def async_unjoin_player(self, player_entity_id: str): + """Remove `player_entity_id` from any group.""" + player_id = self._get_entity_id_to_player_id_map().get(player_entity_id) + if not player_id: + raise HomeAssistantError( + f"The player {player_entity_id} could not be resolved to a HEOS player." + ) + + try: + await self.controller.create_group(player_id, []) + except HeosError as err: + _LOGGER.error( + "Failed to ungroup %s: %s", + player_entity_id, + err, + ) + + async def async_update_groups(self, event, data=None): + """Update the group membership from the controller.""" + if event in ( + heos_const.EVENT_GROUPS_CHANGED, + heos_const.EVENT_CONNECTED, + SIGNAL_HEOS_PLAYER_ADDED, + ): + groups = await self.async_get_group_membership() + if groups: + self._group_membership = groups + _LOGGER.debug("Groups updated due to change event") + # Let players know to update + async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED) + else: + _LOGGER.debug("Groups empty") + + def connect_update(self): + """Connect listener for when groups change and signal player update.""" + self.controller.dispatcher.connect( + heos_const.SIGNAL_CONTROLLER_EVENT, self.async_update_groups + ) + self.controller.dispatcher.connect( + heos_const.SIGNAL_HEOS_EVENT, self.async_update_groups + ) + + # When adding a new HEOS player we need to update the groups. + async def _async_handle_player_added(): + # Avoid calling async_update_groups when `DATA_ENTITY_ID_MAP` has not been + # fully populated yet. This may only happen during early startup. + if ( + len(self._hass.data[DOMAIN][MEDIA_PLAYER_DOMAIN]) + <= len(self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP]) + and not self._initialized + ): + self._initialized = True + await self.async_update_groups(SIGNAL_HEOS_PLAYER_ADDED) + + self._disconnect_player_added = async_dispatcher_connect( + self._hass, SIGNAL_HEOS_PLAYER_ADDED, _async_handle_player_added + ) + + @callback + def disconnect_update(self): + """Disconnect the listeners.""" + if self._disconnect_player_added: + self._disconnect_player_added() + self._disconnect_player_added = None + + @property + def group_membership(self): + """Provide access to group members for player entities.""" + return self._group_membership + + class SourceManager: """Class that manages sources for players.""" @@ -341,7 +497,7 @@ class SourceManager: self.source_list = self._build_source_list() _LOGGER.debug("Sources updated due to changed event") # Let players know to update - hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) + async_dispatcher_send(hass, SIGNAL_HEOS_UPDATED) controller.dispatcher.connect( heos_const.SIGNAL_CONTROLLER_EVENT, update_sources diff --git a/homeassistant/components/heos/const.py b/homeassistant/components/heos/const.py index 503df40ccd4..636751d150b 100644 --- a/homeassistant/components/heos/const.py +++ b/homeassistant/components/heos/const.py @@ -5,9 +5,12 @@ ATTR_USERNAME = "username" COMMAND_RETRY_ATTEMPTS = 2 COMMAND_RETRY_DELAY = 1 DATA_CONTROLLER_MANAGER = "controller" +DATA_ENTITY_ID_MAP = "entity_id_map" +DATA_GROUP_MANAGER = "group_manager" DATA_SOURCE_MANAGER = "source_manager" DATA_DISCOVERED_HOSTS = "heos_discovered_hosts" DOMAIN = "heos" SERVICE_SIGN_IN = "sign_in" SERVICE_SIGN_OUT = "sign_out" +SIGNAL_HEOS_PLAYER_ADDED = "heos_player_added" SIGNAL_HEOS_UPDATED = "heos_updated" diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index a562d8e3d7a..27d172198e4 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_URL, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -30,10 +31,21 @@ from homeassistant.components.media_player.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.dt import utcnow -from .const import DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED +from .const import ( + DATA_ENTITY_ID_MAP, + DATA_GROUP_MANAGER, + DATA_SOURCE_MANAGER, + DOMAIN as HEOS_DOMAIN, + SIGNAL_HEOS_PLAYER_ADDED, + SIGNAL_HEOS_UPDATED, +) BASE_SUPPORTED_FEATURES = ( SUPPORT_VOLUME_MUTE @@ -43,6 +55,7 @@ BASE_SUPPORTED_FEATURES = ( | SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA + | SUPPORT_GROUPING ) PLAY_STATE_TO_STATE = { @@ -97,6 +110,7 @@ class HeosMediaPlayer(MediaPlayerEntity): self._signals = [] self._supported_features = BASE_SUPPORTED_FEATURES self._source_manager = None + self._group_manager = None async def _player_update(self, player_id, event): """Handle player attribute updated.""" @@ -120,16 +134,24 @@ class HeosMediaPlayer(MediaPlayerEntity): ) # Update state when heos changes self._signals.append( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_HEOS_UPDATED, self._heos_updated - ) + async_dispatcher_connect(self.hass, SIGNAL_HEOS_UPDATED, self._heos_updated) ) + # Register this player's entity_id so it can be resolved by the group manager + self.hass.data[HEOS_DOMAIN][DATA_ENTITY_ID_MAP][ + self._player.player_id + ] = self.entity_id + async_dispatcher_send(self.hass, SIGNAL_HEOS_PLAYER_ADDED) @log_command_error("clear playlist") async def async_clear_playlist(self): """Clear players playlist.""" await self._player.clear_queue() + @log_command_error("join_players") + async def async_join_players(self, group_members: list[str]) -> None: + """Join `group_members` as a player group with the current player.""" + await self._group_manager.async_join_players(self.entity_id, group_members) + @log_command_error("pause") async def async_media_pause(self): """Send pause command.""" @@ -238,9 +260,17 @@ class HeosMediaPlayer(MediaPlayerEntity): current_support = [CONTROL_TO_SUPPORT[control] for control in controls] self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) + if self._group_manager is None: + self._group_manager = self.hass.data[HEOS_DOMAIN][DATA_GROUP_MANAGER] + if self._source_manager is None: self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] + @log_command_error("unjoin_player") + async def async_unjoin_player(self): + """Remove this player from any group.""" + await self._group_manager.async_unjoin_player(self.entity_id) + async def async_will_remove_from_hass(self): """Disconnect the device when removed.""" for signal_remove in self._signals: @@ -274,6 +304,11 @@ class HeosMediaPlayer(MediaPlayerEntity): "media_type": self._player.now_playing_media.type, } + @property + def group_members(self) -> list[str]: + """List of players which are grouped together.""" + return self._group_manager.group_membership.get(self.entity_id, []) + @property def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 2c48b7fe8e1..693e665d1ee 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -4,7 +4,15 @@ from __future__ import annotations from typing import Sequence from unittest.mock import Mock, patch as patch -from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const +from pyheos import ( + Dispatcher, + Heos, + HeosGroup, + HeosPlayer, + HeosSource, + InputSource, + const, +) import pytest from homeassistant.components import ssdp @@ -24,7 +32,7 @@ def config_entry_fixture(): @pytest.fixture(name="controller") def controller_fixture( - players, favorites, input_sources, playlists, change_data, dispatcher + players, favorites, input_sources, playlists, change_data, dispatcher, group ): """Create a mock Heos controller fixture.""" mock_heos = Mock(Heos) @@ -40,6 +48,8 @@ def controller_fixture( mock_heos.is_signed_in = True mock_heos.signed_in_username = "user@user.com" mock_heos.connection_state = const.STATE_CONNECTED + mock_heos.get_groups.return_value = group + mock_heos.create_group.return_value = None mock = Mock(return_value=mock_heos) with patch("homeassistant.components.heos.Heos", new=mock), patch( @@ -56,35 +66,51 @@ def config_fixture(): @pytest.fixture(name="players") def player_fixture(quick_selects): - """Create a mock HeosPlayer.""" - player = Mock(HeosPlayer) - player.player_id = 1 - player.name = "Test Player" - player.model = "Test Model" - player.version = "1.0.0" - player.is_muted = False - player.available = True - player.state = const.PLAY_STATE_STOP - player.ip_address = "127.0.0.1" - player.network = "wired" - player.shuffle = False - player.repeat = const.REPEAT_OFF - player.volume = 25 - player.now_playing_media.supported_controls = const.CONTROLS_ALL - player.now_playing_media.album_id = 1 - player.now_playing_media.queue_id = 1 - player.now_playing_media.source_id = 1 - player.now_playing_media.station = "Station Name" - player.now_playing_media.type = "Station" - player.now_playing_media.album = "Album" - player.now_playing_media.artist = "Artist" - player.now_playing_media.media_id = "1" - player.now_playing_media.duration = None - player.now_playing_media.current_position = None - player.now_playing_media.image_url = "http://" - player.now_playing_media.song = "Song" - player.get_quick_selects.return_value = quick_selects - return {player.player_id: player} + """Create two mock HeosPlayers.""" + players = {} + for i in (1, 2): + player = Mock(HeosPlayer) + player.player_id = i + if i > 1: + player.name = f"Test Player {i}" + else: + player.name = "Test Player" + player.model = "Test Model" + player.version = "1.0.0" + player.is_muted = False + player.available = True + player.state = const.PLAY_STATE_STOP + player.ip_address = f"127.0.0.{i}" + player.network = "wired" + player.shuffle = False + player.repeat = const.REPEAT_OFF + player.volume = 25 + player.now_playing_media.supported_controls = const.CONTROLS_ALL + player.now_playing_media.album_id = 1 + player.now_playing_media.queue_id = 1 + player.now_playing_media.source_id = 1 + player.now_playing_media.station = "Station Name" + player.now_playing_media.type = "Station" + player.now_playing_media.album = "Album" + player.now_playing_media.artist = "Artist" + player.now_playing_media.media_id = "1" + player.now_playing_media.duration = None + player.now_playing_media.current_position = None + player.now_playing_media.image_url = "http://" + player.now_playing_media.song = "Song" + player.get_quick_selects.return_value = quick_selects + players[player.player_id] = player + return players + + +@pytest.fixture(name="group") +def group_fixture(players): + """Create a HEOS group consisting of two players.""" + group = Mock(HeosGroup) + group.leader = players[1] + group.members = [players[2]] + group.group_id = 999 + return {group.group_id: group} @pytest.fixture(name="favorites") diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 8b87acfd9fd..d134840d652 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -2,6 +2,7 @@ import asyncio from pyheos import CommandFailedError, const +from pyheos.error import HeosError from homeassistant.components.heos import media_player from homeassistant.components.heos.const import ( @@ -10,6 +11,7 @@ from homeassistant.components.heos.const import ( SIGNAL_HEOS_UPDATED, ) from homeassistant.components.media_player.const import ( + ATTR_GROUP_MEMBERS, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_NAME, @@ -29,8 +31,10 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_URL, SERVICE_CLEAR_PLAYLIST, + SERVICE_JOIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, + SERVICE_UNJOIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -264,10 +268,10 @@ async def test_updates_from_players_changed_new_ids( await event.wait() # Assert device registry identifiers were updated - assert len(device_registry.devices) == 1 + assert len(device_registry.devices) == 2 assert device_registry.async_get_device({(DOMAIN, 101)}) # Assert entity registry unique id was updated - assert len(entity_registry.entities) == 1 + assert len(entity_registry.entities) == 2 assert ( entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "101") == "media_player.test_player" @@ -805,3 +809,99 @@ async def test_play_media_invalid_type(hass, config_entry, config, controller, c blocking=True, ) assert "Unable to play media: Unsupported media type 'Other'" in caplog.text + + +async def test_media_player_join_group(hass, config_entry, config, controller, caplog): + """Test grouping of media players through the join service.""" + await setup_platform(hass, config_entry, config) + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: "media_player.test_player", + ATTR_GROUP_MEMBERS: ["media_player.test_player_2"], + }, + blocking=True, + ) + controller.create_group.assert_called_once_with( + 1, + [ + 2, + ], + ) + assert "Failed to group media_player.test_player with" not in caplog.text + + controller.create_group.side_effect = HeosError("error") + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: "media_player.test_player", + ATTR_GROUP_MEMBERS: ["media_player.test_player_2"], + }, + blocking=True, + ) + assert "Failed to group media_player.test_player with" in caplog.text + + +async def test_media_player_group_members( + hass, config_entry, config, controller, caplog +): + """Test group_members attribute.""" + await setup_platform(hass, config_entry, config) + await hass.async_block_till_done() + player_entity = hass.states.get("media_player.test_player") + assert player_entity.attributes[ATTR_GROUP_MEMBERS] == [ + "media_player.test_player", + "media_player.test_player_2", + ] + controller.get_groups.assert_called_once() + assert "Unable to get HEOS group info" not in caplog.text + + +async def test_media_player_group_members_error( + hass, config_entry, config, controller, caplog +): + """Test error in HEOS API.""" + controller.get_groups.side_effect = HeosError("error") + await setup_platform(hass, config_entry, config) + await hass.async_block_till_done() + assert "Unable to get HEOS group info" in caplog.text + player_entity = hass.states.get("media_player.test_player") + assert player_entity.attributes[ATTR_GROUP_MEMBERS] == [] + + +async def test_media_player_unjoin_group( + hass, config_entry, config, controller, caplog +): + """Test ungrouping of media players through the join service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + + player.heos.dispatcher.send( + const.SIGNAL_PLAYER_EVENT, + player.player_id, + const.EVENT_PLAYER_STATE_CHANGED, + ) + await hass.async_block_till_done() + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_UNJOIN, + { + ATTR_ENTITY_ID: "media_player.test_player", + }, + blocking=True, + ) + controller.create_group.assert_called_once_with(1, []) + assert "Failed to ungroup media_player.test_player" not in caplog.text + + controller.create_group.side_effect = HeosError("error") + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_UNJOIN, + { + ATTR_ENTITY_ID: "media_player.test_player", + }, + blocking=True, + ) + assert "Failed to ungroup media_player.test_player" in caplog.text From 25e526395488c100a51bca5dc4f2ed639291c934 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:51:50 +0100 Subject: [PATCH 0682/1452] Use ZeroconfServiceInfo in nanoleaf (#60045) --- .../components/nanoleaf/config_flow.py | 8 +++--- tests/components/nanoleaf/test_config_flow.py | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 1d0b31549df..f55a0d86b08 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -107,9 +107,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle Nanoleaf Homekit and Zeroconf discovery.""" return await self._async_discovery_handler( - discovery_info["host"], - discovery_info["name"].replace(f".{discovery_info['type']}", ""), - discovery_info["properties"]["id"], + discovery_info[zeroconf.ATTR_HOST], + discovery_info[zeroconf.ATTR_NAME].replace( + f".{discovery_info[zeroconf.ATTR_TYPE]}", "" + ), + discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID], ) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index ba0eff4abe3..5e781499325 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -7,6 +7,7 @@ from aionanoleaf import InvalidToken, NanoleafException, Unauthorized, Unavailab import pytest from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant @@ -235,12 +236,12 @@ async def test_discovery_link_unavailable( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={ - "host": TEST_HOST, - "name": f"{TEST_NAME}.{type_in_discovery_info}", - "type": type_in_discovery_info, - "properties": {"id": TEST_DEVICE_ID}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name=f"{TEST_NAME}.{type_in_discovery_info}", + type=type_in_discovery_info, + properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + ), ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -417,12 +418,12 @@ async def test_import_discovery_integration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={ - "host": TEST_HOST, - "name": f"{TEST_NAME}.{type_in_discovery}", - "type": type_in_discovery, - "properties": {"id": TEST_DEVICE_ID}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name=f"{TEST_NAME}.{type_in_discovery}", + type=type_in_discovery, + properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + ), ) assert result["type"] == "create_entry" assert result["title"] == TEST_NAME From 435eb97495133e81d5eb939a0897efe11c2c3cdd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:54:39 +0100 Subject: [PATCH 0683/1452] Use DhcpServiceInfo in roomba (#60056) --- .../components/roomba/config_flow.py | 13 +- tests/components/roomba/test_config_flow.py | 112 +++++++++--------- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index f17eb0a07c0..e1919941068 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -8,9 +8,10 @@ from roombapy.getpassword import RoombaPassword import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout from .const import ( @@ -77,15 +78,15 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) + self._async_abort_entries_match({CONF_HOST: discovery_info[dhcp.IP_ADDRESS]}) - if not discovery_info[HOSTNAME].startswith(("irobot-", "roomba-")): + if not discovery_info[dhcp.HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - self.host = discovery_info[IP_ADDRESS] - self.blid = _async_blid_from_hostname(discovery_info[HOSTNAME]) + self.host = discovery_info[dhcp.IP_ADDRESS] + self.blid = _async_blid_from_hostname(discovery_info[dhcp.HOSTNAME]) await self.async_set_unique_id(self.blid) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 54554af6ecb..de66a1ae3b7 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from roombapy import RoombaConnectionError, RoombaInfo from homeassistant import config_entries, data_entry_flow -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.roomba import config_flow from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD @@ -16,30 +16,30 @@ MOCK_IP = "1.2.3.4" VALID_CONFIG = {CONF_HOST: MOCK_IP, CONF_BLID: "BLID", CONF_PASSWORD: "password"} DHCP_DISCOVERY_DEVICES = [ - { - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "50:14:79:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, - { - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "80:A5:89:DD:EE:FF", - HOSTNAME: "roomba-blid", - }, + dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="50:14:79:DD:EE:FF", + hostname="irobot-blid", + ), + dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="80:A5:89:DD:EE:FF", + hostname="roomba-blid", + ), ] DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP = [ - { - IP_ADDRESS: "4.4.4.4", - MAC_ADDRESS: "50:14:79:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, - { - IP_ADDRESS: "5.5.5.5", - MAC_ADDRESS: "80:A5:89:DD:EE:FF", - HOSTNAME: "roomba-blid", - }, + dhcp.DhcpServiceInfo( + ip="4.4.4.4", + macaddress="50:14:79:DD:EE:FF", + hostname="irobot-blid", + ), + dhcp.DhcpServiceInfo( + ip="5.5.5.5", + macaddress="80:A5:89:DD:EE:FF", + hostname="roomba-blid", + ), ] @@ -815,11 +815,11 @@ async def test_dhcp_discovery_with_ignored(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-blid", + ), ) await hass.async_block_till_done() @@ -838,11 +838,11 @@ async def test_dhcp_discovery_already_configured_host(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-blid", + ), ) await hass.async_block_till_done() @@ -864,11 +864,11 @@ async def test_dhcp_discovery_already_configured_blid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-blid", + ), ) await hass.async_block_till_done() @@ -890,11 +890,11 @@ async def test_dhcp_discovery_not_irobot(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "Notirobot-blid", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="Notirobot-blid", + ), ) await hass.async_block_till_done() @@ -911,11 +911,11 @@ async def test_dhcp_discovery_partial_hostname(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-blid", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-blid", + ), ) await hass.async_block_till_done() @@ -928,11 +928,11 @@ async def test_dhcp_discovery_partial_hostname(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-blidthatislonger", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-blidthatislonger", + ), ) await hass.async_block_till_done() @@ -949,11 +949,11 @@ async def test_dhcp_discovery_partial_hostname(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - IP_ADDRESS: MOCK_IP, - MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", - HOSTNAME: "irobot-bl", - }, + data=dhcp.DhcpServiceInfo( + ip=MOCK_IP, + macaddress="AA:BB:CC:DD:EE:FF", + hostname="irobot-bl", + ), ) await hass.async_block_till_done() From 8ec30aa9ad506b75125e85767dc02099f6f953f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:55:54 +0100 Subject: [PATCH 0684/1452] Use ZeroconfServiceInfo in rachio (#60054) --- homeassistant/components/rachio/config_flow.py | 13 ++++++++----- tests/components/rachio/test_config_flow.py | 9 +++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index aa4efd971a6..73712135785 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -7,8 +7,10 @@ from requests.exceptions import ConnectTimeout import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_MANUAL_RUN_MINS, @@ -78,13 +80,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle HomeKit discovery.""" self._async_abort_entries_match() - properties = { - key.lower(): value for (key, value) in discovery_info["properties"].items() - } - await self.async_set_unique_id(properties["id"]) + await self.async_set_unique_id( + discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + ) return await self.async_step_user() @staticmethod diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 182f5e45dd7..8afcaca1886 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, CONF_MANUAL_RUN_MINS, @@ -111,7 +112,9 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + data=zeroconf.ZeroconfServiceInfo( + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + ), ) assert result["type"] == "form" assert result["errors"] == {} @@ -128,7 +131,9 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + data=zeroconf.ZeroconfServiceInfo( + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" From c3e9c1a7e8fdc949b8e638d79ab476507ff92f18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 14:56:22 +0100 Subject: [PATCH 0685/1452] Use DhcpServiceInfo in powerwall (#60051) --- .../components/powerwall/config_flow.py | 8 ++--- .../components/powerwall/test_config_flow.py | 32 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 420212a86ba..b8b2f6fdee0 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -10,8 +10,9 @@ from tesla_powerwall import ( import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -57,11 +58,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the powerwall flow.""" self.ip_address = None - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.ip_address = discovery_info[IP_ADDRESS] + self.ip_address = discovery_info[dhcp.IP_ADDRESS] self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) - self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index afad08f3cd2..29a04a70085 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -9,7 +9,7 @@ from tesla_powerwall import ( ) from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD @@ -144,11 +144,11 @@ async def test_already_configured(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -165,11 +165,11 @@ async def test_already_configured_with_ignored(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == "form" @@ -180,11 +180,11 @@ async def test_dhcp_discovery(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == "form" assert result["errors"] == {} From 36a67d060bfc6303e0ba1286a8db68bc042f9f9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:35:24 +0100 Subject: [PATCH 0686/1452] Use ZeroconfServiceInfo in octoprint (#60049) --- .../components/octoprint/config_flow.py | 15 ++++++----- .../components/octoprint/test_config_flow.py | 27 +++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 04efde16952..b2815b2d10f 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -6,6 +6,7 @@ import voluptuous as vol from yarl import URL from homeassistant import config_entries, data_entry_flow, exceptions +from homeassistant.components import zeroconf from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -138,20 +139,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import.""" return await self.async_step_user(user_input) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> data_entry_flow.FlowResult: """Handle discovery flow.""" - uuid = discovery_info["properties"]["uuid"] + uuid = discovery_info[zeroconf.ATTR_PROPERTIES]["uuid"] await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() self.context["title_placeholders"] = { - CONF_HOST: discovery_info[CONF_HOST], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], } self.discovery_schema = _schema_with_defaults( - host=discovery_info[CONF_HOST], - port=discovery_info[CONF_PORT], - path=discovery_info["properties"][CONF_PATH], + host=discovery_info[zeroconf.ATTR_HOST], + port=discovery_info[zeroconf.ATTR_PORT], + path=discovery_info[zeroconf.ATTR_PROPERTIES][CONF_PATH], ) return await self.async_step_user() diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index f176e5ab288..2c8143fb78c 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pyoctoprintapi import ApiError, DiscoverySettings from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.octoprint.const import DOMAIN from homeassistant.core import HomeAssistant @@ -169,13 +170,12 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "port": 80, - "hostname": "example.local.", - "uuid": "83747482", - "properties": {"uuid": "83747482", "path": "/foo/"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + port=80, + hostname="example.local.", + properties={"uuid": "83747482", "path": "/foo/"}, + ), ) assert result["type"] == "form" assert not result["errors"] @@ -485,13 +485,12 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "port": 80, - "hostname": "example.local.", - "uuid": "83747482", - "properties": {"uuid": "83747482", "path": "/foo/"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + port=80, + hostname="example.local.", + properties={"uuid": "83747482", "path": "/foo/"}, + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" From 95075448bd17d65800231dc84f39cd49672016df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:35:51 +0100 Subject: [PATCH 0687/1452] Use DhcpServiceInfo in nuki (#60046) --- homeassistant/components/nuki/config_flow.py | 9 +++++---- tests/components/nuki/test_config_flow.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 330f38c96bd..c3e0ed93372 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -7,8 +7,9 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN @@ -66,15 +67,15 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" return await self.async_step_validate(user_input) - async def async_step_dhcp(self, discovery_info: dict): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Nuki bridge.""" - await self.async_set_unique_id(int(discovery_info.get(HOSTNAME)[12:], 16)) + await self.async_set_unique_id(int(discovery_info[dhcp.HOSTNAME][12:], 16)) self._abort_if_unique_id_configured() self.discovery_schema = vol.Schema( { - vol.Required(CONF_HOST, default=discovery_info[IP_ADDRESS]): str, + vol.Required(CONF_HOST, default=discovery_info[dhcp.IP_ADDRESS]): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, vol.Required(CONF_TOKEN): str, } diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 8e3636dbdef..fd7bfa2137b 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -5,7 +5,7 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.nuki.const import DOMAIN from homeassistant.const import CONF_TOKEN @@ -178,7 +178,7 @@ async def test_dhcp_flow(hass): """Test that DHCP discovery for new bridge works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + data=dhcp.DhcpServiceInfo(hostname=NAME, ip=HOST, macaddress=MAC), context={"source": config_entries.SOURCE_DHCP}, ) @@ -221,7 +221,7 @@ async def test_dhcp_flow_already_configured(hass): await setup_nuki_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + data=dhcp.DhcpServiceInfo(hostname=NAME, ip=HOST, macaddress=MAC), context={"source": config_entries.SOURCE_DHCP}, ) From 2270e920c38a5fe92782bccc772ef60a61298e8c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:37:09 +0100 Subject: [PATCH 0688/1452] Use ZeroconfServiceInfo in modern_forms (#60043) --- .../components/modern_forms/config_flow.py | 6 ++--- .../modern_forms/test_config_flow.py | 23 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index 147976ee2b0..bd52ae6f5a2 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -30,14 +30,14 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - host = discovery_info["hostname"].rstrip(".") + host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") name, _ = host.rsplit(".") self.context.update( { - CONF_HOST: discovery_info["host"], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], CONF_NAME: name, - CONF_MAC: discovery_info["properties"].get(CONF_MAC), + CONF_MAC: discovery_info[zeroconf.ATTR_PROPERTIES].get(CONF_MAC), "title_placeholders": {"name": name}, } ) diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 967e9d354d5..39db815064b 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch import aiohttp from aiomodernforms import ModernFormsConnectionError +from homeassistant.components import zeroconf from homeassistant.components.modern_forms.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON @@ -68,7 +69,9 @@ async def test_full_zeroconf_flow_implementation( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.local.", properties={} + ), ) flows = hass.config_entries.flow.async_progress() @@ -130,7 +133,9 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.local.", properties={} + ), ) assert result.get("type") == RESULT_TYPE_ABORT @@ -154,7 +159,9 @@ async def test_zeroconf_confirm_connection_error( CONF_HOST: "example.com", CONF_NAME: "test", }, - data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.com.", properties={} + ), ) assert result.get("type") == RESULT_TYPE_ABORT @@ -216,11 +223,11 @@ async def test_zeroconf_with_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "hostname": "example.local.", - "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + hostname="example.local.", + properties={CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + ), ) assert result.get("type") == RESULT_TYPE_ABORT From 7560f116805dd34f68d60a3c7cfed95edbb9e913 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 18:27:35 +0100 Subject: [PATCH 0689/1452] Use ZeroconfServiceInfo in roku (#60053) --- homeassistant/components/roku/config_flow.py | 11 +++++++---- tests/components/roku/__init__.py | 15 ++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index b9e93b4f008..63b1fb788a4 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from rokuecp import Roku, RokuError import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, @@ -84,14 +85,16 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle a flow initialized by homekit discovery.""" # If we already have the host configured do # not open connections to it if we can avoid it. - self._async_abort_entries_match({CONF_HOST: discovery_info[CONF_HOST]}) + self._async_abort_entries_match({CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}) - self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]}) + self.discovery_info.update({CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}) try: info = await validate_input(self.hass, self.discovery_info) @@ -104,7 +107,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["serial_number"]) self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[CONF_HOST]}, + updates={CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}, ) self.context.update({"title_placeholders": {"name": info["title"]}}) diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index 0f508f3efc9..d97d30e66ce 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -3,13 +3,14 @@ from http import HTTPStatus import re from socket import gaierror as SocketGIAError +from homeassistant.components import zeroconf from homeassistant.components.roku.const import DOMAIN from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, ) -from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -31,13 +32,13 @@ MOCK_SSDP_DISCOVERY_INFO = { HOMEKIT_HOST = "192.168.1.161" -MOCK_HOMEKIT_DISCOVERY_INFO = { - CONF_NAME: "onn._hap._tcp.local.", - CONF_HOST: HOMEKIT_HOST, - "properties": { - CONF_ID: "2d:97:da:ee:dc:99", +MOCK_HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + name="onn._hap._tcp.local.", + host=HOMEKIT_HOST, + properties={ + zeroconf.ATTR_PROPERTIES_ID: "2d:97:da:ee:dc:99", }, -} +) def mock_connection( From 2675c6d408db0d9a45e604d683e1c2a7f538d623 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:39:37 +0100 Subject: [PATCH 0690/1452] Use DhcpServiceInfo in axis (#60092) Co-authored-by: epenet --- tests/components/axis/test_config_flow.py | 49 ++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 2211936f44b..744868e7911 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -5,7 +5,7 @@ import pytest import respx from homeassistant import data_entry_flow -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( CONF_EVENTS, @@ -16,7 +16,6 @@ from homeassistant.components.axis.const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import ( SOURCE_DHCP, SOURCE_IGNORE, @@ -256,11 +255,11 @@ async def test_reauth_flow_update_configuration(hass): [ ( SOURCE_DHCP, - { - HOSTNAME: f"axis-{MAC}", - IP_ADDRESS: DEFAULT_HOST, - MAC_ADDRESS: MAC, - }, + dhcp.DhcpServiceInfo( + hostname=f"axis-{MAC}", + ip=DEFAULT_HOST, + macaddress=MAC, + ), ), ( SOURCE_SSDP, @@ -347,11 +346,11 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): [ ( SOURCE_DHCP, - { - HOSTNAME: f"axis-{MAC}", - IP_ADDRESS: DEFAULT_HOST, - MAC_ADDRESS: MAC, - }, + dhcp.DhcpServiceInfo( + hostname=f"axis-{MAC}", + ip=DEFAULT_HOST, + macaddress=MAC, + ), ), ( SOURCE_SSDP, @@ -393,11 +392,11 @@ async def test_discovered_device_already_configured( [ ( SOURCE_DHCP, - { - HOSTNAME: f"axis-{MAC}", - IP_ADDRESS: "2.3.4.5", - MAC_ADDRESS: MAC, - }, + dhcp.DhcpServiceInfo( + hostname=f"axis-{MAC}", + ip="2.3.4.5", + macaddress=MAC, + ), 80, ), ( @@ -463,11 +462,11 @@ async def test_discovery_flow_updated_configuration( [ ( SOURCE_DHCP, - { - HOSTNAME: "", - IP_ADDRESS: "", - MAC_ADDRESS: "01234567890", - }, + dhcp.DhcpServiceInfo( + hostname="", + ip="", + macaddress="01234567890", + ), ), ( SOURCE_SSDP, @@ -505,7 +504,11 @@ async def test_discovery_flow_ignore_non_axis_device( [ ( SOURCE_DHCP, - {HOSTNAME: f"axis-{MAC}", IP_ADDRESS: "169.254.3.4", MAC_ADDRESS: MAC}, + dhcp.DhcpServiceInfo( + hostname=f"axis-{MAC}", + ip="169.254.3.4", + macaddress=MAC, + ), ), ( SOURCE_SSDP, From b9cbfbae585e29602b7513c7e8ba8378c85381db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:49:03 +0100 Subject: [PATCH 0691/1452] Use ZeroconfServiceInfo in nut (#60047) --- homeassistant/components/nut/config_flow.py | 10 +++++++--- tests/components/nut/test_config_flow.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index ae22526752d..9a8fc704886 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import ( CONF_ALIAS, CONF_BASE, @@ -14,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import PyNUTData from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -85,13 +87,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ups_list = None self.title = None - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Prepare configuration for a discovered nut device.""" self.discovery_info = discovery_info await self._async_handle_discovery_without_unique_id() self.context["title_placeholders"] = { - CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), - CONF_HOST: discovery_info[CONF_HOST], + CONF_PORT: discovery_info[zeroconf.ATTR_PORT] or DEFAULT_PORT, + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], } return await self.async_step_user() diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 8559ab50f98..892d74b4713 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from pynut2.nut2 import PyNUTError from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import zeroconf from homeassistant.components.nut.const import DOMAIN from homeassistant.const import ( CONF_ALIAS, @@ -34,7 +35,7 @@ async def test_form_zeroconf(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={CONF_HOST: "192.168.1.5", CONF_PORT: 1234}, + data=zeroconf.ZeroconfServiceInfo(host="192.168.1.5", port=1234), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" From b4651311701fe16d235baf9400149ace70eac3c8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:49:50 +0100 Subject: [PATCH 0692/1452] Use ZeroconfServiceInfo in sonos (#60095) --- homeassistant/components/sonos/config_flow.py | 9 ++--- tests/components/sonos/test_config_flow.py | 39 ++++++++++--------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index b26f1bdf40b..59788d17a92 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -5,7 +5,6 @@ import soco from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler @@ -31,13 +30,13 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf.""" - hostname = discovery_info["hostname"] + hostname = discovery_info[zeroconf.ATTR_HOSTNAME] if hostname is None or not hostname.lower().startswith("sonos"): return self.async_abort(reason="not_sonos_device") await self.async_set_unique_id(self._domain, raise_on_progress=False) - host = discovery_info[CONF_HOST] - mdns_name = discovery_info[CONF_NAME] - properties = discovery_info["properties"] + host = discovery_info[zeroconf.ATTR_HOST] + mdns_name = discovery_info[zeroconf.ATTR_NAME] + properties = discovery_info[zeroconf.ATTR_PROPERTIES] boot_seqnum = properties.get("bootseq") model = properties.get("model") uid = hostname_to_uid(hostname) diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 7d6fd02f51d..4d4e0cc4cad 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from unittest.mock import MagicMock, patch from homeassistant import config_entries, core +from homeassistant.components import zeroconf from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN @@ -43,12 +44,12 @@ async def test_zeroconf_form(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.4.2", - "name": "Sonos-aaa@Living Room._sonos._tcp.local.", - "hostname": "Sonos-aaa", - "properties": {"bootseq": "1234"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.4.2", + name="Sonos-aaa@Living Room._sonos._tcp.local.", + hostname="Sonos-aaa", + properties={"bootseq": "1234"}, + ), ) assert result["type"] == "form" assert result["errors"] is None @@ -82,13 +83,13 @@ async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.107", - "port": 1443, - "hostname": "sonos5CAAFDE47AC8.local.", - "type": "_sonos._tcp.local.", - "name": "Sonos-5CAAFDE47AC8._sonos._tcp.local.", - "properties": { + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.107", + port=1443, + hostname="sonos5CAAFDE47AC8.local.", + type="_sonos._tcp.local.", + name="Sonos-5CAAFDE47AC8._sonos._tcp.local.", + properties={ "_raw": { "info": b"/api/v1/players/RINCON_5CAAFDE47AC801400/info", "vers": b"1", @@ -98,7 +99,7 @@ async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): "vers": "1", "protovers": "1.18.9", }, - }, + ), ) assert result["type"] == "form" assert result["errors"] is None @@ -132,11 +133,11 @@ async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "host": "192.168.4.2", - "hostname": "not-aaa", - "properties": {"bootseq": "1234"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.4.2", + hostname="not-aaa", + properties={"bootseq": "1234"}, + ), ) assert result["type"] == "abort" assert result["reason"] == "not_sonos_device" From 1c15544d7ad776abbb616387529c6c0191f3b008 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:50:29 +0100 Subject: [PATCH 0693/1452] Use ZeroconfServiceInfo in smappee (#60096) --- .../components/smappee/config_flow.py | 17 ++- tests/components/smappee/test_config_flow.py | 130 +++++++++--------- 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index b13e540bae3..2fb5abaa05e 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -4,12 +4,13 @@ import logging from pysmappee import helper, mqtt import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from . import api from .const import ( - CONF_HOSTNAME, CONF_SERIALNUMBER, DOMAIN, ENV_CLOUD, @@ -36,14 +37,20 @@ class SmappeeFlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - if not discovery_info[CONF_HOSTNAME].startswith(SUPPORTED_LOCAL_DEVICES): + if not discovery_info[zeroconf.ATTR_HOSTNAME].startswith( + SUPPORTED_LOCAL_DEVICES + ): return self.async_abort(reason="invalid_mdns") serial_number = ( - discovery_info[CONF_HOSTNAME].replace(".local.", "").replace("Smappee", "") + discovery_info[zeroconf.ATTR_HOSTNAME] + .replace(".local.", "") + .replace("Smappee", "") ) # Check if already configured (local) @@ -56,7 +63,7 @@ class SmappeeFlowHandler( self.context.update( { - CONF_IP_ADDRESS: discovery_info["host"], + CONF_IP_ADDRESS: discovery_info[zeroconf.ATTR_HOST], CONF_SERIALNUMBER: serial_number, "title_placeholders": {"name": serial_number}, } diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index d8efe8b2903..f6f988a3caa 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -3,8 +3,8 @@ from http import HTTPStatus from unittest.mock import patch from homeassistant import data_entry_flow, setup +from homeassistant.components import zeroconf from homeassistant.components.smappee.const import ( - CONF_HOSTNAME, CONF_SERIALNUMBER, DOMAIN, ENV_CLOUD, @@ -55,14 +55,14 @@ async def test_show_zeroconf_connection_error_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee1006000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee1006000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee1006000212.local.", + type="_ssh._tcp.local.", + name="Smappee1006000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -84,14 +84,14 @@ async def test_show_zeroconf_connection_error_form_next_generation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee5001000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee5001000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee5001000212.local.", + type="_ssh._tcp.local.", + name="Smappee5001000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -166,14 +166,14 @@ async def test_zeroconf_wrong_mdns(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "example.local.", - "type": "_ssh._tcp.local.", - "name": "example._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="example.local.", + type="_ssh._tcp.local.", + name="example._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["reason"] == "invalid_mdns" @@ -276,14 +276,14 @@ async def test_zeroconf_device_exists_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee1006000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee1006000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee1006000212.local.", + type="_ssh._tcp.local.", + name="Smappee1006000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -325,14 +325,14 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee1006000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee1006000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee1006000212.local.", + type="_ssh._tcp.local.", + name="Smappee1006000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured_device" @@ -344,14 +344,14 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee1006000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee1006000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee1006000212.local.", + type="_ssh._tcp.local.", + name="Smappee1006000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) config_entry = MockConfigEntry( @@ -463,14 +463,14 @@ async def test_full_zeroconf_flow(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee1006000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee1006000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee1006000212.local.", + type="_ssh._tcp.local.", + name="Smappee1006000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "zeroconf_confirm" @@ -538,14 +538,14 @@ async def test_full_zeroconf_flow_next_generation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "1.2.3.4", - "port": 22, - CONF_HOSTNAME: "Smappee5001000212.local.", - "type": "_ssh._tcp.local.", - "name": "Smappee5001000212._ssh._tcp.local.", - "properties": {"_raw": {}}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + port=22, + hostname="Smappee5001000212.local.", + type="_ssh._tcp.local.", + name="Smappee5001000212._ssh._tcp.local.", + properties={"_raw": {}}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "zeroconf_confirm" From a72a5486c24284cb5e4a323802e6c2a568cea26e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:00:59 +0100 Subject: [PATCH 0694/1452] Use ServiceInfo in samsungtv tests (#60097) --- .../components/samsungtv/test_config_flow.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d9c96982aa1..adf23b4f2da 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -7,9 +7,8 @@ from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.components.samsungtv.const import ( - ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, DEFAULT_MANUFACTURER, @@ -85,18 +84,18 @@ MOCK_SSDP_DATA_WRONGMODEL = { ATTR_UPNP_MODEL_NAME: "HW-Qfake", ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", } -MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} +MOCK_DHCP_DATA = dhcp.DhcpServiceInfo(ip="fake_host", macaddress="aa:bb:cc:dd:ee:ff") EXISTING_IP = "192.168.40.221" -MOCK_ZEROCONF_DATA = { - CONF_HOST: "fake_host", - CONF_PORT: 1234, - ATTR_PROPERTIES: { +MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( + host="fake_host", + port=1234, + properties={ "deviceid": "aa:bb:cc:dd:ee:ff", "manufacturer": "fake_manufacturer", "model": "fake_model", "serialNumber": "fake_serial", }, -} +) MOCK_OLD_ENTRY = { CONF_HOST: "fake_host", CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", @@ -1097,7 +1096,7 @@ async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + data=dhcp.DhcpServiceInfo(ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff"), ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -1131,7 +1130,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mo result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + data=dhcp.DhcpServiceInfo(ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff"), ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 From fa9465d0032efbf9dd6d18a6b0f4def7857b1add Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:33:06 +0100 Subject: [PATCH 0695/1452] Use ZeroconfServiceInfo in system_bridge (#60102) --- .../components/system_bridge/config_flow.py | 7 ++-- .../system_bridge/test_config_flow.py | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 8d0b7a5dfff..ef3c1ccd2e4 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -151,8 +151,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - host = discovery_info["properties"].get("ip") - uuid = discovery_info["properties"].get("uuid") + properties = discovery_info[zeroconf.ATTR_PROPERTIES] + host = properties.get("ip") + uuid = properties.get("uuid") if host is None or uuid is None: return self.async_abort(reason="unknown") @@ -164,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._name = host self._input = { CONF_HOST: host, - CONF_PORT: discovery_info["properties"].get("port"), + CONF_PORT: properties.get("port"), } return await self.async_step_authenticate() diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 1b46bb45a6d..f8163eb98d9 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -5,6 +5,7 @@ from aiohttp.client_exceptions import ClientConnectionError from systembridge.exceptions import BridgeAuthenticationException from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.system_bridge.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -27,13 +28,13 @@ FIXTURE_ZEROCONF_INPUT = { CONF_PORT: "9170", } -FIXTURE_ZEROCONF = { - CONF_HOST: "1.1.1.1", - CONF_PORT: 9170, - "hostname": "test-bridge.local.", - "type": "_system-bridge._udp.local.", - "name": "System Bridge - test-bridge._system-bridge._udp.local.", - "properties": { +FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._udp.local.", + name="System Bridge - test-bridge._system-bridge._udp.local.", + properties={ "address": "http://test-bridge:9170", "fqdn": "test-bridge", "host": "test-bridge", @@ -42,18 +43,18 @@ FIXTURE_ZEROCONF = { "port": "9170", "uuid": FIXTURE_UUID, }, -} +) -FIXTURE_ZEROCONF_BAD = { - CONF_HOST: "1.1.1.1", - CONF_PORT: 9170, - "hostname": "test-bridge.local.", - "type": "_system-bridge._udp.local.", - "name": "System Bridge - test-bridge._system-bridge._udp.local.", - "properties": { +FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._udp.local.", + name="System Bridge - test-bridge._system-bridge._udp.local.", + properties={ "something": "bad", }, -} +) FIXTURE_INFORMATION = { From 455582098730bab5bdcdcce94fc647fcc3d6bfb7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:33:20 +0100 Subject: [PATCH 0696/1452] Use ZeroconfServiceInfo in shelly (#60098) --- homeassistant/components/shelly/config_flow.py | 7 ++++--- tests/components/shelly/test_config_flow.py | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b7b9ab72976..0f744fad4c7 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -189,16 +189,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" + host = discovery_info[zeroconf.ATTR_HOST] try: - self.info = await self._async_get_info(discovery_info["host"]) + self.info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") except aioshelly.exceptions.FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") await self.async_set_unique_id(self.info["mac"]) - self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) - self.host = discovery_info["host"] + self._abort_if_unique_id_configured({CONF_HOST: host}) + self.host = host self.context["title_placeholders"] = { "name": discovery_info.get("name", "").split(".")[0] diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index dc6921b9735..ec88856603a 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -8,6 +8,7 @@ import aioshelly import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.shelly.const import DOMAIN from tests.common import MockConfigEntry @@ -16,11 +17,11 @@ MOCK_SETTINGS = { "name": "Test name", "device": {"mac": "test-mac", "hostname": "test-host", "type": "SHSW-1"}, } -DISCOVERY_INFO = { - "host": "1.1.1.1", - "name": "shelly1pm-12345", - "properties": {"id": "shelly1pm-12345"}, -} +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + name="shelly1pm-12345", + properties={"id": "shelly1pm-12345"}, +) MOCK_CONFIG = { "wifi": {"ap": {"ssid": "Test name"}}, } From 8f7f32d844e62c551d78563096a35b728e959809 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:33:26 +0100 Subject: [PATCH 0697/1452] Use DhcpServiceInfo in screenlogic (#60103) --- homeassistant/components/screenlogic/config_flow.py | 13 +++++++------ tests/components/screenlogic/test_config_flow.py | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 1fc01a2e854..e8f2fca1be5 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -7,9 +7,10 @@ from screenlogicpy.requests import login import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -88,15 +89,15 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) return await self.async_step_gateway_select() - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - mac = _extract_mac_from_name(discovery_info[HOSTNAME]) + mac = _extract_mac_from_name(discovery_info[dhcp.HOSTNAME]) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: discovery_info[IP_ADDRESS]} + updates={CONF_IP_ADDRESS: discovery_info[dhcp.IP_ADDRESS]} ) - self.discovered_ip = discovery_info[IP_ADDRESS] - self.context["title_placeholders"] = {"name": discovery_info[HOSTNAME]} + self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] + self.context["title_placeholders"] = {"name": discovery_info[dhcp.HOSTNAME]} return await self.async_step_gateway_entry() async def async_step_gateway_select(self, user_input=None): diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index d1333cb7514..9a6715bc401 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -11,7 +11,7 @@ from screenlogicpy.const import ( ) from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.screenlogic.config_flow import ( GATEWAY_MANUAL_ENTRY, GATEWAY_SELECT_KEY, @@ -138,10 +138,10 @@ async def test_dhcp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "Pentair: 01-01-01", - IP_ADDRESS: "1.1.1.1", - }, + data=dhcp.DhcpServiceInfo( + hostname="Pentair: 01-01-01", + ip="1.1.1.1", + ), ) assert result["type"] == "form" From 4e1089cedbf772ad0d8f6b86a876a58605cf9d66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:33:34 +0100 Subject: [PATCH 0698/1452] Use DhcpServiceInfo in somfy_mylink (#60099) --- .../components/somfy_mylink/config_flow.py | 15 +++++---- .../somfy_mylink/test_config_flow.py | 32 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 79fbf028b16..6f695d06cbe 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -7,9 +7,10 @@ from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -58,18 +59,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.mac = None self.ip_address = None - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) + self._async_abort_entries_match({CONF_HOST: discovery_info[dhcp.IP_ADDRESS]}) - formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) + formatted_mac = format_mac(discovery_info[dhcp.MAC_ADDRESS]) await self.async_set_unique_id(format_mac(formatted_mac)) self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[IP_ADDRESS]} + updates={CONF_HOST: discovery_info[dhcp.IP_ADDRESS]} ) - self.host = discovery_info[HOSTNAME] + self.host = discovery_info[dhcp.HOSTNAME] self.mac = formatted_mac - self.ip_address = discovery_info[IP_ADDRESS] + self.ip_address = discovery_info[dhcp.IP_ADDRESS] self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac} return await self.async_step_user() diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 38cb2a52dfd..d98e7429d8b 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.somfy_mylink.const import ( CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, @@ -252,11 +252,11 @@ async def test_form_user_already_configured_from_dhcp(hass): 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: "somfy_eeff", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="somfy_eeff", + ), ) await hass.async_block_till_done() @@ -276,11 +276,11 @@ async def test_already_configured_with_ignored(hass): 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: "somfy_eeff", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="somfy_eeff", + ), ) assert result["type"] == "form" @@ -291,11 +291,11 @@ async def test_dhcp_discovery(hass): 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: "somfy_eeff", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="somfy_eeff", + ), ) assert result["type"] == "form" assert result["errors"] == {} From 382efef2e25ce077d139661f34e8e21689c4e97d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:33:44 +0100 Subject: [PATCH 0699/1452] Use DhcpServiceInfo in squeezebox (#60100) --- .../components/squeezebox/config_flow.py | 11 +++++-- .../components/squeezebox/test_config_flow.py | 32 +++++++++---------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index e7e0a691e85..ac5bf8b580e 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -2,12 +2,13 @@ import asyncio from http import HTTPStatus import logging +from typing import TYPE_CHECKING from pysqueezebox import Server, async_discover import voluptuous as vol from homeassistant import config_entries, data_entry_flow -from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -184,18 +185,22 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_edit() - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> data_entry_flow.FlowResult: """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])) + await self.async_set_unique_id(format_mac(discovery_info[dhcp.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 TYPE_CHECKING: + assert self.unique_id # 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 diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 7f07576427b..df6d27572b8 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pysqueezebox import Server from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.data_entry_flow import ( @@ -200,11 +200,11 @@ async def test_dhcp_discovery(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "edit" @@ -219,11 +219,11 @@ async def test_dhcp_discovery_no_server_found(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -238,11 +238,11 @@ async def test_dhcp_discovery_existing_player(hass): 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", - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), ) assert result["type"] == RESULT_TYPE_ABORT From c4128f853df137e0cb4fc2458368e8dac17a478d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 21 Nov 2021 23:34:00 +0100 Subject: [PATCH 0700/1452] Add climate platform to Shelly (#59712) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + homeassistant/components/shelly/__init__.py | 9 +- homeassistant/components/shelly/climate.py | 182 ++++++++++++++++++++ homeassistant/components/shelly/const.py | 6 + 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/shelly/climate.py diff --git a/.coveragerc b/.coveragerc index 9de86199803..0e01d0c7849 100644 --- a/.coveragerc +++ b/.coveragerc @@ -934,6 +934,7 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py + homeassistant/components/shelly/climate.py homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 92e13aad664..c85728d3ee6 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -64,7 +64,14 @@ from .utils import ( get_rpc_device_name, ) -BLOCK_PLATFORMS: Final = ["binary_sensor", "cover", "light", "sensor", "switch"] +BLOCK_PLATFORMS: Final = [ + "binary_sensor", + "climate", + "cover", + "light", + "sensor", + "switch", +] BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py new file mode 100644 index 00000000000..8574ac17f46 --- /dev/null +++ b/homeassistant/components/shelly/climate.py @@ -0,0 +1,182 @@ +"""Climate support for Shelly.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Final, cast + +from aioshelly.block_device import Block +import async_timeout + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_NONE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.components.shelly import BlockDeviceWrapper +from homeassistant.components.shelly.entity import ShellyBlockEntity +from homeassistant.components.shelly.utils import get_device_entry_gen +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + AIOSHELLY_DEVICE_TIMEOUT_SEC, + BLOCK, + DATA_CONFIG_ENTRY, + DOMAIN, + SHTRV_01_TEMPERATURE_SETTINGS, +) + +_LOGGER: Final = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate device.""" + + if get_device_entry_gen(config_entry) == 2: + return + + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + for block in wrapper.device.blocks: + if block.type == "device": + device_block = block + if hasattr(block, "targetTemp"): + sensor_block = block + + if sensor_block and device_block: + async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)]) + + +class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): + """Representation of a Shelly climate device.""" + + _attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT] + _attr_icon = "mdi:thermostat" + _attr_max_temp = SHTRV_01_TEMPERATURE_SETTINGS["max"] + _attr_min_temp = SHTRV_01_TEMPERATURE_SETTINGS["min"] + _attr_supported_features: int = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block + ) -> None: + """Initialize climate.""" + super().__init__(wrapper, sensor_block) + + self.device_block = device_block + + assert self.block.channel + + self.control_result: dict[str, Any] | None = None + + self._attr_name = self.wrapper.name + self._attr_unique_id = self.wrapper.mac + self._attr_preset_modes: list[str] = [ + PRESET_NONE, + *wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + + @property + def target_temperature(self) -> float | None: + """Set target temperature.""" + return cast(float, self.block.targetTemp) + + @property + def current_temperature(self) -> float | None: + """Return current temperature.""" + return cast(float, self.block.temp) + + @property + def available(self) -> bool: + """Device availability.""" + return not cast(bool, self.device_block.valveError) + + @property + def hvac_mode(self) -> str: + """HVAC current mode.""" + if self.device_block.mode is None or self._check_is_off(): + return HVAC_MODE_OFF + + return HVAC_MODE_HEAT + + @property + def preset_mode(self) -> str | None: + """Preset current mode.""" + if self.device_block.mode is None: + return None + return self._attr_preset_modes[cast(int, self.device_block.mode)] + + @property + def hvac_action(self) -> str | None: + """HVAC current action.""" + if self.device_block.status is None or self._check_is_off(): + return CURRENT_HVAC_OFF + + return ( + CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT + ) + + def _check_is_off(self) -> bool: + """Return if valve is off or on.""" + return bool( + self.target_temperature is None + or (self.target_temperature <= self._attr_min_temp) + ) + + async def set_state_full_path(self, **kwargs: Any) -> Any: + """Set block state (HTTP request).""" + _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + return await self.wrapper.device.http_request( + "get", f"thermostat/{self.block.channel}", kwargs + ) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.error( + "Setting state for entity %s failed, state: %s, error: %s", + self.name, + kwargs, + repr(err), + ) + self.wrapper.last_update_success = False + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if (current_temp := kwargs.get(ATTR_TEMPERATURE)) is None: + return + await self.set_state_full_path(target_t_enabled=1, target_t=f"{current_temp}") + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.set_state_full_path( + target_t_enabled=1, target_t=f"{self._attr_min_temp}" + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + if not self._attr_preset_modes: + return + + preset_index = self._attr_preset_modes.index(preset_mode) + await self.set_state_full_path( + schedule=(0 if preset_index == 0 else 1), + schedule_profile=f"{preset_index}", + ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index d241998a138..fd06e88cdf1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -141,6 +141,12 @@ SHBLB_1_RGB_EFFECTS: Final = { 6: "Red/Green Change", } +SHTRV_01_TEMPERATURE_SETTINGS: Final = { + "min": 4, + "max": 31, + "step": 1, +} + # Kelvin value for colorTemp KELVIN_MAX_VALUE: Final = 6500 KELVIN_MIN_VALUE_WHITE: Final = 2700 From e4931e242a8ed4f7d5f5a74df49f355b88c1b01c Mon Sep 17 00:00:00 2001 From: jugla <59493499+jugla@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:38:37 +0100 Subject: [PATCH 0701/1452] Allow atome reauthentication on error (#59452) * allow reauthentification on error * Allow reauthentification on error * allow reauthentification on error * allow reauthentification on error * Set one fonction for day/month/week/year and allow retry on error * allow retry and manage return status * local variable in retrieve_period_usage_with_retry * Use If statement rather than exception * remove blank * code format * Reduce variable * remove useless variable * get method return false and pylint no else-return * remove blank * trailing white space --- homeassistant/components/atome/sensor.py | 102 ++++++++++++++--------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 59d193ec8e2..0402e80949e 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -111,11 +111,13 @@ class AtomeData: """Return latest active power value.""" return self._is_connected - @Throttle(LIVE_SCAN_INTERVAL) - def update_live_usage(self): - """Return current power value.""" - try: - values = self.atome_client.get_live() + def _retrieve_live(self): + values = self.atome_client.get_live() + if ( + values.get("last") + and values.get("subscribed") + and (values.get("isConnected") is not None) + ): self._live_power = values["last"] self._subscribed_power = values["subscribed"] self._is_connected = values["isConnected"] @@ -125,9 +127,47 @@ class AtomeData: self._is_connected, self._subscribed_power, ) + return True - except KeyError as error: - _LOGGER.error("Missing last value in values: %s: %s", values, error) + _LOGGER.error("Live Data : Missing last value in values: %s", values) + return False + + @Throttle(LIVE_SCAN_INTERVAL) + def update_live_usage(self): + """Return current power value.""" + if not self._retrieve_live(): + _LOGGER.debug("Perform Reconnect during live request") + self.atome_client.login() + self._retrieve_live() + + def _retrieve_period_usage(self, period_type): + """Return current daily/weekly/monthly/yearly power usage.""" + values = self.atome_client.get_consumption(period_type) + if values.get("total") and values.get("price"): + period_usage = values["total"] / 1000 + period_price = values["price"] + _LOGGER.debug("Updating Atome %s data. Got: %d", period_type, period_usage) + return True, period_usage, period_price + + _LOGGER.error("%s : Missing last value in values: %s", period_type, values) + return False, None, None + + def _retrieve_period_usage_with_retry(self, period_type): + """Return current daily/weekly/monthly/yearly power usage with one retry.""" + ( + retrieve_success, + period_usage, + period_price, + ) = self._retrieve_period_usage(period_type) + if not retrieve_success: + _LOGGER.debug("Perform Reconnect during %s", period_type) + self.atome_client.login() + ( + retrieve_success, + period_usage, + period_price, + ) = self._retrieve_period_usage(period_type) + return (period_usage, period_price) @property def day_usage(self): @@ -142,14 +182,10 @@ class AtomeData: @Throttle(DAILY_SCAN_INTERVAL) def update_day_usage(self): """Return current daily power usage.""" - try: - values = self.atome_client.get_consumption(DAILY_TYPE) - self._day_usage = values["total"] / 1000 - self._day_price = values["price"] - _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) - - except KeyError as error: - _LOGGER.error("Missing last value in values: %s: %s", values, error) + ( + self._day_usage, + self._day_price, + ) = self._retrieve_period_usage_with_retry(DAILY_TYPE) @property def week_usage(self): @@ -164,14 +200,10 @@ class AtomeData: @Throttle(WEEKLY_SCAN_INTERVAL) def update_week_usage(self): """Return current weekly power usage.""" - try: - values = self.atome_client.get_consumption(WEEKLY_TYPE) - self._week_usage = values["total"] / 1000 - self._week_price = values["price"] - _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) - - except KeyError as error: - _LOGGER.error("Missing last value in values: %s: %s", values, error) + ( + self._week_usage, + self._week_price, + ) = self._retrieve_period_usage_with_retry(WEEKLY_TYPE) @property def month_usage(self): @@ -186,14 +218,10 @@ class AtomeData: @Throttle(MONTHLY_SCAN_INTERVAL) def update_month_usage(self): """Return current monthly power usage.""" - try: - values = self.atome_client.get_consumption(MONTHLY_TYPE) - self._month_usage = values["total"] / 1000 - self._month_price = values["price"] - _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) - - except KeyError as error: - _LOGGER.error("Missing last value in values: %s: %s", values, error) + ( + self._month_usage, + self._month_price, + ) = self._retrieve_period_usage_with_retry(MONTHLY_TYPE) @property def year_usage(self): @@ -208,14 +236,10 @@ class AtomeData: @Throttle(YEARLY_SCAN_INTERVAL) def update_year_usage(self): """Return current yearly power usage.""" - try: - values = self.atome_client.get_consumption(YEARLY_TYPE) - self._year_usage = values["total"] / 1000 - self._year_price = values["price"] - _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) - - except KeyError as error: - _LOGGER.error("Missing last value in values: %s: %s", values, error) + ( + self._year_usage, + self._year_price, + ) = self._retrieve_period_usage_with_retry(YEARLY_TYPE) class AtomeSensor(SensorEntity): From 176245e5beeece74506846ca0857815f81abdba2 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:45:40 +0100 Subject: [PATCH 0702/1452] Use format_mac for AsusWRT ScannerEntity Mac Address (#54959) * Use format_mac for AsusWRT ScannerEntity mac * Fix after rebase * Change check for duplicated entities --- homeassistant/components/asuswrt/router.py | 38 +++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 3d2cac85569..986ba828466 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -25,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -250,15 +251,32 @@ class AsusWrtRouter: self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" # Load tracked entities from registry + entity_reg = er.async_get(self.hass) track_entries = er.async_entries_for_config_entry( - er.async_get(self.hass), - self._entry.entry_id, + entity_reg, self._entry.entry_id ) for entry in track_entries: - if entry.domain == TRACKER_DOMAIN: - self._devices[entry.unique_id] = AsusWrtDevInfo( - entry.unique_id, entry.original_name + + if entry.domain != TRACKER_DOMAIN: + continue + device_mac = format_mac(entry.unique_id) + + # migrate entity unique ID if wrong formatted + if device_mac != entry.unique_id: + existing_entity_id = entity_reg.async_get_entity_id( + DOMAIN, TRACKER_DOMAIN, device_mac ) + if existing_entity_id: + # entity with uniqueid properly formatted already + # exists in the registry, we delete this duplicate + entity_reg.async_remove(entry.entity_id) + continue + + entity_reg.async_update_entity( + entry.entity_id, new_unique_id=device_mac + ) + + self._devices[device_mac] = AsusWrtDevInfo(device_mac, entry.original_name) # Update devices await self.update_devices() @@ -279,7 +297,7 @@ class AsusWrtRouter: new_device = False _LOGGER.debug("Checking devices for ASUS router %s", self._host) try: - wrt_devices = await self._api.async_get_connected_devices() + api_devices = await self._api.async_get_connected_devices() except OSError as exc: if not self._connect_error: self._connect_error = True @@ -294,18 +312,18 @@ class AsusWrtRouter: self._connect_error = False _LOGGER.info("Reconnected to ASUS router %s", self._host) + self._connected_devices = len(api_devices) consider_home = self._options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() ) track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN) + wrt_devices = {format_mac(mac): dev for mac, dev in api_devices.items()} for device_mac, device in self._devices.items(): - dev_info = wrt_devices.get(device_mac) + dev_info = wrt_devices.pop(device_mac, None) device.update(dev_info, consider_home) for device_mac, dev_info in wrt_devices.items(): - if device_mac in self._devices: - continue if not track_unknown and not dev_info.name: continue new_device = True @@ -316,8 +334,6 @@ class AsusWrtRouter: async_dispatcher_send(self.hass, self.signal_device_update) if new_device: async_dispatcher_send(self.hass, self.signal_device_new) - - self._connected_devices = len(wrt_devices) await self._update_unpolled_sensors() async def init_sensors_coordinator(self) -> None: From 31d4239b64e6357054f37459cae9d9436cc0c164 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 22 Nov 2021 00:13:02 +0000 Subject: [PATCH 0703/1452] [ci skip] Translation update --- .../components/bosch_shc/translations/ja.json | 12 +++- .../components/broadlink/translations/ja.json | 2 + .../components/brother/translations/ja.json | 1 + .../components/climacell/translations/ja.json | 6 +- .../cloudflare/translations/ja.json | 1 + .../components/coinbase/translations/ja.json | 1 + .../components/denonavr/translations/ja.json | 1 + .../devolo_home_control/translations/ja.json | 4 +- .../components/dlna_dmr/translations/ja.json | 2 +- .../enphase_envoy/translations/ja.json | 3 +- .../components/ezviz/translations/ja.json | 28 +++++++-- .../components/flipr/translations/ja.json | 1 + .../freedompro/translations/ja.json | 3 + .../components/fritz/translations/ja.json | 3 +- .../fritzbox_callmonitor/translations/ja.json | 5 ++ .../google_travel_time/translations/ja.json | 3 + .../growatt_server/translations/ja.json | 5 +- .../components/habitica/translations/ja.json | 7 ++- .../components/hive/translations/ja.json | 6 +- .../homekit_controller/translations/ja.json | 1 + .../huisbaasje/translations/ja.json | 3 +- .../components/ipp/translations/ja.json | 3 +- .../keenetic_ndms2/translations/ja.json | 9 +++ .../components/kmtronic/translations/ja.json | 3 +- .../components/knx/translations/ja.json | 28 ++++++++- .../components/knx/translations/zh-Hant.json | 63 +++++++++++++++++++ .../components/konnected/translations/ja.json | 3 +- .../components/litejet/translations/ja.json | 3 +- .../litterrobot/translations/ja.json | 3 +- .../lutron_caseta/translations/ja.json | 1 + .../components/lyric/translations/ja.json | 9 +++ .../components/mazda/translations/ja.json | 10 ++- .../meteoclimatic/translations/ja.json | 1 + .../motion_blinds/translations/ja.json | 1 + .../components/motioneye/translations/ja.json | 1 + .../components/mqtt/translations/ja.json | 1 + .../components/mysensors/translations/ja.json | 33 +++++++++- .../components/netatmo/translations/ja.json | 5 ++ .../components/number/translations/ja.json | 3 + .../opentherm_gw/translations/ja.json | 10 +++ .../philips_js/translations/ja.json | 3 +- .../components/plaato/translations/ja.json | 6 +- .../components/ps4/translations/ja.json | 2 + .../components/ring/translations/ja.json | 3 +- .../translations/ja.json | 4 +- .../components/roku/translations/ja.json | 4 ++ .../screenlogic/translations/ja.json | 4 ++ .../components/sensor/translations/ja.json | 2 + .../components/sia/translations/ja.json | 9 ++- .../components/sma/translations/ja.json | 5 ++ .../components/smarttub/translations/ja.json | 1 + .../components/spider/translations/ja.json | 3 +- .../components/subaru/translations/ja.json | 11 ++++ .../components/syncthing/translations/ja.json | 3 +- .../system_bridge/translations/ja.json | 3 +- .../components/tasmota/translations/ja.json | 14 +++++ .../totalconnect/translations/ja.json | 3 + .../components/tplink/translations/ja.json | 2 +- .../components/tractive/translations/ja.json | 1 + .../components/tuya/translations/ja.json | 3 +- .../components/twinkly/translations/ja.json | 3 +- .../components/unifi/translations/ja.json | 3 + .../components/upb/translations/ja.json | 7 ++- .../components/upcloud/translations/ja.json | 3 +- .../components/upnp/translations/ja.json | 15 ++++- .../uptimerobot/translations/ja.json | 3 + .../components/vera/translations/ja.json | 7 +++ .../components/verisure/translations/ja.json | 21 +++++++ .../components/vesync/translations/ja.json | 3 +- .../components/vilfo/translations/ja.json | 3 +- .../components/vizio/translations/ja.json | 14 +++++ .../components/volumio/translations/ja.json | 3 + .../water_heater/translations/ja.json | 16 +++++ .../waze_travel_time/translations/ja.json | 14 ++++- .../components/whirlpool/translations/ja.json | 3 +- .../components/wiffi/translations/ja.json | 12 +++- .../wled/translations/select.ca.json | 9 +++ .../wled/translations/select.de.json | 9 +++ .../wled/translations/select.et.json | 9 +++ .../wled/translations/select.ja.json | 9 +++ .../wled/translations/select.ru.json | 9 +++ .../wled/translations/select.zh-Hant.json | 9 +++ .../components/wolflink/translations/ja.json | 8 ++- .../wolflink/translations/sensor.ja.json | 47 +++++++++++++- .../xiaomi_aqara/translations/ja.json | 26 +++++++- .../xiaomi_miio/translations/ja.json | 50 ++++++++++++--- .../yale_smart_alarm/translations/ja.json | 6 +- .../yamaha_musiccast/translations/ja.json | 3 + .../components/yeelight/translations/ja.json | 17 ++++- .../components/youless/translations/ja.json | 3 +- .../components/zha/translations/ja.json | 12 ++++ .../zodiac/translations/sensor.ja.json | 18 ++++++ .../zoneminder/translations/ja.json | 5 +- .../components/zwave/translations/ja.json | 1 + .../components/zwave_js/translations/ja.json | 42 ++++++++++++- 95 files changed, 720 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/knx/translations/zh-Hant.json create mode 100644 homeassistant/components/lyric/translations/ja.json create mode 100644 homeassistant/components/number/translations/ja.json create mode 100644 homeassistant/components/tasmota/translations/ja.json create mode 100644 homeassistant/components/water_heater/translations/ja.json create mode 100644 homeassistant/components/wled/translations/select.ca.json create mode 100644 homeassistant/components/wled/translations/select.de.json create mode 100644 homeassistant/components/wled/translations/select.et.json create mode 100644 homeassistant/components/wled/translations/select.ja.json create mode 100644 homeassistant/components/wled/translations/select.ru.json create mode 100644 homeassistant/components/wled/translations/select.zh-Hant.json create mode 100644 homeassistant/components/zodiac/translations/sensor.ja.json diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index c1bfa7efbf5..7fa13765ae5 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -1,20 +1,28 @@ { "config": { "error": { - "pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Bosch Smart Home Controller\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b(LED\u304c\u70b9\u6ec5)\u3053\u3068\u3068\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u3044\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Bosch Smart Home Controller\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b(LED\u304c\u70b9\u6ec5)\u3053\u3068\u3068\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u3044\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "session_error": "\u30bb\u30c3\u30b7\u30e7\u30f3\u30a8\u30e9\u30fc: API\u304c\u3001OK\u4ee5\u5916\u306e\u7d50\u679c\u3092\u8fd4\u3057\u307e\u3059\u3002" }, "flow_title": "Bosch SHC: {name}", "step": { + "confirm_discovery": { + "description": "LED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u3067\u3001Bosch Smart Home Controller\u306e\u5168\u9762\u306b\u3042\u308b\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nHome Assistant\u3067\u3001{model} @ {host} \u3092\u8a2d\u5b9a\u3059\u308b\u6e96\u5099\u306f\u3067\u304d\u307e\u3057\u305f\u304b\uff1f" + }, "credentials": { "data": { "password": "Smart Home Controller\u306e\u30d1\u30b9\u30ef\u30fc\u30c9" } }, + "reauth_confirm": { + "description": "bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Bosch Smart Home Controller\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3067\u76e3\u8996\u304a\u3088\u3073\u5236\u5fa1\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + "description": "Bosch Smart Home Controller\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3067\u76e3\u8996\u304a\u3088\u3073\u5236\u5fa1\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", + "title": "SHC\u8a8d\u8a3c\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc" } } }, diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index b490a2f58ce..8002e7bc119 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -15,12 +15,14 @@ "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "reset": { + "description": "{name} ({model} \u306e {host}) \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a8d\u8a3c\u3057\u3066\u8a2d\u5b9a\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u624b\u9806:\n1. Broadlink\u30a2\u30d7\u30ea\u3092\u958b\u304d\u307e\u3059\u3002\n2. \u30c7\u30d0\u30a4\u30b9\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n3. \u53f3\u4e0a\u306e`...`\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n4. \u30da\u30fc\u30b8\u306e\u4e00\u756a\u4e0b\u307e\u3067\u30b9\u30af\u30ed\u30fc\u30eb\u3057\u307e\u3059\u3002\n5. \u30ed\u30c3\u30af\u3092\u7121\u52b9\u306b\u3057\u307e\u3059\u3002", "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664" }, "unlock": { "data": { "unlock": "\u306f\u3044\u3001\u3084\u308a\u307e\u3059\u3002" }, + "description": "{name} ({model} \u306e {host}) \u304c\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u308c\u306b\u3088\u308a\u3001Home Assistant\u3067\u306e\u8a8d\u8a3c\u554f\u984c\u306b\u3064\u306a\u304c\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3057\u307e\u3059\u304b\uff1f", "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, "user": { diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index e468ecc114f..a6ed0fda293 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -20,6 +20,7 @@ "data": { "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" }, + "description": "Brother\u793e\u306e\u30d7\u30ea\u30f3\u30bf\u30fc {model} \u3067\u3001\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c `{serial_number}` \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "\u30d6\u30e9\u30b6\u30fc\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u767a\u898b" } } diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index c2ff6bbb145..6b00260a579 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index c6c9388af87..1931145bec3 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -4,6 +4,7 @@ "step": { "reauth_confirm": { "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3", "description": "Cloudflare\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" } }, diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index ad0d0bfef01..5146e8f34fc 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "api_token": "API\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", "currencies": "\u53e3\u5ea7\u6b8b\u9ad8 \u901a\u8ca8", "exchange_rates": "\u70ba\u66ff\u30ec\u30fc\u30c8" diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 92b40eeb2b0..2e97efde076 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -31,6 +31,7 @@ "init": { "data": { "show_all_sources": "\u3059\u3079\u3066\u306e\u30bd\u30fc\u30b9\u3092\u8868\u793a", + "update_audyssey": "Audyssey\u8a2d\u5b9a\u3092\u66f4\u65b0", "zone2": "\u30be\u30fc\u30f32\u306e\u8a2d\u5b9a", "zone3": "\u30be\u30fc\u30f33\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json index c6de7b0261c..e72a8ca97af 100644 --- a/homeassistant/components/devolo_home_control/translations/ja.json +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -11,7 +11,9 @@ }, "zeroconf_confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "mydevolo_url": "mydevolo URL", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "Email / devolo ID" } } } diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index b02784f8a2b..818f73e02f3 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "discovery_error": "\u4e00\u81f4\u3059\u308b DLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", + "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", "non_unique_id": "\u540c\u4e00\u306eID\u3067\u8907\u6570\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index 006bff4d921..18e8106cf4f 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -5,7 +5,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/ezviz/translations/ja.json b/homeassistant/components/ezviz/translations/ja.json index 94fe0f846e2..68333ed59b7 100644 --- a/homeassistant/components/ezviz/translations/ja.json +++ b/homeassistant/components/ezviz/translations/ja.json @@ -1,19 +1,37 @@ { "config": { + "flow_title": "{serial}", "step": { "confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "IP {ip_address} \u3092\u6301\u3064Ezviz\u30ab\u30e1\u30e9 {serial} \u306eRTSP\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u767a\u898b\u3055\u308c\u305fEzviz\u30ab\u30e1\u30e9" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "title": "Ezviz Cloud\u306b\u63a5\u7d9a" }, "user_custom_url": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u30ea\u30af\u30a8\u30b9\u30c8\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u79d2)" } } } diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index 8c91a4e364f..2ff7c7a8a6c 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -13,6 +13,7 @@ }, "user": { "data": { + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u3042\u306a\u305f\u306eFlipr\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/freedompro/translations/ja.json b/homeassistant/components/freedompro/translations/ja.json index 4709477a760..398f34ccd8a 100644 --- a/homeassistant/components/freedompro/translations/ja.json +++ b/homeassistant/components/freedompro/translations/ja.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "description": "https://home.freedompro.eu \u304b\u3089\u53d6\u5f97\u3057\u305fAPI\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Freedompro API\u30ad\u30fc" } diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index c1f3f102204..4661f8d77f9 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -4,7 +4,8 @@ "step": { "confirm": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index 0c55d7d61a8..8fc558056d0 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -2,6 +2,11 @@ "config": { "flow_title": "{name}", "step": { + "phonebook": { + "data": { + "phonebook": "\u96fb\u8a71\u5e33" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index 135a2c2bdcc..49d09cb4676 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -4,6 +4,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "destination": "\u76ee\u7684\u5730", "name": "\u540d\u524d", "origin": "\u30aa\u30ea\u30b8\u30f3" } @@ -14,6 +15,8 @@ "step": { "init": { "data": { + "avoid": "\u907f\u3051\u308b", + "time": "\u6642\u9593", "units": "\u5358\u4f4d" } } diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json index a464db24f8c..45df3d0df6d 100644 --- a/homeassistant/components/growatt_server/translations/ja.json +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -12,7 +12,10 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Growatt\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/habitica/translations/ja.json b/homeassistant/components/habitica/translations/ja.json index e8697990afb..bf5d578612e 100644 --- a/homeassistant/components/habitica/translations/ja.json +++ b/homeassistant/components/habitica/translations/ja.json @@ -3,10 +3,13 @@ "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "api_user": "Habitica API\u306e\u30e6\u30fc\u30b6\u30fcID", + "url": "URL" }, "description": "Habitica profile\u306b\u63a5\u7d9a\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3068\u30bf\u30b9\u30af\u3092\u76e3\u8996\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002 \u6ce8\u610f: api_id\u3068api_key\u306f\u3001https://habitica.com/user/settings/api \u304b\u3089\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } - } + }, + "title": "Habitica" } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 0e1c75dd769..132d1e483d3 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -15,13 +15,15 @@ }, "reauth": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Hive\u30ed\u30b0\u30a4\u30f3" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Hive\u30ed\u30b0\u30a4\u30f3" } diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 6681a5c9a0b..fd4a5bf6a28 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "error": { diff --git a/homeassistant/components/huisbaasje/translations/ja.json b/homeassistant/components/huisbaasje/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/huisbaasje/translations/ja.json +++ b/homeassistant/components/huisbaasje/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 34f73e9770b..7bdbfbf94a0 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -3,7 +3,8 @@ "abort": { "connection_upgrade": "\u63a5\u7d9a\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9(connection upgrade)\u304c\u5fc5\u8981\u306a\u305f\u3081\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "ipp_version_error": "IPP\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30d7\u30ea\u30f3\u30bf\u30fc\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "unique_id_required": "\u691c\u51fa\u306b\u5fc5\u8981\u306a\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index f858f676cbf..6c7513d9142 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -16,5 +16,14 @@ "title": "Keenetic NDMS2\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json index 9f68231f0d2..2981d97e3c6 100644 --- a/homeassistant/components/kmtronic/translations/ja.json +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index db2f15d53fc..18694617e2f 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -6,7 +6,30 @@ "step": { "manual_tunnel": { "data": { - "port": "\u30dd\u30fc\u30c8" + "host": "\u30db\u30b9\u30c8", + "individual_address": "\u63a5\u7d9a\u7528\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", + "port": "\u30dd\u30fc\u30c8", + "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" + }, + "description": "\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "routing": { + "data": { + "individual_address": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u63a5\u7d9a\u306e\u500b\u5225\u306e\u30a2\u30c9\u30ec\u30b9", + "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", + "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8" + }, + "description": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "tunnel": { + "data": { + "gateway": "KNX\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a" + }, + "description": "\u30ea\u30b9\u30c8\u304b\u3089\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "type": { + "data": { + "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7" } } } @@ -16,6 +39,9 @@ "init": { "data": { "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7", + "individual_address": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", + "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", + "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8", "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", "state_updater": "KNX\u30d0\u30b9\u304b\u3089\u306e\u8aad\u307f\u53d6\u308a\u72b6\u614b\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u306b\u3059\u308b" } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json new file mode 100644 index 00000000000..c8185f1a867 --- /dev/null +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "individual_address": "\u9023\u7dda\u500b\u5225\u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" + }, + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + }, + "routing": { + "data": { + "individual_address": "\u8def\u7531\u9023\u7dda\u500b\u5225\u4f4d\u5740", + "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u7fa4\u7d44", + "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" + }, + "tunnel": { + "data": { + "gateway": "KNX \u901a\u9053\u9023\u7dda" + }, + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" + }, + "type": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b" + }, + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u578b\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b", + "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", + "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u7fa4\u7d44", + "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", + "rate_limit": "\u6700\u5927\u6bcf\u79d2\u767c\u51fa Telegram", + "state_updater": "\u7531 KNX Bus \u8b80\u53d6\u72c0\u614b\u5168\u555f\u7528" + } + }, + "tunnel": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 175136568f6..4639e810259 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -44,7 +44,8 @@ "6": "\u30be\u30fc\u30f36", "7": "\u30be\u30fc\u30f37", "out": "OUT" - } + }, + "description": "{model} \u3067 {host} \u3092\u767a\u898b\u3057\u307e\u3057\u305f\u3002\u5404I/O\u306e\u57fa\u672c\u69cb\u6210\u3092\u4ee5\u4e0b\u304b\u3089\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002I/O\u306b\u5fdc\u3058\u3066\u3001\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc(\u30aa\u30fc\u30d7\u30f3(\u958b)/\u30af\u30ed\u30fc\u30ba(\u9589)\u63a5\u70b9)\u3001\u30c7\u30b8\u30bf\u30eb\u30bb\u30f3\u30b5\u30fc(dht\u304a\u3088\u3073ds18b20)\u3001\u307e\u305f\u306f\u5207\u308a\u66ff\u3048\u53ef\u80fd\u306a\u51fa\u529b(switchable outputs)\u304c\u53ef\u80fd\u3067\u3059\u3002\u6b21\u306e\u30b9\u30c6\u30c3\u30d7\u3067\u3001\u8a73\u7d30\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002" }, "options_io_ext": { "data": { diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index 91cf6162551..5886fc609c6 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -8,7 +8,8 @@ "data": { "port": "\u30dd\u30fc\u30c8" }, - "description": "LiteJet\u306eRS232-2\u30dd\u30fc\u30c8\u3092\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3057\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n LiteJet MCP\u306f\u300119.2 K\u30dc\u30fc\u30018\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u30011\u30b9\u30c8\u30c3\u30d7\u30d3\u30c3\u30c8\u3001\u30d1\u30ea\u30c6\u30a3\u306a\u3057\u3001\u5404\u5fdc\u7b54\u306e\u5f8c\u306b 'CR' \u3092\u9001\u4fe1\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "description": "LiteJet\u306eRS232-2\u30dd\u30fc\u30c8\u3092\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a\u3057\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n LiteJet MCP\u306f\u300119.2 K\u30dc\u30fc\u30018\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u30011\u30b9\u30c8\u30c3\u30d7\u30d3\u30c3\u30c8\u3001\u30d1\u30ea\u30c6\u30a3\u306a\u3057\u3001\u5404\u5fdc\u7b54\u306e\u5f8c\u306b 'CR' \u3092\u9001\u4fe1\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "LiteJet\u306b\u63a5\u7d9a" } } }, diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 85cf75a1105..b1267f4f2c2 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -15,6 +15,7 @@ }, "device_automation": { "trigger_subtype": { + "off": "\u30aa\u30d5", "on": "\u30aa\u30f3" } } diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json new file mode 100644 index 00000000000..11afa8cd6a7 --- /dev/null +++ b/homeassistant/components/lyric/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ja.json b/homeassistant/components/mazda/translations/ja.json index 8562ef078cc..24ac22d7950 100644 --- a/homeassistant/components/mazda/translations/ja.json +++ b/homeassistant/components/mazda/translations/ja.json @@ -3,10 +3,14 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "email": "Email", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "MyMazda\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u969b\u306b\u4f7f\u7528\u3059\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "MyMazda\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u969b\u306b\u4f7f\u7528\u3059\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30de\u30c4\u30c0 \u30b3\u30cd\u30af\u30c6\u30c3\u30c9\u30b5\u30fc\u30d3\u30b9 - \u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8ffd\u52a0" } } - } + }, + "title": "\u30de\u30c4\u30c0 \u30b3\u30cd\u30af\u30c6\u30c3\u30c9\u30b5\u30fc\u30d3\u30b9" } \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ja.json b/homeassistant/components/meteoclimatic/translations/ja.json index f8e6a9c606b..e720b5151c4 100644 --- a/homeassistant/components/meteoclimatic/translations/ja.json +++ b/homeassistant/components/meteoclimatic/translations/ja.json @@ -5,6 +5,7 @@ "data": { "code": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u30b3\u30fc\u30c9" }, + "description": "Meteoclimatic\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u30b3\u30fc\u30c9\u3092\u5165\u529b(\u4f8b: ESCAT4300000043206B)", "title": "Meteoclimatic" } } diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index b8f5a423124..e3e621f048b 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" }, "step": { diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 89f065c295e..f19ab66421b 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -5,6 +5,7 @@ }, "step": { "hassio_confirm": { + "description": "\u30a2\u30c9\u30aa\u30f3: {addon}\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u308b\u3001motionEye\u30b5\u30fc\u30d3\u30b9\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306b\u3001Home Assistant\u3092\u8a2d\u3057\u307e\u3059\u304b\uff1f", "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306emotionEye" }, "user": { diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 4b4977a298b..9c74bf06f54 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -43,6 +43,7 @@ "data": { "birth_enable": "\u30d0\u30fc\u30b9(birth)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", "birth_payload": "\u30d0\u30fc\u30b9(Birth)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9", + "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b", "will_enable": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", "will_payload": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9" }, diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index bbe592fc697..bc768f5db1c 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -7,19 +7,46 @@ "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "error": { + "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "step": { + "gw_mqtt": { + "data": { + "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" + }, + "description": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "gw_serial": { + "data": { + "baud_rate": "\u30dc\u30fc\u30ec\u30fc\u30c8", + "device": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" + }, + "description": "\u30b7\u30ea\u30a2\u30eb\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, "gw_tcp": { "data": { "device": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306eIP\u30a2\u30c9\u30ec\u30b9", - "tcp_port": "\u30dd\u30fc\u30c8" - } + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "tcp_port": "\u30dd\u30fc\u30c8", + "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" + }, + "description": "\u30a4\u30fc\u30b5\u30cd\u30c3\u30c8 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "user": { + "data": { + "gateway_type": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u7a2e\u985e" + }, + "description": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3078\u306e\u63a5\u7d9a\u65b9\u6cd5\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" } } - } + }, + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 56c111e95fa..e523422b9e8 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -6,6 +6,11 @@ } } }, + "device_automation": { + "trigger_subtype": { + "schedule": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/number/translations/ja.json b/homeassistant/components/number/translations/ja.json new file mode 100644 index 00000000000..1672cb3bf4f --- /dev/null +++ b/homeassistant/components/number/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u6570" +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 362eaac4cd0..45adb506487 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -13,5 +13,15 @@ "title": "OpenTherm\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" } } + }, + "options": { + "step": { + "init": { + "data": { + "read_precision": "\u7cbe\u5ea6\u3092\u8aad\u307f\u8fbc\u3080", + "set_precision": "\u7cbe\u5ea6\u3092\u8a2d\u5b9a\u3059\u308b" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index 761f9072c66..fded9e5f9fd 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -7,7 +7,8 @@ "pair": { "data": { "pin": "PIN\u30b3\u30fc\u30c9" - } + }, + "title": "\u30da\u30a2" }, "user": { "data": { diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index fef09a888b9..50d46d5dcce 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -5,14 +5,16 @@ }, "error": { "invalid_webhook_device": "Webhook\u3078\u306e\u30c7\u30fc\u30bf\u9001\u4fe1\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002Airlock\u3067\u306e\u307f\u5229\u7528\u53ef\u80fd\u3067\u3059", - "no_api_method": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8ffd\u52a0\u3059\u308b\u304b\u3001webhook\u3092\u9078\u629e\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "no_api_method": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8ffd\u52a0\u3059\u308b\u304b\u3001webhook\u3092\u9078\u629e\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "no_auth_token": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "step": { "api_method": { "data": { "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059", "use_webhook": "webhook\u3092\u4f7f\u7528" - } + }, + "title": "API\u65b9\u5f0f\u3092\u9078\u629e" }, "user": { "title": "Plaato Wdebhookvices\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index c2bcc8971ec..f774ad7d667 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -24,8 +24,10 @@ }, "mode": { "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9(\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "mode": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30e2\u30fc\u30c9" }, + "description": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e2\u30fc\u30c9\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u81ea\u52d5\u691c\u51fa\u3092\u9078\u629e\u3059\u308b\u3068\u3001IP\u30a2\u30c9\u30ec\u30b9\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u3067\u3082\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002", "title": "Play Station 4" } } diff --git a/homeassistant/components/ring/translations/ja.json b/homeassistant/components/ring/translations/ja.json index f030f1c48b4..513210e74b8 100644 --- a/homeassistant/components/ring/translations/ja.json +++ b/homeassistant/components/ring/translations/ja.json @@ -11,7 +11,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Ring\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/ja.json b/homeassistant/components/rituals_perfume_genie/translations/ja.json index 896966aee6c..3cc00df0e04 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/ja.json +++ b/homeassistant/components/rituals_perfume_genie/translations/ja.json @@ -3,8 +3,10 @@ "step": { "user": { "data": { + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "Rituals\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index a42202307f2..02452958bdb 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -1,6 +1,10 @@ { "config": { "step": { + "discovery_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "Roku" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index a6f3c7c8023..01672ec18e6 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -4,12 +4,16 @@ "step": { "gateway_entry": { "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" }, "description": "ScreenLogic\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "ScreenLogic" }, "gateway_select": { + "data": { + "selected_gateway": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" + }, "title": "ScreenLogic" } } diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 546d0d2d1df..9c38c87bf66 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_frequency": "\u73fe\u5728\u306e {entity_name} \u983b\u5ea6(frequency)", "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrogen_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -14,6 +15,7 @@ "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb" }, "trigger_type": { + "frequency": "{entity_name} \u983b\u5ea6(frequency)\u306e\u5909\u66f4", "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrogen_monoxide": "{entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index a9988181049..7f253907705 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "error": { + "invalid_key_format": "\u30ad\u30fc\u304c\u300116\u9032\u6570\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u30020\u301c9\u3068A\uff5eF\u306e\u307f\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_key_length": "\u30ad\u30fc\u306e\u9577\u3055\u304c\u9069\u5207\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u300216\u300124\u3001\u307e\u305f\u306f32\u306e16\u9032\u6570\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "invalid_ping": "ping\u9593\u9694\u306f1\u301c1440\u5206\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { @@ -14,7 +16,8 @@ "encryption_key": "\u6697\u53f7\u5316\u30ad\u30fc", "ping_interval": "Ping\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u5206)", "port": "\u30dd\u30fc\u30c8", - "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", + "zones": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30be\u30fc\u30f3\u6570" } } } @@ -22,6 +25,10 @@ "options": { "step": { "options": { + "data": { + "ignore_timestamps": "SIA\u30a4\u30d9\u30f3\u30c8\u306e\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7 \u30c1\u30a7\u30c3\u30af\u3092\u7121\u8996\u3059\u308b" + }, + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a: {account}", "title": "SIA\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3002" } } diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json index 59ce3959a5f..cd624e3113f 100644 --- a/homeassistant/components/sma/translations/ja.json +++ b/homeassistant/components/sma/translations/ja.json @@ -1,11 +1,16 @@ { "config": { + "error": { + "cannot_retrieve_device_info": "\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093" + }, "step": { "user": { "data": { + "group": "\u30b0\u30eb\u30fc\u30d7", "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, + "description": "SMA\u30c7\u30d0\u30a4\u30b9\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "SMA Solar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index 1efd17a5cfa..cdf81a2bd91 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "SmartTub\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 896966aee6c..7ad990022dc 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "mijn.ithodaalderop.nl\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3" } } } diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index cd57c7bfddd..d48e1770c74 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -17,5 +17,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "\u8eca\u4e21\u30dd\u30fc\u30ea\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b" + }, + "description": "\u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u8eca\u4e21\u30dd\u30fc\u30ea\u30f3\u30b0\u306f2\u6642\u9593\u3054\u3068\u306b\u8eca\u4e21\u306b\u30ea\u30e2\u30fc\u30c8\u30b3\u30de\u30f3\u30c9\u3092\u9001\u4fe1\u3057\u3066\u3001\u65b0\u3057\u3044\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\u8eca\u4e21\u30dd\u30fc\u30ea\u30f3\u30b0\u3092\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u8eca\u4e21\u304c\u81ea\u52d5\u7684\u306b\u30c7\u30fc\u30bf\u3092\u30d7\u30c3\u30b7\u30e5\u3057\u305f\u3068\u304d(\u901a\u5e38\u306f\u30a8\u30f3\u30b8\u30f3\u306e\u505c\u6b62\u5f8c)\u306b\u306e\u307f\u3001\u65b0\u3057\u3044\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3059\u3002", + "title": "Subaru Starlink\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index c9bd081bdbe..a34853576cf 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", - "token": "\u30c8\u30fc\u30af\u30f3" + "token": "\u30c8\u30fc\u30af\u30f3", + "url": "URL" } } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index fce5e777fd2..d6bd56bb379 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -5,7 +5,8 @@ "authenticate": { "data": { "api_key": "API\u30ad\u30fc" - } + }, + "description": "{name} \u306e\u8a2d\u5b9a\u3067\u8a2d\u5b9a\u3057\u305fAPI\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json new file mode 100644 index 00000000000..fa90ba9f383 --- /dev/null +++ b/homeassistant/components/tasmota/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_discovery_topic": "(\u4e0d\u6b63\u306a)Invalid discovery topic prefix." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "(\u691c\u51fa)Discovery topic prefix" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index dbbb20739c7..1413e073362 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -13,6 +13,9 @@ }, "description": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u5834\u6240 {location_id} \u306b\u5165\u529b\u3057\u307e\u3059", "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" + }, + "reauth_confirm": { + "description": "Total Connect\u306f\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } } diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 3be6be36350..1ef29de844c 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -17,7 +17,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(discovery)\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index 62c565c4f57..eed21e239b6 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 3f6692e28b1..2ed28caa2b1 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -49,7 +49,8 @@ "init": { "data": { "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)" - } + }, + "title": "Tuya\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index 5eeb2e30bd7..687cb7a55c3 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -5,7 +5,8 @@ "data": { "host": "Twinkly device\u306e\u30db\u30b9\u30c8(\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9)" }, - "description": "Twinkly led string\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Twinkly led string\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "title": "Twinkly" } } } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index a27dbb2c732..2c1eac4bbdb 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "configuration_updated": "\u8a2d\u5b9a\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + }, "error": { "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, diff --git a/homeassistant/components/upb/translations/ja.json b/homeassistant/components/upb/translations/ja.json index 0d7aaf26840..34a87ae39a5 100644 --- a/homeassistant/components/upb/translations/ja.json +++ b/homeassistant/components/upb/translations/ja.json @@ -6,8 +6,11 @@ "step": { "user": { "data": { - "file_path": "UPStart UPB\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u30d1\u30b9\u3068\u540d\u524d\u3002" - } + "address": "\u30a2\u30c9\u30ec\u30b9(\u4e0a\u8a18\u306e\u8aac\u660e\u3092\u53c2\u7167)", + "file_path": "UPStart UPB\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u30d1\u30b9\u3068\u540d\u524d\u3002", + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" + }, + "title": "UPB PIM\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/upcloud/translations/ja.json b/homeassistant/components/upcloud/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/upcloud/translations/ja.json +++ b/homeassistant/components/upcloud/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index 0c81caf16aa..e42ef641473 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "incomplete_discovery": "\u4e0d\u5b8c\u5168\u306a\u691c\u51fa" + }, "flow_title": "{name}", "step": { "user": { "data": { - "unique_id": "\u30c7\u30d0\u30a4\u30b9" + "unique_id": "\u30c7\u30d0\u30a4\u30b9", + "usn": "\u30c7\u30d0\u30a4\u30b9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694(\u79d2\u3001\u6700\u5c0f30)" } } } diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 0ad91fd3bd7..713e6f6f91f 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -8,6 +8,9 @@ }, "step": { "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" }, "user": { diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json index 58165f3240e..3aef59492de 100644 --- a/homeassistant/components/vera/translations/ja.json +++ b/homeassistant/components/vera/translations/ja.json @@ -2,6 +2,13 @@ "config": { "abort": { "cannot_connect": "URL {base_url} \u3092\u6301\u3064\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "step": { + "user": { + "data": { + "vera_controller_url": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 2cfa9ea621d..52d2993104e 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -1,16 +1,37 @@ { "config": { "step": { + "installation": { + "data": { + "giid": "\u30a4\u30f3\u30b9\u30c8\u30ec\u30fc\u30b7\u30e7\u30f3" + } + }, "reauth_confirm": { "data": { + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, "user": { "data": { + "description": "Verisure My Pages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3059\u3002", + "email": "Email", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } } + }, + "options": { + "error": { + "code_format_mismatch": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306ePIN\u30b3\u30fc\u30c9\u304c\u5fc5\u8981\u306a\u6841\u6570\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "\u30ed\u30c3\u30af\u306ePIN\u30b3\u30fc\u30c9\u306e\u6841\u6570", + "lock_default_code": "\u30ed\u30c3\u30af\u7528\u306e\u30c7\u30d5\u30a9\u30eb\u30c8PIN\u30b3\u30fc\u30c9(\u4f55\u3082\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index b152338b86a..8c2f5c25764 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "Email" }, "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json index 2a002535882..8a4520bbf59 100644 --- a/homeassistant/components/vilfo/translations/ja.json +++ b/homeassistant/components/vilfo/translations/ja.json @@ -5,7 +5,8 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Vilfo\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index a6ab7c13dd8..2dbb5827e78 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -9,16 +9,30 @@ "pin": "PIN\u30b3\u30fc\u30c9" } }, + "pairing_complete": { + "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" + }, "pairing_complete_import": { "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" }, "user": { "data": { + "device_class": "\u30c7\u30d0\u30a4\u30b9\u30bf\u30a4\u30d7", "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, "title": "VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9" } } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "\u542b\u3081\u308b\u30a2\u30d7\u30ea\u3001\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea", + "include_or_exclude": "\u30a2\u30d7\u30ea\u3092\u542b\u3081\u308b\u304b\u3001\u9664\u5916\u3057\u307e\u3059\u304b\uff1f" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json index 31f1eeb3228..01d512c72e3 100644 --- a/homeassistant/components/volumio/translations/ja.json +++ b/homeassistant/components/volumio/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u691c\u51fa\u3055\u308c\u305fVolumio\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093" + }, "step": { "discovery_confirm": { "description": "Volumio (`{name}`) \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/water_heater/translations/ja.json b/homeassistant/components/water_heater/translations/ja.json new file mode 100644 index 00000000000..bfbcb3fb697 --- /dev/null +++ b/homeassistant/components/water_heater/translations/ja.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + } + }, + "state": { + "_": { + "eco": "\u30a8\u30b3", + "gas": "\u30ac\u30b9", + "off": "\u30aa\u30d5", + "performance": "\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index c42311694a0..8612554fb0c 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -3,7 +3,10 @@ "step": { "user": { "data": { - "name": "\u540d\u524d" + "destination": "\u76ee\u7684\u5730", + "name": "\u540d\u524d", + "origin": "\u30aa\u30ea\u30b8\u30f3", + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30ab\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" } @@ -13,9 +16,14 @@ "step": { "init": { "data": { - "units": "\u5358\u4f4d" + "avoid_ferries": "\u30d5\u30a7\u30ea\u30fc\u3092\u907f\u3051\u307e\u3059\u304b\uff1f", + "avoid_toll_roads": "\u6709\u6599\u9053\u8def\u3092\u907f\u3051\u307e\u3059\u304b\uff1f", + "realtime": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u306e\u79fb\u52d5\u6642\u9593\uff1f", + "units": "\u5358\u4f4d", + "vehicle_type": "\u8eca\u4e21\u30bf\u30a4\u30d7" } } } - } + }, + "title": "Waze Travel Time" } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/whirlpool/translations/ja.json +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/wiffi/translations/ja.json b/homeassistant/components/wiffi/translations/ja.json index 1102213699d..ba9f63c6e7c 100644 --- a/homeassistant/components/wiffi/translations/ja.json +++ b/homeassistant/components/wiffi/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "addr_in_use": "\u30b5\u30fc\u30d0\u30fc\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + "addr_in_use": "\u30b5\u30fc\u30d0\u30fc\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "start_server_failed": "\u30b5\u30fc\u30d0\u30fc\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, "step": { "user": { @@ -11,5 +12,14 @@ "title": "WIFFI\u30c7\u30d0\u30a4\u30b9\u7528\u306eTCP\u30b5\u30fc\u30d0\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u5206)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.ca.json b/homeassistant/components/wled/translations/select.ca.json new file mode 100644 index 00000000000..5992a3b25f3 --- /dev/null +++ b/homeassistant/components/wled/translations/select.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "OFF", + "1": "ON", + "2": "Fins que el dispositiu es reinici\u00ef" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.de.json b/homeassistant/components/wled/translations/select.de.json new file mode 100644 index 00000000000..3af2afb8cce --- /dev/null +++ b/homeassistant/components/wled/translations/select.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Aus", + "1": "An", + "2": "Bis zum Neustart des Ger\u00e4ts" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.et.json b/homeassistant/components/wled/translations/select.et.json new file mode 100644 index 00000000000..8d52083bca5 --- /dev/null +++ b/homeassistant/components/wled/translations/select.et.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "V\u00e4ljas", + "1": "Sees", + "2": "Seade taask\u00e4ivitub" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.ja.json b/homeassistant/components/wled/translations/select.ja.json new file mode 100644 index 00000000000..32e63bcae8c --- /dev/null +++ b/homeassistant/components/wled/translations/select.ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "\u30aa\u30d5", + "1": "\u30aa\u30f3", + "2": "\u30c7\u30d0\u30a4\u30b9\u304c\u518d\u8d77\u52d5\u3059\u308b\u307e\u3067" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.ru.json b/homeassistant/components/wled/translations/select.ru.json new file mode 100644 index 00000000000..7dc9f76c13f --- /dev/null +++ b/homeassistant/components/wled/translations/select.ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "1": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "2": "\u0414\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.zh-Hant.json b/homeassistant/components/wled/translations/select.zh-Hant.json new file mode 100644 index 00000000000..fe8229341f1 --- /dev/null +++ b/homeassistant/components/wled/translations/select.zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "\u95dc\u9589", + "1": "\u958b\u555f", + "2": "\u76f4\u5230\u88dd\u7f6e\u91cd\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/ja.json b/homeassistant/components/wolflink/translations/ja.json index 896966aee6c..66f33d9411e 100644 --- a/homeassistant/components/wolflink/translations/ja.json +++ b/homeassistant/components/wolflink/translations/ja.json @@ -1,9 +1,15 @@ { "config": { "step": { + "device": { + "data": { + "device_name": "\u30c7\u30d0\u30a4\u30b9" + } + }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index f9b6778da08..229aa40a4ef 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -1,9 +1,54 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "aktiviert": "\u6709\u52b9\u5316", + "antilegionellenfunktion": "\u30ec\u30b8\u30aa\u30cd\u30e9\u83cc\u5bfe\u7b56\u6a5f\u80fd", + "at_abschaltung": "OT\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3", + "at_frostschutz": "OT\u971c\u9632\u6b62", + "aus": "\u7121\u52b9", + "auto": "\u30aa\u30fc\u30c8", + "automatik_aus": "\u81ea\u52d5\u30aa\u30d5", + "automatik_ein": "\u81ea\u52d5\u30aa\u30f3", + "deaktiviert": "\u975e\u6d3b\u6027", + "eco": "\u30a8\u30b3", + "ein": "\u6709\u52b9", + "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", + "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", + "parallelbetrieb": "\u30d1\u30e9\u30ec\u30eb\u30e2\u30fc\u30c9", + "partymodus": "\u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9", "permanent": "\u6c38\u7d9a", - "stabilisierung": "\u5b89\u5b9a\u5316" + "permanentbetrieb": "\u30d1\u30fc\u30de\u30cd\u30f3\u30c8\u30e2\u30fc\u30c9", + "reduzierter_betrieb": "\u5236\u9650\u4ed8\u304d\u30e2\u30fc\u30c9", + "rt_abschaltung": "RT\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3", + "rt_frostschutz": "RT\u971c\u9632\u6b62", + "ruhekontakt": "\u6b8b\u308a\u306e\u9023\u7d61\u5148(Rest contact)", + "schornsteinfeger": "\u6392\u51fa\u91cf\u30c6\u30b9\u30c8", + "smart_grid": "\u30b9\u30de\u30fc\u30c8\u30b0\u30ea\u30c3\u30c9", + "smart_home": "\u30b9\u30de\u30fc\u30c8\u30db\u30fc\u30e0", + "softstart": "\u30bd\u30d5\u30c8\u30b9\u30bf\u30fc\u30c8", + "solarbetrieb": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30fc\u30c9", + "sparbetrieb": "\u30a8\u30b3\u30ce\u30df\u30fc\u30e2\u30fc\u30c9", + "sparen": "\u30a8\u30b3\u30ce\u30df\u30fc", + "spreizung_hoch": "dT\u304c\u5e83\u3059\u304e\u308b(dT too wide)", + "spreizung_kf": "\u5e83\u3052\u308b(Spread) KF", + "stabilisierung": "\u5b89\u5b9a\u5316", + "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4", + "start": "\u8d77\u52d5", + "storung": "\u30d5\u30a9\u30fc\u30eb\u30c8", + "taktsperre": "\u30a2\u30f3\u30c1\u30b5\u30a4\u30af\u30eb", + "telefonfernschalter": "\u96fb\u8a71\u306e\u30ea\u30e2\u30fc\u30c8\u30b9\u30a4\u30c3\u30c1", + "test": "\u30c6\u30b9\u30c8", + "tpw": "TPW", + "urlaubsmodus": "\u30db\u30ea\u30c7\u30fc(\u4f11\u65e5)\u30e2\u30fc\u30c9", + "ventilprufung": "\u30d0\u30eb\u30d6\u30c6\u30b9\u30c8", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW\u30af\u30a4\u30c3\u30af\u30b9\u30bf\u30fc\u30c8", + "warmwasserbetrieb": "DHW\u30e2\u30fc\u30c9", + "warmwasservorrang": "DHW\u306e\u512a\u5148\u9806\u4f4d", + "zunden": "\u30a4\u30b0\u30cb\u30c3\u30b7\u30e7\u30f3" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index 463949a796e..fc1b1c265a2 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -1,17 +1,39 @@ { "config": { + "abort": { + "not_xiaomi_aqara": "Xiaomi Aqara Gateway\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u304c\u65e2\u77e5\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "error": { + "discovery_error": "Xiaomi Aqara Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Home Assistant\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u306eIP\u3092\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044", + "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9", + "invalid_key": "\u7121\u52b9\u306a\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 \u30ad\u30fc", + "invalid_mac": "\u7121\u52b9\u306aMAC\u30a2\u30c9\u30ec\u30b9" + }, "flow_title": "{name}", "step": { "select": { "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" - } + }, + "description": "\u8ffd\u52a0\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u63a5\u7d9a\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u63a5\u7d9a\u3057\u305f\u3044Xiaomi Aqara Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + }, + "settings": { + "data": { + "key": "\u3042\u306a\u305f\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30ad\u30fc", + "name": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u540d\u524d" + }, + "description": "\u30ad\u30fc(\u30d1\u30b9\u30ef\u30fc\u30c9)\u306f\u3001\u3053\u3061\u3089\u306e\u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz \u3092\u53c2\u8003\u306b\u3057\u3066\u53d6\u5f97\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u30ad\u30fc\u304c\u63d0\u4f9b\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u307f\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u307e\u3059\u3002", + "title": "Xiaomi Aqara Gateway\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" }, "user": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9", "mac": "Mac\u30a2\u30c9\u30ec\u30b9 (\u30aa\u30d7\u30b7\u30e7\u30f3)" - } + }, + "description": "Xiaomi Aqara Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u3068MAC\u30a2\u30c9\u30ec\u30b9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", + "title": "Xiaomi Aqara Gateway" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 3395d2eff44..91c53d294f2 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -6,6 +6,10 @@ }, "error": { "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cloud_login_error": "Xiaomi Miio Cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a8d\u8a3c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cloud_no_devices": "\u3053\u306eXiaomi Miio cloud\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", + "no_device_selected": "\u30c7\u30d0\u30a4\u30b9\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown_device": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb\u304c\u4e0d\u660e\u306a\u306e\u3067\u3001\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3067\u304d\u307e\u305b\u3093\u3002", "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" }, "flow_title": "{name}", @@ -16,34 +20,59 @@ "cloud_password": "\u30af\u30e9\u30a6\u30c9\u30d1\u30b9\u30ef\u30fc\u30c9", "cloud_username": "\u30af\u30e9\u30a6\u30c9\u306e\u30e6\u30fc\u30b6\u30fc\u540d", "manual": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b(\u975e\u63a8\u5968)" - } + }, + "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "connect": { "data": { "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" }, - "description": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u304b\u3089\u30c7\u30d0\u30a4\u30b9 \u30e2\u30c7\u30eb\u3092\u624b\u52d5\u3067\u9078\u629e\u3057\u307e\u3059\u3002" + "description": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u304b\u3089\u30c7\u30d0\u30a4\u30b9 \u30e2\u30c7\u30eb\u3092\u624b\u52d5\u3067\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "device": { "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9" - } + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "name": "\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d", + "token": "API\u30c8\u30fc\u30af\u30f3" + }, + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", + "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "gateway": { "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9" - } + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "name": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u540d\u524d", + "token": "API\u30c8\u30fc\u30af\u30f3" + }, + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", + "title": "Xiaomi Gateway\u306b\u63a5\u7d9a" }, "manual": { "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9" - } + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "token": "API\u30c8\u30fc\u30af\u30f3" + }, + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", + "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" + }, + "reauth_confirm": { + "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8cc7\u683c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "select": { "data": { "select_device": "Miio\u30c7\u30d0\u30a4\u30b9" }, - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308bXiaomi Miio\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308bXiaomi Miio\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" + }, + "user": { + "data": { + "gateway": "Xiaomi Gateway\u306b\u63a5\u7d9a" + }, + "description": "\u63a5\u7d9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Xiaomi Miio" } } }, @@ -53,6 +82,9 @@ }, "step": { "init": { + "data": { + "cloud_subdevices": "\u30af\u30e9\u30a6\u30c9\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3055\u308c\u305f\u30b5\u30d6\u30c7\u30d0\u30a4\u30b9\u3092\u53d6\u5f97\u3059\u308b" + }, "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", "title": "Xiaomi Miio" } diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json index f537c6a63a5..f17f9d146c7 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ja.json +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -5,14 +5,16 @@ "data": { "area_id": "\u30a8\u30ea\u30a2ID", "name": "\u540d\u524d", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { "data": { "area_id": "\u30a8\u30ea\u30a2ID", "name": "\u540d\u524d", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/ja.json b/homeassistant/components/yamaha_musiccast/translations/ja.json index 346c1b50ee0..647878af0dd 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ja.json +++ b/homeassistant/components/yamaha_musiccast/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "yxc_control_url_missing": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebURL\u306f\u3001ssdp\u306e\u8a18\u8ff0\u306b\u3042\u308a\u307e\u305b\u3093\u3002" + }, "error": { "no_musiccast_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fMusicCast\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3088\u3046\u3067\u3059\u3002" }, diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index 5b9670a9960..3d4fc5381aa 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -5,10 +5,16 @@ "discovery_confirm": { "description": "{model} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } }, @@ -16,8 +22,13 @@ "step": { "init": { "data": { - "model": "\u30e2\u30c7\u30eb(\u30aa\u30d7\u30b7\u30e7\u30f3)" - } + "model": "\u30e2\u30c7\u30eb", + "nightlight_switch": "\u5e38\u591c\u706f\u30b9\u30a4\u30c3\u30c1\u3092\u4f7f\u7528\u3059\u308b", + "save_on_change": "\u5909\u66f4\u6642\u306b\u30b9\u30c6\u30fc\u30bf\u30b9\u3092\u4fdd\u5b58", + "transition": "\u9077\u79fb\u6642\u9593(Transition Time)(ms)", + "use_music_mode": "\u97f3\u697d\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3059\u308b" + }, + "description": "\u30e2\u30c7\u30eb\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/youless/translations/ja.json b/homeassistant/components/youless/translations/ja.json index a42202307f2..5021773b5ad 100644 --- a/homeassistant/components/youless/translations/ja.json +++ b/homeassistant/components/youless/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" } } } diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 83b7fd64acb..f906597a8cb 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -9,6 +9,13 @@ "confirm": { "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" }, + "pick_radio": { + "data": { + "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, + "description": "Zigbee\u7121\u7dda\u6a5f\u306e\u30bf\u30a4\u30d7\u3092\u9078\u3076", + "title": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, "port_config": { "data": { "baudrate": "\u30dd\u30fc\u30c8\u901f\u5ea6", @@ -19,6 +26,9 @@ "title": "\u8a2d\u5b9a" }, "user": { + "data": { + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "description": "Zigbee radio\u7528\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e", "title": "ZHA" } @@ -36,6 +46,7 @@ "device_automation": { "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", + "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", @@ -51,6 +62,7 @@ "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { + "device_offline": "\u30c7\u30d0\u30a4\u30b9\u304c\u30aa\u30d5\u30e9\u30a4\u30f3", "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b" } diff --git a/homeassistant/components/zodiac/translations/sensor.ja.json b/homeassistant/components/zodiac/translations/sensor.ja.json new file mode 100644 index 00000000000..fc95414254a --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.ja.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u6c34\u74f6\u5ea7", + "aries": "\u7261\u7f8a\u5ea7", + "cancer": "\u304b\u306b\u5ea7", + "capricorn": "\u5c71\u7f8a\u5ea7", + "gemini": "\u53cc\u5b50\u5ea7", + "leo": "\u7345\u5b50\u5ea7", + "libra": "\u5929\u79e4\u5ea7", + "pisces": "\u3046\u304a\u5ea7", + "sagittarius": "\u5c04\u624b\u5ea7", + "scorpio": "\u880d\u5ea7", + "taurus": "\u7261\u725b\u5ea7", + "virgo": "\u4e59\u5973\u5ea7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json index 52e0afbe059..b4772630366 100644 --- a/homeassistant/components/zoneminder/translations/ja.json +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -16,7 +16,10 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8(\u4f8b: 10.10.0.4:8010)", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "path": "ZM\u30d1\u30b9", + "path_zms": "ZMS\u30d1\u30b9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index dab546b6f2a..003efcd2fcc 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -14,6 +14,7 @@ }, "state": { "_": { + "dead": "\u30c7\u30c3\u30c9", "initializing": "\u521d\u671f\u5316\u4e2d", "ready": "\u6e96\u5099\u5b8c\u4e86", "sleeping": "\u30b9\u30ea\u30fc\u30d7" diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index deacc4c6e4b..abedcd5a7b1 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -1,12 +1,23 @@ { "config": { "abort": { - "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002" + "addon_get_discovery_info_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u691c\u51fa\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_info_failed": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_install_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_set_config_failed": "Z-Wave JS\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002", + "not_zwave_device": "\u767a\u898b\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Z-Wave\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { + "addon_start_failed": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" }, "flow_title": "{name}", + "progress": { + "install_addon": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002", + "start_addon": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u79d2\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" + }, "step": { "configure_addon": { "data": { @@ -15,16 +26,30 @@ "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" - } + }, + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, + "install_addon": { + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" + }, "manual": { "data": { "url": "URL" } }, + "on_supervisor": { + "data": { + "use_addon": "\u30a2\u30c9\u30aa\u30f3 Z-Wave JS Supervisor\u3092\u4f7f\u7528" + }, + "description": "Z-Wave JS Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f", + "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" + }, + "start_addon": { + "title": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u3092\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059\u3002" + }, "usb_confirm": { "description": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } @@ -41,22 +66,33 @@ "set_value": "Z-Wave\u5024\u306e\u8a2d\u5b9a\u5024" }, "condition_type": { + "config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024", "node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9" }, "trigger_type": { "event.notification.entry_control": "\u30a8\u30f3\u30c8\u30ea\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", "event.notification.notification": "\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", - "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" + "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", + "zwave_js.value_updated.config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u306e\u5909\u66f4" } }, "options": { "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u691c\u51fa\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_info_failed": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_install_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_set_config_failed": "Z-Wave JS\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3057\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" }, + "progress": { + "install_addon": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002", + "start_addon": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u79d2\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" + }, "step": { "configure_addon": { "data": { From 263101b2ab56a58f0bc9989f5a43fd404044f80d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 01:14:42 +0100 Subject: [PATCH 0704/1452] Create new usb constants (#60086) --- homeassistant/components/usb/__init__.py | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 355b60906b3..8c39ba9e6be 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import TypedDict +from typing import Final, TypedDict from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -31,6 +31,15 @@ _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown +# Attributes for UsbServiceInfo +ATTR_DESCRIPTION: Final = "description" +ATTR_DEVICE: Final = "device" +ATTR_MANUFACTURER: Final = "manufacturer" +ATTR_PID: Final = "pid" +ATTR_SERIAL_NUMBER: Final = "serial_number" +ATTR_VID: Final = "vid" + + class UsbServiceInfo(TypedDict): """Prepared info from usb entries.""" @@ -171,20 +180,20 @@ class USBDiscovery: self.seen.add(device_tuple) matched = [] for matcher in self.usb: - if "vid" in matcher and device.vid != matcher["vid"]: + if ATTR_VID in matcher and device.vid != matcher[ATTR_VID]: continue - if "pid" in matcher and device.pid != matcher["pid"]: + if ATTR_PID in matcher and device.pid != matcher[ATTR_PID]: continue - if "serial_number" in matcher and not _fnmatch_lower( - device.serial_number, matcher["serial_number"] + if ATTR_SERIAL_NUMBER in matcher and not _fnmatch_lower( + device.serial_number, matcher[ATTR_SERIAL_NUMBER] ): continue - if "manufacturer" in matcher and not _fnmatch_lower( - device.manufacturer, matcher["manufacturer"] + if ATTR_MANUFACTURER in matcher and not _fnmatch_lower( + device.manufacturer, matcher[ATTR_MANUFACTURER] ): continue - if "description" in matcher and not _fnmatch_lower( - device.description, matcher["description"] + if ATTR_DESCRIPTION in matcher and not _fnmatch_lower( + device.description, matcher[ATTR_DESCRIPTION] ): continue matched.append(matcher) From aa5cf175f4fef42df213a503a3e6f1ce2d834963 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 21 Nov 2021 18:48:57 -0600 Subject: [PATCH 0705/1452] Set Sonos availability based on activity and discovery (#59994) --- homeassistant/components/sonos/__init__.py | 31 ++-- homeassistant/components/sonos/const.py | 8 +- homeassistant/components/sonos/entity.py | 2 - homeassistant/components/sonos/exception.py | 5 + homeassistant/components/sonos/helpers.py | 48 +++++-- homeassistant/components/sonos/speaker.py | 150 +++++++++----------- homeassistant/components/sonos/switch.py | 7 + 7 files changed, 139 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 72e5a33ca28..c8defb1ff14 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -5,6 +5,7 @@ import asyncio from collections import OrderedDict import datetime from enum import Enum +from functools import partial import logging import socket from urllib.parse import urlparse @@ -22,17 +23,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from .alarms import SonosAlarms from .const import ( + AVAILABILITY_CHECK_INTERVAL, DATA_SONOS, DATA_SONOS_DISCOVERY_MANAGER, DISCOVERY_INTERVAL, DOMAIN, PLATFORMS, + SONOS_CHECK_ACTIVITY, SONOS_REBOOTED, - SONOS_SEEN, + SONOS_SPEAKER_ACTIVITY, UPNP_ST, ) from .favorites import SonosFavorites @@ -187,7 +190,7 @@ class SonosDiscoveryManager: 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()) + *(speaker.async_offline() for speaker in self.data.discovered.values()) ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() @@ -212,7 +215,7 @@ class SonosDiscoveryManager: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) coord_dict[soco.household_id] = new_coordinator - speaker.setup() + speaker.setup(self.entry) except (OSError, SoCoException): _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) @@ -228,10 +231,7 @@ class SonosDiscoveryManager: ), None, ) - - if known_uid: - dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}") - else: + if not known_uid: soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED) if soco and soco.is_visible: self._discovered_player(soco) @@ -261,7 +261,9 @@ class SonosDiscoveryManager: ): async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}", soco) else: - async_dispatcher_send(self.hass, f"{SONOS_SEEN}-{uid}") + async_dispatcher_send( + self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery" + ) async def _async_ssdp_discovered_player(self, info, change): if change == ssdp.SsdpChange.BYEBYE: @@ -327,3 +329,14 @@ class SonosDiscoveryManager: self.hass, self._async_ssdp_discovered_player, {"st": UPNP_ST} ) ) + + self.entry.async_on_unload( + self.hass.helpers.event.async_track_time_interval( + partial( + async_dispatcher_send, + self.hass, + SONOS_CHECK_ACTIVITY, + ), + AVAILABILITY_CHECK_INTERVAL, + ) + ) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index abd04652936..05abc662b48 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -135,6 +135,7 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] +SONOS_CHECK_ACTIVITY = "sonos_check_activity" SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" @@ -143,18 +144,17 @@ SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" +SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity" SONOS_SPEAKER_ADDED = "sonos_speaker_added" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_REBOOTED = "sonos_rebooted" -SONOS_SEEN = "sonos_seen" SOURCE_LINEIN = "Line-in" SOURCE_TV = "TV" +AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) +AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) SCAN_INTERVAL = datetime.timedelta(seconds=10) DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) -SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL SUBSCRIPTION_TIMEOUT = 1200 - -MDNS_SERVICE = "_sonos._tcp.local." diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 0579c4f5c9b..d8196ffdfa6 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -39,8 +39,6 @@ class SonosEntity(Entity): async def async_added_to_hass(self) -> None: """Handle common setup when added to hass.""" - await self.speaker.async_seen() - self.async_on_remove( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index 3d5a1230bcb..d7f1a2e6a96 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -1,6 +1,11 @@ """Sonos specific exceptions.""" from homeassistant.components.media_player.errors import BrowseError +from homeassistant.exceptions import HomeAssistantError class UnknownMediaType(BrowseError): """Unknown media type.""" + + +class SpeakerUnavailable(HomeAssistantError): + """Speaker is unavailable.""" diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 490bcdefba5..e80d16a491b 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -1,32 +1,43 @@ """Helper methods for common tasks.""" from __future__ import annotations -from collections.abc import Callable -import functools as ft import logging -from typing import Any +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast from soco.exceptions import SoCoException, SoCoUPnPException from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import SONOS_SPEAKER_ACTIVITY +from .exception import SpeakerUnavailable + +if TYPE_CHECKING: + from .entity import SonosEntity + from .speaker import SonosSpeaker UID_PREFIX = "RINCON_" UID_POSTFIX = "01400" +WrapFuncType = TypeVar("WrapFuncType", bound=Callable[..., Any]) + _LOGGER = logging.getLogger(__name__) -def soco_error(errorcodes: list[str] | None = None) -> Callable: +def soco_error( + errorcodes: list[str] | None = None, raise_on_err: bool = True +) -> Callable: """Filter out specified UPnP errors and raise exceptions for service calls.""" - def decorator(funct: Callable) -> Callable: + def decorator(funct: WrapFuncType) -> WrapFuncType: """Decorate functions.""" - @ft.wraps(funct) - def wrapper(*args: Any, **kwargs: Any) -> Any: + def wrapper(self: SonosSpeaker | SonosEntity, *args: Any, **kwargs: Any) -> Any: """Wrap for all soco UPnP exception.""" try: - return funct(*args, **kwargs) + result = funct(self, *args, **kwargs) + except SpeakerUnavailable: + return None except (OSError, SoCoException, SoCoUPnPException) as err: error_code = getattr(err, "error_code", None) function = funct.__name__ @@ -34,10 +45,25 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: _LOGGER.debug( "Error code %s ignored in call to %s", error_code, function ) - return - raise HomeAssistantError(f"Error calling {function}: {err}") from err + return None - return wrapper + # Prefer the entity_id if available, zone name as a fallback + # Needed as SonosSpeaker instances are not entities + zone_name = getattr(self, "speaker", self).zone_name + target = getattr(self, "entity_id", zone_name) + message = f"Error calling {function} on {target}: {err}" + if raise_on_err: + raise HomeAssistantError(message) from err + + _LOGGER.warning(message) + return None + + dispatcher_send( + self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", funct.__name__ + ) + return result + + return cast(WrapFuncType, wrapper) return decorator diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 593bd8f034a..1acb814ee17 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -7,6 +7,7 @@ import contextlib import datetime from functools import partial import logging +import time from typing import Any import urllib.parse @@ -19,30 +20,30 @@ from soco.music_library import MusicLibrary from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot -from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send, - dispatcher_connect, dispatcher_send, ) from homeassistant.util import dt as dt_util from .alarms import SonosAlarms from .const import ( + AVAILABILITY_TIMEOUT, BATTERY_SCAN_INTERVAL, DATA_SONOS, DOMAIN, - MDNS_SERVICE, PLATFORMS, SCAN_INTERVAL, - SEEN_EXPIRE_TIME, + SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, @@ -50,7 +51,7 @@ from .const import ( SONOS_ENTITY_CREATED, SONOS_POLL_UPDATE, SONOS_REBOOTED, - SONOS_SEEN, + SONOS_SPEAKER_ACTIVITY, SONOS_SPEAKER_ADDED, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, @@ -154,6 +155,7 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) self._share_link_plugin: ShareLinkPlugin | None = None + self.available = True # Synchronization helpers self._is_ready: bool = False @@ -164,16 +166,13 @@ class SonosSpeaker: self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} + self._last_activity: datetime.datetime | None = None # Scheduled callback handles self._poll_timer: Callable | None = None - self._seen_timer: Callable | None = None # Dispatcher handles - self._entity_creation_dispatcher: Callable | None = None - self._group_dispatcher: Callable | None = None - self._reboot_dispatcher: Callable | None = None - self._seen_dispatcher: Callable | None = None + self.dispatchers: list[Callable] = [] # Device information self.mac_address = speaker_info["mac_address"] @@ -208,26 +207,32 @@ class SonosSpeaker: self.snapshot_group: list[SonosSpeaker] | None = None self._group_members_missing: set[str] = set() - def setup(self) -> None: + async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: + """Connect dispatchers in async context during setup.""" + dispatch_pairs = ( + (SONOS_CHECK_ACTIVITY, self.async_check_activity), + (SONOS_SPEAKER_ADDED, self.update_group_for_uid), + (f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity), + (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), + (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity), + ) + + for (signal, target) in dispatch_pairs: + entry.async_on_unload( + async_dispatcher_connect( + self.hass, + signal, + target, + ) + ) + + def setup(self, entry: ConfigEntry) -> None: """Run initial setup of the speaker.""" self.set_basic_info() - - self._entity_creation_dispatcher = dispatcher_connect( - self.hass, - f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", - self.async_handle_new_entity, - ) - self._seen_dispatcher = dispatcher_connect( - self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen - ) - self._reboot_dispatcher = dispatcher_connect( - self.hass, f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted - ) - self._group_dispatcher = dispatcher_connect( - self.hass, - SONOS_SPEAKER_ADDED, - self.update_group_for_uid, + future = asyncio.run_coroutine_threadsafe( + self.async_setup_dispatchers(entry), self.hass.loop ) + future.result(timeout=10) if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info @@ -291,11 +296,6 @@ class SonosSpeaker: # # Properties # - @property - def available(self) -> bool: - """Return whether this speaker is available.""" - return self._seen_timer is not None - @property def alarms(self) -> SonosAlarms: """Return the SonosAlarms instance for this household.""" @@ -408,7 +408,7 @@ class SonosSpeaker: self.zone_name, exc_info=exception, ) - await self.async_unseen() + await self.async_offline() @callback def async_dispatch_event(self, event: SonosEvent) -> None: @@ -420,6 +420,8 @@ class SonosSpeaker: self._poll_timer() self._poll_timer = None + self.speaker_activity(f"{event.service.service_type} subscription") + dispatcher = self._event_dispatchers[event.service.service_type] dispatcher(event) @@ -500,65 +502,43 @@ class SonosSpeaker: # Speaker availability methods # @callback - def _async_reset_seen_timer(self): - """Reset the _seen_timer scheduler.""" - 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 - ) - - async def async_seen(self, soco: SoCo | None = None) -> None: - """Record that this speaker was seen right now.""" - if soco is not None: - self.soco = soco - + def speaker_activity(self, source): + """Track the last activity on this speaker, set availability and resubscribe.""" + _LOGGER.debug("Activity on %s from %s", self.zone_name, source) + self._last_activity = time.monotonic() was_available = self.available - - self._async_reset_seen_timer() - - if was_available: + self.available = True + if not was_available: self.async_write_entity_states() + self.hass.async_create_task(self.async_subscribe()) + + async def async_check_activity(self, now: datetime.datetime) -> None: + """Validate availability of the speaker based on recent activity.""" + if time.monotonic() - self._last_activity < AVAILABILITY_TIMEOUT: + return + + try: + _ = await self.hass.async_add_executor_job(getattr, self.soco, "volume") + except (OSError, SoCoException): + pass + else: + self.speaker_activity("timeout poll") + return + + if not self.available: return _LOGGER.debug( - "%s [%s] was not available, setting up", + "No recent activity and cannot reach %s, marking unavailable", self.zone_name, - self.soco.ip_address, - ) - - if self._is_ready and not self.subscriptions_failed: - done = await self.async_subscribe() - if not done: - await self.async_unseen() - - self.async_write_entity_states() - - async def async_unseen( - self, callback_timestamp: datetime.datetime | None = None - ) -> None: - """Make this player unavailable when it was not seen recently.""" - data = self.hass.data[DATA_SONOS] - if (zcname := data.mdns_names.get(self.soco.uid)) and callback_timestamp: - # Called by a _seen_timer timeout, check mDNS one more time - # This should not be checked in an "active" unseen scenario - 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._async_reset_seen_timer() - return - - _LOGGER.debug( - "No activity and could not locate %s on the network. Marking unavailable", - zcname, ) + await self.async_offline() + async def async_offline(self) -> None: + """Handle removal of speaker when unavailable.""" + self.available = False self._share_link_plugin = None - if self._seen_timer: - self._seen_timer() - self._seen_timer = None - if self._poll_timer: self._poll_timer() self._poll_timer = None @@ -575,11 +555,9 @@ class SonosSpeaker: self.zone_name, soco, ) - await self.async_unsubscribe() + await self.async_offline() self.soco = soco - await self.async_subscribe() - self._async_reset_seen_timer() - self.async_write_entity_states() + self.speaker_activity("reboot") # # Battery management diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 830f3b09481..78469dc8bbc 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -20,6 +20,8 @@ from .const import ( SONOS_CREATE_SWITCHES, ) from .entity import SonosEntity +from .exception import SpeakerUnavailable +from .helpers import soco_error from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -144,8 +146,12 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): if not self.should_poll: await self.hass.async_add_executor_job(self.update) + @soco_error(raise_on_err=False) def update(self) -> None: """Fetch switch state if necessary.""" + if not self.available: + raise SpeakerUnavailable + state = getattr(self.soco, self.feature_type) setattr(self.speaker, self.feature_type, state) @@ -164,6 +170,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): """Turn the entity off.""" self.send_command(False) + @soco_error() def send_command(self, enable: bool) -> None: """Enable or disable the feature on the device.""" if self.needs_coordinator: From 02b72d8c238d6a39690f3691577ac689c67050bf Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 22 Nov 2021 02:51:22 +0200 Subject: [PATCH 0706/1452] Bump ezviz api 0.2.0.5 (#60090) --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 0c6fe4e9dcf..4618f7e4404 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], - "requirements": ["pyezviz==0.1.9.8"], + "requirements": ["pyezviz==0.2.0.5"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 1a81f10b22e..de2f298f927 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,7 +1474,7 @@ pyeverlights==0.1.0 pyevilgenius==1.0.0 # homeassistant.components.ezviz -pyezviz==0.1.9.8 +pyezviz==0.2.0.5 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d43dfb63f07..eef41267bb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ pyeverlights==0.1.0 pyevilgenius==1.0.0 # homeassistant.components.ezviz -pyezviz==0.1.9.8 +pyezviz==0.2.0.5 # homeassistant.components.fido pyfido==2.1.1 From 82a422930b64d2ac77554fc914c01c513f4d7987 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 22 Nov 2021 01:51:48 +0100 Subject: [PATCH 0707/1452] Honor "Enable newly added entities" for Fritz (#59948) --- homeassistant/components/fritz/common.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 09926e5d9ac..77d21c269bd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -442,11 +442,6 @@ class FritzDeviceBase(Entity): """No polling needed.""" return False - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - async def async_process_update(self) -> None: """Update device.""" raise NotImplementedError() From 1da251860e1061224cdb72cb12f698d5e72d8172 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 21 Nov 2021 21:32:03 -0700 Subject: [PATCH 0708/1452] Fix bugs causing SimpliSafe entities to incorrectly show `unavailable` (#59955) --- .../components/simplisafe/__init__.py | 61 ++++++++++++++----- .../simplisafe/alarm_control_panel.py | 12 +++- homeassistant/components/simplisafe/lock.py | 15 +++-- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 66 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a773304896d..044bd3651bc 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -107,7 +107,7 @@ ATTR_TIMESTAMP = "timestamp" DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel" -DEFAULT_REST_API_ERROR_COUNT = 2 +DEFAULT_ERROR_THRESHOLD = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 @@ -231,7 +231,9 @@ def _async_register_base_station( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up SimpliSafe as config entry.""" _async_standardize_config_entry(hass, entry) @@ -351,6 +353,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): async_register_admin_service(hass, DOMAIN, service, method, schema=schema) + current_options = {**entry.options} + + async def async_reload_entry(_: HomeAssistant, updated_entry: ConfigEntry) -> None: + """Handle an options update. + + This method will get called in two scenarios: + 1. When SimpliSafeOptionsFlowHandler is initiated + 2. When a new refresh token is saved to the config entry data + + We only want #1 to trigger an actual reload. + """ + nonlocal current_options + updated_options = {**updated_entry.options} + + if updated_options == current_options: + return + + await hass.config_entries.async_reload(entry.entry_id) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True @@ -365,11 +386,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle an options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - class SimpliSafe: """Define a SimpliSafe data object.""" @@ -564,7 +580,11 @@ class SimpliSafeEntity(CoordinatorEntity): assert simplisafe.coordinator super().__init__(simplisafe.coordinator) - self._rest_api_errors = 0 + # SimpliSafe can incorrectly return an error state when there isn't any + # error. This can lead to entities having an unknown state frequently. + # To protect against that, we measure an error count for each entity and only + # mark the state as unavailable if we detect a few in a row: + self._error_count = 0 if device: model = device.type.name @@ -630,7 +650,7 @@ class SimpliSafeEntity(CoordinatorEntity): system_offline = False return ( - self._rest_api_errors < DEFAULT_REST_API_ERROR_COUNT + self._error_count < DEFAULT_ERROR_THRESHOLD and self._online and not system_offline ) @@ -638,14 +658,10 @@ class SimpliSafeEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self) -> None: """Update the entity with new REST API data.""" - # SimpliSafe can incorrectly return an error state when there isn't any - # error. This can lead to the system having an unknown state frequently. - # To protect against that, we measure how many "error states" we receive - # and only alter the state if we detect a few in a row: if self.coordinator.last_update_success: - self._rest_api_errors = 0 + self.async_reset_error_count() else: - self._rest_api_errors += 1 + self.async_increment_error_count() self.async_update_from_rest_api() self.async_write_ha_state() @@ -713,6 +729,21 @@ class SimpliSafeEntity(CoordinatorEntity): self.async_update_from_rest_api() + @callback + def async_increment_error_count(self) -> None: + """Increment this entity's error count.""" + LOGGER.debug('Error for entity "%s" (total: %s)', self.name, self._error_count) + self._error_count += 1 + + @callback + def async_reset_error_count(self) -> None: + """Reset this entity's error count.""" + if self._error_count == 0: + return + + LOGGER.debug('Resetting error count for "%s"', self.name) + self._error_count = 0 + @callback def async_update_from_rest_api(self) -> None: """Update the entity when new data comes from the REST API.""" diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 7a09db18b07..e610b9139b8 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -154,11 +154,14 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Set the state based on the latest REST API data.""" if self._system.alarm_going_off: self._attr_state = STATE_ALARM_TRIGGERED + elif self._system.state == SystemStates.ERROR: + self.async_increment_error_count() elif state := STATE_MAP_FROM_REST_API.get(self._system.state): self._attr_state = state + self.async_reset_error_count() else: LOGGER.error("Unknown system state (REST API): %s", self._system.state) - self._attr_state = None + self.async_increment_error_count() async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -237,4 +240,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_changed_by = event.changed_by if TYPE_CHECKING: assert event.event_type - self._attr_state = STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type) + if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type): + self._attr_state = state + self.async_reset_error_count() + else: + LOGGER.error("Unknown alarm websocket event: %s", event.event_type) + self.async_increment_error_count() diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 12d06c1028a..15757fde2e9 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -6,12 +6,7 @@ from typing import TYPE_CHECKING, Any from simplipy.device.lock import Lock, LockStates from simplipy.errors import SimplipyError from simplipy.system.v3 import SystemV3 -from simplipy.websocket import ( - EVENT_LOCK_ERROR, - EVENT_LOCK_LOCKED, - EVENT_LOCK_UNLOCKED, - WebsocketEvent, -) +from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, WebsocketEvent from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -25,7 +20,6 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" STATE_MAP_FROM_WEBSOCKET_EVENT = { - EVENT_LOCK_ERROR: None, EVENT_LOCK_LOCKED: True, EVENT_LOCK_UNLOCKED: False, } @@ -105,4 +99,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Update the entity when new data comes from the websocket.""" if TYPE_CHECKING: assert event.event_type - self._attr_is_locked = STATE_MAP_FROM_WEBSOCKET_EVENT[event.event_type] + if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type): + self._attr_is_locked = state + self.async_reset_error_count() + else: + LOGGER.error("Unknown lock websocket event: %s", event.event_type) + self.async_increment_error_count() diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index ecc4578d878..81cb5b7febc 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==2021.11.0"], + "requirements": ["simplisafe-python==2021.11.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index de2f298f927..08043ffa094 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.11.0 +simplisafe-python==2021.11.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eef41267bb7..f66a2594459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1263,7 +1263,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.11.0 +simplisafe-python==2021.11.2 # homeassistant.components.slack slackclient==2.5.0 From 0df2904b89f86d41d156039602ccc2130a1240c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Nov 2021 10:24:37 +0100 Subject: [PATCH 0709/1452] Improve some entity registry tests (#59902) --- tests/common.py | 76 ++++++----- .../components/config/test_entity_registry.py | 126 +++++++++--------- tests/helpers/test_entity_platform.py | 25 +++- tests/helpers/test_entity_registry.py | 119 +++++++++++------ 4 files changed, 202 insertions(+), 144 deletions(-) diff --git a/tests/common.py b/tests/common.py index a9ba4ae86b4..19f0aaec44b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -942,6 +942,41 @@ class MockEntity(entity.Entity): if "entity_id" in values: self.entity_id = values["entity_id"] + @property + def available(self): + """Return True if entity is available.""" + return self._handle("available") + + @property + def capability_attributes(self): + """Info about capabilities.""" + return self._handle("capability_attributes") + + @property + def device_class(self): + """Info how device should be classified.""" + return self._handle("device_class") + + @property + def device_info(self): + """Info how it links to a device.""" + return self._handle("device_info") + + @property + def entity_category(self): + """Return the entity category.""" + return self._handle("entity_category") + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._handle("entity_registry_enabled_default") + + @property + def icon(self): + """Return the suggested icon.""" + return self._handle("icon") + @property def name(self): """Return the name of the entity.""" @@ -952,50 +987,25 @@ class MockEntity(entity.Entity): """Return the ste of the polling.""" return self._handle("should_poll") - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._handle("unique_id") - @property def state(self): """Return the state of the entity.""" return self._handle("state") - @property - def available(self): - """Return True if entity is available.""" - return self._handle("available") - - @property - def device_info(self): - """Info how it links to a device.""" - return self._handle("device_info") - - @property - def device_class(self): - """Info how device should be classified.""" - return self._handle("device_class") - - @property - def unit_of_measurement(self): - """Info on the units the entity state is in.""" - return self._handle("unit_of_measurement") - - @property - def capability_attributes(self): - """Info about capabilities.""" - return self._handle("capability_attributes") - @property def supported_features(self): """Info about supported features.""" return self._handle("supported_features") @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return self._handle("entity_registry_enabled_default") + def unique_id(self): + """Return the unique ID of the entity.""" + return self._handle("unique_id") + + @property + def unit_of_measurement(self): + """Info on the units the entity state is in.""" + return self._handle("unit_of_measurement") def _handle(self, attr): """Return attribute value.""" diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 3faff1222d4..6257037e57e 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -100,19 +100,19 @@ async def test_get_entity(hass, client): msg = await client.receive_json() assert msg["result"] == { + "area_id": None, + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.name", - "name": "Hello World", - "icon": None, - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.name", + "icon": None, + "name": "Hello World", + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", } await client.send_json( @@ -125,19 +125,19 @@ async def test_get_entity(hass, client): msg = await client.receive_json() assert msg["result"] == { + "area_id": None, + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.no_name", - "name": None, - "icon": None, - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "6789", "entity_category": None, + "entity_id": "test_domain.no_name", + "icon": None, + "name": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "6789", } @@ -181,19 +181,19 @@ async def test_update_entity(hass, client): assert msg["result"] == { "entity_entry": { + "area_id": "mock-area-id", + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": "mock-area-id", "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.world", - "name": "after update", - "icon": "icon:after update", - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.world", + "icon": "icon:after update", + "name": "after update", + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", } } @@ -230,19 +230,19 @@ async def test_update_entity(hass, client): assert msg["result"] == { "entity_entry": { + "area_id": "mock-area-id", + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": "mock-area-id", "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.world", - "name": "after update", - "icon": "icon:after update", - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.world", + "icon": "icon:after update", + "name": "after update", + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", }, "reload_delay": 30, } @@ -285,19 +285,19 @@ async def test_update_entity_require_restart(hass, client): assert msg["result"] == { "entity_entry": { + "area_id": None, + "capabilities": None, "config_entry_id": config_entry.entry_id, "device_id": None, - "area_id": None, "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.world", - "name": None, - "icon": None, - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.world", + "icon": None, + "name": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", }, "require_restart": True, } @@ -387,19 +387,19 @@ async def test_update_entity_no_changes(hass, client): assert msg["result"] == { "entity_entry": { + "area_id": None, + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.world", - "name": "name of entity", - "icon": None, - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.world", + "icon": None, + "name": "name of entity", + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", } } @@ -468,19 +468,19 @@ async def test_update_entity_id(hass, client): assert msg["result"] == { "entity_entry": { + "area_id": None, + "capabilities": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "platform": "test_platform", - "entity_id": "test_domain.planet", - "name": None, - "icon": None, - "original_name": None, - "original_icon": None, - "capabilities": None, - "unique_id": "1234", "entity_category": None, + "entity_id": "test_domain.planet", + "icon": None, + "name": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", } } diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index a151a3b7ef3..4e40bcd9882 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1085,10 +1085,13 @@ async def test_entity_info_added_to_entity_registry(hass): component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) entity_default = MockEntity( - unique_id="default", capability_attributes={"max": 100}, - supported_features=5, device_class="mock-device-class", + entity_category="config", + icon="nice:icon", + name="best name", + supported_features=5, + unique_id="default", unit_of_measurement=PERCENTAGE, ) @@ -1097,10 +1100,20 @@ async def test_entity_info_added_to_entity_registry(hass): registry = er.async_get(hass) entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") - assert entry_default.capabilities == {"max": 100} - assert entry_default.supported_features == 5 - assert entry_default.device_class == "mock-device-class" - assert entry_default.unit_of_measurement == PERCENTAGE + assert entry_default == er.RegistryEntry( + "test_domain.best_name", + "default", + "test_domain", + capabilities={"max": 100}, + device_class="mock-device-class", + entity_category="config", + icon=None, + name=None, + original_icon="nice:icon", + original_name="best name", + supported_features=5, + unit_of_measurement=PERCENTAGE, + ) async def test_override_restored_entities(hass): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index b07c4131795..543cf6c0045 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -71,26 +71,37 @@ def test_get_or_create_updates_data(registry): "light", "hue", "5678", - config_entry=orig_config_entry, - device_id="mock-dev-id", + area_id="mock-area-id", capabilities={"max": 100}, - supported_features=5, + config_entry=orig_config_entry, device_class="mock-device-class", + device_id="mock-dev-id", disabled_by=er.DISABLED_HASS, - unit_of_measurement="initial-unit_of_measurement", - original_name="initial-original_name", + entity_category="config", original_icon="initial-original_icon", + original_name="initial-original_name", + supported_features=5, + unit_of_measurement="initial-unit_of_measurement", ) - assert orig_entry.config_entry_id == orig_config_entry.entry_id - assert orig_entry.device_id == "mock-dev-id" - assert orig_entry.capabilities == {"max": 100} - assert orig_entry.supported_features == 5 - assert orig_entry.device_class == "mock-device-class" - assert orig_entry.disabled_by == er.DISABLED_HASS - assert orig_entry.unit_of_measurement == "initial-unit_of_measurement" - assert orig_entry.original_name == "initial-original_name" - assert orig_entry.original_icon == "initial-original_icon" + assert orig_entry == er.RegistryEntry( + "light.hue_5678", + "5678", + "hue", + area_id="mock-area-id", + capabilities={"max": 100}, + config_entry_id=orig_config_entry.entry_id, + device_class="mock-device-class", + device_id="mock-dev-id", + disabled_by=er.DISABLED_HASS, + entity_category="config", + icon=None, + name=None, + original_icon="initial-original_icon", + original_name="initial-original_name", + supported_features=5, + unit_of_measurement="initial-unit_of_measurement", + ) new_config_entry = MockConfigEntry(domain="light") @@ -98,27 +109,37 @@ def test_get_or_create_updates_data(registry): "light", "hue", "5678", - config_entry=new_config_entry, - device_id="new-mock-dev-id", + area_id="new-mock-area-id", capabilities={"new-max": 100}, - supported_features=10, + config_entry=new_config_entry, device_class="new-mock-device-class", + device_id="new-mock-dev-id", disabled_by=er.DISABLED_USER, - unit_of_measurement="updated-unit_of_measurement", - original_name="updated-original_name", + entity_category=None, original_icon="updated-original_icon", + original_name="updated-original_name", + supported_features=10, + unit_of_measurement="updated-unit_of_measurement", ) - assert new_entry.config_entry_id == new_config_entry.entry_id - assert new_entry.device_id == "new-mock-dev-id" - assert new_entry.capabilities == {"new-max": 100} - assert new_entry.supported_features == 10 - assert new_entry.device_class == "new-mock-device-class" - assert new_entry.unit_of_measurement == "updated-unit_of_measurement" - assert new_entry.original_name == "updated-original_name" - assert new_entry.original_icon == "updated-original_icon" - # Should not be updated - assert new_entry.disabled_by == er.DISABLED_HASS + assert new_entry == er.RegistryEntry( + "light.hue_5678", + "5678", + "hue", + area_id="new-mock-area-id", + capabilities={"new-max": 100}, + config_entry_id=new_config_entry.entry_id, + device_class="new-mock-device-class", + device_id="new-mock-dev-id", + disabled_by=er.DISABLED_HASS, # Should not be updated + entity_category="config", + icon=None, + name=None, + original_icon="updated-original_icon", + original_name="updated-original_name", + supported_features=10, + unit_of_measurement="updated-unit_of_measurement", + ) def test_get_or_create_suggested_object_id_conflict_register(registry): @@ -158,15 +179,17 @@ async def test_loading_saving_data(hass, registry): "light", "hue", "5678", - device_id="mock-dev-id", area_id="mock-area-id", - config_entry=mock_config, capabilities={"max": 100}, - supported_features=5, + config_entry=mock_config, device_class="mock-device-class", + device_id="mock-dev-id", disabled_by=er.DISABLED_HASS, - original_name="Original Name", + entity_category="config", original_icon="hass:original-icon", + original_name="Original Name", + supported_features=5, + unit_of_measurement="initial-unit_of_measurement", ) orig_entry2 = registry.async_update_entity( orig_entry2.entity_id, name="User Name", icon="hass:user-icon" @@ -187,16 +210,19 @@ async def test_loading_saving_data(hass, registry): assert orig_entry1 == new_entry1 assert orig_entry2 == new_entry2 - assert new_entry2.device_id == "mock-dev-id" assert new_entry2.area_id == "mock-area-id" - assert new_entry2.disabled_by == er.DISABLED_HASS assert new_entry2.capabilities == {"max": 100} - assert new_entry2.supported_features == 5 + assert new_entry2.config_entry_id == mock_config.entry_id assert new_entry2.device_class == "mock-device-class" - assert new_entry2.name == "User Name" + assert new_entry2.device_id == "mock-dev-id" + assert new_entry2.disabled_by == er.DISABLED_HASS + assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" - assert new_entry2.original_name == "Original Name" + assert new_entry2.name == "User Name" assert new_entry2.original_icon == "hass:original-icon" + assert new_entry2.original_name == "Original Name" + assert new_entry2.supported_features == 5 + assert new_entry2.unit_of_measurement == "initial-unit_of_measurement" def test_generate_entity_considers_registered_entities(registry): @@ -354,8 +380,8 @@ async def test_removing_area_id(registry): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration(hass): - """Test migration from old data to new.""" +async def test_migration_yaml_to_json(hass): + """Test migration from old (yaml) data to new.""" mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id") old_conf = { @@ -385,8 +411,9 @@ async def test_migration(hass): assert entry.config_entry_id == "test-config-id" +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_entity_id(hass, hass_storage): - """Test we autofix invalid entity IDs.""" + """Test we skip entities with invalid entity IDs.""" hass_storage[er.STORAGE_KEY] = { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, @@ -396,41 +423,49 @@ async def test_loading_invalid_entity_id(hass, hass_storage): "entity_id": "test.invalid__middle", "platform": "super_platform", "unique_id": "id-invalid-middle", - "name": "registry override", + "name": "registry override 1", }, { "entity_id": "test.invalid_end_", "platform": "super_platform", "unique_id": "id-invalid-end", + "name": "registry override 2", }, { "entity_id": "test._invalid_start", "platform": "super_platform", "unique_id": "id-invalid-start", + "name": "registry override 3", }, ] }, } + await er.async_load(hass) registry = er.async_get(hass) + assert len(registry.entities) == 0 entity_invalid_middle = registry.async_get_or_create( "test", "super_platform", "id-invalid-middle" ) assert valid_entity_id(entity_invalid_middle.entity_id) + # Check name to make sure we created a new entity + assert entity_invalid_middle.name is None entity_invalid_end = registry.async_get_or_create( "test", "super_platform", "id-invalid-end" ) assert valid_entity_id(entity_invalid_end.entity_id) + assert entity_invalid_end.name is None entity_invalid_start = registry.async_get_or_create( "test", "super_platform", "id-invalid-start" ) assert valid_entity_id(entity_invalid_start.entity_id) + assert entity_invalid_start.name is None async def test_update_entity_unique_id(registry): From bac3c1fd7538ac466e5b4c550d90aa3955b4bef9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:47:32 +0100 Subject: [PATCH 0710/1452] Use DhcpServiceInfo in verisure tests (#60117) Co-authored-by: epenet --- tests/components/verisure/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index f850487fe26..41cabb976c9 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from verisure import Error as VerisureError, LoginError as VerisureLoginError from homeassistant import config_entries -from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.verisure.const import ( CONF_GIID, CONF_LOCK_CODE_DIGITS, @@ -176,7 +176,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: """Test that DHCP discovery works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data={MAC_ADDRESS: "01:23:45:67:89:ab"}, + data=dhcp.DhcpServiceInfo(macaddress="01:23:45:67:89:ab"), context={"source": config_entries.SOURCE_DHCP}, ) From 5a40322cda8a508107ade5746201458ea01e3b91 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:48:06 +0100 Subject: [PATCH 0711/1452] Use ZeroconfServiceInfo in volumio (#60116) Co-authored-by: epenet --- .../components/volumio/config_flow.py | 13 ++++++++----- tests/components/volumio/test_config_flow.py | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index a499b7827b5..e77aad05df2 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -93,12 +94,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_zeroconf(self, discovery_info: zeroconf.ZeroconfServiceInfo): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - self._host = discovery_info["host"] - self._port = discovery_info["port"] - self._name = discovery_info["properties"]["volumioName"] - self._uuid = discovery_info["properties"]["UUID"] + self._host = discovery_info[zeroconf.ATTR_HOST] + self._port = discovery_info[zeroconf.ATTR_PORT] + self._name = discovery_info[zeroconf.ATTR_PROPERTIES]["volumioName"] + self._uuid = discovery_info[zeroconf.ATTR_PROPERTIES]["UUID"] await self._set_uid_and_abort() diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index b4b3c8f24ed..f217ed9919b 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN @@ -16,17 +17,17 @@ TEST_CONNECTION = { } -TEST_DISCOVERY = { - "host": "1.1.1.1", - "port": 3000, - "properties": {"volumioName": "discovered", "UUID": "2222-2222-2222-2222"}, -} +TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + port=3000, + properties={"volumioName": "discovered", "UUID": "2222-2222-2222-2222"}, +) TEST_DISCOVERY_RESULT = { - "host": TEST_DISCOVERY["host"], - "port": TEST_DISCOVERY["port"], - "id": TEST_DISCOVERY["properties"]["UUID"], - "name": TEST_DISCOVERY["properties"]["volumioName"], + "host": TEST_DISCOVERY[zeroconf.ATTR_HOST], + "port": TEST_DISCOVERY[zeroconf.ATTR_PORT], + "id": TEST_DISCOVERY[zeroconf.ATTR_PROPERTIES]["UUID"], + "name": TEST_DISCOVERY[zeroconf.ATTR_PROPERTIES]["volumioName"], } From 70f43a14156566162d073b3e029d39b956b58b4c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:49:37 +0100 Subject: [PATCH 0712/1452] Use ZeroconfServiceInfo in tradfri (#60112) Co-authored-by: epenet --- .../components/tradfri/config_flow.py | 15 ++++++++---- tests/components/tradfri/test_config_flow.py | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index f0f4016ba9b..af3ed00e974 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -96,10 +96,14 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle homekit discovery.""" - await self.async_set_unique_id(discovery_info["properties"]["id"]) - self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) + await self.async_set_unique_id( + discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + ) + self._abort_if_unique_id_configured( + {CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} + ) - host = discovery_info["host"] + host = discovery_info[zeroconf.ATTR_HOST] for entry in self._async_current_entries(): if entry.data.get(CONF_HOST) != host: @@ -108,7 +112,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Backwards compat, we update old entries if not entry.unique_id: self.hass.config_entries.async_update_entry( - entry, unique_id=discovery_info["properties"]["id"] + entry, + unique_id=discovery_info[zeroconf.ATTR_PROPERTIES][ + zeroconf.ATTR_PROPERTIES_ID + ], ) return self.async_abort(reason="already_configured") diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 60f3043f4f5..b4d80343238 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.tradfri import config_flow from . import TRADFRI_PATH @@ -103,7 +104,10 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, + data=zeroconf.ZeroconfServiceInfo( + host="123.123.123.123", + properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + ), ) result = await hass.config_entries.flow.async_configure( @@ -251,7 +255,9 @@ async def test_discovery_duplicate_aborted(hass): flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "new-host", "properties": {"id": "homekit-id"}}, + data=zeroconf.ZeroconfServiceInfo( + host="new-host", properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"} + ), ) assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -279,7 +285,10 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): result = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, + data=zeroconf.ZeroconfServiceInfo( + host="123.123.123.123", + properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -287,7 +296,10 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): result2 = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, + data=zeroconf.ZeroconfServiceInfo( + host="123.123.123.123", + properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + ), ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -304,7 +316,9 @@ async def test_discovery_updates_unique_id(hass): flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "some-host", "properties": {"id": "homekit-id"}}, + data=zeroconf.ZeroconfServiceInfo( + host="some-host", properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"} + ), ) assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT From eb70d328caf98c04336e29756dc024169507d0a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:51:47 +0100 Subject: [PATCH 0713/1452] Use DhcpServiceInfo in tplink (#60114) Co-authored-by: epenet --- tests/components/tplink/test_config_flow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 03757efa0ca..d19b47ffd86 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, setup +from homeassistant.components import dhcp from homeassistant.components.tplink import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant @@ -308,7 +309,9 @@ async def test_discovered_by_discovery_and_dhcp(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": IP_ADDRESS, "macaddress": MAC_ADDRESS, "hostname": ALIAS}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS + ), ) await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT @@ -318,7 +321,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": IP_ADDRESS, "macaddress": "00:00:00:00:00:00"}, + data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="00:00:00:00:00:00"), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -328,7 +331,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): 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"}, + data=dhcp.DhcpServiceInfo(ip="1.2.3.5", macaddress="00:00:00:00:00:01"), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -340,7 +343,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): [ ( config_entries.SOURCE_DHCP, - {"ip": IP_ADDRESS, "macaddress": MAC_ADDRESS, "hostname": ALIAS}, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), ), ( config_entries.SOURCE_DISCOVERY, @@ -381,7 +384,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): [ ( config_entries.SOURCE_DHCP, - {"ip": IP_ADDRESS, "macaddress": MAC_ADDRESS, "hostname": ALIAS}, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), ), ( config_entries.SOURCE_DISCOVERY, From 39149e19f71294d4496345ebb022112951bce2e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:54:15 +0100 Subject: [PATCH 0714/1452] Use ZeroconfServiceInfo in tado (#60111) Co-authored-by: epenet --- homeassistant/components/tado/config_flow.py | 11 ++++++++--- tests/components/tado/test_config_flow.py | 9 +++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index d762329d658..a1b7661b0f6 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -6,8 +6,10 @@ import requests.exceptions import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import CONF_FALLBACK, DOMAIN, UNIQUE_ID @@ -80,13 +82,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle HomeKit discovery.""" self._async_abort_entries_match() properties = { - key.lower(): value for (key, value) in discovery_info["properties"].items() + key.lower(): value + for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() } - await self.async_set_unique_id(properties["id"]) + await self.async_set_unique_id(properties[zeroconf.ATTR_PROPERTIES_ID]) return await self.async_step_user() def _username_already_configured(self, user_input): diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index b181b78bf16..327747cb841 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import requests from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -126,7 +127,9 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + data=zeroconf.ZeroconfServiceInfo( + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + ), ) assert result["type"] == "form" assert result["errors"] == {} @@ -145,6 +148,8 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + data=zeroconf.ZeroconfServiceInfo( + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + ), ) assert result["type"] == "abort" From 8b30cb509fd1e6f53c666e56b6755809c2357b69 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Nov 2021 12:01:56 +0100 Subject: [PATCH 0715/1452] Cleanup customize API endpoint (#59824) --- homeassistant/components/config/__init__.py | 1 - homeassistant/components/config/customize.py | 43 ------- tests/components/config/test_customize.py | 118 ------------------- 3 files changed, 162 deletions(-) delete mode 100644 homeassistant/components/config/customize.py delete mode 100644 tests/components/config/test_customize.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 96dbd79da1e..ff7b1e4d4cd 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -22,7 +22,6 @@ SECTIONS = ( "automation", "config_entries", "core", - "customize", "device_registry", "entity_registry", "group", diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py deleted file mode 100644 index 3b1122fc3a5..00000000000 --- a/homeassistant/components/config/customize.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Provide configuration end points for Customize.""" -from homeassistant.components.homeassistant import SERVICE_RELOAD_CORE_CONFIG -from homeassistant.config import DATA_CUSTOMIZE -from homeassistant.core import DOMAIN -import homeassistant.helpers.config_validation as cv - -from . import EditKeyBasedConfigView - -CONFIG_PATH = "customize.yaml" - - -async def async_setup(hass): - """Set up the Customize config API.""" - - async def hook(action, config_key): - """post_write_hook for Config View that reloads groups.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - - hass.http.register_view( - CustomizeConfigView( - "customize", "config", CONFIG_PATH, cv.entity_id, dict, post_write_hook=hook - ) - ) - - return True - - -class CustomizeConfigView(EditKeyBasedConfigView): - """Configure a list of entries.""" - - def _get_value(self, hass, data, config_key): - """Get value.""" - customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {} - return {"global": customize, "local": data.get(config_key, {})} - - def _write_value(self, hass, data, config_key, new_value): - """Set value.""" - data[config_key] = new_value - - state = hass.states.get(config_key) - state_attributes = dict(state.attributes) - state_attributes.update(new_value) - hass.states.async_set(config_key, state.state, state_attributes) diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py deleted file mode 100644 index 9ea18ff2ae0..00000000000 --- a/tests/components/config/test_customize.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Test Customize config panel.""" -from http import HTTPStatus -import json -from unittest.mock import patch - -import pytest - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import config -from homeassistant.config import DATA_CUSTOMIZE - - -@pytest.fixture(autouse=True) -async def setup_homeassistant(hass): - """Set up homeassistant integration.""" - assert await async_setup_component(hass, "homeassistant", {}) - - -async def test_get_entity(hass, hass_client): - """Test getting entity.""" - with patch.object(config, "SECTIONS", ["customize"]): - await async_setup_component(hass, "config", {}) - - client = await hass_client() - - def mock_read(path): - """Mock reading data.""" - return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} - - hass.data[DATA_CUSTOMIZE] = {"hello.beer": {"cold": "beer"}} - with patch("homeassistant.components.config._read", mock_read): - resp = await client.get("/api/config/customize/config/hello.beer") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {"local": {"free": "beer"}, "global": {"cold": "beer"}} - - -async def test_update_entity(hass, hass_client): - """Test updating entity.""" - with patch.object(config, "SECTIONS", ["customize"]): - await async_setup_component(hass, "config", {}) - - client = await hass_client() - - orig_data = { - "hello.beer": {"ignored": True}, - "other.entity": {"polling_intensity": 2}, - } - - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - hass.states.async_set("hello.world", "state", {"a": "b"}) - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch( - "homeassistant.config.async_hass_config_yaml", - return_value={}, - ): - resp = await client.post( - "/api/config/customize/config/hello.world", - data=json.dumps( - {"name": "Beer", "entities": ["light.top", "light.bottom"]} - ), - ) - await hass.async_block_till_done() - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert result == {"result": "ok"} - - state = hass.states.get("hello.world") - assert state.state == "state" - assert dict(state.attributes) == { - "a": "b", - "name": "Beer", - "entities": ["light.top", "light.bottom"], - } - - orig_data["hello.world"]["name"] = "Beer" - orig_data["hello.world"]["entities"] = ["light.top", "light.bottom"] - - assert written[0] == orig_data - - -async def test_update_entity_invalid_key(hass, hass_client): - """Test updating entity.""" - with patch.object(config, "SECTIONS", ["customize"]): - await async_setup_component(hass, "config", {}) - - client = await hass_client() - - resp = await client.post( - "/api/config/customize/config/not_entity", data=json.dumps({"name": "YO"}) - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_update_entity_invalid_json(hass, hass_client): - """Test updating entity.""" - with patch.object(config, "SECTIONS", ["customize"]): - await async_setup_component(hass, "config", {}) - - client = await hass_client() - - resp = await client.post("/api/config/customize/config/hello.beer", data="not json") - - assert resp.status == HTTPStatus.BAD_REQUEST From c70a7020f5cf41e89cf659442fb2863870aaeace Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 22 Nov 2021 05:03:56 -0600 Subject: [PATCH 0716/1452] Fix todoist due_today attribute for calendar events (#60038) The attribute was comparing a UTC date to a local date which caused the attribute to not be true in some cases. --- homeassistant/components/todoist/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 7097446ba4f..4caef136f3f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -421,7 +421,7 @@ class TodoistProjectData: # it shouldn't be counted. return None - task[DUE_TODAY] = task[END].date() == datetime.today().date() + task[DUE_TODAY] = task[END].date() == dt.utcnow().date() # Special case: Task is overdue. if task[END] <= task[START]: From 29761e6eeff4ca6a2577cacbe5511ca27de4b735 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 22 Nov 2021 12:17:55 +0100 Subject: [PATCH 0717/1452] Fix logger setting Yale Smart Living (#60033) --- homeassistant/components/yale_smart_alarm/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py index 618f9ad073a..cbb96579f4f 100644 --- a/homeassistant/components/yale_smart_alarm/const.py +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -25,7 +25,7 @@ COORDINATOR = "coordinator" DEFAULT_SCAN_INTERVAL = 15 -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__package__) ATTR_ONLINE = "online" ATTR_STATUS = "status" From 10d0870198a58edc8b84f9c2e3b7e0881532259e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 12:35:52 +0100 Subject: [PATCH 0718/1452] Use ZeroconfServiceInfo in plugwise (#60050) --- .../components/plugwise/config_flow.py | 35 +++++++++++-------- tests/components/plugwise/test_config_flow.py | 29 +++++++-------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 1dbf4324590..eaf728aa801 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Plugwise integration.""" +from __future__ import annotations + import logging from plugwise.exceptions import InvalidAuthentication, PlugwiseException @@ -6,6 +8,7 @@ from plugwise.smile import Smile import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components import zeroconf from homeassistant.const import ( CONF_BASE, CONF_HOST, @@ -16,8 +19,8 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( API, @@ -98,30 +101,34 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Plugwise config flow.""" - self.discovery_info = {} + self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None + self._username: str = DEFAULT_USERNAME - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info - self.discovery_info[CONF_USERNAME] = DEFAULT_USERNAME - _properties = self.discovery_info.get("properties") + _properties = discovery_info[zeroconf.ATTR_PROPERTIES] # unique_id is needed here, to be able to determine whether the discovered device is known, or not. - unique_id = self.discovery_info.get("hostname").split(".")[0] + unique_id = discovery_info[zeroconf.ATTR_HOSTNAME].split(".")[0] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured({CONF_HOST: self.discovery_info[CONF_HOST]}) + self._abort_if_unique_id_configured( + {CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} + ) if DEFAULT_USERNAME not in unique_id: - self.discovery_info[CONF_USERNAME] = STRETCH_USERNAME + self._username = STRETCH_USERNAME _product = _properties.get("product", None) _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" self.context["title_placeholders"] = { - CONF_HOST: self.discovery_info[CONF_HOST], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], CONF_NAME: _name, - CONF_PORT: self.discovery_info[CONF_PORT], - CONF_USERNAME: self.discovery_info[CONF_USERNAME], + CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_USERNAME: self._username, } return await self.async_step_user_gateway() @@ -136,9 +143,9 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input.pop(FLOW_TYPE, None) if self.discovery_info: - user_input[CONF_HOST] = self.discovery_info[CONF_HOST] - user_input[CONF_PORT] = self.discovery_info[CONF_PORT] - user_input[CONF_USERNAME] = self.discovery_info[CONF_USERNAME] + user_input[CONF_HOST] = self.discovery_info[zeroconf.ATTR_HOST] + user_input[CONF_PORT] = self.discovery_info[zeroconf.ATTR_PORT] + user_input[CONF_USERNAME] = self._username try: api = await validate_gw_input(self.hass, user_input) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 7f270e23cc1..72e695a4e9c 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -8,6 +8,7 @@ from plugwise.exceptions import ( ) import pytest +from homeassistant.components import zeroconf from homeassistant.components.plugwise.const import ( API, DEFAULT_PORT, @@ -39,28 +40,28 @@ TEST_PORT = 81 TEST_USERNAME = "smile" TEST_USERNAME2 = "stretch" -TEST_DISCOVERY = { - "host": TEST_HOST, - "port": DEFAULT_PORT, - "hostname": f"{TEST_HOSTNAME}.local.", - "server": f"{TEST_HOSTNAME}.local.", - "properties": { +TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + port=DEFAULT_PORT, + hostname=f"{TEST_HOSTNAME}.local.", + server=f"{TEST_HOSTNAME}.local.", + properties={ "product": "smile", "version": "1.2.3", "hostname": f"{TEST_HOSTNAME}.local.", }, -} -TEST_DISCOVERY2 = { - "host": TEST_HOST, - "port": DEFAULT_PORT, - "hostname": f"{TEST_HOSTNAME2}.local.", - "server": f"{TEST_HOSTNAME2}.local.", - "properties": { +) +TEST_DISCOVERY2 = zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + port=DEFAULT_PORT, + hostname=f"{TEST_HOSTNAME2}.local.", + server=f"{TEST_HOSTNAME2}.local.", + properties={ "product": "stretch", "version": "1.2.3", "hostname": f"{TEST_HOSTNAME2}.local.", }, -} +) @pytest.fixture(name="mock_smile") From 65d1f8183f9b0898c19ac6adbd62a489fde4868c Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 22 Nov 2021 06:40:25 -0500 Subject: [PATCH 0719/1452] Fix Environment Canada server loading (#60087) * Reduce number of requests hitting EC servers. * Bump lib. --- homeassistant/components/environment_canada/camera.py | 1 + homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index a4707bb7576..86f6299585c 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -55,6 +55,7 @@ class ECCamera(CoordinatorEntity, Camera): self._attr_name = f"{coordinator.config_entry.title} Radar" self._attr_unique_id = f"{coordinator.config_entry.unique_id}-radar" self._attr_attribution = self.radar_object.metadata["attribution"] + self._attr_entity_registry_enabled_default = False self.content_type = "image/gif" self.image = None diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 3a2ee1d8b8f..b340674b480 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.14"], + "requirements": ["env_canada==0.5.18"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 08043ffa094..ad76cd83ff9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -600,7 +600,7 @@ enocean==0.50 enturclient==0.2.2 # homeassistant.components.environment_canada -env_canada==0.5.14 +env_canada==0.5.18 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f66a2594459..7acbdb7674c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -372,7 +372,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.14 +env_canada==0.5.18 # homeassistant.components.enphase_envoy envoy_reader==0.20.0 From d95c615f8661765d51b26a3b76cd8fa0e0fa933a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Nov 2021 13:22:43 +0100 Subject: [PATCH 0720/1452] Add binary sensor platform to WLED (#59798) --- homeassistant/components/wled/__init__.py | 2 + .../components/wled/binary_sensor.py | 58 +++++++++++++++++++ tests/components/wled/test_binary_sensor.py | 54 +++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 homeassistant/components/wled/binary_sensor.py create mode 100644 tests/components/wled/test_binary_sensor.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index d54cfe3cd1c..ec2ad1bb6fe 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,6 +1,7 @@ """Support for WLED.""" from __future__ import annotations +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN @@ -14,6 +15,7 @@ from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator PLATFORMS = ( + BINARY_SENSOR_DOMAIN, BUTTON_DOMAIN, LIGHT_DOMAIN, SELECT_DOMAIN, diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py new file mode 100644 index 00000000000..45df589de61 --- /dev/null +++ b/homeassistant/components/wled/binary_sensor.py @@ -0,0 +1,58 @@ +"""Support for WLED binary sensor.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .models import WLEDEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a WLED binary sensor based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + WLEDUpdateBinarySensor(coordinator), + ] + ) + + +class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): + """Defines a WLED firmware binary sensor.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = DEVICE_CLASS_UPDATE + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize the button entity.""" + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Firmware" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + current = self.coordinator.data.info.version + beta = self.coordinator.data.info.version_latest_beta + stable = self.coordinator.data.info.version_latest_stable + + return current is not None and ( + (stable is not None and stable > current) + or ( + beta is not None + and (current.alpha or current.beta or current.release_candidate) + and beta > current + ) + ) diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py new file mode 100644 index 00000000000..61a883b780c --- /dev/null +++ b/tests/components/wled/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""Tests for the WLED binary sensor platform.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.binary_sensor import DEVICE_CLASS_UPDATE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_update_available( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the firmware update binary sensor.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("binary_sensor.wled_rgb_light_firmware") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_UPDATE + assert state.state == STATE_ON + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("binary_sensor.wled_rgb_light_firmware") + assert entry + assert entry.unique_id == "aabbccddeeff_update" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_no_update_available( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Test the update binary sensor. There is no update available.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("binary_sensor.wled_websocket_firmware") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_UPDATE + assert state.state == STATE_OFF + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("binary_sensor.wled_websocket_firmware") + assert entry + assert entry.unique_id == "aabbccddeeff_update" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC From 5608b4fb182e0fbc2c553c5ecffcbd7a9283d51b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 22 Nov 2021 13:43:04 +0100 Subject: [PATCH 0721/1452] Update CODEOWNERS for tradfri (#60023) Co-authored-by: rianadon --- CODEOWNERS | 1 - homeassistant/components/tradfri/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c143826fe14..7fe3842761d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -546,7 +546,6 @@ homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus homeassistant/components/trace/* @home-assistant/core homeassistant/components/tractive/* @Danielhiversen @zhulik @bieniu -homeassistant/components/tradfri/* @janiversen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 5b23ec4e418..00c8143e0f1 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,6 +7,6 @@ "homekit": { "models": ["TRADFRI"] }, - "codeowners": ["@janiversen"], + "codeowners": [], "iot_class": "local_polling" } From 3b5a7d001f3b0ff57859133dbdc21bd4959025fb Mon Sep 17 00:00:00 2001 From: h2zero <32826625+h2zero@users.noreply.github.com> Date: Mon, 22 Nov 2021 06:01:37 -0700 Subject: [PATCH 0722/1452] Fix Konnected DS18B20 poll interval (#59954) * Konnected - Fix DS18B20 poll interval. The poll interval for the DS18B20 was not being sent to the device. * Fix tests --- homeassistant/components/konnected/panel.py | 4 +++- tests/components/konnected/test_panel.py | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index a9999df3bc0..d02b4c31c33 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -304,7 +304,9 @@ class AlarmPanel: def async_ds18b20_sensor_configuration(self): """Return the configuration map for syncing DS18B20 sensors.""" return [ - self.format_zone(sensor[CONF_ZONE]) + self.format_zone( + sensor[CONF_ZONE], {CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]} + ) for sensor in self.stored_configuration[CONF_SENSORS] if sensor[CONF_TYPE] == "ds18b20" ] diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 38507aa973c..bf2da15b7a4 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -144,7 +144,7 @@ async def test_create_and_setup(hass, mock_panel): "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], "dht_sensors": [{"poll_interval": 3, "pin": "6"}], - "ds18b20_sensors": [{"pin": "7"}], + "ds18b20_sensors": [{"poll_interval": 3, "pin": "7"}], "auth_token": "11223344556677889900", "blink": True, "discovery": True, @@ -240,7 +240,7 @@ async def test_create_and_setup_pro(hass, mock_panel): ], "sensors": [ {"zone": "3", "type": "dht", "poll_interval": 5}, - {"zone": "7", "type": "ds18b20", "name": "temper"}, + {"zone": "7", "type": "ds18b20", "poll_interval": 1, "name": "temper"}, ], "switches": [ {"zone": "4"}, @@ -302,7 +302,7 @@ async def test_create_and_setup_pro(hass, mock_panel): {"trigger": 1, "zone": "alarm1"}, ], "dht_sensors": [{"poll_interval": 5, "zone": "3"}], - "ds18b20_sensors": [{"zone": "7"}], + "ds18b20_sensors": [{"poll_interval": 1, "zone": "7"}], "auth_token": "11223344556677889900", "blink": True, "discovery": True, @@ -344,7 +344,7 @@ async def test_create_and_setup_pro(hass, mock_panel): "type": "dht", "zone": "3", }, - {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "7"}, + {"name": "temper", "poll_interval": 1, "type": "ds18b20", "zone": "7"}, ], "switches": [ { @@ -492,7 +492,7 @@ async def test_default_options(hass, mock_panel): "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], "dht_sensors": [{"poll_interval": 3, "pin": "6"}], - "ds18b20_sensors": [{"pin": "7"}], + "ds18b20_sensors": [{"poll_interval": 3, "pin": "7"}], "auth_token": "11223344556677889900", "blink": True, "discovery": True, From a7382c80923fcc52a9cbf38335943f4124b4146a Mon Sep 17 00:00:00 2001 From: Cliffano Subagio Date: Tue, 23 Nov 2021 00:38:36 +1100 Subject: [PATCH 0723/1452] Fix KeyError when Twitter app permission is set to Read (#60018) --- homeassistant/components/twitter/notify.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 54aaf2142e5..fd97303a360 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -243,7 +243,12 @@ class TwitterNotificationService(BaseNotificationService): def log_error_resp(resp): """Log error response.""" obj = json.loads(resp.text) - error_message = obj["errors"] + if "errors" in obj: + error_message = obj["errors"] + elif "error" in obj: + error_message = obj["error"] + else: + error_message = resp.text _LOGGER.error("Error %s: %s", resp.status_code, error_message) @staticmethod From 38b976e6d61e0cc94d9016e39a1fed749c0c6e4b Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 22 Nov 2021 15:06:42 +0100 Subject: [PATCH 0724/1452] Add vicare config flow (#56691) * Configuration via UI Config flow / YAML deprecation - Support discovery via MAC address - Support import of YAML config - Switch to ConfigEntry, get rid of platform setup * Fix review comments * More tests for vicare yaml import --- .coveragerc | 7 +- homeassistant/components/vicare/__init__.py | 91 ++++-- .../components/vicare/binary_sensor.py | 28 +- homeassistant/components/vicare/climate.py | 33 +-- .../components/vicare/config_flow.py | 110 +++++++ homeassistant/components/vicare/const.py | 1 - homeassistant/components/vicare/manifest.json | 8 +- homeassistant/components/vicare/sensor.py | 30 +- .../components/vicare/translations/en.json | 20 ++ .../components/vicare/water_heater.py | 29 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 4 + requirements_test_all.txt | 3 + tests/components/vicare/__init__.py | 20 ++ tests/components/vicare/test_config_flow.py | 274 ++++++++++++++++++ 15 files changed, 564 insertions(+), 95 deletions(-) create mode 100644 homeassistant/components/vicare/config_flow.py create mode 100644 homeassistant/components/vicare/translations/en.json create mode 100644 tests/components/vicare/__init__.py create mode 100644 tests/components/vicare/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0e01d0c7849..9591a8705d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1194,7 +1194,12 @@ omit = homeassistant/components/vesync/light.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py - homeassistant/components/vicare/* + homeassistant/components/vicare/binary_sensor.py + homeassistant/components/vicare/climate.py + homeassistant/components/vicare/const.py + homeassistant/components/vicare/__init__.py + homeassistant/components/vicare/sensor.py + homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py homeassistant/components/vilfo/sensor.py homeassistant/components/vilfo/const.py diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index c1571c2f91b..d4e4db3fbe9 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -9,6 +9,7 @@ from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_NAME, @@ -16,7 +17,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR @@ -30,7 +31,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, HeatingType, ) @@ -61,8 +61,8 @@ CONFIG_SCHEMA = vol.Schema( ): int, # Ignored: All circuits are now supported. Will be removed when switching to Setup via UI. vol.Optional(CONF_NAME, default="ViCare"): cv.string, vol.Optional( - CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE - ): cv.enum(HeatingType), + CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value + ): vol.In([e.value for e in HeatingType]), } ), ) @@ -71,44 +71,75 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Create the ViCare component.""" - conf = config[DOMAIN] - params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} +async def async_setup(hass: HomeAssistant, config) -> bool: + """Set up the ViCare component from yaml.""" + if DOMAIN not in config: + # Setup via UI. No need to continue yaml-based setup + return True - params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) - params["client_id"] = conf.get(CONF_CLIENT_ID) - - hass.data[DOMAIN] = {} - hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] - setup_vicare_api(hass, conf, hass.data[DOMAIN]) - - hass.data[DOMAIN][CONF_HEATING_TYPE] = conf[CONF_HEATING_TYPE] - - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) return True -def setup_vicare_api(hass, conf, entity_data): - """Set up PyVicare API.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from config entry.""" + _LOGGER.debug("Setting up ViCare component") + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][entry.entry_id] = {} + + await hass.async_add_executor_job(setup_vicare_api, hass, entry) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +def vicare_login(hass, entry_data): + """Login via PyVicare API.""" vicare_api = PyViCare() - vicare_api.setCacheDuration(conf[CONF_SCAN_INTERVAL]) + vicare_api.setCacheDuration(entry_data[CONF_SCAN_INTERVAL]) vicare_api.initWithCredentials( - conf[CONF_USERNAME], - conf[CONF_PASSWORD], - conf[CONF_CLIENT_ID], + entry_data[CONF_USERNAME], + entry_data[CONF_PASSWORD], + entry_data[CONF_CLIENT_ID], hass.config.path(STORAGE_DIR, "vicare_token.save"), ) + return vicare_api + + +def setup_vicare_api(hass, entry): + """Set up PyVicare API.""" + vicare_api = vicare_login(hass, entry.data) - device = vicare_api.devices[0] for device in vicare_api.devices: _LOGGER.info( "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) ) - entity_data[VICARE_DEVICE_CONFIG] = device - entity_data[VICARE_API] = getattr( - device, HEATING_TYPE_TO_CREATOR_METHOD[conf[CONF_HEATING_TYPE]] + + # Currently we only support a single device + device = vicare_api.devices[0] + hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device + hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr( + device, + HEATING_TYPE_TO_CREATOR_METHOD[HeatingType(entry.data[CONF_HEATING_TYPE])], )() - entity_data[VICARE_CIRCUITS] = entity_data[VICARE_API].circuits + hass.data[DOMAIN][entry.entry_id][VICARE_CIRCUITS] = hass.data[DOMAIN][ + entry.entry_id + ][VICARE_API].circuits + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload ViCare 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 diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 4484d5f5040..510c332f89f 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -17,9 +17,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import CONF_NAME from . import ViCareRequiredKeysMixin -from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME +from .const import DOMAIN, VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,7 @@ def _build_entity(name, vicare_api, device_config, sensor): async def _entities_from_descriptions( - hass, name, all_devices, sensor_descriptions, iterables + hass, name, all_devices, sensor_descriptions, iterables, config_entry ): """Create entities from descriptions and list of burners/circuits.""" for description in sensor_descriptions: @@ -96,33 +97,30 @@ async def _entities_from_descriptions( _build_entity, f"{name} {description.name}{suffix}", current, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Create the ViCare binary sensor devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] - api = hass.data[DOMAIN][VICARE_API] + name = config_entry.data[CONF_NAME] + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] all_devices = [] for description in CIRCUIT_SENSORS: - for circuit in api.circuits: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(api.circuits) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = await hass.async_add_executor_job( _build_entity, f"{name} {description.name}{suffix}", circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: @@ -130,19 +128,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await _entities_from_descriptions( - hass, name, all_devices, BURNER_SENSORS, api.burners + hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: await _entities_from_descriptions( - hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No compressors found") - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index cdc5e826cab..1521ad1cf89 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -22,7 +22,12 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -32,7 +37,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, ) _LOGGER = logging.getLogger(__name__) @@ -99,33 +103,26 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Create the ViCare climate devices.""" - # Legacy setup. Remove after configuration.yaml deprecation end - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the ViCare climate platform.""" + name = config_entry.data[CONF_NAME] - name = hass.data[DOMAIN][VICARE_NAME] all_devices = [] - for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = _build_entity( f"{name} Heating{suffix}", - hass.data[DOMAIN][VICARE_API], - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_API], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], circuit, - hass.data[DOMAIN][CONF_HEATING_TYPE], + config_entry.data[CONF_HEATING_TYPE], ) if entity is not None: all_devices.append(entity) - async_add_entities(all_devices) - platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -134,6 +131,8 @@ async def async_setup_platform( "set_vicare_mode", ) + async_add_devices(all_devices) + class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py new file mode 100644 index 00000000000..659b90b6dad --- /dev/null +++ b/homeassistant/components/vicare/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for ViCare integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from . import vicare_login +from .const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DEFAULT_HEATING_TYPE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + HeatingType, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for ViCare.""" + + VERSION = 1 + + async def async_step_user(self, user_input: dict[str, Any] | None = None): + """Invoke when a user initiates a flow via the user interface.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + data_schema = { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value): vol.In( + [e.value for e in HeatingType] + ), + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=30) + ), + } + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + vicare_login, self.hass, user_input + ) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except PyViCareInvalidCredentialsError as ex: + _LOGGER.debug("Could not log in to ViCare, %s", ex) + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info): + """Invoke when a Viessmann MAC address is discovered on the network.""" + formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) + _LOGGER.info("Found device with mac %s", formatted_mac) + + await self.async_set_unique_id(formatted_mac) + self._abort_if_unique_id_configured() + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user() + + async def async_step_import(self, import_info): + """Handle a flow initiated by a YAML config import.""" + + await self.async_set_unique_id("Configuration.yaml") + self._abort_if_unique_id_configured() + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # Remove now unsupported config parameters + if import_info.get(CONF_CIRCUIT): + import_info.pop(CONF_CIRCUIT) + + # Add former optional config if missing + if import_info.get(CONF_HEATING_TYPE) is None: + import_info[CONF_HEATING_TYPE] = DEFAULT_HEATING_TYPE.value + + return self.async_create_entry( + title="Configuration.yaml", + data=import_info, + ) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index 0cae3f7e0d1..db8b782399e 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -14,7 +14,6 @@ PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] VICARE_DEVICE_CONFIG = "device_conf" VICARE_API = "api" -VICARE_NAME = "name" VICARE_CIRCUITS = "circuits" CONF_CIRCUIT = "circuit" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 0fe1c1f95e2..dda2ed63a89 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,5 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], "requirements": ["PyViCare==2.13.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "config_flow": true, + "dhcp": [ + { + "macaddress": "B87424*" + } + ] } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 2ff8ce4bf7d..50ad64ab0c8 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -21,6 +21,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( + CONF_NAME, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, @@ -36,8 +37,8 @@ from . import ViCareRequiredKeysMixin from .const import ( DOMAIN, VICARE_API, + VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, VICARE_UNIT_TO_DEVICE_CLASS, VICARE_UNIT_TO_UNIT_OF_MEASUREMENT, ) @@ -338,7 +339,7 @@ def _build_entity(name, vicare_api, device_config, sensor): async def _entities_from_descriptions( - hass, name, all_devices, sensor_descriptions, iterables + hass, name, all_devices, sensor_descriptions, iterables, config_entry ): """Create entities from descriptions and list of burners/circuits.""" for description in sensor_descriptions: @@ -350,20 +351,17 @@ async def _entities_from_descriptions( _build_entity, f"{name} {description.name}{suffix}", current, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Create the ViCare sensor devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] - api = hass.data[DOMAIN][VICARE_API] + name = config_entry.data[CONF_NAME] + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] all_devices = [] for description in GLOBAL_SENSORS: @@ -371,22 +369,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _build_entity, f"{name} {description.name}", api, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) for description in CIRCUIT_SENSORS: - for circuit in api.circuits: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(api.circuits) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = await hass.async_add_executor_job( _build_entity, f"{name} {description.name}{suffix}", circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: @@ -394,19 +392,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await _entities_from_descriptions( - hass, name, all_devices, BURNER_SENSORS, api.burners + hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: await _entities_from_descriptions( - hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No compressors found") - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareSensor(SensorEntity): diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json new file mode 100644 index 00000000000..b6ce73e1c2b --- /dev/null +++ b/homeassistant/components/vicare/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "Password", + "client_id": "API Key", + "username": "Username", + "heating_type": "Heating type" + }, + "description": "Setup ViCare to control your Viessmann device.\nMinimum needed: username, password, API key.", + "title": "Setup ViCare" + } + }, + "error": { + "invalid_auth": "Invalid authentication" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 883bcd3efd5..1b98ad31992 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -13,7 +13,12 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterEntity, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from .const import ( CONF_HEATING_TYPE, @@ -21,7 +26,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, ) _LOGGER = logging.getLogger(__name__) @@ -66,29 +70,26 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Create the ViCare water_heater devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the ViCare climate platform.""" + name = config_entry.data[CONF_NAME] all_devices = [] - for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = _build_entity( f"{name} Water{suffix}", - hass.data[DOMAIN][VICARE_API], + hass.data[DOMAIN][config_entry.entry_id][VICARE_API], circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], - hass.data[DOMAIN][CONF_HEATING_TYPE], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], + config_entry.data[CONF_HEATING_TYPE], ) if entity is not None: all_devices.append(entity) - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareWater(WaterHeaterEntity): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4d9cb7d7bc4..35bc6269d6c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -318,6 +318,7 @@ FLOWS = [ "vera", "verisure", "vesync", + "vicare", "vilfo", "vizio", "vlc_telnet", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f11cf63df9c..31481df3495 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -544,6 +544,10 @@ DHCP = [ "domain": "verisure", "macaddress": "0023C1*" }, + { + "domain": "vicare", + "macaddress": "B87424*" + }, { "domain": "yeelight", "hostname": "yeelink-*" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7acbdb7674c..1b18ccac32d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,6 +32,9 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera PyTurboJPEG==1.6.1 +# homeassistant.components.vicare +PyViCare==2.13.1 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/tests/components/vicare/__init__.py b/tests/components/vicare/__init__.py new file mode 100644 index 00000000000..f67e50be1d6 --- /dev/null +++ b/tests/components/vicare/__init__.py @@ -0,0 +1,20 @@ +"""Test for ViCare.""" +from homeassistant.components.vicare.const import CONF_HEATING_TYPE +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +ENTRY_CONFIG = { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + CONF_HEATING_TYPE: "auto", + CONF_SCAN_INTERVAL: 60, + CONF_NAME: "ViCare", +} + +MOCK_MAC = "B874241B7B9" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py new file mode 100644 index 00000000000..6d977934de3 --- /dev/null +++ b/tests/components/vicare/test_config_flow.py @@ -0,0 +1,274 @@ +"""Test the ViCare config flow.""" +from unittest.mock import patch + +from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import dhcp +from homeassistant.components.vicare.const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DOMAIN, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME + +from . import ENTRY_CONFIG, MOCK_MAC + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert len(result["errors"]) == 0 + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=None, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ViCare" + assert result2["data"] == ENTRY_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.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=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_removes_circuit(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + ENTRY_CONFIG[CONF_CIRCUIT] = 1 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_adds_heating_type(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + del ENTRY_CONFIG[CONF_HEATING_TYPE] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_invalid_login(hass) -> None: + """Test a flow with an invalid Vicare login.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + side_effect=PyViCareInvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_dhcp(hass): + """Test we can setup from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=None, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ViCare" + assert result2["data"] == ENTRY_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_already_configured(hass): + """Test that configuring same instance is rejectes.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_user_input_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="ViCare", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" From 8b26b69366fd492513d22813ae86a9001197e021 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 15:15:14 +0100 Subject: [PATCH 0725/1452] Use ServiceInfo in yeelight (#60127) Co-authored-by: epenet --- .../components/yeelight/config_flow.py | 21 +++++++++------ tests/components/yeelight/__init__.py | 18 ++++++------- tests/components/yeelight/test_config_flow.py | 26 +++++++++++++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index fbc9270d72a..d876f35a2dc 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -8,10 +8,11 @@ from yeelight.aio import AsyncBulb from yeelight.main import get_known_models from homeassistant import config_entries, exceptions -from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import ( @@ -53,21 +54,25 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_model = None self._discovered_ip = None - async def async_step_homekit(self, discovery_info): + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle discovery from homekit.""" - self._discovered_ip = discovery_info["host"] + self._discovered_ip = discovery_info[zeroconf.ATTR_HOST] return await self._async_handle_discovery() - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery from dhcp.""" - self._discovered_ip = discovery_info[IP_ADDRESS] + self._discovered_ip = discovery_info[dhcp.IP_ADDRESS] return await self._async_handle_discovery() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle discovery from zeroconf.""" - self._discovered_ip = discovery_info["host"] + self._discovered_ip = discovery_info[zeroconf.ATTR_HOST] await self.async_set_unique_id( - "{0:#0{1}x}".format(int(discovery_info["name"][-26:-18]), 18) + "{0:#0{1}x}".format(int(discovery_info[zeroconf.ATTR_NAME][-26:-18]), 18) ) return await self._async_handle_discovery_with_unique_id() diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 4d673dfaa94..2fa9c029d92 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -8,7 +8,7 @@ from async_upnp_client.search import SsdpSearchListener from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS -from homeassistant.components import yeelight as hass_yeelight +from homeassistant.components import yeelight as hass_yeelight, zeroconf from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -39,14 +39,14 @@ CAPABILITIES = { ID_DECIMAL = f"{int(ID, 16):08d}" -ZEROCONF_DATA = { - "host": IP_ADDRESS, - "port": 54321, - "hostname": f"yeelink-light-strip1_miio{ID_DECIMAL}.local.", - "type": "_miio._udp.local.", - "name": f"yeelink-light-strip1_miio{ID_DECIMAL}._miio._udp.local.", - "properties": {"epoch": "1", "mac": "000000000000"}, -} +ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + port=54321, + hostname=f"yeelink-light-strip1_miio{ID_DECIMAL}.local.", + type="_miio._udp.local.", + name=f"yeelink-light-strip1_miio{ID_DECIMAL}._miio._udp.local.", + properties={"epoch": "1", "mac": "000000000000"}, +) NAME = "name" SHORT_ID = hex(int("0x000000000015243f", 16)) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 258477c9569..b3466fb2525 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries +from homeassistant.components import dhcp, zeroconf from homeassistant.components.yeelight import ( CONF_DETECTED_MODEL, CONF_MODE_MUSIC, @@ -455,7 +456,10 @@ async def test_discovered_by_homekit_and_dhcp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + data=zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM @@ -467,7 +471,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), ) await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT @@ -479,7 +483,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": IP_ADDRESS, "macaddress": "00:00:00:00:00:00"}, + data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="00:00:00:00:00:00"), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -493,7 +497,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): 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"}, + data=dhcp.DhcpServiceInfo(ip="1.2.3.5", macaddress="00:00:00:00:00:01"), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -505,11 +509,14 @@ async def test_discovered_by_homekit_and_dhcp(hass): [ ( config_entries.SOURCE_DHCP, - {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), ), ( config_entries.SOURCE_HOMEKIT, - {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ), ], ) @@ -563,11 +570,14 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): [ ( config_entries.SOURCE_DHCP, - {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), ), ( config_entries.SOURCE_HOMEKIT, - {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + ), ), ], ) From 2cd241ff0aea662f239f2e0f12bbaa686101a175 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 15:18:22 +0100 Subject: [PATCH 0726/1452] Use ZeroconfServiceInfo in xiaomi_miio (#60132) Co-authored-by: epenet --- .../components/xiaomi_miio/config_flow.py | 14 ++++--- .../xiaomi_miio/test_config_flow.py | 39 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 744d80494c8..4e4f1688a04 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -7,9 +7,11 @@ from micloud.micloudexception import MiCloudAccessDenied import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -154,13 +156,15 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" return await self.async_step_cloud() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - name = discovery_info.get("name") - self.host = discovery_info.get("host") - self.mac = discovery_info.get("properties", {}).get("mac") + name = discovery_info[zeroconf.ATTR_NAME] + self.host = discovery_info[zeroconf.ATTR_HOST] + self.mac = discovery_info[zeroconf.ATTR_PROPERTIES].get("mac") if self.mac is None: - poch = discovery_info.get("properties", {}).get("poch", "") + poch = discovery_info[zeroconf.ATTR_PROPERTIES].get("poch", "") if (result := search(r"mac=\w+", poch)) is not None: self.mac = result.group(0).split("=")[1] diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 206da7ad4ae..16c9057379b 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -7,6 +7,7 @@ from miio import DeviceException import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN @@ -391,11 +392,11 @@ async def test_zeroconf_gateway_success(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: TEST_HOST, - ZEROCONF_NAME: TEST_ZEROCONF_NAME, - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name=TEST_ZEROCONF_NAME, + properties={ZEROCONF_MAC: TEST_MAC}, + ), ) assert result["type"] == "form" @@ -430,11 +431,11 @@ async def test_zeroconf_unknown_device(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: TEST_HOST, - ZEROCONF_NAME: "not-a-xiaomi-miio-device", - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name="not-a-xiaomi-miio-device", + properties={ZEROCONF_MAC: TEST_MAC}, + ), ) assert result["type"] == "abort" @@ -444,7 +445,9 @@ async def test_zeroconf_unknown_device(hass): async def test_zeroconf_no_data(hass): """Test a failed zeroconf discovery because of no data.""" result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data={} + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo(host=None, name=None, properties={}), ) assert result["type"] == "abort" @@ -456,7 +459,9 @@ async def test_zeroconf_missing_data(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={CONF_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME}, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, name=TEST_ZEROCONF_NAME, properties={} + ), ) assert result["type"] == "abort" @@ -746,11 +751,11 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: TEST_HOST, - ZEROCONF_NAME: zeroconf_name_to_test, - ZEROCONF_PROP: {"poch": f"0:mac={TEST_MAC_DEVICE}\x00"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name=zeroconf_name_to_test, + properties={"poch": f"0:mac={TEST_MAC_DEVICE}\x00"}, + ), ) assert result["type"] == "form" From c8451001a00856169efd3e92c06cfd83a2c7ef2b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 15:19:01 +0100 Subject: [PATCH 0727/1452] Use ZeroconfServiceInfo in xiaomi_aqara (#60131) Co-authored-by: epenet --- .../components/xiaomi_aqara/config_flow.py | 12 ++++++--- .../xiaomi_aqara/test_config_flow.py | 25 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 3fbe7a71496..2b486cb87ad 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -6,8 +6,10 @@ import voluptuous as vol from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -144,11 +146,13 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="select", data_schema=select_schema, errors=errors ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" - name = discovery_info.get("name") - self.host = discovery_info.get("host") - mac_address = discovery_info.get("properties", {}).get("mac") + name = discovery_info[zeroconf.ATTR_NAME] + self.host = discovery_info[zeroconf.ATTR_HOST] + mac_address = discovery_info[zeroconf.ATTR_PROPERTIES].get("mac") if not name or not self.host or not mac_address: return self.async_abort(reason="not_xiaomi_aqara") diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 3f445a1fdec..27000af8a05 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL @@ -400,11 +401,11 @@ async def test_zeroconf_success(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: TEST_HOST, - ZEROCONF_NAME: TEST_ZEROCONF_NAME, - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name=TEST_ZEROCONF_NAME, + properties={ZEROCONF_MAC: TEST_MAC}, + ), ) assert result["type"] == "form" @@ -443,7 +444,9 @@ async def test_zeroconf_missing_data(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={CONF_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME}, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, name=TEST_ZEROCONF_NAME, properties={} + ), ) assert result["type"] == "abort" @@ -455,11 +458,11 @@ async def test_zeroconf_unknown_device(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - CONF_HOST: TEST_HOST, - ZEROCONF_NAME: "not-a-xiaomi-aqara-gateway", - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, - }, + data=zeroconf.ZeroconfServiceInfo( + host=TEST_HOST, + name="not-a-xiaomi-aqara-gateway", + properties={ZEROCONF_MAC: TEST_MAC}, + ), ) assert result["type"] == "abort" From 7e1b00c491d99b1a6fa9c4ae10848ece2422b91e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 15:19:54 +0100 Subject: [PATCH 0728/1452] Use ZeroconfServiceInfo in wled (#60130) Co-authored-by: epenet --- homeassistant/components/wled/config_flow.py | 6 ++--- tests/components/wled/test_config_flow.py | 27 +++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 828dfc3368a..fa02c14ed27 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -44,14 +44,14 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" # Hostname is format: wled-livingroom.local. - host = discovery_info["hostname"].rstrip(".") + host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") name, _ = host.rsplit(".") self.context.update( { - CONF_HOST: discovery_info["host"], + CONF_HOST: discovery_info[zeroconf.ATTR_HOST], CONF_NAME: name, - CONF_MAC: discovery_info["properties"].get(CONF_MAC), + CONF_MAC: discovery_info[zeroconf.ATTR_PROPERTIES].get(CONF_MAC), "title_placeholders": {"name": name}, } ) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 842e7e332e0..5fc735b4742 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from wled import WLEDConnectionError +from homeassistant.components import zeroconf from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME @@ -47,7 +48,9 @@ async def test_full_zeroconf_flow_implementation( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.local.", properties={} + ), ) flows = hass.config_entries.flow.async_progress() @@ -100,7 +103,9 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.local.", properties={} + ), ) assert result.get("type") == RESULT_TYPE_ABORT @@ -120,7 +125,9 @@ async def test_zeroconf_confirm_connection_error( CONF_HOST: "example.com", CONF_NAME: "test", }, - data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.com.", properties={} + ), ) assert result.get("type") == RESULT_TYPE_ABORT @@ -152,7 +159,9 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", hostname="example.local.", properties={} + ), ) assert result.get("type") == RESULT_TYPE_ABORT @@ -168,11 +177,11 @@ async def test_zeroconf_with_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "hostname": "example.local.", - "properties": {CONF_MAC: "aabbccddeeff"}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + hostname="example.local.", + properties={CONF_MAC: "aabbccddeeff"}, + ), ) assert result.get("type") == RESULT_TYPE_ABORT From 69b749532429a4cdfdebee01dc680deb8cd93770 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Nov 2021 17:04:06 +0100 Subject: [PATCH 0729/1452] Sensor: Handle local->UTC conversion and reject timezoneless timestamps (#59971) --- homeassistant/components/sensor/__init__.py | 20 +++++- tests/components/mobile_app/test_sensor.py | 11 +-- tests/components/octoprint/test_sensor.py | 9 +-- tests/components/picnic/test_sensor.py | 28 ++++---- tests/components/sensor/test_init.py | 80 +++++++++++++++++++-- 5 files changed, 115 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index ca083a0480d..b91b4b3051e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone import inspect import logging from typing import Any, Final, cast, final @@ -308,15 +308,31 @@ class SensorEntity(Entity): f"while it has device class '{device_class}'" ) from error + if value.tzinfo is not None and value.tzinfo != timezone.utc: + value = value.astimezone(timezone.utc) + # Convert the date object to a standardized state string. if device_class == DEVICE_CLASS_DATE: return value.date().isoformat() + return value.isoformat(timespec="seconds") # Received a datetime if value is not None and device_class == DEVICE_CLASS_TIMESTAMP: try: - return value.isoformat(timespec="seconds") # type: ignore + # We cast the value, to avoid using isinstance, but satisfy + # typechecking. The errors are guarded in this try. + value = cast(datetime, value) + if value.tzinfo is None: + raise ValueError( + f"Invalid datetime: {self.entity_id} provides state '{value}', " + "which is missing timezone information" + ) + + if value.tzinfo != timezone.utc: + value = value.astimezone(timezone.utc) + + return value.isoformat(timespec="seconds") except (AttributeError, TypeError) as err: raise ValueError( f"Invalid datetime: {self.entity_id} has a timestamp device class" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 2fea00a692a..cfd9efa34c2 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -287,18 +287,13 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client (DEVICE_CLASS_DATE, "2021-11-18", "2021-11-18"), ( DEVICE_CLASS_TIMESTAMP, - "2021-11-18T20:25:00", - "2021-11-18T20:25:00", - ), - ( - DEVICE_CLASS_TIMESTAMP, - "2021-11-18 20:25:00", - "2021-11-18T20:25:00", + "2021-11-18T20:25:00+00:00", + "2021-11-18T20:25:00+00:00", ), ( DEVICE_CLASS_TIMESTAMP, "2021-11-18 20:25:00+01:00", - "2021-11-18T20:25:00+01:00", + "2021-11-18T19:25:00+00:00", ), ], ) diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index ccc7dfacaf2..a7da0579c10 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -1,5 +1,5 @@ """The tests for Octoptint binary sensor module.""" -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import patch from homeassistant.helpers import entity_registry as er @@ -22,7 +22,8 @@ async def test_sensors(hass): "state": "Printing", } with patch( - "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) + "homeassistant.util.dt.utcnow", + return_value=datetime(2020, 2, 20, 9, 10, 0, tzinfo=timezone.utc), ): await init_integration(hass, "sensor", printer=printer, job=job) @@ -65,14 +66,14 @@ async def test_sensors(hass): state = hass.states.get("sensor.octoprint_start_time") assert state is not None - assert state.state == "2020-02-20T09:00:00" + assert state.state == "2020-02-20T09:00:00+00:00" assert state.name == "OctoPrint Start Time" entry = entity_registry.async_get("sensor.octoprint_start_time") assert entry.unique_id == "Start Time-uuid" state = hass.states.get("sensor.octoprint_estimated_finish_time") assert state is not None - assert state.state == "2020-02-20T10:50:00" + assert state.state == "2020-02-20T10:50:00+00:00" assert state.name == "OctoPrint Estimated Finish Time" entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") assert entry.unique_id == "Estimated Finish Time-uuid" diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 58426e310ed..4edf2c3ec98 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -210,44 +210,44 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): ) self._assert_sensor( "sensor.picnic_selected_slot_start", - "2021-03-03T14:45:00+01:00", + "2021-03-03T13:45:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_end", - "2021-03-03T15:45:00+01:00", + "2021-03-03T14:45:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_max_order_time", - "2021-03-02T22:00:00+01:00", + "2021-03-02T21:00:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0") self._assert_sensor( "sensor.picnic_last_order_slot_start", - "2021-02-26T20:15:00+01:00", + "2021-02-26T19:15:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_slot_end", - "2021-02-26T21:15:00+01:00", + "2021-02-26T20:15:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") self._assert_sensor( "sensor.picnic_last_order_eta_start", - "2021-02-26T20:54:00+01:00", + "2021-02-26T19:54:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_eta_end", - "2021-02-26T21:14:00+01:00", + "2021-02-26T20:14:00+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_delivery_time", - "2021-02-26T20:54:05+01:00", + "2021-02-26T19:54:05+00:00", cls=DEVICE_CLASS_TIMESTAMP, ) self._assert_sensor( @@ -305,10 +305,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): # Assert delivery time is not available, but eta is self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00+01:00" + "sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00+01:00" + "sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00" ) async def test_sensors_use_detailed_eta_if_available(self): @@ -322,8 +322,8 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): self.picnic_mock().get_deliveries.return_value = [delivery_response] self.picnic_mock().get_delivery_position.return_value = { "eta_window": { - "start": "2021-03-05T11:19:20.452+01:00", - "end": "2021-03-05T11:39:20.452+01:00", + "start": "2021-03-05T10:19:20.452+00:00", + "end": "2021-03-05T10:39:20.452+00:00", } } await self._coordinator.async_refresh() @@ -333,10 +333,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): delivery_response["delivery_id"] ) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20+01:00" + "sensor.picnic_last_order_eta_start", "2021-03-05T10:19:20+00:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20+01:00" + "sensor.picnic_last_order_eta_end", "2021-03-05T10:39:20+00:00" ) async def test_sensors_no_data(self): diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index d3fb6e89229..67f750ece96 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -117,6 +117,9 @@ async def test_deprecated_unit_of_measurement(hass, caplog, enable_custom_integr async def test_datetime_conversion(hass, caplog, enable_custom_integrations): """Test conversion of datetime.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) + test_local_timestamp = test_timestamp.astimezone( + dt_util.get_time_zone("Europe/Amsterdam") + ) test_date = date(2017, 12, 19) platform = getattr(hass.components, "test.sensor") platform.init(empty=True) @@ -132,6 +135,11 @@ async def test_datetime_conversion(hass, caplog, enable_custom_integrations): platform.ENTITIES["3"] = platform.MockSensor( name="Test", native_value=None, device_class=DEVICE_CLASS_DATE ) + platform.ENTITIES["4"] = platform.MockSensor( + name="Test", + native_value=test_local_timestamp, + device_class=DEVICE_CLASS_TIMESTAMP, + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -148,16 +156,58 @@ async def test_datetime_conversion(hass, caplog, enable_custom_integrations): state = hass.states.get(platform.ENTITIES["3"].entity_id) assert state.state == STATE_UNKNOWN + state = hass.states.get(platform.ENTITIES["4"].entity_id) + assert state.state == test_timestamp.isoformat() + @pytest.mark.parametrize( - "device_class,native_value", + "device_class,native_value,state_value", [ - (DEVICE_CLASS_DATE, "2021-11-09"), - (DEVICE_CLASS_TIMESTAMP, "2021-01-09T12:00:00+00:00"), + (DEVICE_CLASS_DATE, "2021-11-09", "2021-11-09"), + ( + DEVICE_CLASS_DATE, + "2021-01-09T12:00:00+00:00", + "2021-01-09", + ), + ( + DEVICE_CLASS_DATE, + "2021-01-09T00:00:00+01:00", + "2021-01-08", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09T12:00:00+00:00", + "2021-01-09T12:00:00+00:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09 12:00:00+00:00", + "2021-01-09T12:00:00+00:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09T12:00:00+04:00", + "2021-01-09T08:00:00+00:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09 12:00:00+01:00", + "2021-01-09T11:00:00+00:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09 12:00:00", + "2021-01-09T12:00:00", + ), + ( + DEVICE_CLASS_TIMESTAMP, + "2021-01-09T12:00:00", + "2021-01-09T12:00:00", + ), ], ) async def test_deprecated_datetime_str( - hass, caplog, enable_custom_integrations, device_class, native_value + hass, caplog, enable_custom_integrations, device_class, native_value, state_value ): """Test warning on deprecated str for a date(time) value.""" platform = getattr(hass.components, "test.sensor") @@ -171,9 +221,29 @@ async def test_deprecated_datetime_str( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == native_value + assert state.state == state_value assert ( "is providing a string for its state, while the device class is " f"'{device_class}', this is not valid and will be unsupported " "from Home Assistant 2022.2." ) in caplog.text + + +async def test_reject_timezoneless_datetime_str( + hass, caplog, enable_custom_integrations +): + """Test rejection of timezone-less datetime objects as timestamp.""" + test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=None) + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", native_value=test_timestamp, device_class=DEVICE_CLASS_TIMESTAMP + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "Invalid datetime: sensor.test provides state '2017-12-19 18:29:42', " + "which is missing timezone information" + ) in caplog.text From 67e13b35db80ba4f716719733ed378820efca6d8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:28:10 +0100 Subject: [PATCH 0730/1452] Revert "Create new usb constants (#60086)" (#60137) Co-authored-by: epenet --- homeassistant/components/usb/__init__.py | 27 ++++++++---------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 8c39ba9e6be..355b60906b3 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import Final, TypedDict +from typing import TypedDict from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -31,15 +31,6 @@ _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown -# Attributes for UsbServiceInfo -ATTR_DESCRIPTION: Final = "description" -ATTR_DEVICE: Final = "device" -ATTR_MANUFACTURER: Final = "manufacturer" -ATTR_PID: Final = "pid" -ATTR_SERIAL_NUMBER: Final = "serial_number" -ATTR_VID: Final = "vid" - - class UsbServiceInfo(TypedDict): """Prepared info from usb entries.""" @@ -180,20 +171,20 @@ class USBDiscovery: self.seen.add(device_tuple) matched = [] for matcher in self.usb: - if ATTR_VID in matcher and device.vid != matcher[ATTR_VID]: + if "vid" in matcher and device.vid != matcher["vid"]: continue - if ATTR_PID in matcher and device.pid != matcher[ATTR_PID]: + if "pid" in matcher and device.pid != matcher["pid"]: continue - if ATTR_SERIAL_NUMBER in matcher and not _fnmatch_lower( - device.serial_number, matcher[ATTR_SERIAL_NUMBER] + if "serial_number" in matcher and not _fnmatch_lower( + device.serial_number, matcher["serial_number"] ): continue - if ATTR_MANUFACTURER in matcher and not _fnmatch_lower( - device.manufacturer, matcher[ATTR_MANUFACTURER] + if "manufacturer" in matcher and not _fnmatch_lower( + device.manufacturer, matcher["manufacturer"] ): continue - if ATTR_DESCRIPTION in matcher and not _fnmatch_lower( - device.description, matcher[ATTR_DESCRIPTION] + if "description" in matcher and not _fnmatch_lower( + device.description, matcher["description"] ): continue matched.append(matcher) From 49a27e12ad6e550a3239c33f3ca4537e6072988c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Nov 2021 17:38:06 +0100 Subject: [PATCH 0731/1452] Add support to entity registry for overriding device_class (#59985) --- .../components/config/entity_registry.py | 5 +- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/hue/migration.py | 2 +- .../components/mobile_app/binary_sensor.py | 2 +- homeassistant/components/mobile_app/sensor.py | 2 +- homeassistant/components/shelly/entity.py | 2 +- homeassistant/helpers/entity.py | 6 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 25 ++++++-- .../binary_sensor/test_device_condition.py | 2 +- .../binary_sensor/test_device_trigger.py | 2 +- tests/components/blebox/test_cover.py | 2 +- tests/components/canary/test_sensor.py | 4 +- .../components/config/test_entity_registry.py | 21 ++++++- tests/components/directv/test_media_player.py | 6 +- tests/components/greeneye_monitor/conftest.py | 6 +- tests/components/group/test_binary_sensor.py | 2 +- tests/components/homekit/test_homekit.py | 16 ++--- tests/components/homekit/test_type_covers.py | 4 +- tests/components/homekit/test_type_fans.py | 2 +- tests/components/homekit/test_type_lights.py | 2 +- .../homekit/test_type_media_players.py | 4 +- tests/components/homekit/test_type_sensors.py | 4 +- .../homekit/test_type_thermostats.py | 4 +- tests/components/hue/test_migration.py | 2 +- tests/components/nzbget/test_sensor.py | 2 +- tests/components/roku/test_media_player.py | 4 +- .../sensor/test_device_condition.py | 4 +- .../components/sensor/test_device_trigger.py | 2 +- tests/helpers/test_entity_platform.py | 3 +- tests/helpers/test_entity_registry.py | 59 +++++++++++++++---- 31 files changed, 136 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 62f2aec232c..64cbfd7de1e 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -70,6 +70,7 @@ async def websocket_get_entity(hass, connection, msg): vol.Required("entity_id"): cv.entity_id, # If passed in, we update value. Passing None will remove old value. vol.Optional("area_id"): vol.Any(str, None), + vol.Optional("device_class"): vol.Any(str, None), vol.Optional("icon"): vol.Any(str, None), vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, @@ -92,7 +93,7 @@ async def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("area_id", "disabled_by", "icon", "name"): + for key in ("area_id", "device_class", "disabled_by", "icon", "name"): if key in msg: changes[key] = msg[key] @@ -185,6 +186,8 @@ def _entry_ext_dict(entry): """Convert entry to API format.""" data = _entry_dict(entry) data["capabilities"] = entry.capabilities + data["device_class"] = entry.device_class + data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon data["original_name"] = entry.original_name data["unique_id"] = entry.unique_id diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ee8a3add610..050f0b3db12 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -859,7 +859,7 @@ class HomeKit: ent_reg_ent is None or ent_reg_ent.device_id is None or ent_reg_ent.device_id not in device_lookup - or ent_reg_ent.device_class + or (ent_reg_ent.device_class or ent_reg_ent.original_device_class) in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY) ): return diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index ca41478b97c..5396e646ce1 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -126,7 +126,7 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N continue # migrate sensors matched_dev_class = sensor_class_mapping.get( - ent.device_class or "unknown" + ent.original_device_class or "unknown" ) if matched_dev_class is None: # this may happen if we're looking at orphaned or unsupported entity diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 2e3681b7618..edce0b8f456 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue config = { ATTR_SENSOR_ATTRIBUTES: {}, - ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class or entry.original_device_class, ATTR_SENSOR_ICON: entry.original_icon, ATTR_SENSOR_NAME: entry.original_name, ATTR_SENSOR_STATE: None, diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index f8b71da7c29..b58beef96ba 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue config = { ATTR_SENSOR_ATTRIBUTES: {}, - ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class or entry.original_device_class, ATTR_SENSOR_ICON: entry.original_icon, ATTR_SENSOR_NAME: entry.original_name, ATTR_SENSOR_STATE: None, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 5df87b0531c..65ce8eeba56 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -135,7 +135,7 @@ async def async_restore_block_attribute_entities( name="", icon=entry.original_icon, unit=entry.unit_of_measurement, - device_class=entry.device_class, + device_class=entry.original_device_class, ) entities.append( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a0ad081a782..d2592febe33 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -127,7 +127,7 @@ def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None: if not (entry := entity_registry.async_get(entity_id)): raise HomeAssistantError(f"Unknown entity {entity_id}") - return entry.device_class + return entry.device_class or entry.original_device_class def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: @@ -540,7 +540,9 @@ class Entity(ABC): if (attribution := self.attribution) is not None: attr[ATTR_ATTRIBUTION] = attribution - if (device_class := self.device_class) is not None: + if ( + device_class := (entry and entry.device_class) or self.device_class + ) is not None: attr[ATTR_DEVICE_CLASS] = str(device_class) if (entity_picture := self.entity_picture) is not None: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2d555b82c10..8b78e985ff9 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -511,11 +511,11 @@ class EntityPlatform: entity.unique_id, capabilities=entity.capability_attributes, config_entry=self.config_entry, - device_class=entity.device_class, device_id=device_id, disabled_by=disabled_by, entity_category=entity.entity_category, known_object_ids=self.entities.keys(), + original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity.name, suggested_object_id=suggested_object_id, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 27a4eaa98de..076ea652f79 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -105,6 +105,7 @@ class RegistryEntry: icon: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) # As set by integration + original_device_class: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) original_name: str | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) @@ -128,8 +129,9 @@ class RegistryEntry: if self.capabilities is not None: attrs.update(self.capabilities) - if self.device_class is not None: - attrs[ATTR_DEVICE_CLASS] = self.device_class + device_class = self.device_class or self.original_device_class + if device_class is not None: + attrs[ATTR_DEVICE_CLASS] = device_class icon = self.icon or self.original_icon if icon is not None: @@ -190,7 +192,8 @@ class EntityRegistry: for entity in self.entities.values(): if not entity.device_id: continue - domain_device_class = (entity.domain, entity.device_class) + device_class = entity.device_class or entity.original_device_class + domain_device_class = (entity.domain, device_class) if domain_device_class not in domain_device_classes: continue if entity.device_id not in lookup: @@ -268,9 +271,9 @@ class EntityRegistry: area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, config_entry: ConfigEntry | None = None, - device_class: str | None = None, device_id: str | None = None, entity_category: str | None = None, + original_device_class: str | None = None, original_icon: str | None = None, original_name: str | None = None, supported_features: int | None = None, @@ -289,9 +292,9 @@ class EntityRegistry: area_id=area_id or UNDEFINED, capabilities=capabilities or UNDEFINED, config_entry_id=config_entry_id or UNDEFINED, - device_class=device_class or UNDEFINED, device_id=device_id or UNDEFINED, entity_category=entity_category or UNDEFINED, + original_device_class=original_device_class or UNDEFINED, original_icon=original_icon or UNDEFINED, original_name=original_name or UNDEFINED, supported_features=supported_features or UNDEFINED, @@ -320,11 +323,11 @@ class EntityRegistry: area_id=area_id, capabilities=capabilities, config_entry_id=config_entry_id, - device_class=device_class, device_id=device_id, disabled_by=disabled_by, entity_category=entity_category, entity_id=entity_id, + original_device_class=original_device_class, original_icon=original_icon, original_name=original_name, platform=platform, @@ -409,6 +412,7 @@ class EntityRegistry: name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, + original_device_class: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, @@ -425,6 +429,7 @@ class EntityRegistry: name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, + original_device_class=original_device_class, original_icon=original_icon, original_name=original_name, unit_of_measurement=unit_of_measurement, @@ -446,6 +451,7 @@ class EntityRegistry: name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, + original_device_class: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, supported_features: int | UndefinedType = UNDEFINED, @@ -467,6 +473,7 @@ class EntityRegistry: ("entity_category", entity_category), ("icon", icon), ("name", name), + ("original_device_class", original_device_class), ("original_icon", original_icon), ("original_name", original_name), ("supported_features", supported_features), @@ -552,6 +559,7 @@ class EntityRegistry: entity_id=entity["entity_id"], icon=entity["icon"], name=entity["name"], + original_device_class=entity["original_device_class"], original_icon=entity["original_icon"], original_name=entity["original_name"], platform=entity["platform"], @@ -585,6 +593,7 @@ class EntityRegistry: "entity_id": entry.entity_id, "icon": entry.icon, "name": entry.name, + "original_device_class": entry.original_device_class, "original_icon": entry.original_icon, "original_name": entry.original_name, "platform": entry.platform, @@ -741,6 +750,10 @@ async def _async_migrate( entity["platform"] = entity["platform"] entity["supported_features"] = entity.get("supported_features", 0) entity["unit_of_measurement"] = entity.get("unit_of_measurement") + + # Move device_class to original_device_class + entity["original_device_class"] = entity["device_class"] + entity["device_class"] = None if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 3d1b694c7ce..5a609fbc30a 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -93,7 +93,7 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): "test", f"5678_{device_class}", device_id=device_entry.id, - device_class=device_class, + original_device_class=device_class, ).entity_id await hass.async_block_till_done() diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 8bd80be6524..0cf76453238 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -96,7 +96,7 @@ async def test_get_triggers_no_state(hass, device_reg, entity_reg): "test", f"5678_{device_class}", device_id=device_entry.id, - device_class=device_class, + original_device_class=device_class, ).entity_id await hass.async_block_till_done() diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index 8355411a0bb..e8cf67dad01 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -136,7 +136,7 @@ async def test_init_shutterbox(shutterbox, hass, config): state = hass.states.get(entity_id) assert state.name == "shutterBox-position" - assert entry.device_class == DEVICE_CLASS_SHUTTER + assert entry.original_device_class == DEVICE_CLASS_SHUTTER supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & SUPPORT_OPEN diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index c59810ac72f..530ca1cccc1 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -78,7 +78,7 @@ async def test_sensors_pro(hass, canary) -> None: for (sensor_id, data) in sensors.items(): entity_entry = registry.async_get(f"sensor.{sensor_id}") assert entity_entry - assert entity_entry.device_class == data[3] + assert entity_entry.original_device_class == data[3] assert entity_entry.unique_id == data[0] assert entity_entry.original_icon == data[4] @@ -197,7 +197,7 @@ async def test_sensors_flex(hass, canary) -> None: for (sensor_id, data) in sensors.items(): entity_entry = registry.async_get(f"sensor.{sensor_id}") assert entity_entry - assert entity_entry.device_class == data[3] + assert entity_entry.original_device_class == data[3] assert entity_entry.unique_id == data[0] assert entity_entry.original_icon == data[4] diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 6257037e57e..074d8e223da 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -103,12 +103,14 @@ async def test_get_entity(hass, client): "area_id": None, "capabilities": None, "config_entry_id": None, + "device_class": None, "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.name", "icon": None, "name": "Hello World", + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -128,12 +130,14 @@ async def test_get_entity(hass, client): "area_id": None, "capabilities": None, "config_entry_id": None, + "device_class": None, "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.no_name", "icon": None, "name": None, + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -165,15 +169,16 @@ async def test_update_entity(hass, client): assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" - # UPDATE NAME & ICON & AREA + # UPDATE AREA, DEVICE_CLASS, ICON AND NAME await client.send_json( { "id": 6, "type": "config/entity_registry/update", "entity_id": "test_domain.world", - "name": "after update", - "icon": "icon:after update", "area_id": "mock-area-id", + "device_class": "custom_device_class", + "icon": "icon:after update", + "name": "after update", } ) @@ -184,12 +189,14 @@ async def test_update_entity(hass, client): "area_id": "mock-area-id", "capabilities": None, "config_entry_id": None, + "device_class": "custom_device_class", "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", "icon": "icon:after update", "name": "after update", + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -233,12 +240,14 @@ async def test_update_entity(hass, client): "area_id": "mock-area-id", "capabilities": None, "config_entry_id": None, + "device_class": "custom_device_class", "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", "icon": "icon:after update", "name": "after update", + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -288,12 +297,14 @@ async def test_update_entity_require_restart(hass, client): "area_id": None, "capabilities": None, "config_entry_id": config_entry.entry_id, + "device_class": None, "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", "icon": None, "name": None, + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -390,12 +401,14 @@ async def test_update_entity_no_changes(hass, client): "area_id": None, "capabilities": None, "config_entry_id": None, + "device_class": None, "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", "icon": None, "name": "name of entity", + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", @@ -471,12 +484,14 @@ async def test_update_entity_id(hass, client): "area_id": None, "capabilities": None, "config_entry_id": None, + "device_class": None, "device_id": None, "disabled_by": None, "entity_category": None, "entity_id": "test_domain.planet", "icon": None, "name": None, + "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 14bc121bf86..41431748d36 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -160,15 +160,15 @@ async def test_unique_id( entity_registry = er.async_get(hass) main = entity_registry.async_get(MAIN_ENTITY_ID) - assert main.device_class == DEVICE_CLASS_RECEIVER + assert main.original_device_class == DEVICE_CLASS_RECEIVER assert main.unique_id == "028877455858" client = entity_registry.async_get(CLIENT_ENTITY_ID) - assert client.device_class == DEVICE_CLASS_RECEIVER + assert client.original_device_class == DEVICE_CLASS_RECEIVER assert client.unique_id == "2CA17D1CD30X" unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) - assert unavailable_client.device_class == DEVICE_CLASS_RECEIVER + assert unavailable_client.original_device_class == DEVICE_CLASS_RECEIVER assert unavailable_client.unique_id == "9XXXXXXXXXX9" diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index b6fa49032cc..a68cc9e8b96 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -45,7 +45,7 @@ def assert_temperature_sensor_registered( ): """Assert that a temperature sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "temp", number, name) - assert sensor.device_class == DEVICE_CLASS_TEMPERATURE + assert sensor.original_device_class == DEVICE_CLASS_TEMPERATURE def assert_pulse_counter_registered( @@ -67,7 +67,7 @@ def assert_power_sensor_registered( """Assert that a power sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "current", number, name) assert sensor.unit_of_measurement == POWER_WATT - assert sensor.device_class == DEVICE_CLASS_POWER + assert sensor.original_device_class == DEVICE_CLASS_POWER def assert_voltage_sensor_registered( @@ -76,7 +76,7 @@ def assert_voltage_sensor_registered( """Assert that a voltage sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "volts", number, name) assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT - assert sensor.device_class == DEVICE_CLASS_VOLTAGE + assert sensor.original_device_class == DEVICE_CLASS_VOLTAGE def assert_sensor_registered( diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 0738c31eb62..9da54be2ab4 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -40,7 +40,7 @@ async def test_default_state(hass): assert entry assert entry.unique_id == "unique_identifier" assert entry.original_name == "Bedroom Group" - assert entry.device_class == "presence" + assert entry.original_device_class == "presence" async def test_state_reporting_all(hass): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 44ed71afaca..deff07ecdb4 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1118,14 +1118,14 @@ async def test_homekit_finds_linked_batteries( "powerwall", "battery_charging", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=DEVICE_CLASS_BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY, + original_device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id @@ -1187,14 +1187,14 @@ async def test_homekit_async_get_integration_fails( "invalid_integration_does_not_exist", "battery_charging", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=DEVICE_CLASS_BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "invalid_integration_does_not_exist", "battery", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY, + original_device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id @@ -1334,14 +1334,14 @@ async def test_homekit_ignored_missing_devices( "powerwall", "battery_charging", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=DEVICE_CLASS_BATTERY_CHARGING, ) entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, - device_class=DEVICE_CLASS_BATTERY, + original_device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id @@ -1404,7 +1404,7 @@ async def test_homekit_finds_linked_motion_sensors( "camera", "motion_sensor", device_id=device_entry.id, - device_class=DEVICE_CLASS_MOTION, + original_device_class=DEVICE_CLASS_MOTION, ) camera = entity_reg.async_get_or_create( "camera", "camera", "demo", device_id=device_entry.id @@ -1466,7 +1466,7 @@ async def test_homekit_finds_linked_humidity_sensors( "humidifier", "humidity_sensor", device_id=device_entry.id, - device_class=DEVICE_CLASS_HUMIDITY, + original_device_class=DEVICE_CLASS_HUMIDITY, ) humidifier = entity_reg.async_get_or_create( "humidifier", "humidifier", "demo", device_id=device_entry.id diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 89407edfbef..ab85147475b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -506,7 +506,7 @@ async def test_windowcovering_basic_restore(hass, hk_driver, events): suggested_object_id="all_info_set", capabilities={}, supported_features=SUPPORT_STOP, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) @@ -544,7 +544,7 @@ async def test_windowcovering_restore(hass, hk_driver, events): suggested_object_id="all_info_set", capabilities={}, supported_features=SUPPORT_STOP, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 646d1baad63..c8990169741 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -546,7 +546,7 @@ async def test_fan_restore(hass, hk_driver, events): suggested_object_id="all_info_set", capabilities={"speed_list": ["off", "low", "medium", "high"]}, supported_features=SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index de0fd532ec9..9c0d45126fc 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -546,7 +546,7 @@ async def test_light_restore(hass, hk_driver, events): suggested_object_id="all_info_set", capabilities={"supported_color_modes": ["brightness"], "max": 100}, supported_features=5, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index c0184667e2c..46787c98a3c 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -431,7 +431,7 @@ async def test_tv_restore(hass, hk_driver, events): "generic", "1234", suggested_object_id="simple", - device_class=DEVICE_CLASS_TV, + original_device_class=DEVICE_CLASS_TV, ) registry.async_get_or_create( "media_player", @@ -442,7 +442,7 @@ async def test_tv_restore(hass, hk_driver, events): ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"], }, supported_features=3469, - device_class=DEVICE_CLASS_TV, + original_device_class=DEVICE_CLASS_TV, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index c7c06dcc90a..7ad48bfbce6 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -333,14 +333,14 @@ async def test_sensor_restore(hass, hk_driver, events): "generic", "1234", suggested_object_id="temperature", - device_class="temperature", + original_device_class="temperature", ) registry.async_get_or_create( "sensor", "generic", "12345", suggested_object_id="humidity", - device_class="humidity", + original_device_class="humidity", unit_of_measurement=PERCENTAGE, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ef517f4ab96..9d2b5066ce4 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1018,7 +1018,7 @@ async def test_thermostat_restore(hass, hk_driver, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF], }, supported_features=0, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) @@ -1826,7 +1826,7 @@ async def test_water_heater_restore(hass, hk_driver, events): suggested_object_id="all_info_set", capabilities={ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}, supported_features=0, - device_class="mock-device-class", + original_device_class="mock-device-class", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 382a075bbbd..8457ed04170 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -118,7 +118,7 @@ async def test_sensor_entity_migration( f"{device_mac}-{dev_class}", suggested_object_id=f"hue_migrated_{dev_class}_sensor", device_id=device.id, - device_class=dev_class, + original_device_class=dev_class, ) # now run the migration and check results diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index de5e2382a63..d47e67c96c6 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -45,7 +45,7 @@ async def test_sensors(hass, nzbget_api) -> None: for (sensor_id, data) in sensors.items(): entity_entry = registry.async_get(f"sensor.nzbgettest_{sensor_id}") assert entity_entry - assert entity_entry.device_class == data[3] + assert entity_entry.original_device_class == data[3] assert entity_entry.unique_id == f"{entry.entry_id}_{data[0]}" state = hass.states.get(f"sensor.nzbgettest_{sensor_id}") diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index eb0d0028417..9d97e467c68 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -89,7 +89,7 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - assert hass.states.get(MAIN_ENTITY_ID) assert main - assert main.device_class == DEVICE_CLASS_RECEIVER + assert main.original_device_class == DEVICE_CLASS_RECEIVER assert main.unique_id == UPNP_SERIAL @@ -121,7 +121,7 @@ async def test_tv_setup( assert hass.states.get(TV_ENTITY_ID) assert tv - assert tv.device_class == DEVICE_CLASS_TV + assert tv.original_device_class == DEVICE_CLASS_TV assert tv.unique_id == TV_SERIAL diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index daf452cf715..5742f7b47c4 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -100,7 +100,7 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): "test", f"5678_{device_class}", device_id=device_entry.id, - device_class=device_class, + original_device_class=device_class, unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), ).entity_id @@ -155,7 +155,7 @@ async def test_get_condition_capabilities( "test", platform.ENTITIES["battery"].unique_id, device_id=device_entry.id, - device_class=device_class_reg, + original_device_class=device_class_reg, unit_of_measurement=unit_reg, ).entity_id if set_state: diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 21a52f691e9..b8b3ee46a43 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -122,7 +122,7 @@ async def test_get_trigger_capabilities( "test", platform.ENTITIES["battery"].unique_id, device_id=device_entry.id, - device_class=device_class_reg, + original_device_class=device_class_reg, unit_of_measurement=unit_reg, ).entity_id if set_state: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 4e40bcd9882..8573e90f592 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1105,10 +1105,11 @@ async def test_entity_info_added_to_entity_registry(hass): "default", "test_domain", capabilities={"max": 100}, - device_class="mock-device-class", + device_class=None, entity_category="config", icon=None, name=None, + original_device_class="mock-device-class", original_icon="nice:icon", original_name="best name", supported_features=5, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 543cf6c0045..00900e013c0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -74,10 +74,10 @@ def test_get_or_create_updates_data(registry): area_id="mock-area-id", capabilities={"max": 100}, config_entry=orig_config_entry, - device_class="mock-device-class", device_id="mock-dev-id", disabled_by=er.DISABLED_HASS, entity_category="config", + original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", supported_features=5, @@ -91,12 +91,13 @@ def test_get_or_create_updates_data(registry): area_id="mock-area-id", capabilities={"max": 100}, config_entry_id=orig_config_entry.entry_id, - device_class="mock-device-class", + device_class=None, device_id="mock-dev-id", disabled_by=er.DISABLED_HASS, entity_category="config", icon=None, name=None, + original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", supported_features=5, @@ -112,10 +113,10 @@ def test_get_or_create_updates_data(registry): area_id="new-mock-area-id", capabilities={"new-max": 100}, config_entry=new_config_entry, - device_class="new-mock-device-class", device_id="new-mock-dev-id", disabled_by=er.DISABLED_USER, entity_category=None, + original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", supported_features=10, @@ -129,12 +130,13 @@ def test_get_or_create_updates_data(registry): area_id="new-mock-area-id", capabilities={"new-max": 100}, config_entry_id=new_config_entry.entry_id, - device_class="new-mock-device-class", + device_class=None, device_id="new-mock-dev-id", disabled_by=er.DISABLED_HASS, # Should not be updated entity_category="config", icon=None, name=None, + original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", supported_features=10, @@ -182,17 +184,20 @@ async def test_loading_saving_data(hass, registry): area_id="mock-area-id", capabilities={"max": 100}, config_entry=mock_config, - device_class="mock-device-class", device_id="mock-dev-id", disabled_by=er.DISABLED_HASS, entity_category="config", + original_device_class="mock-device-class", original_icon="hass:original-icon", original_name="Original Name", supported_features=5, unit_of_measurement="initial-unit_of_measurement", ) orig_entry2 = registry.async_update_entity( - orig_entry2.entity_id, name="User Name", icon="hass:user-icon" + orig_entry2.entity_id, + device_class="user-class", + name="User Name", + icon="hass:user-icon", ) assert len(registry.entities) == 2 @@ -213,12 +218,13 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.area_id == "mock-area-id" assert new_entry2.capabilities == {"max": 100} assert new_entry2.config_entry_id == mock_config.entry_id - assert new_entry2.device_class == "mock-device-class" + assert new_entry2.device_class == "user-class" assert new_entry2.device_id == "mock-dev-id" assert new_entry2.disabled_by == er.DISABLED_HASS assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" assert new_entry2.name == "User Name" + assert new_entry2.original_device_class == "mock-device-class" assert new_entry2.original_icon == "hass:original-icon" assert new_entry2.original_name == "Original Name" assert new_entry2.supported_features == 5 @@ -411,6 +417,33 @@ async def test_migration_yaml_to_json(hass): assert entry.config_entry_id == "test-config-id" +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_1_to_1_2(hass, hass_storage): + """Test migration from version 1.1 to 1.2.""" + hass_storage[er.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "data": { + "entities": [ + { + "device_class": "best_class", + "entity_id": "test.entity", + "platform": "super_platform", + "unique_id": "very_unique", + }, + ] + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + entry = registry.async_get_or_create("test", "super_platform", "very_unique") + + assert entry.device_class is None + assert entry.original_device_class == "best_class" + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_entity_id(hass, hass_storage): """Test we skip entities with invalid entity IDs.""" @@ -599,7 +632,7 @@ async def test_restore_states(hass): suggested_object_id="all_info_set", capabilities={"max": 100}, supported_features=5, - device_class="mock-device-class", + original_device_class="mock-device-class", original_name="Mock Original Name", original_icon="hass:original-icon", ) @@ -649,14 +682,14 @@ async def test_async_get_device_class_lookup(hass): "light", "battery_charging", device_id="light_device_entry_id", - device_class="battery_charging", + original_device_class="battery_charging", ) ent_reg.async_get_or_create( "sensor", "light", "battery", device_id="light_device_entry_id", - device_class="battery", + original_device_class="battery", ) ent_reg.async_get_or_create( "light", "light", "demo", device_id="light_device_entry_id" @@ -666,14 +699,14 @@ async def test_async_get_device_class_lookup(hass): "vacuum", "battery_charging", device_id="vacuum_device_entry_id", - device_class="battery_charging", + original_device_class="battery_charging", ) ent_reg.async_get_or_create( "sensor", "vacuum", "battery", device_id="vacuum_device_entry_id", - device_class="battery", + original_device_class="battery", ) ent_reg.async_get_or_create( "vacuum", "vacuum", "demo", device_id="vacuum_device_entry_id" @@ -683,7 +716,7 @@ async def test_async_get_device_class_lookup(hass): "remote", "battery_charging", device_id="remote_device_entry_id", - device_class="battery_charging", + original_device_class="battery_charging", ) ent_reg.async_get_or_create( "remote", "remote", "demo", device_id="remote_device_entry_id" From 4a5238efa50580148ce15e26d883e5b2738671d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Nov 2021 18:10:54 +0100 Subject: [PATCH 0732/1452] Add support for calculating daily and monthly fossil energy consumption (#59588) --- .../components/energy/websocket_api.py | 147 ++++- .../components/recorder/statistics.py | 81 +-- tests/components/energy/test_websocket_api.py | 532 +++++++++++++++++- 3 files changed, 724 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 7af7b306f79..e15713ff8ad 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -2,18 +2,22 @@ from __future__ import annotations import asyncio +from collections import defaultdict +from datetime import datetime, timedelta import functools +from itertools import chain from types import ModuleType from typing import Any, Awaitable, Callable, cast import voluptuous as vol -from homeassistant.components import websocket_api +from homeassistant.components import recorder, websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) from homeassistant.helpers.singleton import singleton +from homeassistant.util import dt as dt_util from .const import DOMAIN from .data import ( @@ -44,6 +48,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_validate) websocket_api.async_register_command(hass, ws_solar_forecast) + websocket_api.async_register_command(hass, ws_get_fossil_energy_consumption) @singleton("energy_platforms") @@ -218,3 +223,143 @@ async def ws_solar_forecast( forecasts[config_entry_id] = forecast connection.send_result(msg["id"], forecasts) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/fossil_energy_consumption", + vol.Required("start_time"): str, + vol.Required("end_time"): str, + vol.Required("energy_statistic_ids"): [str], + vol.Required("co2_statistic_id"): str, + vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), + } +) +@websocket_api.async_response +async def ws_get_fossil_energy_consumption( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Calculate amount of fossil based energy.""" + start_time_str = msg["start_time"] + end_time_str = msg["end_time"] + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + + statistic_ids = list(msg["energy_statistic_ids"]) + statistic_ids.append(msg["co2_statistic_id"]) + + # Fetch energy + CO2 statistics + statistics = await hass.async_add_executor_job( + recorder.statistics.statistics_during_period, + hass, + start_time, + end_time, + statistic_ids, + "hour", + True, + ) + + def _combine_sum_statistics( + stats: dict[str, list[dict[str, Any]]], statistic_ids: list[str] + ) -> dict[datetime, float]: + """Combine multiple statistics, returns a dict indexed by start time.""" + result: defaultdict[datetime, float] = defaultdict(float) + + for statistics_id, stat in stats.items(): + if statistics_id not in statistic_ids: + continue + for period in stat: + if period["sum"] is None: + continue + result[period["start"]] += period["sum"] + + return {key: result[key] for key in sorted(result)} + + def _calculate_deltas(sums: dict[datetime, float]) -> dict[datetime, float]: + prev: float | None = None + result: dict[datetime, float] = {} + for period, sum_ in sums.items(): + if prev is not None: + result[period] = sum_ - prev + prev = sum_ + return result + + def _reduce_deltas( + stat_list: list[dict[str, Any]], + same_period: Callable[[datetime, datetime], bool], + period_start_end: Callable[[datetime], tuple[datetime, datetime]], + period: timedelta, + ) -> list[dict[str, Any]]: + """Reduce hourly deltas to daily or monthly deltas.""" + result: list[dict[str, Any]] = [] + deltas: list[float] = [] + prev_stat: dict[str, Any] = stat_list[0] + + # Loop over the hourly deltas + a fake entry to end the period + for statistic in chain( + stat_list, ({"start": stat_list[-1]["start"] + period},) + ): + if not same_period(prev_stat["start"], statistic["start"]): + start, _ = period_start_end(prev_stat["start"]) + # The previous statistic was the last entry of the period + result.append( + { + "start": start.isoformat(), + "delta": sum(deltas), + } + ) + deltas = [] + if statistic.get("delta") is not None: + deltas.append(statistic["delta"]) + prev_stat = statistic + + return result + + merged_energy_statistics = _combine_sum_statistics( + statistics, msg["energy_statistic_ids"] + ) + energy_deltas = _calculate_deltas(merged_energy_statistics) + indexed_co2_statistics = { + period["start"]: period["mean"] + for period in statistics.get(msg["co2_statistic_id"], {}) + } + + # Calculate amount of fossil based energy, assume 100% fossil if missing + fossil_energy = [ + {"start": start, "delta": delta * indexed_co2_statistics.get(start, 100) / 100} + for start, delta in energy_deltas.items() + ] + + if msg["period"] == "hour": + reduced_fossil_energy = [ + {"start": period["start"].isoformat(), "delta": period["delta"]} + for period in fossil_energy + ] + + elif msg["period"] == "day": + reduced_fossil_energy = _reduce_deltas( + fossil_energy, + recorder.statistics.same_day, + recorder.statistics.day_start_end, + timedelta(days=1), + ) + else: + reduced_fossil_energy = _reduce_deltas( + fossil_energy, + recorder.statistics.same_month, + recorder.statistics.month_start_end, + timedelta(days=1), + ) + + result = {period["start"]: period["delta"] for period in reduced_fossil_energy} + connection.send_result(msg["id"], result) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 938519a119a..2b60e6fbf00 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -697,8 +697,8 @@ def _reduce_statistics( "mean": mean(mean_values) if mean_values else None, "min": min(min_values) if min_values else None, "max": max(max_values) if max_values else None, - "last_reset": prev_stat["last_reset"], - "state": prev_stat["state"], + "last_reset": prev_stat.get("last_reset"), + "state": prev_stat.get("state"), "sum": prev_stat["sum"], } ) @@ -716,50 +716,54 @@ def _reduce_statistics( return result +def same_day(time1: datetime, time2: datetime) -> bool: + """Return True if time1 and time2 are in the same date.""" + date1 = dt_util.as_local(time1).date() + date2 = dt_util.as_local(time2).date() + return date1 == date2 + + +def day_start_end(time: datetime) -> tuple[datetime, datetime]: + """Return the start and end of the period (day) time is within.""" + start = dt_util.as_utc( + dt_util.as_local(time).replace(hour=0, minute=0, second=0, microsecond=0) + ) + end = start + timedelta(days=1) + return (start, end) + + def _reduce_statistics_per_day( stats: dict[str, list[dict[str, Any]]] ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to daily statistics.""" - def same_period(time1: datetime, time2: datetime) -> bool: - """Return True if time1 and time2 are in the same date.""" - date1 = dt_util.as_local(time1).date() - date2 = dt_util.as_local(time2).date() - return date1 == date2 + return _reduce_statistics(stats, same_day, day_start_end, timedelta(days=1)) - def period_start_end(time: datetime) -> tuple[datetime, datetime]: - """Return the start and end of the period (day) time is within.""" - start = dt_util.as_utc( - dt_util.as_local(time).replace(hour=0, minute=0, second=0, microsecond=0) - ) - end = start + timedelta(days=1) - return (start, end) - return _reduce_statistics(stats, same_period, period_start_end, timedelta(days=1)) +def same_month(time1: datetime, time2: datetime) -> bool: + """Return True if time1 and time2 are in the same year and month.""" + date1 = dt_util.as_local(time1).date() + date2 = dt_util.as_local(time2).date() + return (date1.year, date1.month) == (date2.year, date2.month) + + +def month_start_end(time: datetime) -> tuple[datetime, datetime]: + """Return the start and end of the period (month) time is within.""" + start_local = dt_util.as_local(time).replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) + start = dt_util.as_utc(start_local) + end_local = (start_local + timedelta(days=31)).replace(day=1) + end = dt_util.as_utc(end_local) + return (start, end) def _reduce_statistics_per_month( - stats: dict[str, list[dict[str, Any]]] + stats: dict[str, list[dict[str, Any]]], ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to monthly statistics.""" - def same_period(time1: datetime, time2: datetime) -> bool: - """Return True if time1 and time2 are in the same year and month.""" - date1 = dt_util.as_local(time1).date() - date2 = dt_util.as_local(time2).date() - return (date1.year, date1.month) == (date2.year, date2.month) - - def period_start_end(time: datetime) -> tuple[datetime, datetime]: - """Return the start and end of the period (month) time is within.""" - start_local = dt_util.as_local(time).replace( - day=1, hour=0, minute=0, second=0, microsecond=0 - ) - start = dt_util.as_utc(start_local) - end_local = (start_local + timedelta(days=31)).replace(day=1) - end = dt_util.as_utc(end_local) - return (start, end) - - return _reduce_statistics(stats, same_period, period_start_end, timedelta(days=31)) + return _reduce_statistics(stats, same_month, month_start_end, timedelta(days=31)) def statistics_during_period( @@ -768,6 +772,7 @@ def statistics_during_period( end_time: datetime | None = None, statistic_ids: list[str] | None = None, period: Literal["5minute", "day", "hour", "month"] = "hour", + start_time_as_datetime: bool = False, ) -> dict[str, list[dict[str, Any]]]: """Return statistics during UTC period start_time - end_time for the statistic_ids. @@ -808,7 +813,15 @@ def statistics_during_period( # Return statistics combined with metadata if period not in ("day", "month"): return _sorted_statistics_to_dict( - hass, session, stats, statistic_ids, metadata, True, table, start_time + hass, + session, + stats, + statistic_ids, + metadata, + True, + table, + start_time, + start_time_as_datetime, ) result = _sorted_statistics_to_dict( diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 09a3b7aed94..f86e43dd1b2 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -4,9 +4,17 @@ from unittest.mock import AsyncMock, Mock import pytest from homeassistant.components.energy import data, is_configured +from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry, flush_store, mock_platform +from tests.common import ( + MockConfigEntry, + flush_store, + init_recorder_component, + mock_platform, +) +from tests.components.recorder.common import async_wait_recording_done_without_instance @pytest.fixture(autouse=True) @@ -289,3 +297,525 @@ async def test_get_solar_forecast(hass, hass_ws_client, mock_energy_platform) -> } } } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client): + """Test fossil_energy_consumption when co2 data is missing.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 80, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx(33.0 - 22.0), + period3.isoformat(): pytest.approx(55.0 - 33.0), + period4.isoformat(): pytest.approx(88.0 - 55.0), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx(33.0 - 22.0), + period3.isoformat(): pytest.approx(55.0 - 33.0), + period4_day_start.isoformat(): pytest.approx(88.0 - 55.0), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx(33.0 - 22.0), + period3.isoformat(): pytest.approx((55.0 - 33.0) + (88.0 - 55.0)), + } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_hole(hass, hass_ws_client): + """Test fossil_energy_consumption when some data points lack sum.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": None, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": None, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 80, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx(55.0 - 3.0), + period4.isoformat(): pytest.approx(88.0 - 55.0), + } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption(hass, hass_ws_client): + """Test fossil_energy_consumption with co2 sensor data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + async_add_external_statistics(hass, external_co2_metadata, external_co2_statistics) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx( + ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) + ), + } + + +async def test_fossil_energy_consumption_checks(hass, hass_ws_client): + """Test fossil_energy_consumption parameter validation.""" + client = await hass_ws_client(hass) + now = dt_util.utcnow() + + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": "donald_duck", + "end_time": now.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 1 + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_start_time", + "message": "Invalid start_time", + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": "donald_duck", + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 2 + assert not msg["success"] + assert msg["error"] == {"code": "invalid_end_time", "message": "Invalid end_time"} From 053c4561998a6d7cd451899cee796fc50113bef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 22 Nov 2021 19:14:15 +0200 Subject: [PATCH 0733/1452] Change device entry type to an StrEnum (#59940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ville Skyttä Co-authored-by: Franck Nijhof --- .../components/accuweather/sensor.py | 3 +- .../components/accuweather/weather.py | 3 +- homeassistant/components/adguard/__init__.py | 3 +- homeassistant/components/airly/sensor.py | 3 +- homeassistant/components/ambee/const.py | 2 -- homeassistant/components/ambee/sensor.py | 5 +-- homeassistant/components/aurora/__init__.py | 3 +- .../components/azure_devops/__init__.py | 3 +- homeassistant/components/co2signal/sensor.py | 3 +- .../components/forecast_solar/const.py | 2 -- .../components/forecast_solar/sensor.py | 5 +-- homeassistant/components/gios/sensor.py | 3 +- .../components/google_travel_time/sensor.py | 3 +- homeassistant/components/kraken/sensor.py | 2 +- homeassistant/components/met/weather.py | 3 +- .../components/mutesync/binary_sensor.py | 3 +- .../components/openweathermap/sensor.py | 3 +- .../components/openweathermap/weather.py | 3 +- homeassistant/components/p1_monitor/const.py | 2 -- homeassistant/components/p1_monitor/sensor.py | 4 +-- homeassistant/components/picnic/sensor.py | 3 +- homeassistant/components/rdw/binary_sensor.py | 5 +-- homeassistant/components/rdw/const.py | 1 - homeassistant/components/rdw/sensor.py | 5 +-- homeassistant/components/sonarr/entity.py | 3 +- .../components/speedtestdotnet/sensor.py | 3 +- .../components/stookalert/binary_sensor.py | 5 +-- homeassistant/components/stookalert/const.py | 2 -- homeassistant/components/syncthing/sensor.py | 3 +- .../components/twentemilieu/const.py | 2 -- .../components/twentemilieu/sensor.py | 5 +-- .../components/uptimerobot/entity.py | 3 +- .../components/vlc_telnet/media_player.py | 3 +- .../components/waze_travel_time/sensor.py | 3 +- homeassistant/helpers/device_registry.py | 26 +++++++++++--- homeassistant/helpers/entity.py | 3 +- homeassistant/util/enum.py | 31 ++++++++++++++++ tests/components/ambee/test_sensor.py | 10 ++---- .../components/forecast_solar/test_sensor.py | 4 +-- tests/components/p1_monitor/test_sensor.py | 8 ++--- tests/components/rdw/test_binary_sensor.py | 4 +-- tests/components/rdw/test_sensor.py | 4 +-- tests/components/twentemilieu/test_sensor.py | 4 +-- tests/helpers/test_device_registry.py | 2 +- tests/util/test_enum.py | 35 +++++++++++++++++++ 45 files changed, 167 insertions(+), 71 deletions(-) create mode 100644 homeassistant/util/enum.py create mode 100644 tests/util/test_enum.py diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 3159293a4cc..62047f801fb 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -95,7 +96,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.location_key)}, manufacturer=MANUFACTURER, name=NAME, diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index af2d1c15b2b..a17c1a090b1 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -19,6 +19,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -68,7 +69,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): ) self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.location_key)}, manufacturer=MANUFACTURER, name=NAME, diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index d76d32ce066..6dc0ea63a2b 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -21,6 +21,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.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( @@ -197,7 +198,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): def device_info(self) -> DeviceInfo: """Return device information about this AdGuard Home instance.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={ (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore }, diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 76b4e7d9d48..8da42b86a7c 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -154,7 +155,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 42b19a52995..a63aa4b804d 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -21,8 +21,6 @@ DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ENTRY_TYPE_SERVICE: Final = "service" - DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" SERVICE_AIR_QUALITY: Final = "air_quality" diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index 2ddd60a9168..bf9cfe74f31 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -16,7 +17,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES +from .const import DOMAIN, SENSORS, SERVICES async def async_setup_entry( @@ -59,7 +60,7 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" self._attr_device_info = DeviceInfo( - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{entry_id}_{service_key}")}, manufacturer="Ambee", name=service, diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 1cc378983ca..a88378edaa9 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -140,7 +141,7 @@ class AuroraEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Define the device based on name.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(self.unique_id))}, manufacturer="NOAA", model="Aurora Visibility Sensor", diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index f97dfe730d0..d3599aa3021 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -14,6 +14,7 @@ import aiohttp from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -120,7 +121,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): def device_info(self) -> DeviceInfo: """Return device information about this Azure DevOps instance.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore manufacturer=self._organization, name=self._project_name, diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index b7a36623a3c..53a02afb560 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.helpers import config_validation as cv, update_coordinator +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType @@ -104,7 +105,7 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE } self._attr_device_info = DeviceInfo( configuration_url="https://www.electricitymap.org/", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.entry_id)}, manufacturer="Tmrow.com", name="CO2 signal", diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 1ec8c3e4df1..97caba531e6 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -from typing import Final from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( @@ -21,7 +20,6 @@ CONF_DECLINATION = "declination" CONF_AZIMUTH = "azimuth" CONF_MODULES_POWER = "modules power" CONF_DAMPING = "damping" -ENTRY_TYPE_SERVICE: Final = "service" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index c1ebbaf60bd..4efad600d66 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -14,7 +15,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS +from .const import DOMAIN, SENSORS from .models import ForecastSolarSensorEntityDescription @@ -53,7 +54,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{entry_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="Forecast.Solar", model=coordinator.data.account_type.value, diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index f60a8e99d5a..ff589d34791 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get_registry @@ -83,7 +84,7 @@ class GiosSensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 1b999edc8b7..2bd8e15795d 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util @@ -213,7 +214,7 @@ class GoogleTravelTimeSensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._api_key)}, name=DOMAIN, ) diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index f18d0e78d94..297435b3d1d 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -121,7 +121,7 @@ class KrakenSensor(CoordinatorEntity[Optional[KrakenResponse]], SensorEntity): self._available = True self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=device_registry.DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{source_asset}_{self._target_asset}")}, manufacturer="Kraken.com", name=self._device_name, diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 04ce37a388e..43aeaad5872 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.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.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -281,7 +282,7 @@ class MetWeather(CoordinatorEntity, WeatherEntity): """Device info.""" return DeviceInfo( default_name="Forecast", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN,)}, # type: ignore[arg-type] manufacturer="Met.no", model="Forecast", diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py index 3186b6fc8f0..df8b0ceb9bb 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -1,6 +1,7 @@ """mütesync binary sensor entities.""" from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers import update_coordinator +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN @@ -46,7 +47,7 @@ class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): def device_info(self) -> DeviceInfo: """Return the device info of the sensor.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.coordinator.data["user-id"])}, manufacturer="mütesync", model="mutesync app", diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index dcdb1c0c8fb..07d122d6b00 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -80,7 +81,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): self._attr_unique_id = unique_id split_unique_id = unique_id.split("-") self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index d3b5c488c56..aafa1a9c808 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -5,6 +5,7 @@ from homeassistant.components.weather import Forecast, WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.pressure import convert as pressure_convert @@ -71,7 +72,7 @@ class OpenWeatherMapWeather(WeatherEntity): def device_info(self) -> DeviceInfo: """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._unique_id)}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py index 79b53eeed9e..d72927a80f6 100644 --- a/homeassistant/components/p1_monitor/const.py +++ b/homeassistant/components/p1_monitor/const.py @@ -9,8 +9,6 @@ DOMAIN: Final = "p1_monitor" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=5) -ENTRY_TYPE_SERVICE: Final = "service" - SERVICE_SMARTMETER: Final = "smartmeter" SERVICE_PHASES: Final = "phases" SERVICE_SETTINGS: Final = "settings" diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index e3cebf94a68..67ed6a160d8 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -33,7 +34,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import P1MonitorDataUpdateCoordinator from .const import ( DOMAIN, - ENTRY_TYPE_SERVICE, SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER, @@ -264,7 +264,7 @@ class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity): ) self._attr_device_info = DeviceInfo( - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={ (DOMAIN, f"{coordinator.config_entry.entry_id}_{service_key}") }, diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index d3de3d1dfb3..95612e7b272 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -6,6 +6,7 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -79,7 +80,7 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, cast(str, self._service_unique_id))}, manufacturer="Picnic", model=self._service_unique_id, diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 626afcf2b0a..75077a53966 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -20,7 +21,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN, ENTRY_TYPE_SERVICE +from .const import DOMAIN @dataclass @@ -87,7 +88,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): self._attr_unique_id = f"{coordinator.data.license_plate}_{description.key}" self._attr_device_info = DeviceInfo( - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.data.license_plate)}, manufacturer=coordinator.data.brand, name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", diff --git a/homeassistant/components/rdw/const.py b/homeassistant/components/rdw/const.py index 10058019aa2..e39e4048791 100644 --- a/homeassistant/components/rdw/const.py +++ b/homeassistant/components/rdw/const.py @@ -10,5 +10,4 @@ DOMAIN: Final = "rdw" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ENTRY_TYPE_SERVICE: Final = "service" CONF_LICENSE_PLATE: Final = "license_plate" diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 06ad446fccb..f9b1dd09847 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -21,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import CONF_LICENSE_PLATE, DOMAIN, ENTRY_TYPE_SERVICE +from .const import CONF_LICENSE_PLATE, DOMAIN @dataclass @@ -89,7 +90,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{license_plate}_{description.key}" self._attr_device_info = DeviceInfo( - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{license_plate}")}, manufacturer=coordinator.data.brand, name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 8f3f1188bac..1d0cb2ce6f3 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from sonarr import Sonarr +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -38,6 +39,6 @@ class SonarrEntity(Entity): name="Activity Sensor", manufacturer="Sonarr", sw_version=self.sonarr.app.info.version, - entry_type="service", + entry_type=DeviceEntryType.SERVICE, configuration_url=configuration_url, ) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index e6d2138bfc3..10071bf9054 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.speedtestdotnet import SpeedTestDataCoordinator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -64,7 +65,7 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, name=DEFAULT_NAME, - entry_type="service", + entry_type=DeviceEntryType.SERVICE, configuration_url="https://www.speedtest.net/", ) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index b75790cef8a..d41e24f7c94 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -15,11 +15,12 @@ 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.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_PROVINCE, DOMAIN, ENTRY_TYPE_SERVICE, LOGGER, PROVINCES +from .const import CONF_PROVINCE, DOMAIN, LOGGER, PROVINCES DEFAULT_NAME = "Stookalert" ATTRIBUTION = "Data provided by rivm.nl" @@ -83,7 +84,7 @@ class StookalertBinarySensor(BinarySensorEntity): name=entry.data[CONF_PROVINCE], manufacturer="RIVM", model="Stookalert", - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, configuration_url="https://www.rivm.nl/stookalert", ) diff --git a/homeassistant/components/stookalert/const.py b/homeassistant/components/stookalert/const.py index 72e39e60048..3d370fb135d 100644 --- a/homeassistant/components/stookalert/const.py +++ b/homeassistant/components/stookalert/const.py @@ -21,5 +21,3 @@ PROVINCES: Final = ( "Zeeland", "Zuid-Holland", ) - -ENTRY_TYPE_SERVICE: Final = "service" diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 192b4c5c395..b57315b43b9 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -5,6 +5,7 @@ import aiosyncthing from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -134,7 +135,7 @@ class FolderSensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return device information.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._server_id)}, manufacturer="Syncthing Team", name=f"Syncthing ({self._syncthing.url})", diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 8717d26b0f3..95ab903cc17 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -11,5 +11,3 @@ SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" - -ENTRY_TYPE_SERVICE: Final = "service" diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 259f3c4e969..e75d7f372ac 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -17,7 +18,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN, ENTRY_TYPE_SERVICE +from .const import DOMAIN @dataclass @@ -96,7 +97,7 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" self._attr_device_info = DeviceInfo( configuration_url="https://www.twentemilieu.nl", - entry_type=ENTRY_TYPE_SERVICE, + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, manufacturer="Twente Milieu", name="Twente Milieu", diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index eee3d774d98..318e5a5094e 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -31,7 +32,7 @@ class UptimeRobotEntity(CoordinatorEntity): identifiers={(DOMAIN, str(self.monitor.id))}, name=self.monitor.friendly_name, manufacturer="UptimeRobot Team", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, model=self.monitor.type.name, configuration_url=f"https://uptimerobot.com/dashboard#{self.monitor.id}", ) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index d83b3f39f40..0bb157ab2ae 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -36,6 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -142,7 +143,7 @@ class VlcDevice(MediaPlayerEntity): config_entry_id = config_entry.entry_id self._attr_unique_id = config_entry_id self._attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry_id)}, manufacturer="VideoLAN", name=name, diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 8612f3ee54b..7a37a012680 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import Config, CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType @@ -165,7 +166,7 @@ class WazeTravelTime(SensorEntity): _attr_native_unit_of_measurement = TIME_MINUTES _attr_device_info = DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, name="Waze", identifiers={(DOMAIN, DOMAIN)}, ) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e28f01b9e7a..5395218b48e 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,7 +11,9 @@ import attr from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import RequiredParameterMissing +from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass +from homeassistant.util.enum import StrEnum import homeassistant.util.uuid as uuid_util from .debounce import Debouncer @@ -49,6 +51,12 @@ class _DeviceIndex(NamedTuple): connections: dict[tuple[str, str], str] +class DeviceEntryType(StrEnum): + """Device entry type.""" + + SERVICE = "service" + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -68,7 +76,7 @@ class DeviceEntry: ) ), ) - entry_type: str | None = attr.ib(default=None) + entry_type: DeviceEntryType | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) manufacturer: str | None = attr.ib(default=None) @@ -254,7 +262,7 @@ class DeviceRegistry: default_name: str | None | UndefinedType = UNDEFINED, # To disable a device if it gets created disabled_by: str | None | UndefinedType = UNDEFINED, - entry_type: str | None | UndefinedType = UNDEFINED, + entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None = None, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, @@ -303,6 +311,14 @@ class DeviceRegistry: else: via_device_id = UNDEFINED + if isinstance(entry_type, str) and not isinstance(entry_type, DeviceEntryType): + report( # type: ignore[unreachable] + "uses str for device registry entry_type. This is deprecated, " + "it should be updated to use DeviceEntryType instead", + error_if_core=False, + ) + entry_type = DeviceEntryType(entry_type) + device = self._async_update_device( device.id, add_config_entry_id=config_entry_id, @@ -370,7 +386,7 @@ class DeviceRegistry: area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, - entry_type: str | None | UndefinedType = UNDEFINED, + entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, @@ -511,7 +527,9 @@ class DeviceRegistry: name=device["name"], sw_version=device["sw_version"], # Introduced in 0.110 - entry_type=device.get("entry_type"), + entry_type=DeviceEntryType(device["entry_type"]) + if device.get("entry_type") + else None, id=device["id"], # Introduced in 0.79 # renamed in 0.95 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d2592febe33..a26da695e27 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -37,6 +37,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.event import Event, async_track_entity_registry_updated_event from homeassistant.helpers.typing import StateType @@ -168,7 +169,7 @@ class DeviceInfo(TypedDict, total=False): default_manufacturer: str default_model: str default_name: str - entry_type: str | None + entry_type: DeviceEntryType | None identifiers: set[tuple[str, str]] manufacturer: str | None model: str | None diff --git a/homeassistant/util/enum.py b/homeassistant/util/enum.py new file mode 100644 index 00000000000..88df4ba19c3 --- /dev/null +++ b/homeassistant/util/enum.py @@ -0,0 +1,31 @@ +"""Enum related utilities.""" +from __future__ import annotations + +from enum import Enum +from typing import Any + + +class StrEnum(str, Enum): + """Partial backport of Python 3.11's StrEnum for our basic use cases.""" + + def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: + """Create a new StrEnum instance.""" + if not isinstance(value, str): + raise TypeError(f"{value!r} is not a string") + return super().__new__(cls, value, *args, **kwargs) # type: ignore[call-overload,no-any-return] + + def __str__(self) -> str: + """Return self.value.""" + return str(self.value) + + @staticmethod + def _generate_next_value_( # pylint: disable=arguments-differ # https://github.com/PyCQA/pylint/issues/5371 + name: str, start: int, count: int, last_values: list[Any] + ) -> Any: + """ + Make `auto()` explicitly unsupported. + + We may revisit this when it's very clear that Python 3.11's + `StrEnum.auto()` behavior will no longer change. + """ + raise TypeError("auto() is not supported by this implementation") diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index 34eaa273901..a198d420378 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -3,11 +3,7 @@ from unittest.mock import AsyncMock import pytest -from homeassistant.components.ambee.const import ( - DEVICE_CLASS_AMBEE_RISK, - DOMAIN, - ENTRY_TYPE_SERVICE, -) +from homeassistant.components.ambee.const import DEVICE_CLASS_AMBEE_RISK, DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, @@ -147,7 +143,7 @@ async def test_air_quality( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} assert device_entry.manufacturer == "Ambee" assert device_entry.name == "Air Quality" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -248,7 +244,7 @@ async def test_pollen( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_pollen")} assert device_entry.manufacturer == "Ambee" assert device_entry.name == "Pollen" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 6c910d699c4..a6fb45158f8 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from homeassistant.components.forecast_solar.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.forecast_solar.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, @@ -141,7 +141,7 @@ async def test_sensors( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")} assert device_entry.manufacturer == "Forecast.Solar" assert device_entry.name == "Solar Production Forecast" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "public" assert not device_entry.sw_version diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index 960f68315e5..61f3f027b6b 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the sensors provided by the P1 Monitor integration.""" import pytest -from homeassistant.components.p1_monitor.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, @@ -80,7 +80,7 @@ async def test_smartmeter( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_smartmeter")} assert device_entry.manufacturer == "P1 Monitor" assert device_entry.name == "SmartMeter" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -136,7 +136,7 @@ async def test_phases( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_phases")} assert device_entry.manufacturer == "P1 Monitor" assert device_entry.name == "Phases" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -182,7 +182,7 @@ async def test_settings( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_settings")} assert device_entry.manufacturer == "P1 Monitor" assert device_entry.name == "Settings" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index 39fc981424d..b3c4d5a9b3c 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests for the sensors provided by the RDW integration.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM -from homeassistant.components.rdw.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.rdw.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -42,7 +42,7 @@ async def test_vehicle_binary_sensors( assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" assert device_entry.name == "Skoda: 11ZKZ3" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( device_entry.configuration_url diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 3f6d4b9b732..5eeea579194 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -1,5 +1,5 @@ """Tests for the sensors provided by the RDW integration.""" -from homeassistant.components.rdw.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.rdw.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -52,7 +52,7 @@ async def test_vehicle_sensors( assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" assert device_entry.name == "Skoda: 11ZKZ3" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( device_entry.configuration_url diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 11717d3e285..62acbe686a9 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -1,5 +1,5 @@ """Tests for the Twente Milieu sensors.""" -from homeassistant.components.twentemilieu.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.twentemilieu.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -72,7 +72,7 @@ async def test_waste_pickup_sensors( assert device_entry.identifiers == {(DOMAIN, "12345")} assert device_entry.manufacturer == "Twente Milieu" assert device_entry.name == "Twente Milieu" - assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.configuration_url == "https://www.twentemilieu.nl" assert not device_entry.model assert not device_entry.sw_version diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 557647c5c7f..8c311d44588 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -484,7 +484,7 @@ async def test_loading_saving_data(hass, registry, area_registry): model="via", name="Original Name", sw_version="Orig SW 1", - entry_type="device", + entry_type=None, ) orig_light = registry.async_get_or_create( diff --git a/tests/util/test_enum.py b/tests/util/test_enum.py new file mode 100644 index 00000000000..8fb2091bd11 --- /dev/null +++ b/tests/util/test_enum.py @@ -0,0 +1,35 @@ +"""Test Home Assistant enum utils.""" + +from enum import auto + +import pytest + +from homeassistant.util.enum import StrEnum + + +def test_strenum(): + """Test StrEnum.""" + + class TestEnum(StrEnum): + Test = "test" + + assert str(TestEnum.Test) == "test" + assert TestEnum.Test == "test" + assert TestEnum("test") is TestEnum.Test + assert TestEnum(TestEnum.Test) is TestEnum.Test + + with pytest.raises(ValueError): + TestEnum(42) + + with pytest.raises(ValueError): + TestEnum("str but unknown") + + with pytest.raises(TypeError): + + class FailEnum(StrEnum): + Test = 42 + + with pytest.raises(TypeError): + + class FailEnum2(StrEnum): + Test = auto() From 39d6aba3bcf754a9fdebbf8108ee013a98cc6577 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Nov 2021 18:20:38 +0100 Subject: [PATCH 0734/1452] Improve startup of unavailable template entities (#59827) --- homeassistant/helpers/event.py | 52 ++++-- tests/helpers/test_event.py | 284 +++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d9fdce8b1f3..1404b3730f1 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -789,7 +789,32 @@ class _TrackTemplateResultInfo: def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: """Activation of template tracking.""" + block_render = False + super_template = self._track_templates[0] if self._has_super_template else None + + # Render the super template first + if super_template is not None: + template = super_template.template + variables = super_template.variables + self._info[template] = info = template.async_render_to_info( + variables, strict=strict + ) + + # If the super template did not render to True, don't update other templates + try: + super_result: str | TemplateError = info.result() + except TemplateError as ex: + super_result = ex + if ( + super_result is not None + and self._super_template_as_boolean(super_result) is not True + ): + block_render = True + + # Then update the remaining templates unless blocked by the super template for track_template_ in self._track_templates: + if block_render or track_template_ == super_template: + continue template = track_template_.template variables = track_template_.variables self._info[template] = info = template.async_render_to_info( @@ -810,9 +835,10 @@ class _TrackTemplateResultInfo: ) self._update_time_listeners() _LOGGER.debug( - "Template group %s listens for %s", + "Template group %s listens for %s, first render blocker by super template: %s", self._track_templates, self.listeners, + block_render, ) @property @@ -932,6 +958,14 @@ class _TrackTemplateResultInfo: return TrackTemplateResult(template, last_result, result) + @staticmethod + def _super_template_as_boolean(result: bool | str | TemplateError) -> bool: + """Return True if the result is truthy or a TemplateError.""" + if isinstance(result, TemplateError): + return True + + return result_as_boolean(result) + @callback def _refresh( self, @@ -974,13 +1008,6 @@ class _TrackTemplateResultInfo: track_templates = track_templates or self._track_templates - def _super_template_as_boolean(result: bool | str | TemplateError) -> bool: - """Return True if the result is truthy or a TemplateError.""" - if isinstance(result, TemplateError): - return True - - return result_as_boolean(result) - # Update the super template first if super_template is not None: update = self._render_template_if_ready(super_template, now, event) @@ -994,14 +1021,14 @@ class _TrackTemplateResultInfo: # If the super template did not render to True, don't update other templates if ( super_result is not None - and _super_template_as_boolean(super_result) is not True + and self._super_template_as_boolean(super_result) is not True ): block_updates = True if ( isinstance(update, TrackTemplateResult) - and _super_template_as_boolean(update.last_result) is not True - and _super_template_as_boolean(update.result) is True + and self._super_template_as_boolean(update.last_result) is not True + and self._super_template_as_boolean(update.result) is True ): # Super template changed from not True to True, force re-render # of all templates in the group @@ -1030,9 +1057,10 @@ class _TrackTemplateResultInfo: ) ) _LOGGER.debug( - "Template group %s listens for %s", + "Template group %s listens for %s, re-render blocker by super template: %s", self._track_templates, self.listeners, + block_updates, ) if not updates: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 6d78ae089da..4f62d50da34 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1233,6 +1233,151 @@ async def test_track_template_result_super_template(hass): assert len(wildercard_runs_availability) == 6 +async def test_track_template_result_super_template_initially_false(hass): + """Test tracking template with super template listening to same entity.""" + specific_runs = [] + specific_runs_availability = [] + wildcard_runs = [] + wildcard_runs_availability = [] + wildercard_runs = [] + wildercard_runs_availability = [] + + template_availability = Template("{{ is_number(states('sensor.test')) }}", hass) + template_condition = Template("{{states.sensor.test.state}}", hass) + template_condition_var = Template( + "{{(states.sensor.test.state|int) + test }}", hass + ) + + # Make the super template initially false + hass.states.async_set("sensor.test", "unavailable") + await hass.async_block_till_done() + + def specific_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + specific_runs.append(int(track_result.result)) + elif track_result.template is template_availability: + specific_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + specific_run_callback, + has_super_template=True, + ) + + @ha.callback + def wildcard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + wildcard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildcard_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + wildcard_run_callback, + has_super_template=True, + ) + + async def wildercard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition_var: + wildercard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildercard_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition_var, {"test": 5}), + ], + wildercard_run_callback, + has_super_template=True, + ) + await hass.async_block_till_done() + + assert specific_runs_availability == [] + assert wildcard_runs_availability == [] + assert wildercard_runs_availability == [] + assert specific_runs == [] + assert wildcard_runs == [] + assert wildercard_runs == [] + + hass.states.async_set("sensor.test", 5) + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5] + assert wildcard_runs == [(0, 5)] + assert wildercard_runs == [(0, 10)] + + hass.states.async_set("sensor.test", "unknown") + await hass.async_block_till_done() + + assert specific_runs_availability == [True, False] + assert wildcard_runs_availability == [True, False] + assert wildercard_runs_availability == [True, False] + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert specific_runs_availability == [True, False, True] + assert wildcard_runs_availability == [True, False, True] + assert wildercard_runs_availability == [True, False, True] + + assert specific_runs == [5, 30] + assert wildcard_runs == [(0, 5), (5, 30)] + assert wildercard_runs == [(0, 10), (10, 35)] + + hass.states.async_set("sensor.test", "other") + await hass.async_block_till_done() + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + hass.states.async_set("sensor.test", 31) + await hass.async_block_till_done() + + assert len(specific_runs) == 3 + assert len(wildcard_runs) == 3 + assert len(wildercard_runs) == 3 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + @pytest.mark.parametrize( "availability_template", [ @@ -1370,6 +1515,145 @@ async def test_track_template_result_super_template_2(hass, availability_templat assert wildercard_runs == [(0, 10), (10, 35)] +@pytest.mark.parametrize( + "availability_template", + [ + "{{ states('sensor.test2') != 'unavailable' }}", + "{% if states('sensor.test2') != 'unavailable' -%} true {%- else -%} false {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} 1 {%- else -%} 0 {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} yes {%- else -%} no {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} on {%- else -%} off {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} enable {%- else -%} disable {%- endif %}", + # This will throw when sensor.test2 is not "unavailable" + "{% if states('sensor.test2') != 'unavailable' -%} {{'a' + 5}} {%- else -%} false {%- endif %}", + ], +) +async def test_track_template_result_super_template_2_initially_false( + hass, availability_template +): + """Test tracking template with super template listening to different entities.""" + specific_runs = [] + specific_runs_availability = [] + wildcard_runs = [] + wildcard_runs_availability = [] + wildercard_runs = [] + wildercard_runs_availability = [] + + template_availability = Template(availability_template) + template_condition = Template("{{states.sensor.test.state}}", hass) + template_condition_var = Template( + "{{(states.sensor.test.state|int) + test }}", hass + ) + + hass.states.async_set("sensor.test2", "unavailable") + await hass.async_block_till_done() + + def _super_template_as_boolean(result): + if isinstance(result, TemplateError): + return True + + return result_as_boolean(result) + + def specific_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + specific_runs.append(int(track_result.result)) + elif track_result.template is template_availability: + specific_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + specific_run_callback, + has_super_template=True, + ) + + @ha.callback + def wildcard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + wildcard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildcard_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + wildcard_run_callback, + has_super_template=True, + ) + + async def wildercard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition_var: + wildercard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildercard_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition_var, {"test": 5}), + ], + wildercard_run_callback, + has_super_template=True, + ) + await hass.async_block_till_done() + + assert specific_runs_availability == [] + assert wildcard_runs_availability == [] + assert wildercard_runs_availability == [] + assert specific_runs == [] + assert wildcard_runs == [] + assert wildercard_runs == [] + + hass.states.async_set("sensor.test", 5) + hass.states.async_set("sensor.test2", "available") + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5] + assert wildcard_runs == [(0, 5)] + assert wildercard_runs == [(0, 10)] + + hass.states.async_set("sensor.test2", "unknown") + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + + hass.states.async_set("sensor.test2", "available") + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5, 30] + assert wildcard_runs == [(0, 5), (5, 30)] + assert wildercard_runs == [(0, 10), (10, 35)] + + async def test_track_template_result_complex(hass): """Test tracking template.""" specific_runs = [] From 637b6bbb971574d036e0e52b1c312f09f787d6a2 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 22 Nov 2021 19:49:24 +0100 Subject: [PATCH 0735/1452] Upgrade aionanoleaf to 0.0.4 (#60160) --- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index b5e57ac842d..f8caf5be7eb 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.0.3"], + "requirements": ["aionanoleaf==0.0.4"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index ad76cd83ff9..5a6f14b3726 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.13.1 # homeassistant.components.nanoleaf -aionanoleaf==0.0.3 +aionanoleaf==0.0.4 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b18ccac32d..313ae45a336 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.13.1 # homeassistant.components.nanoleaf -aionanoleaf==0.0.3 +aionanoleaf==0.0.4 # homeassistant.components.notion aionotion==3.0.2 From 93ad48643916a22dd717738ad0b0c9bf74b92d11 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 22 Nov 2021 12:24:51 -0700 Subject: [PATCH 0736/1452] Fix missing pending state for SimpliSafe (#60151) --- homeassistant/components/simplisafe/alarm_control_panel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index e610b9139b8..7cbf62b04e8 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -38,6 +38,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant, callback @@ -67,6 +68,7 @@ ATTR_WIFI_STRENGTH = "wifi_strength" STATE_MAP_FROM_REST_API = { SystemStates.ALARM: STATE_ALARM_TRIGGERED, + SystemStates.ALARM_COUNT: STATE_ALARM_PENDING, SystemStates.AWAY: STATE_ALARM_ARMED_AWAY, SystemStates.AWAY_COUNT: STATE_ALARM_ARMING, SystemStates.EXIT_DELAY: STATE_ALARM_ARMING, From c87ab574a77574bad86fa147d67fe4a90e3930ae Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 22 Nov 2021 12:28:22 -0700 Subject: [PATCH 0737/1452] Fix bug with how SimpliSafe lock's respond to websocket events (#60152) --- homeassistant/components/simplisafe/lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 15757fde2e9..435b60af44b 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -99,7 +99,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Update the entity when new data comes from the websocket.""" if TYPE_CHECKING: assert event.event_type - if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type): + if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type) is not None: self._attr_is_locked = state self.async_reset_error_count() else: From f510534c583331076fc408f4e6497a755b1b988b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Nov 2021 20:38:51 +0100 Subject: [PATCH 0738/1452] Bump entity registry version to 1.3 (#60164) --- homeassistant/helpers/entity_registry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 076ea652f79..6e68d4f5499 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -59,7 +59,7 @@ DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 2 +STORAGE_VERSION_MINOR = 3 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -751,9 +751,13 @@ async def _async_migrate( entity["supported_features"] = entity.get("supported_features", 0) entity["unit_of_measurement"] = entity.get("unit_of_measurement") + if old_major_version < 2 and old_minor_version < 3: + # From version 1.2 + for entity in data["entities"]: # Move device_class to original_device_class entity["original_device_class"] = entity["device_class"] entity["device_class"] = None + if old_major_version > 1: raise NotImplementedError return data From 766c889e7079d6312e0d10f45ce54322c3193a98 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 00:46:51 +0100 Subject: [PATCH 0739/1452] Add button support to HomeKit (#60165) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 9 +++- .../components/homekit/config_flow.py | 1 + .../components/homekit/type_switches.py | 5 ++- script/hassfest/dependencies.py | 1 + .../homekit/test_get_accessories.py | 2 + .../components/homekit/test_type_switches.py | 43 +++++++++++++++++++ 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 30ee2e72589..06c70c09f2e 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -195,7 +195,14 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 elif state.domain == "remote" and features & SUPPORT_ACTIVITY: a_type = "ActivityRemote" - elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): + elif state.domain in ( + "automation", + "button", + "input_boolean", + "remote", + "scene", + "script", + ): a_type = "Switch" elif state.domain in ("input_select", "select"): diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 79a0e71f969..dc7f7b9013e 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -76,6 +76,7 @@ SUPPORTED_DOMAINS = [ "alarm_control_panel", "automation", "binary_sensor", + "button", CAMERA_DOMAIN, "climate", "cover", diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 9b9ff1f4df2..ec6813a82f1 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -12,6 +12,7 @@ from pyhap.const import ( CATEGORY_SWITCH, ) +from homeassistant.components import button from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION from homeassistant.components.switch import DOMAIN from homeassistant.components.vacuum import ( @@ -69,7 +70,7 @@ VALVE_TYPE: dict[str, ValveInfo] = { } -ACTIVATE_ONLY_SWITCH_DOMAINS = {"scene", "script"} +ACTIVATE_ONLY_SWITCH_DOMAINS = {"button", "scene", "script"} ACTIVATE_ONLY_RESET_SECONDS = 10 @@ -149,6 +150,8 @@ class Switch(HomeAccessory): if self._domain == "script": service = self._object_id params = {} + elif self._domain == button.DOMAIN: + service = button.SERVICE_PRESS else: service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 41510a46cb4..9e66e05899c 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -95,6 +95,7 @@ ALLOWED_USED_COMPONENTS = { "alert", "automation", "conversation", + "button", "device_automation", "frontend", "group", diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index be2429c79cf..8cc416adabd 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -30,6 +30,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, LIGHT_LUX, PERCENTAGE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -270,6 +271,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): [ ("Outlet", "switch.test", "on", {}, {CONF_TYPE: TYPE_OUTLET}), ("Switch", "automation.test", "on", {}, {}), + ("Switch", "button.test", STATE_UNKNOWN, {}, {}), ("Switch", "input_boolean.test", "on", {}, {}), ("Switch", "remote.test", "on", {}, {}), ("Switch", "scene.test", "on", {}, {}), diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index c13f7ea2538..59f871f31ac 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -449,3 +449,46 @@ async def test_input_select_switch(hass, hk_driver, events, domain): assert acc.select_chars["option1"].value is False assert acc.select_chars["option2"].value is False assert acc.select_chars["option3"].value is False + + +async def test_button_switch(hass, hk_driver, events): + """Test switch accessory from a button entity.""" + domain = "button" + entity_id = "button.test" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.activate_only is True + assert acc.char_on.value is False + + call_press = async_mock_service(hass, domain, "press") + + acc.char_on.client_update_value(True) + await hass.async_block_till_done() + assert acc.char_on.value is True + assert len(call_press) == 1 + assert call_press[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + 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 len(call_press) == 1 + + acc.char_on.client_update_value(False) + await hass.async_block_till_done() + assert acc.char_on.value is False + assert len(events) == 1 From 3639481027e6684dbb3c6dede2f708540b3ee999 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:05:12 -0700 Subject: [PATCH 0740/1452] Add sensors to venstar integration (#58851) --- .coveragerc | 1 + homeassistant/components/venstar/__init__.py | 16 +-- homeassistant/components/venstar/climate.py | 2 + homeassistant/components/venstar/sensor.py | 135 ++++++++++++++++++ .../venstar/fixtures/colortouch_alerts.json | 1 + .../venstar/fixtures/t2k_alerts.json | 1 + tests/components/venstar/test_climate.py | 8 +- tests/components/venstar/test_init.py | 2 + tests/components/venstar/util.py | 4 + 9 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/venstar/sensor.py create mode 100644 tests/components/venstar/fixtures/colortouch_alerts.json create mode 100644 tests/components/venstar/fixtures/t2k_alerts.json diff --git a/.coveragerc b/.coveragerc index 9591a8705d1..bb7e429fa12 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1178,6 +1178,7 @@ omit = homeassistant/components/venstar/__init__.py homeassistant/components/venstar/binary_sensor.py homeassistant/components/venstar/climate.py + homeassistant/components/venstar/sensor.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py homeassistant/components/verisure/binary_sensor.py diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 72e8b3e32d9..2ed6080f84f 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -PLATFORMS = ["binary_sensor", "climate"] +PLATFORMS = ["binary_sensor", "climate", "sensor"] async def async_setup_entry(hass, config): @@ -88,7 +88,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(3) + await asyncio.sleep(1) try: await self.hass.async_add_executor_job(self.client.update_sensors) @@ -98,7 +98,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(3) + await asyncio.sleep(1) try: await self.hass.async_add_executor_job(self.client.update_alerts) @@ -129,16 +129,6 @@ class VenstarEntity(CoordinatorEntity): """Handle updated data from the coordinator.""" self.async_write_ha_state() - @property - def name(self): - """Return the name of the thermostat.""" - return self._client.name - - @property - def unique_id(self): - """Set unique_id for this entity.""" - return f"{self._config.entry_id}" - @property def device_info(self): """Return the device information for this entity.""" diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 5d28d41db53..cb4e8ff1527 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -120,6 +120,8 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): HVAC_MODE_COOL: self._client.MODE_COOL, HVAC_MODE_AUTO: self._client.MODE_AUTO, } + self._attr_unique_id = config.entry_id + self._attr_name = self._client.name @property def supported_features(self): diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py new file mode 100644 index 00000000000..dc13269f7df --- /dev/null +++ b/homeassistant/components/venstar/sensor.py @@ -0,0 +1,135 @@ +"""Representation of Venstar sensors.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import Entity + +from . import VenstarDataUpdateCoordinator, VenstarEntity +from .const import DOMAIN + + +@dataclass +class VenstarSensorTypeMixin: + """Mixin for sensor required keys.""" + + cls: type[VenstarSensor] + stype: str + + +@dataclass +class VenstarSensorEntityDescription(SensorEntityDescription, VenstarSensorTypeMixin): + """Base description of a Sensor entity.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up Vensar device binary_sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities: list[Entity] = [] + + sensors = coordinator.client.get_sensor_list() + if not sensors: + return + + entities = [] + + for sensor_name in sensors: + entities.extend( + [ + description.cls(coordinator, config_entry, description, sensor_name) + for description in SENSOR_ENTITIES + if coordinator.client.get_sensor(sensor_name, description.stype) + is not None + ] + ) + + async_add_entities(entities) + + +class VenstarSensor(VenstarEntity, SensorEntity): + """Base class for a Venstar sensor.""" + + entity_description: VenstarSensorEntityDescription + + def __init__( + self, + coordinator: VenstarDataUpdateCoordinator, + config: ConfigEntry, + entity_description: VenstarSensorEntityDescription, + sensor_name: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, config) + self.entity_description = entity_description + self.sensor_name = sensor_name + self._config = config + + @property + def unique_id(self): + """Return the unique id.""" + return f"{self._config.entry_id}_{self.sensor_name.replace(' ', '_')}_{self.entity_description.key}" + + +class VenstarHumiditySensor(VenstarSensor): + """Represent a Venstar humidity sensor.""" + + @property + def name(self): + """Return the name of the device.""" + return f"{self._client.name} {self.sensor_name} Humidity" + + @property + def native_value(self) -> int: + """Return state of the sensor.""" + return self._client.get_sensor(self.sensor_name, "hum") + + +class VenstarTemperatureSensor(VenstarSensor): + """Represent a Venstar temperature sensor.""" + + @property + def name(self): + """Return the name of the device.""" + return ( + f"{self._client.name} {self.sensor_name.replace(' Temp', '')} Temperature" + ) + + @property + def native_unit_of_measurement(self) -> str: + """Return unit of measurement the value is expressed in.""" + if self._client.tempunits == self._client.TEMPUNITS_F: + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def native_value(self) -> float: + """Return state of the sensor.""" + return round(float(self._client.get_sensor(self.sensor_name, "temp")), 1) + + +SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( + VenstarSensorEntityDescription( + key="humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + cls=VenstarHumiditySensor, + stype="hum", + ), + VenstarSensorEntityDescription( + key="temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + cls=VenstarTemperatureSensor, + stype="temp", + ), +) diff --git a/tests/components/venstar/fixtures/colortouch_alerts.json b/tests/components/venstar/fixtures/colortouch_alerts.json new file mode 100644 index 00000000000..54a29b9eb3a --- /dev/null +++ b/tests/components/venstar/fixtures/colortouch_alerts.json @@ -0,0 +1 @@ +{"alerts":[{"name":"Air Filter","active":true},{"name":"UV Lamp","active":false},{"name":"Service","active":false}]} diff --git a/tests/components/venstar/fixtures/t2k_alerts.json b/tests/components/venstar/fixtures/t2k_alerts.json new file mode 100644 index 00000000000..54a29b9eb3a --- /dev/null +++ b/tests/components/venstar/fixtures/t2k_alerts.json @@ -0,0 +1 @@ +{"alerts":[{"name":"Air Filter","active":true},{"name":"UV Lamp","active":false},{"name":"Service","active":false}]} diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index 9461032060b..babd946073b 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -1,5 +1,7 @@ """The climate tests for the venstar integration.""" +from unittest.mock import patch + from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, @@ -18,7 +20,8 @@ EXPECTED_BASE_SUPPORTED_FEATURES = ( async def test_colortouch(hass): """Test interfacing with a venstar colortouch with attached humidifier.""" - await async_init_integration(hass) + with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + await async_init_integration(hass) state = hass.states.get("climate.colortouch") assert state.state == "heat" @@ -53,7 +56,8 @@ async def test_colortouch(hass): async def test_t2000(hass): """Test interfacing with a venstar T2000 presently turned off.""" - await async_init_integration(hass) + with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + await async_init_integration(hass) state = hass.states.get("climate.t2000") assert state.state == "off" diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 08dae732466..b245f4eef6d 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -36,6 +36,8 @@ async def test_setup_entry(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, + ), patch( + "homeassistant.components.onewire.sensor.asyncio.sleep" ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py index b86f8475798..aa53a9c0a8d 100644 --- a/tests/components/venstar/util.py +++ b/tests/components/venstar/util.py @@ -33,6 +33,10 @@ def mock_venstar_devices(f): f"http://venstar-{model}.localdomain/query/sensors", text=load_fixture(f"venstar/{model}_sensors.json"), ) + m.get( + f"http://venstar-{model}.localdomain/query/alerts", + text=load_fixture(f"venstar/{model}_alerts.json"), + ) return await f(hass) return wrapper From 9e606abb0c50b3d485d6e15bb8688ee84d8d6b80 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 23 Nov 2021 00:13:54 +0000 Subject: [PATCH 0741/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 7 ++- .../accuweather/translations/ja.json | 2 + .../components/adax/translations/ja.json | 7 +++ .../components/adguard/translations/ja.json | 1 + .../components/aemet/translations/ja.json | 8 +++ .../components/airly/translations/ja.json | 5 ++ .../components/airnow/translations/ja.json | 10 ++- .../components/airthings/translations/ja.json | 8 +++ .../components/airtouch4/translations/ja.json | 4 ++ .../components/airvisual/translations/ja.json | 4 +- .../components/almond/translations/ja.json | 7 +++ .../components/ambee/translations/ja.json | 9 +++ .../ambiclimate/translations/ja.json | 3 + .../ambient_station/translations/ja.json | 4 ++ .../components/apple_tv/translations/id.json | 4 +- .../components/apple_tv/translations/ja.json | 22 ++++++- .../components/asuswrt/translations/ja.json | 12 +++- .../components/august/translations/ja.json | 8 +++ .../components/aurora/translations/ja.json | 5 ++ .../aurora_abb_powerone/translations/ja.json | 3 +- .../components/awair/translations/ja.json | 4 +- .../components/axis/translations/ja.json | 4 ++ .../binary_sensor/translations/ja.json | 12 ++++ .../components/blink/translations/ja.json | 2 +- .../bmw_connected_drive/translations/ja.json | 8 +++ .../components/bosch_shc/translations/id.json | 2 +- .../components/bosch_shc/translations/ja.json | 12 +++- .../components/brother/translations/ja.json | 1 + .../components/brunt/translations/ja.json | 11 +++- .../buienradar/translations/ja.json | 27 ++++++++ .../components/cast/translations/ja.json | 9 +++ .../components/climacell/translations/ja.json | 18 +++++- .../components/cloud/translations/ja.json | 8 ++- .../cloudflare/translations/ja.json | 3 + .../components/co2signal/translations/ja.json | 15 ++++- .../components/coinbase/translations/ja.json | 11 +++- .../components/control4/translations/ja.json | 3 +- .../coronavirus/translations/ja.json | 15 +++++ .../crownstone/translations/ja.json | 19 +++++- .../components/daikin/translations/ja.json | 3 + .../devolo_home_control/translations/ja.json | 8 ++- .../devolo_home_network/translations/ja.json | 8 ++- .../components/directv/translations/ja.json | 7 +++ .../components/dlna_dmr/translations/ja.json | 6 ++ .../components/dsmr/translations/ja.json | 7 ++- .../components/ecobee/translations/ja.json | 3 + .../components/econet/translations/ja.json | 13 +++- .../components/efergy/translations/ja.json | 9 +++ .../components/elgato/translations/ja.json | 3 + .../components/emonitor/translations/ja.json | 11 ++++ .../enphase_envoy/translations/ja.json | 9 +++ .../environment_canada/translations/ja.json | 4 +- .../components/esphome/translations/ja.json | 4 ++ .../evil_genius_labs/translations/ja.json | 4 ++ .../components/ezviz/translations/ja.json | 13 +++- .../faa_delays/translations/ja.json | 7 ++- .../fjaraskupan/translations/ja.json | 4 ++ .../flick_electric/translations/ja.json | 3 +- .../components/flipr/translations/ja.json | 10 ++- .../components/flume/translations/ja.json | 3 + .../flunearyou/translations/ja.json | 12 ++++ .../components/flux_led/translations/ja.json | 8 +++ .../forecast_solar/translations/ja.json | 2 + .../components/foscam/translations/ja.json | 11 +++- .../components/freebox/translations/ja.json | 11 +++- .../freedompro/translations/ja.json | 7 +++ .../components/fritz/translations/ja.json | 15 ++++- .../components/fritzbox/translations/ja.json | 6 +- .../fritzbox_callmonitor/translations/ja.json | 7 +++ .../garages_amsterdam/translations/ja.json | 5 ++ .../components/gdacs/translations/ja.json | 15 +++++ .../geonetnz_quakes/translations/ja.json | 3 + .../components/gios/translations/ja.json | 8 +++ .../components/glances/translations/ja.json | 9 +++ .../components/goalzero/translations/ja.json | 4 ++ .../google_travel_time/translations/ja.json | 11 ++++ .../growatt_server/translations/ja.json | 3 + .../components/habitica/translations/ja.json | 5 ++ .../components/hangouts/translations/ja.json | 6 +- .../components/harmony/translations/ja.json | 3 +- .../components/hassio/translations/ja.json | 7 ++- .../hisense_aehw4a1/translations/ja.json | 4 ++ .../components/hive/translations/ja.json | 15 ++++- .../home_plus_control/translations/ja.json | 20 ++++++ .../homeassistant/translations/ja.json | 4 ++ .../homekit_controller/translations/ja.json | 2 + .../homematicip_cloud/translations/ja.json | 1 + .../components/honeywell/translations/ja.json | 3 + .../huawei_lte/translations/ja.json | 2 + .../components/hue/translations/ja.json | 8 +++ .../huisbaasje/translations/ja.json | 8 +++ .../components/hyperion/translations/ja.json | 18 +++++- .../components/ialarm/translations/ja.json | 7 +++ .../components/icloud/translations/ja.json | 6 +- .../components/insteon/translations/ja.json | 3 +- .../components/ios/translations/ja.json | 12 ++++ .../components/iotawatt/translations/ja.json | 5 ++ .../components/ipma/translations/ja.json | 5 +- .../components/isy994/translations/ja.json | 3 +- .../components/izone/translations/ja.json | 4 ++ .../components/jellyfin/translations/ja.json | 8 +++ .../keenetic_ndms2/translations/ja.json | 11 +++- .../components/kmtronic/translations/ja.json | 8 +++ .../components/knx/translations/bg.json | 22 +++++++ .../components/knx/translations/id.json | 29 +++++++++ .../components/knx/translations/ja.json | 9 ++- .../components/knx/translations/no.json | 63 +++++++++++++++++++ .../components/konnected/translations/ja.json | 38 ++++++++--- .../kostal_plenticore/translations/ja.json | 11 +++- .../components/kraken/translations/ja.json | 13 +++- .../components/kulersky/translations/ja.json | 13 ++++ .../components/lifx/translations/ja.json | 4 ++ .../components/litejet/translations/ja.json | 3 + .../litterrobot/translations/ja.json | 8 +++ .../components/local_ip/translations/ja.json | 1 + .../components/locative/translations/ja.json | 1 + .../components/lookin/translations/ja.json | 11 ++++ .../components/lovelace/translations/ja.json | 3 +- .../components/luftdaten/translations/ja.json | 2 + .../lutron_caseta/translations/ja.json | 17 ++++- .../components/lyric/translations/ja.json | 14 ++++- .../components/mazda/translations/ja.json | 12 +++- .../media_player/translations/ja.json | 6 ++ .../components/melcloud/translations/ja.json | 12 +++- .../components/met/translations/ja.json | 5 ++ .../met_eireann/translations/ja.json | 11 +++- .../meteo_france/translations/ja.json | 9 +++ .../meteoclimatic/translations/ja.json | 7 +++ .../components/metoffice/translations/ja.json | 7 ++- .../components/mikrotik/translations/ja.json | 3 + .../components/mill/translations/id.json | 13 ++++ .../components/mill/translations/ja.json | 3 +- .../minecraft_server/translations/ja.json | 3 + .../mobile_app/translations/ja.json | 5 ++ .../modem_callerid/translations/ja.json | 5 ++ .../modern_forms/translations/ja.json | 10 +++ .../motion_blinds/translations/ja.json | 9 ++- .../components/motioneye/translations/ja.json | 11 +++- .../components/mqtt/translations/ja.json | 11 +++- .../components/mullvad/translations/ja.json | 7 +++ .../components/mutesync/translations/ja.json | 5 ++ .../components/myq/translations/ja.json | 3 + .../components/mysensors/translations/ja.json | 17 ++++- .../components/nam/translations/ja.json | 7 +++ .../components/nanoleaf/translations/ja.json | 11 +++- .../components/neato/translations/ja.json | 22 +++++++ .../components/nest/translations/ja.json | 15 ++++- .../components/netatmo/translations/ja.json | 30 ++++++++- .../components/netgear/translations/id.json | 7 +++ .../components/netgear/translations/ja.json | 4 ++ .../nfandroidtv/translations/ja.json | 7 +++ .../nmap_tracker/translations/ja.json | 13 +++- .../components/notion/translations/ja.json | 10 ++- .../components/nuki/translations/ja.json | 17 ++++- .../components/nut/translations/ja.json | 4 ++ .../components/nws/translations/ja.json | 4 +- .../components/octoprint/translations/id.json | 1 + .../components/octoprint/translations/ja.json | 9 ++- .../ondilo_ico/translations/ja.json | 16 +++++ .../components/onewire/translations/ja.json | 3 + .../opengarage/translations/ja.json | 13 +++- .../opentherm_gw/translations/ja.json | 2 + .../openweathermap/translations/ja.json | 2 + .../ovo_energy/translations/ja.json | 6 +- .../components/ozw/translations/ja.json | 16 ++++- .../p1_monitor/translations/ja.json | 4 ++ .../philips_js/translations/ja.json | 14 ++++- .../components/pi_hole/translations/ja.json | 3 +- .../components/picnic/translations/ja.json | 8 +++ .../components/plaato/translations/ja.json | 18 ++++++ .../components/plex/translations/ja.json | 4 ++ .../plum_lightpad/translations/ja.json | 3 +- .../components/point/translations/ja.json | 15 ++++- .../components/poolsense/translations/ja.json | 1 + .../components/powerwall/translations/ja.json | 6 ++ .../components/prosegur/translations/ja.json | 9 +++ .../components/ps4/translations/ja.json | 2 + .../pvpc_hourly_pricing/translations/ja.json | 3 +- .../components/rachio/translations/ja.json | 8 +++ .../rainforest_eagle/translations/ja.json | 8 +++ .../rainmachine/translations/ja.json | 3 + .../components/rdw/translations/ja.json | 1 + .../recollect_waste/translations/ja.json | 18 ++++++ .../components/renault/translations/ja.json | 10 ++- .../components/ridwell/translations/id.json | 3 +- .../components/ridwell/translations/ja.json | 11 +++- .../components/ring/translations/ja.json | 7 +++ .../components/risco/translations/ja.json | 3 +- .../translations/ja.json | 10 ++- .../components/roku/translations/ja.json | 3 + .../components/roomba/translations/ja.json | 11 +++- .../ruckus_unleashed/translations/ja.json | 3 +- .../components/samsungtv/translations/ja.json | 13 +++- .../screenlogic/translations/ja.json | 6 ++ .../components/sense/translations/id.json | 3 +- .../components/sense/translations/ja.json | 8 +++ .../components/sensor/translations/ja.json | 4 ++ .../components/sharkiq/translations/ja.json | 6 +- .../components/shelly/translations/ja.json | 14 ++++- .../shopping_list/translations/ja.json | 3 + .../components/sia/translations/ja.json | 16 ++++- .../simplisafe/translations/id.json | 6 ++ .../simplisafe/translations/ja.json | 3 +- .../components/sma/translations/ja.json | 13 +++- .../smart_meter_texas/translations/ja.json | 3 +- .../components/smarthab/translations/ja.json | 1 + .../components/smarttub/translations/ja.json | 13 +++- .../components/smhi/translations/ja.json | 2 + .../components/solarlog/translations/ja.json | 7 +++ .../components/soma/translations/ja.json | 4 ++ .../components/somfy/translations/ja.json | 9 +++ .../somfy_mylink/translations/ja.json | 30 ++++++++- .../components/sonos/translations/ja.json | 4 +- .../components/spider/translations/ja.json | 3 +- .../components/spotify/translations/ja.json | 5 ++ .../squeezebox/translations/ja.json | 3 +- .../srp_energy/translations/ja.json | 13 +++- .../stookalert/translations/ja.json | 3 + .../components/subaru/translations/ja.json | 17 ++++- .../surepetcare/translations/ja.json | 8 +++ .../components/switchbot/translations/ja.json | 8 ++- .../switcher_kis/translations/ja.json | 13 ++++ .../components/syncthing/translations/ja.json | 10 ++- .../synology_dsm/translations/ja.json | 4 +- .../system_bridge/translations/ja.json | 10 +++ .../tellduslive/translations/ja.json | 4 ++ .../components/tile/translations/ja.json | 3 +- .../totalconnect/translations/ja.json | 12 +++- .../components/tplink/translations/ja.json | 8 +++ .../components/tractive/translations/ja.json | 10 ++- .../components/tradfri/translations/ja.json | 3 + .../transmission/translations/ja.json | 7 +++ .../components/tuya/translations/ja.json | 9 +++ .../components/twilio/translations/ja.json | 1 + .../components/twinkly/translations/ja.json | 6 ++ .../components/unifi/translations/ja.json | 11 +++- .../components/upnp/translations/ja.json | 4 +- .../uptimerobot/translations/ja.json | 13 +++- .../components/venstar/translations/ja.json | 8 +++ .../components/verisure/translations/ja.json | 12 +++- .../components/vesync/translations/ja.json | 2 +- .../components/vicare/translations/en.json | 20 ------ .../components/vizio/translations/ja.json | 10 ++- .../vlc_telnet/translations/ja.json | 12 ++++ .../components/wallbox/translations/ja.json | 9 ++- .../water_heater/translations/ja.json | 2 + .../components/watttime/translations/ja.json | 16 ++++- .../waze_travel_time/translations/ja.json | 6 ++ .../components/whirlpool/translations/ja.json | 5 ++ .../components/withings/translations/ja.json | 7 +++ .../wled/translations/select.bg.json | 8 +++ .../wled/translations/select.id.json | 9 +++ .../wled/translations/select.no.json | 9 +++ .../wled/translations/select.tr.json | 9 +++ .../xiaomi_miio/translations/ja.json | 7 ++- .../yale_smart_alarm/translations/ja.json | 6 ++ .../yamaha_musiccast/translations/ja.json | 4 ++ .../components/youless/translations/ja.json | 3 + .../components/zha/translations/ja.json | 4 ++ .../components/zwave/translations/ja.json | 6 +- .../components/zwave_js/translations/ja.json | 26 ++++++-- 261 files changed, 1985 insertions(+), 186 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/ja.json create mode 100644 homeassistant/components/coronavirus/translations/ja.json create mode 100644 homeassistant/components/flunearyou/translations/ja.json create mode 100644 homeassistant/components/gdacs/translations/ja.json create mode 100644 homeassistant/components/home_plus_control/translations/ja.json create mode 100644 homeassistant/components/ios/translations/ja.json create mode 100644 homeassistant/components/knx/translations/bg.json create mode 100644 homeassistant/components/knx/translations/id.json create mode 100644 homeassistant/components/knx/translations/no.json create mode 100644 homeassistant/components/kulersky/translations/ja.json create mode 100644 homeassistant/components/meteo_france/translations/ja.json create mode 100644 homeassistant/components/neato/translations/ja.json create mode 100644 homeassistant/components/ondilo_ico/translations/ja.json create mode 100644 homeassistant/components/recollect_waste/translations/ja.json create mode 100644 homeassistant/components/somfy/translations/ja.json create mode 100644 homeassistant/components/switcher_kis/translations/ja.json delete mode 100644 homeassistant/components/vicare/translations/en.json create mode 100644 homeassistant/components/wled/translations/select.bg.json create mode 100644 homeassistant/components/wled/translations/select.id.json create mode 100644 homeassistant/components/wled/translations/select.no.json create mode 100644 homeassistant/components/wled/translations/select.tr.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index 33b2cf9caea..77e6382f84c 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { "invalid_mfa_code": "\u7121\u52b9\u306aMFA\u30b3\u30fc\u30c9" }, @@ -13,14 +16,14 @@ "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "Email" + "username": "E\u30e1\u30fc\u30eb" }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "Email" + "username": "E\u30e1\u30fc\u30eb" }, "title": "Abode\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 6bc1875187f..8e4d5ad858c 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -7,6 +7,8 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "description": "\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/accuweather/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306e\u8a2d\u5b9a\u5f8c\u306b\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json index 1d914a84b18..7fea6e56932 100644 --- a/homeassistant/components/adax/translations/ja.json +++ b/homeassistant/components/adax/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index 05380f8a830..a4bd092db59 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" }, "step": { diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index 8d296476c15..a3a0904c223 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" + }, "step": { "user": { "data": { "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 20b2a5c92dc..cc0b861718a 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "wrong_location": "\u3053\u306e\u30a8\u30ea\u30a2\u306b\u3001Airly\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, @@ -7,6 +10,8 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index ce1f2f06287..1cd0b8398e8 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -1,12 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "invalid_location": "\u305d\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u7d50\u679c\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_location": "\u305d\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u7d50\u679c\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" }, "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/airthings/translations/ja.json b/homeassistant/components/airthings/translations/ja.json index 28c23934570..a959fc54980 100644 --- a/homeassistant/components/airthings/translations/ja.json +++ b/homeassistant/components/airthings/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airtouch4/translations/ja.json b/homeassistant/components/airtouch4/translations/ja.json index e161df46ad2..454d0434603 100644 --- a/homeassistant/components/airtouch4/translations/ja.json +++ b/homeassistant/components/airtouch4/translations/ja.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "no_units": "AirTouch 4 Groups\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index 925cf20c769..cd1b1403827 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -6,7 +6,9 @@ "step": { "geography_by_coords": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" }, "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u7def\u5ea6/\u7d4c\u5ea6\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", "title": "Geography\u306e\u8a2d\u5b9a" diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index 64c0a6b8bb7..ce80c17ea52 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "hassio_confirm": { "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAlmond" + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } } diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index f382bfe91de..3a308ec4b70 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" + }, "step": { "reauth_confirm": { "data": { @@ -10,6 +17,8 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" diff --git a/homeassistant/components/ambiclimate/translations/ja.json b/homeassistant/components/ambiclimate/translations/ja.json index e1b79448f55..64c9bcaf9b7 100644 --- a/homeassistant/components/ambiclimate/translations/ja.json +++ b/homeassistant/components/ambiclimate/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, "error": { "follow_link": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_token": "Ambiclimate\u3067\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093" diff --git a/homeassistant/components/ambient_station/translations/ja.json b/homeassistant/components/ambient_station/translations/ja.json index 2a92b237822..63a42f88a64 100644 --- a/homeassistant/components/ambient_station/translations/ja.json +++ b/homeassistant/components/ambient_station/translations/ja.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "invalid_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "step": { diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json index 209ecbf8a83..443a0dd49f1 100644 --- a/homeassistant/components/apple_tv/translations/id.json +++ b/homeassistant/components/apple_tv/translations/id.json @@ -24,14 +24,14 @@ }, "pair_no_pin": { "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di Apple TV untuk melanjutkan.", - "title": "Memasangkan" + "title": "Pemasangan" }, "pair_with_pin": { "data": { "pin": "Kode PIN" }, "description": "Pemasangan diperlukan untuk protokol `{protocol}`. Masukkan kode PIN yang ditampilkan pada layar. Angka nol di awal harus dihilangkan. Misalnya, masukkan 123 jika kode yang ditampilkan adalah 0123.", - "title": "Memasangkan" + "title": "Pemasangan" }, "reconfigure": { "description": "Apple TV ini mengalami masalah koneksi dan harus dikonfigurasi ulang.", diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index f2c2a46ab3a..a73cb4cbbd6 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -1,12 +1,20 @@ { "config": { "abort": { + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "backoff": "\u73fe\u5728\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u8981\u6c42\u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u305b\u3093(\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9\u3092\u4f55\u5ea6\u3082\u5165\u529b\u3057\u305f\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059)\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_did_not_pair": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u3092\u7d42\u4e86\u3059\u308b\u8a66\u307f\u306f\u884c\u308f\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", - "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "no_usable_service": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u78ba\u7acb\u3059\u308b\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u5f15\u304d\u7d9a\u304d\u8868\u793a\u3055\u308c\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3059\u308b\u304b\u3001Apple TV\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "no_usable_service": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u78ba\u7acb\u3059\u308b\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u5f15\u304d\u7d9a\u304d\u8868\u793a\u3055\u308c\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3059\u308b\u304b\u3001Apple TV\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", "step": { @@ -42,5 +50,15 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant\u306e\u8d77\u52d5\u6642\u306b\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u3092\u5165\u308c\u306a\u3044" + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u4e00\u822c\u7684\u306a\u8a2d\u5b9a" + } + } + }, "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 5c82bf452df..53ed33fd9ee 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "pwd_and_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u306e\u307f\u63d0\u4f9b", "pwd_or_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3092\u63d0\u4f9b\u3057\u3066\u304f\u3060\u3055\u3044", - "ssh_not_file": "SSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "ssh_not_file": "SSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "mode": "\u30e2\u30fc\u30c9", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", @@ -27,7 +34,8 @@ "data": { "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)", - "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)" + "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)", + "track_unknown": "\u8ffd\u8de1\u4e0d\u660e/\u540d\u524d\u306e\u306a\u3044\u30c7\u30d0\u30a4\u30b9" }, "title": "AsusWRT\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index 5b2d0535a85..f97a29b576e 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_validate": { "data": { diff --git a/homeassistant/components/aurora/translations/ja.json b/homeassistant/components/aurora/translations/ja.json index 4d822454b31..504b77e6906 100644 --- a/homeassistant/components/aurora/translations/ja.json +++ b/homeassistant/components/aurora/translations/ja.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/ja.json b/homeassistant/components/aurora_abb_powerone/translations/ja.json index 5977b1513b0..a558f088f07 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ja.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ja.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3001\u30a2\u30c9\u30ec\u30b9\u3001\u96fb\u6c17\u7684\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3001\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044(\u663c\u9593)", "cannot_open_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u3051\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044", - "invalid_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u304c\u6709\u52b9\u306a\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3001\u3082\u3057\u304f\u306f\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "invalid_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u304c\u6709\u52b9\u306a\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3001\u3082\u3057\u304f\u306f\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 4e2226fd83d..73ba8e2b341 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -3,12 +3,12 @@ "step": { "reauth": { "data": { - "email": "Email" + "email": "E\u30e1\u30fc\u30eb" } }, "user": { "data": { - "email": "Email" + "email": "E\u30e1\u30fc\u30eb" } } } diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index db2e5941f30..d1867fc2ae5 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -1,9 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", "not_axis_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fAxis\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{name} ({host})", "step": { "user": { diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index e78d8b3d669..15afd63be23 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -102,6 +102,10 @@ "off": "\u901a\u5e38", "on": "\u4f4e" }, + "battery_charging": { + "off": "\u5145\u96fb\u3057\u3066\u3044\u306a\u3044", + "on": "\u5145\u96fb" + }, "cold": { "off": "\u901a\u5e38", "on": "\u4f4e\u6e29" @@ -126,6 +130,10 @@ "off": "\u6b63\u5e38", "on": "\u9ad8\u6e29" }, + "light": { + "off": "\u30e9\u30a4\u30c8\u306a\u3057", + "on": "\u30e9\u30a4\u30c8\u3092\u691c\u51fa" + }, "lock": { "off": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", "on": "\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u305b\u3093" @@ -146,6 +154,10 @@ "off": "\u9589\u9396", "on": "\u958b\u653e" }, + "plug": { + "off": "\u30a2\u30f3\u30d7\u30e9\u30b0\u30c9", + "on": "\u30d7\u30e9\u30b0\u30a4\u30f3" + }, "presence": { "off": "\u5916\u51fa", "on": "\u5728\u5b85" diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index 978befe8202..55e13ee6ec1 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -5,7 +5,7 @@ "data": { "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, - "description": "\u30e1\u30fc\u30eb\u3067\u9001\u3089\u308c\u3066\u304d\u305fPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "E\u30e1\u30fc\u30eb\u3067\u9001\u3089\u308c\u3066\u304d\u305fPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "2\u8981\u7d20\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 7fd445b29af..47723fe6951 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "region": "ConnectedDrive\u30ea\u30fc\u30b8\u30e7\u30f3", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } diff --git a/homeassistant/components/bosch_shc/translations/id.json b/homeassistant/components/bosch_shc/translations/id.json index db20e06cd76..723c6bdabbe 100644 --- a/homeassistant/components/bosch_shc/translations/id.json +++ b/homeassistant/components/bosch_shc/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "pairing_failed": "Pairing gagal; periksa apakah Bosch Smart Home Controller dalam mode pairing (LED berkedip) dan kata sandi Anda benar.", + "pairing_failed": "Pemasangan gagal; periksa apakah Bosch Smart Home Controller dalam mode pemasangan (LED berkedip) dan kata sandi Anda benar.", "session_error": "Kesalahan sesi: API mengembalikan hasil Non-OK.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 7fa13765ae5..7525cf016f4 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Bosch Smart Home Controller\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b(LED\u304c\u70b9\u6ec5)\u3053\u3068\u3068\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u3044\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "session_error": "\u30bb\u30c3\u30b7\u30e7\u30f3\u30a8\u30e9\u30fc: API\u304c\u3001OK\u4ee5\u5916\u306e\u7d50\u679c\u3092\u8fd4\u3057\u307e\u3059\u3002" + "session_error": "\u30bb\u30c3\u30b7\u30e7\u30f3\u30a8\u30e9\u30fc: API\u304c\u3001OK\u4ee5\u5916\u306e\u7d50\u679c\u3092\u8fd4\u3057\u307e\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "Bosch SHC: {name}", "step": { @@ -15,7 +22,8 @@ } }, "reauth_confirm": { - "description": "bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "description": "bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index a6ed0fda293..5d6c140419d 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "unsupported_model": "\u3053\u306e\u30d7\u30ea\u30f3\u30bf\u30fc\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json index 48b2c0d6259..631d5e54f47 100644 --- a/homeassistant/components/brunt/translations/ja.json +++ b/homeassistant/components/brunt/translations/ja.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {username}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u8a8d\u8a3c" + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/buienradar/translations/ja.json b/homeassistant/components/buienradar/translations/ja.json new file mode 100644 index 00000000000..e7ef59b81b4 --- /dev/null +++ b/homeassistant/components/buienradar/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "delta": "\u30ab\u30e1\u30e9\u753b\u50cf\u306e\u66f4\u65b0\u9593\u9694(\u79d2)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 7b71ae2b72e..577b6d07bc5 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" + }, "step": { "config": { "data": { @@ -14,6 +20,9 @@ } }, "options": { + "error": { + "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" + }, "step": { "advanced_options": { "data": { diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index 6b00260a579..f5843d0478e 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -1,15 +1,29 @@ { "config": { "error": { - "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "api_key": "API\u30ad\u30fc", + "api_version": "API\u30d0\u30fc\u30b8\u30e7\u30f3", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" } } } - } + }, + "options": { + "step": { + "init": { + "title": "ClimaCell\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" + } + } + }, + "title": "ClimaCell" } \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json index a2aee4b89e2..26b21da35bf 100644 --- a/homeassistant/components/cloud/translations/ja.json +++ b/homeassistant/components/cloud/translations/ja.json @@ -1,9 +1,15 @@ { "system_health": { "info": { + "alexa_enabled": "Alexa\u6709\u52b9", "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "google_enabled": "Google\u6709\u52b9", "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08", - "remote_server": "\u30ea\u30e2\u30fc\u30c8\u30b5\u30fc\u30d0\u30fc" + "relayer_connected": "\u63a5\u7d9a\u3055\u308c\u305f\u518d\u30ec\u30a4\u30e4\u30fc", + "remote_connected": "\u30ea\u30e2\u30fc\u30c8\u63a5\u7d9a", + "remote_enabled": "\u30ea\u30e2\u30fc\u30c8\u6709\u52b9", + "remote_server": "\u30ea\u30e2\u30fc\u30c8\u30b5\u30fc\u30d0\u30fc", + "subscription_expiration": "\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u306e\u6709\u52b9\u671f\u9650" } } } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 1931145bec3..f0a63b8b492 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "flow_title": "{name}", "step": { "reauth_confirm": { diff --git a/homeassistant/components/co2signal/translations/ja.json b/homeassistant/components/co2signal/translations/ja.json index f630e7a0659..cd3f422022d 100644 --- a/homeassistant/components/co2signal/translations/ja.json +++ b/homeassistant/components/co2signal/translations/ja.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f" + "api_ratelimit": "API\u30ec\u30fc\u30c8\u5236\u9650\u3092\u8d85\u3048\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "coordinates": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + } + }, "country": { "data": { "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9" @@ -14,6 +24,7 @@ }, "user": { "data": { + "api_key": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "location": "\uff5e\u306e\u30c7\u30fc\u30bf\u3092\u53d6\u5f97" }, "description": "\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001https://co2signal.com/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index 5146e8f34fc..64bf9340450 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -16,7 +24,8 @@ "options": { "error": { "currency_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u901a\u8ca8\u6b8b\u9ad8\u306e1\u3064\u4ee5\u4e0a\u304c\u3001Coinbase API\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "exchange_rate_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u70ba\u66ff\u30ec\u30fc\u30c8\u306e1\u3064\u4ee5\u4e0a\u304cCoinbase\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + "exchange_rate_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u70ba\u66ff\u30ec\u30fc\u30c8\u306e1\u3064\u4ee5\u4e0a\u304cCoinbase\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "init": { diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json index 73f6391a238..a9dfd62f83c 100644 --- a/homeassistant/components/control4/translations/ja.json +++ b/homeassistant/components/control4/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "Control4\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8a73\u7d30\u3068\u3001\u30ed\u30fc\u30ab\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/coronavirus/translations/ja.json b/homeassistant/components/coronavirus/translations/ja.json new file mode 100644 index 00000000000..0a0b23917fa --- /dev/null +++ b/homeassistant/components/coronavirus/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "step": { + "user": { + "data": { + "country": "\u56fd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json index fd336ad42a4..6ab8f858af4 100644 --- a/homeassistant/components/crownstone/translations/ja.json +++ b/homeassistant/components/crownstone/translations/ja.json @@ -1,18 +1,27 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "usb_setup_complete": "Crownstone USB\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u304c\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002", "usb_setup_unsuccessful": "Crownstone USB\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, "error": { - "account_not_verified": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002Crownstone\u304b\u3089\u306e\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30b7\u30e7\u30f3\u30e1\u30fc\u30eb\u3092\u901a\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "account_not_verified": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002Crownstone\u304b\u3089\u306e\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30b7\u30e7\u30f3\u30e1\u30fc\u30eb\u3092\u901a\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30a2\u30af\u30c6\u30a3\u30d9\u30fc\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "usb_config": { + "data": { + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e\u3059\u308b\u304b\u3001USB\u30c9\u30f3\u30b0\u30eb\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u306a\u3044\u5834\u5408\u306f\u3001\"USB\u3092\u4f7f\u7528\u3057\u306a\u3044\" \u3092\u9078\u629e\u3057\u307e\u3059\u3002 \n\n VID 10C4 \u3067 PID EA60 \u306a\u30c7\u30d0\u30a4\u30b9\u3092\u898b\u3064\u3051\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, "usb_manual_config": { + "data": { + "usb_manual_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" }, @@ -25,7 +34,7 @@ }, "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "Crownstone\u30a2\u30ab\u30a6\u30f3\u30c8" @@ -48,6 +57,9 @@ "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, "usb_config_option": { + "data": { + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, @@ -59,6 +71,9 @@ "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" }, "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" }, diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index 74b683cc36b..b68357e82d9 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "api_password": "\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002API\u30ad\u30fc\u307e\u305f\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u3044\u305a\u308c\u304b\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json index e72a8ca97af..0a916bf48dc 100644 --- a/homeassistant/components/devolo_home_control/translations/ja.json +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -1,19 +1,23 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { "reauth_failed": "\u4ee5\u524d\u306e\u3068\u540c\u3058mydevolo\u30e6\u30fc\u30b6\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb / devolo ID" } }, "zeroconf_confirm": { "data": { "mydevolo_url": "mydevolo URL", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "Email / devolo ID" + "username": "E\u30e1\u30fc\u30eb / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index c95621b3cc7..2d7f1b072ad 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -1,14 +1,20 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{product} ({name})", "step": { "user": { "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" - } + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" }, "zeroconf_confirm": { "description": "\u30db\u30b9\u30c8\u540d\u304c `{host_name}` \u306e devolo\u793e\u306e\u30db\u30fc\u30e0\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/directv/translations/ja.json b/homeassistant/components/directv/translations/ja.json index ad4cf1544ef..65534b7f338 100644 --- a/homeassistant/components/directv/translations/ja.json +++ b/homeassistant/components/directv/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 818f73e02f3..1d999efe833 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", @@ -9,11 +11,15 @@ "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, "import_turn_on": { "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u3092\u5165\u308c\u3001\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u79fb\u884c\u3092\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044" }, diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index d92dceb1a2a..44592de0237 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "error": { - "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "step": { "setup_network": { diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 55bdacb325e..bde13c55394 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -6,6 +6,9 @@ "title": "ecobee.com\u306e\u30a2\u30d7\u30ea\u3092\u8a8d\u8a3c\u3059\u308b" }, "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, "title": "ecobee API\u30ad\u30fc" } } diff --git a/homeassistant/components/econet/translations/ja.json b/homeassistant/components/econet/translations/ja.json index 896966aee6c..a552bfb6977 100644 --- a/homeassistant/components/econet/translations/ja.json +++ b/homeassistant/components/econet/translations/ja.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "Rheem EcoNet Account\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json index 664ab46bafe..c9c542a6c6e 100644 --- a/homeassistant/components/efergy/translations/ja.json +++ b/homeassistant/components/efergy/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index 304ec628c87..167f57fb8b8 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/ja.json b/homeassistant/components/emonitor/translations/ja.json index ad4cf1544ef..c05a3c64696 100644 --- a/homeassistant/components/emonitor/translations/ja.json +++ b/homeassistant/components/emonitor/translations/ja.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?", + "title": "SiteSage Emonitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index 18e8106cf4f..1fd206d2331 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{serial} ({host})", "step": { "user": { diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json index c363d19e784..0fcf4e4f2b4 100644 --- a/homeassistant/components/environment_canada/translations/ja.json +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -2,8 +2,10 @@ "config": { "error": { "bad_station_id": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID\u304c\u7121\u52b9\u3001\u6b20\u843d\u3057\u3066\u3044\u308b\u3001\u307e\u305f\u306f\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID \u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u5185\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "error_response": "\u30ab\u30ca\u30c0\u74b0\u5883\u304b\u3089\u306e\u5fdc\u7b54\u30a8\u30e9\u30fc", - "too_many_attempts": "\u30ab\u30ca\u30c0\u74b0\u5883\u7701\u3078\u306e\u63a5\u7d9a\u306f\u30ec\u30fc\u30c8\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u300260\u79d2\u5f8c\u306b\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044" + "too_many_attempts": "\u30ab\u30ca\u30c0\u74b0\u5883\u7701\u3078\u306e\u63a5\u7d9a\u306f\u30ec\u30fc\u30c8\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u300260\u79d2\u5f8c\u306b\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index a374a3a7e38..98ab9160f30 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/evil_genius_labs/translations/ja.json b/homeassistant/components/evil_genius_labs/translations/ja.json index a42202307f2..677ed4b719c 100644 --- a/homeassistant/components/evil_genius_labs/translations/ja.json +++ b/homeassistant/components/evil_genius_labs/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/ja.json b/homeassistant/components/ezviz/translations/ja.json index 68333ed59b7..0b533525258 100644 --- a/homeassistant/components/ezviz/translations/ja.json +++ b/homeassistant/components/ezviz/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + }, "flow_title": "{serial}", "step": { "confirm": { @@ -23,7 +32,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "url": "URL", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "\u30ea\u30fc\u30b8\u30e7\u30f3\u306eURL\u3092\u624b\u52d5\u3067\u6307\u5b9a\u3059\u308b" } } }, @@ -31,6 +41,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "ffmpeg\u306b\u6e21\u3055\u308c\u308b\u30ab\u30e1\u30e9\u7528\u306e\u5f15\u6570", "timeout": "\u30ea\u30af\u30a8\u30b9\u30c8\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u79d2)" } } diff --git a/homeassistant/components/faa_delays/translations/ja.json b/homeassistant/components/faa_delays/translations/ja.json index 420618e8f66..c3dd00812e6 100644 --- a/homeassistant/components/faa_delays/translations/ja.json +++ b/homeassistant/components/faa_delays/translations/ja.json @@ -4,14 +4,17 @@ "already_configured": "\u3053\u306e\u7a7a\u6e2f\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" }, "error": { - "invalid_airport": "\u7a7a\u6e2f\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_airport": "\u7a7a\u6e2f\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "id": "\u7a7a\u6e2f" }, - "description": "IATA\u5f62\u5f0f\u3067\u7c73\u56fd\u306e\u7a7a\u6e2f\u30b3\u30fc\u30c9(US Airport Code)\u3092\u5165\u529b\u3057\u307e\u3059" + "description": "IATA\u5f62\u5f0f\u3067\u7c73\u56fd\u306e\u7a7a\u6e2f\u30b3\u30fc\u30c9(US Airport Code)\u3092\u5165\u529b\u3057\u307e\u3059", + "title": "FAA Delays" } } } diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json index de08aede9ff..f22b3c04c84 100644 --- a/homeassistant/components/fjaraskupan/translations/ja.json +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "confirm": { "description": "Fj\u00e4r\u00e5skupan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/flick_electric/translations/ja.json b/homeassistant/components/flick_electric/translations/ja.json index 54c94a474e1..6574e5275b5 100644 --- a/homeassistant/components/flick_electric/translations/ja.json +++ b/homeassistant/components/flick_electric/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID(\u30aa\u30d7\u30b7\u30e7\u30f3)", - "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index 2ff7c7a8a6c..7a159542dfc 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "no_flipr_id_found": "\u73fe\u5728\u3001\u3042\u306a\u305f\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u95a2\u9023\u4ed8\u3051\u3089\u308c\u3066\u3044\u308bflipr id\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u307e\u305a\u306f\u3001Flipr\u306e\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u3067\u52d5\u4f5c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_flipr_id_found": "\u73fe\u5728\u3001\u3042\u306a\u305f\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u95a2\u9023\u4ed8\u3051\u3089\u308c\u3066\u3044\u308bflipr id\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u307e\u305a\u306f\u3001Flipr\u306e\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u3067\u52d5\u4f5c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "flipr_id": { @@ -13,7 +19,7 @@ }, "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u3042\u306a\u305f\u306eFlipr\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json index 8c51c540482..19cc78a4a2b 100644 --- a/homeassistant/components/flume/translations/ja.json +++ b/homeassistant/components/flume/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json new file mode 100644 index 00000000000..2b4009f9d08 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/ja.json b/homeassistant/components/flux_led/translations/ja.json index 45e7d8a606b..7e0f053305f 100644 --- a/homeassistant/components/flux_led/translations/ja.json +++ b/homeassistant/components/flux_led/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json index 7a38264a2df..62090376bed 100644 --- a/homeassistant/components/forecast_solar/translations/ja.json +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -5,6 +5,8 @@ "data": { "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b", "name": "\u540d\u524d" }, diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 0f2f95f65b8..81c10c8f353 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "invalid_response": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u7121\u52b9\u306a\u5fdc\u7b54" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_response": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u7121\u52b9\u306a\u5fdc\u7b54", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { @@ -15,5 +21,6 @@ } } } - } + }, + "title": "Foscam" } \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json index 2691a4a91e7..11811d5a067 100644 --- a/homeassistant/components/freebox/translations/ja.json +++ b/homeassistant/components/freebox/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "link": { "description": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u30eb\u30fc\u30bf\u30fc\u306e\u53f3\u77e2\u5370\u3092\u30bf\u30c3\u30c1\u3057\u3066\u3001Freebox\u3092Home Assistant\u306b\u767b\u9332\u3057\u307e\u3059\u3002 \n\n\uff01[\u30eb\u30fc\u30bf\u30fc\u306e\u30dc\u30bf\u30f3\u306e\u5834\u6240](/static/images/config_freebox.png)", @@ -9,7 +17,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "Freebox" } } } diff --git a/homeassistant/components/freedompro/translations/ja.json b/homeassistant/components/freedompro/translations/ja.json index 398f34ccd8a..6e5184bfdef 100644 --- a/homeassistant/components/freedompro/translations/ja.json +++ b/homeassistant/components/freedompro/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 4661f8d77f9..af6fc0b8e29 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -1,5 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{name}", "step": { "confirm": { @@ -13,7 +25,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "FRITZ!Box Tools\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 - \u8cc7\u683c\u60c5\u5831" }, "start_config": { "data": { diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index 3dfaff6d15f..24d7b8b1e2e 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "flow_title": "{name}", "step": { "confirm": { @@ -16,7 +19,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index 8fc558056d0..3523f811349 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{name}", "step": { "phonebook": { diff --git a/homeassistant/components/garages_amsterdam/translations/ja.json b/homeassistant/components/garages_amsterdam/translations/ja.json index 64d3e6fac92..2f0e09c36bb 100644 --- a/homeassistant/components/garages_amsterdam/translations/ja.json +++ b/homeassistant/components/garages_amsterdam/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gdacs/translations/ja.json b/homeassistant/components/gdacs/translations/ja.json new file mode 100644 index 00000000000..de9079d249f --- /dev/null +++ b/homeassistant/components/gdacs/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f84" + }, + "title": "\u30d5\u30a3\u30eb\u30bf\u30fc\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/ja.json b/homeassistant/components/geonetnz_quakes/translations/ja.json index 97dd549584c..deca31ec45e 100644 --- a/homeassistant/components/geonetnz_quakes/translations/ja.json +++ b/homeassistant/components/geonetnz_quakes/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index 0616dab260d..7d5f1e2dc27 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { @@ -8,5 +11,10 @@ "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach GIO\u015a server" + } } } \ No newline at end of file diff --git a/homeassistant/components/glances/translations/ja.json b/homeassistant/components/glances/translations/ja.json index a773b6499e0..2de3d643a33 100644 --- a/homeassistant/components/glances/translations/ja.json +++ b/homeassistant/components/glances/translations/ja.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "wrong_version": "\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3\u306e\u307f)" }, "step": { @@ -10,7 +14,9 @@ "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b", "version": "Glances API\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3)" }, "title": "Glances\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" @@ -20,6 +26,9 @@ "options": { "step": { "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u5ea6" + }, "description": "Glances\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a" } } diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index 0192d489ed7..be92bad7ba3 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "confirm_discovery": { "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index 49d09cb4676..d0047d97fe5 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { @@ -16,7 +22,12 @@ "init": { "data": { "avoid": "\u907f\u3051\u308b", + "language": "\u8a00\u8a9e", + "mode": "\u30c8\u30e9\u30d9\u30eb\u30e2\u30fc\u30c9", "time": "\u6642\u9593", + "time_type": "\u6642\u9593\u30bf\u30a4\u30d7", + "transit_mode": "\u30c8\u30e9\u30f3\u30b8\u30c3\u30c8\u30e2\u30fc\u30c9", + "transit_routing_preference": "\u30c8\u30e9\u30f3\u30b8\u30c3\u30c8\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306e\u8a2d\u5b9a", "units": "\u5358\u4f4d" } } diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json index 45df3d0df6d..1693f027c78 100644 --- a/homeassistant/components/growatt_server/translations/ja.json +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "no_plants": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u690d\u7269(plants)\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "plant": { "data": { diff --git a/homeassistant/components/habitica/translations/ja.json b/homeassistant/components/habitica/translations/ja.json index bf5d578612e..28d877a4788 100644 --- a/homeassistant/components/habitica/translations/ja.json +++ b/homeassistant/components/habitica/translations/ja.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "invalid_credentials": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "api_key": "API\u30ad\u30fc", "api_user": "Habitica API\u306e\u30e6\u30fc\u30b6\u30fcID", + "name": "Habitica\u2019s\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002\u30b5\u30fc\u30d3\u30b9\u30b3\u30fc\u30eb\u3067\u4f7f\u7528\u3055\u308c\u307e\u3059", "url": "URL" }, "description": "Habitica profile\u306b\u63a5\u7d9a\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3068\u30bf\u30b9\u30af\u3092\u76e3\u8996\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002 \u6ce8\u610f: api_id\u3068api_key\u306f\u3001https://habitica.com/user/settings/api \u304b\u3089\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json index dcfa29bcf54..72f5ce09313 100644 --- a/homeassistant/components/hangouts/translations/ja.json +++ b/homeassistant/components/hangouts/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "error": { "invalid_2fa": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "invalid_2fa_method": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9(\u96fb\u8a71\u3067\u78ba\u8a8d)", @@ -16,7 +20,7 @@ "user": { "data": { "authorization_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9(\u624b\u52d5\u8a8d\u8a3c\u6642\u306b\u5fc5\u8981)", - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u7a7a", diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index aad26b140c4..04577fab9f6 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -7,7 +7,8 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8", + "name": "\u30cf\u30d6\u540d" }, "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index 102cc549b5f..63eebb77c54 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -1,9 +1,14 @@ { "system_health": { "info": { + "docker_version": "Docker\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", "healthy": "\u5143\u6c17", "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0", - "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059" + "installed_addons": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30a2\u30c9\u30aa\u30f3", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", + "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "version_api": "\u30d0\u30fc\u30b8\u30e7\u30f3API" } } } \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/ja.json b/homeassistant/components/hisense_aehw4a1/translations/ja.json index a9edf17eedc..75107c4c0fc 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ja.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "confirm": { "description": "Hisense AEH-W4A1\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 132d1e483d3..74e2a1cfa66 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown_entry": "\u65e2\u5b58\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "error": { - "invalid_password": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + "invalid_password": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "2fa": { @@ -23,10 +26,20 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json new file mode 100644 index 00000000000..00e35f402ba --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 28cc613ca10..fdd0ec93caa 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -1,7 +1,11 @@ { "system_health": { "info": { + "arch": "CPU \u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", + "dev": "\u958b\u767a", "docker": "Docker", + "hassio": "Supervisor", + "installation_type": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7a2e\u985e", "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30d5\u30a1\u30df\u30ea\u30fc", "user": "\u30e6\u30fc\u30b6\u30fc" } diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index fd4a5bf6a28..cbedf19c794 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -8,12 +8,14 @@ }, "error": { "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u6027\u8cea\u306a\u305f\u3081\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { "pair": { "data": { + "allow_insecure_setup_codes": "\u5b89\u5168\u3067\u306a\u3044\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8a31\u53ef\u3059\u308b\u3002", "pairing_code": "\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9" } }, diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 42151b3e0cc..dfd4a413f79 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306f\u65e2\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "connection_aborted": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "error": { diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index d4932c4c47e..1d3e19750c6 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 4c1d4282c3c..113f2f6ba4b 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index b3b39dfb185..275ef6f0f34 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue bridge\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", "no_bridges": "Hue bridge\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", @@ -37,6 +38,13 @@ "2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + }, + "trigger_type": { + "double_short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3059", + "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", + "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", + "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", + "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" } }, "options": { diff --git a/homeassistant/components/huisbaasje/translations/ja.json b/homeassistant/components/huisbaasje/translations/ja.json index 38abb3ce5b6..2f714747433 100644 --- a/homeassistant/components/huisbaasje/translations/ja.json +++ b/homeassistant/components/huisbaasje/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index dd4ac2cd8bf..cc6f7799716 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -1,14 +1,28 @@ { "config": { "abort": { - "no_id": "Hyperion Ambilight\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u305d\u306eID\u3092\u30ec\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "no_id": "Hyperion Ambilight\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u305d\u306eID\u3092\u30ec\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, "step": { + "auth": { + "data": { + "create_token": "\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3059\u308b" + } + }, "confirm": { "description": "\u3053\u306eHyperion Ambilight\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f \n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}" }, "create_token": { - "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3057\u307e\u3059" }, "user": { "data": { diff --git a/homeassistant/components/ialarm/translations/ja.json b/homeassistant/components/ialarm/translations/ja.json index 5dc41a91227..1cbb749800b 100644 --- a/homeassistant/components/ialarm/translations/ja.json +++ b/homeassistant/components/ialarm/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 076ab1294b6..3f1df84ff34 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "validate_verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u78ba\u8a8d\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u518d\u5ea6\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, @@ -19,7 +22,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" }, "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index a9b0ae57fe8..6fb2f55cc39 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -31,7 +31,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Insteon" } } } diff --git a/homeassistant/components/ios/translations/ja.json b/homeassistant/components/ios/translations/ja.json new file mode 100644 index 00000000000..60fbfbea06f --- /dev/null +++ b/homeassistant/components/ios/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json index 1d29e607aa5..3f176af1693 100644 --- a/homeassistant/components/iotawatt/translations/ja.json +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json index a8c46878fe5..3e5e2a4d462 100644 --- a/homeassistant/components/ipma/translations/ja.json +++ b/homeassistant/components/ipma/translations/ja.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "\u30dd\u30eb\u30c8\u30ac\u30eb\u6d77\u6d0b\u5927\u6c17\u7814\u7a76\u6240(Instituto Portugu\u00eas do Mar e Atmosfera)" + "description": "\u30dd\u30eb\u30c8\u30ac\u30eb\u6d77\u6d0b\u5927\u6c17\u7814\u7a76\u6240(Instituto Portugu\u00eas do Mar e Atmosfera)", + "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index f9605b589ce..d2c5559da87 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -8,7 +8,8 @@ "user": { "data": { "host": "URL", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80" } diff --git a/homeassistant/components/izone/translations/ja.json b/homeassistant/components/izone/translations/ja.json index 5b9bea94d7a..bd5ae39dec4 100644 --- a/homeassistant/components/izone/translations/ja.json +++ b/homeassistant/components/izone/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "confirm": { "description": "iZone\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 38e13174ac8..2b14d614f02 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index 6c7513d9142..471263e916c 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -1,9 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_udn": "SSDP\u691c\u51fa\u60c5\u5831\u306b\u3001UDN\u304c\u3042\u308a\u307e\u305b\u3093", "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic router\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name} ({host})", "step": { "user": { @@ -21,7 +25,12 @@ "step": { "user": { "data": { - "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694" + "consider_home": "\u30db\u30fc\u30e0\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb\u3092\u691c\u8a0e", + "include_arp": "ARP\u30c7\u30fc\u30bf\u3092\u4f7f\u7528(\u30db\u30c3\u30c8\u30b9\u30dd\u30c3\u30c8 \u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3057\u305f\u5834\u5408\u306f\u7121\u8996)", + "include_associated": "WiFi AP\u30a2\u30bd\u30b7\u30a8\u30fc\u30b7\u30e7\u30f3 \u30c7\u30fc\u30bf\u3092\u4f7f\u7528(\u30db\u30c3\u30c8\u30b9\u30dd\u30c3\u30c8\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3057\u305f\u5834\u5408\u306f\u7121\u8996)", + "interfaces": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u3092\u9078\u629e", + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", + "try_hotspot": "'ip hotspot'\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3059\u308b(\u6700\u3082\u6b63\u78ba)" } } } diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json index 2981d97e3c6..5bd6c037226 100644 --- a/homeassistant/components/kmtronic/translations/ja.json +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json new file mode 100644 index 00000000000..6fe37eac691 --- /dev/null +++ b/homeassistant/components/knx/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "manual_tunnel": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json new file mode 100644 index 00000000000..95e1045e749 --- /dev/null +++ b/homeassistant/components/knx/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 18694617e2f..0ad86b88945 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { - "cannot_connect": "\u30db\u30b9\u30c8" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "step": { "manual_tunnel": { @@ -30,7 +34,8 @@ "type": { "data": { "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7" - } + }, + "description": "KNX\u63a5\u7d9a\u306b\u4f7f\u7528\u3059\u308b\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n AUTOMATIC - \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u30b9\u30ad\u30e3\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3001KNX \u30d0\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u884c\u3044\u307e\u3059\u3002 \n TUNNELING - \u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u3092\u4ecb\u3057\u3066\u3001KNX\u30d0\u30b9\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002 \n ROUTING - \u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3092\u4ecb\u3057\u3066\u3001KNX \u30d0\u30b9\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json new file mode 100644 index 00000000000..223dd66402a --- /dev/null +++ b/homeassistant/components/knx/translations/no.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Vert", + "individual_address": "Individuell adresse for tilkoblingen", + "port": "Port", + "route_back": "Rute tilbake / NAT-modus" + }, + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + }, + "routing": { + "data": { + "individual_address": "Individuell adresse for ruteforbindelsen", + "multicast_group": "Multicast-gruppen som brukes til ruting", + "multicast_port": "Multicast-porten som brukes til ruting" + }, + "description": "Vennligst konfigurer rutealternativene." + }, + "tunnel": { + "data": { + "gateway": "KNX Tunneltilkobling" + }, + "description": "Vennligst velg en gateway fra listen." + }, + "type": { + "data": { + "connection_type": "KNX tilkoblingstype" + }, + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til KNX-bussen din via tunnelering.\n ROUTING - Integrasjonen vil kobles til din KNX-bussen via ruting." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "KNX tilkoblingstype", + "individual_address": "Standard individuell adresse", + "multicast_group": "Multicast-gruppe brukt til ruting og oppdagelse", + "multicast_port": "Multicast-port som brukes til ruting og oppdagelse", + "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund", + "state_updater": "Aktiver lesetilstander globalt fra KNX-bussen" + } + }, + "tunnel": { + "data": { + "host": "Vert", + "port": "Port", + "route_back": "Rute tilbake / NAT-modus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 4639e810259..cb0acbf326b 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "not_konn_panel": "\u8a8d\u8b58\u3055\u308c\u305f\u3001Konnected.io\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "confirm": { - "description": "\u30e2\u30c7\u30eb: {model}\n ID: {id}\n\u30db\u30b9\u30c8: {host}\n\u30dd\u30fc\u30c8: {port} \n\nKonnected Alarm Panel\u306e\u8a2d\u5b9a\u3067\u3001IO\u3068\u30d1\u30cd\u30eb\u306e\u52d5\u4f5c\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002" + "description": "\u30e2\u30c7\u30eb: {model}\n ID: {id}\n\u30db\u30b9\u30c8: {host}\n\u30dd\u30fc\u30c8: {port} \n\nKonnected Alarm Panel\u306e\u8a2d\u5b9a\u3067\u3001IO\u3068\u30d1\u30cd\u30eb\u306e\u52d5\u4f5c\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002", + "title": "Konnected\u30c7\u30d0\u30a4\u30b9\u306e\u6e96\u5099\u5b8c\u4e86" }, "import_confirm": { "description": "ID {id} \u306eKonnected\u30a2\u30e9\u30fc\u30e0\u30d1\u30cd\u30eb\u304c\u8a2d\u5b9a\u3067\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u30fc\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u304d\u307e\u3059\u3002", @@ -18,21 +28,30 @@ } }, "options": { + "abort": { + "not_konn_panel": "\u8a8d\u8b58\u3055\u308c\u305f\u3001Konnected.io\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, "error": { "bad_host": "\u7121\u52b9\u306a\u4e0a\u66f8\u304d(Override)API\u30db\u30b9\u30c8URL" }, "step": { "options_binary": { "data": { - "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "inverse": "\u958b\u9589\u72b6\u614b\u3092\u53cd\u8ee2", + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "type": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc \u30bf\u30a4\u30d7" }, - "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3" + "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3", + "title": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a" }, "options_digital": { "data": { - "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "poll_interval": "\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u5206)(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "type": "\u30bb\u30f3\u30b5\u30fc\u30bf\u30a4\u30d7" }, - "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3" + "description": "{zone} \u30aa\u30d7\u30b7\u30e7\u30f3", + "title": "\u30c7\u30b8\u30bf\u30eb\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a" }, "options_io": { "data": { @@ -45,7 +64,8 @@ "7": "\u30be\u30fc\u30f37", "out": "OUT" }, - "description": "{model} \u3067 {host} \u3092\u767a\u898b\u3057\u307e\u3057\u305f\u3002\u5404I/O\u306e\u57fa\u672c\u69cb\u6210\u3092\u4ee5\u4e0b\u304b\u3089\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002I/O\u306b\u5fdc\u3058\u3066\u3001\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc(\u30aa\u30fc\u30d7\u30f3(\u958b)/\u30af\u30ed\u30fc\u30ba(\u9589)\u63a5\u70b9)\u3001\u30c7\u30b8\u30bf\u30eb\u30bb\u30f3\u30b5\u30fc(dht\u304a\u3088\u3073ds18b20)\u3001\u307e\u305f\u306f\u5207\u308a\u66ff\u3048\u53ef\u80fd\u306a\u51fa\u529b(switchable outputs)\u304c\u53ef\u80fd\u3067\u3059\u3002\u6b21\u306e\u30b9\u30c6\u30c3\u30d7\u3067\u3001\u8a73\u7d30\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002" + "description": "{model} \u3067 {host} \u3092\u767a\u898b\u3057\u307e\u3057\u305f\u3002\u5404I/O\u306e\u57fa\u672c\u69cb\u6210\u3092\u4ee5\u4e0b\u304b\u3089\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002I/O\u306b\u5fdc\u3058\u3066\u3001\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc(\u30aa\u30fc\u30d7\u30f3(\u958b)/\u30af\u30ed\u30fc\u30ba(\u9589)\u63a5\u70b9)\u3001\u30c7\u30b8\u30bf\u30eb\u30bb\u30f3\u30b5\u30fc(dht\u304a\u3088\u3073ds18b20)\u3001\u307e\u305f\u306f\u5207\u308a\u66ff\u3048\u53ef\u80fd\u306a\u51fa\u529b(switchable outputs)\u304c\u53ef\u80fd\u3067\u3059\u3002\u6b21\u306e\u30b9\u30c6\u30c3\u30d7\u3067\u3001\u8a73\u7d30\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002", + "title": "I/O\u306e\u8a2d\u5b9a" }, "options_io_ext": { "data": { @@ -54,12 +74,16 @@ "12": "\u30be\u30fc\u30f312", "8": "\u30be\u30fc\u30f38", "9": "\u30be\u30fc\u30f39", + "alarm1": "\u30a2\u30e9\u30fc\u30e01", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" - } + }, + "description": "\u6b8b\u308a\u306eI/O\u306e\u69cb\u6210\u3092\u4ee5\u4e0b\u304b\u3089\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6b21\u306e\u30b9\u30c6\u30c3\u30d7\u3067\u8a73\u7d30\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", + "title": "\u62e1\u5f35I/O\u306e\u8a2d\u5b9a" }, "options_misc": { "data": { + "blink": "\u72b6\u614b\u5909\u66f4\u3092\u9001\u4fe1\u3059\u308b\u3068\u304d\u306b\u3001\u30d1\u30cd\u30eb\u306eLED\u3092\u70b9\u6ec5\u3055\u305b\u308b", "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" } }, diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json index 9f68231f0d2..63c2dcb9928 100644 --- a/homeassistant/components/kostal_plenticore/translations/ja.json +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -8,5 +16,6 @@ } } } - } + }, + "title": "Kostal Plenticore\u30bd\u30fc\u30e9\u30fc\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index d516569d86a..1d581131252 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -1,9 +1,20 @@ { + "config": { + "abort": { + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, "options": { "step": { "init": { "data": { - "scan_interval": "\u66f4\u65b0\u9593\u9694" + "scan_interval": "\u66f4\u65b0\u9593\u9694", + "tracked_asset_pairs": "\u8ffd\u8de1\u3055\u308c\u305f\u30a2\u30bb\u30c3\u30c8\u30da\u30a2" } } } diff --git a/homeassistant/components/kulersky/translations/ja.json b/homeassistant/components/kulersky/translations/ja.json new file mode 100644 index 00000000000..d1234b69652 --- /dev/null +++ b/homeassistant/components/kulersky/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 09e11849452..6cfa33a7ace 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "confirm": { "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index 5886fc609c6..c26dc073113 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { "open_failed": "\u6307\u5b9a\u3055\u308c\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index 38abb3ce5b6..580a8bb3a3d 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json index 46274c545ee..5da03e5b724 100644 --- a/homeassistant/components/local_ip/translations/ja.json +++ b/homeassistant/components/local_ip/translations/ja.json @@ -2,6 +2,7 @@ "config": { "step": { "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", "title": "\u30ed\u30fc\u30ab\u30ebIP\u30a2\u30c9\u30ec\u30b9" } } diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index 3ce29779b8d..86507260cf0 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", "title": "Locative Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/lookin/translations/ja.json b/homeassistant/components/lookin/translations/ja.json index 04b723e15e0..d4bad858c63 100644 --- a/homeassistant/components/lookin/translations/ja.json +++ b/homeassistant/components/lookin/translations/ja.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name} ({host})", "step": { "device_name": { diff --git a/homeassistant/components/lovelace/translations/ja.json b/homeassistant/components/lovelace/translations/ja.json index 2d1f1af3c61..88d75d3fcbc 100644 --- a/homeassistant/components/lovelace/translations/ja.json +++ b/homeassistant/components/lovelace/translations/ja.json @@ -1,7 +1,8 @@ { "system_health": { "info": { - "resources": "\u30ea\u30bd\u30fc\u30b9" + "resources": "\u30ea\u30bd\u30fc\u30b9", + "views": "\u30d3\u30e5\u30fc" } } } \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/ja.json b/homeassistant/components/luftdaten/translations/ja.json index a6ca1a31316..ec793c99368 100644 --- a/homeassistant/components/luftdaten/translations/ja.json +++ b/homeassistant/components/luftdaten/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "invalid_sensor": "\u30bb\u30f3\u30b5\u30fc\u304c\u5229\u7528\u3067\u304d\u306a\u3044\u304b\u3001\u7121\u52b9\u3067\u3059" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index b1267f4f2c2..ece2e8e75f6 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -15,8 +15,23 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "group_1_button_1": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", + "group_1_button_2": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "group_2_button_1": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", + "group_2_button_2": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "off": "\u30aa\u30d5", - "on": "\u30aa\u30f3" + "on": "\u30aa\u30f3", + "stop": "\u505c\u6b62(\u304a\u6c17\u306b\u5165\u308a)", + "stop_1": "\u505c\u6b62 1", + "stop_2": "\u505c\u6b62 2", + "stop_3": "\u505c\u6b62 3", + "stop_4": "\u505c\u6b62 4", + "stop_all": "\u3059\u3079\u3066\u505c\u6b62" + }, + "trigger_type": { + "press": "\"{subtype}\" \u62bc\u3059", + "release": "\"{subtype}\" \u96e2\u3059" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 11afa8cd6a7..1ffd6fdacf0 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, "reauth_confirm": { - "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/mazda/translations/ja.json b/homeassistant/components/mazda/translations/ja.json index 24ac22d7950..f285bda19b7 100644 --- a/homeassistant/components/mazda/translations/ja.json +++ b/homeassistant/components/mazda/translations/ja.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "account_locked": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f\u3002\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, diff --git a/homeassistant/components/media_player/translations/ja.json b/homeassistant/components/media_player/translations/ja.json index 459da77a6f9..cea87342685 100644 --- a/homeassistant/components/media_player/translations/ja.json +++ b/homeassistant/components/media_player/translations/ja.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, "state": { "_": { "idle": "\u30a2\u30a4\u30c9\u30eb", diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index 896966aee6c..41d0b6c48a1 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,10 +1,18 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "description": "MELCloud\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3057\u307e\u3059\u3002", + "title": "MELCloud\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/met/translations/ja.json b/homeassistant/components/met/translations/ja.json index 9d1865e0115..ae6a3a0f221 100644 --- a/homeassistant/components/met/translations/ja.json +++ b/homeassistant/components/met/translations/ja.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "no_home": "Home Assistant\u306e\u8a2d\u5b9a\u3067\u3001\u5bb6\u306e\u5ea7\u6a19\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, "step": { "user": { "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "description": "Meteorologisk institutt" diff --git a/homeassistant/components/met_eireann/translations/ja.json b/homeassistant/components/met_eireann/translations/ja.json index 169c0bcb555..58f6c2b5f36 100644 --- a/homeassistant/components/met_eireann/translations/ja.json +++ b/homeassistant/components/met_eireann/translations/ja.json @@ -1,8 +1,17 @@ { "config": { + "error": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { - "description": "Met \u00c9ireann Public Weather Forecast API\u306e\u6c17\u8c61\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3059\u308b\u5834\u6240\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", + "name": "\u540d\u524d" + }, + "description": "Met \u00c9ireann Public Weather Forecast API\u306e\u6c17\u8c61\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3059\u308b\u5834\u6240\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/meteo_france/translations/ja.json b/homeassistant/components/meteo_france/translations/ja.json new file mode 100644 index 00000000000..97e650554c7 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "M\u00e9t\u00e9o-France" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ja.json b/homeassistant/components/meteoclimatic/translations/ja.json index e720b5151c4..0274ff70e88 100644 --- a/homeassistant/components/meteoclimatic/translations/ja.json +++ b/homeassistant/components/meteoclimatic/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/ja.json b/homeassistant/components/metoffice/translations/ja.json index c2ff6bbb145..ca01be12afa 100644 --- a/homeassistant/components/metoffice/translations/ja.json +++ b/homeassistant/components/metoffice/translations/ja.json @@ -3,8 +3,11 @@ "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" - } + "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + }, + "description": "\u7def\u5ea6\u3068\u7d4c\u5ea6\u306f\u3001\u6700\u3082\u8fd1\u3044\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u898b\u3064\u3051\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index 6f07407a010..acc93cd2971 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mill/translations/id.json b/homeassistant/components/mill/translations/id.json index ab929d3d7c8..d6de96051d3 100644 --- a/homeassistant/components/mill/translations/id.json +++ b/homeassistant/components/mill/translations/id.json @@ -7,8 +7,21 @@ "cannot_connect": "Gagal terhubung" }, "step": { + "cloud": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "local": { + "data": { + "ip_address": "Alamat IP" + }, + "description": "Alamat IP lokal perangkat." + }, "user": { "data": { + "connection_type": "Pilih jenis koneksi", "password": "Kata Sandi", "username": "Nama Pengguna" } diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json index 07f88e63c7c..0705c93038d 100644 --- a/homeassistant/components/mill/translations/ja.json +++ b/homeassistant/components/mill/translations/ja.json @@ -16,7 +16,8 @@ "user": { "data": { "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30ed\u30fc\u30ab\u30eb\u306b\u306f\u7b2c3\u4e16\u4ee3\u306e\u30d2\u30fc\u30bf\u30fc\u304c\u5fc5\u8981\u3067\u3059" } diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json index ae3838f3cc1..4bfe44c4fec 100644 --- a/homeassistant/components/minecraft_server/translations/ja.json +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "cannot_connect": "\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u30b5\u30fc\u30d0\u30fc\u3067Minecraft\u306e\u30d0\u30fc\u30b8\u30e7\u30f31.7\u4ee5\u4e0a\u306e\u3082\u306e\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3082\u3042\u308f\u305b\u3066\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_ip": "IP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059(MAC\u30a2\u30c9\u30ec\u30b9\u3092\u7279\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f)\u3002\u4fee\u6b63\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index c41021733c2..da9774e8350 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -6,5 +6,10 @@ } } }, + "device_automation": { + "action_type": { + "notify": "\u901a\u77e5\u306e\u9001\u4fe1" + } + }, "title": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index 8da72b9d45d..595c04d13fd 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_devices_found": "\u6b8b\u308a\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "usb_confirm": { "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json index 4848a748574..f06fea88935 100644 --- a/homeassistant/components/modern_forms/translations/ja.json +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index e3e621f048b..12749d3aabf 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "error": { "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" @@ -7,6 +12,7 @@ "step": { "connect": { "data": { + "api_key": "API\u30ad\u30fc", "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" }, "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" @@ -14,7 +20,8 @@ "select": { "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" - } + }, + "title": "\u63a5\u7d9a\u3057\u305f\u3044Motion Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "user": { "data": { diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index f19ab66421b..311ad7d0957 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { - "invalid_url": "\u7121\u52b9\u306aURL" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_url": "\u7121\u52b9\u306aURL", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "hassio_confirm": { @@ -11,7 +18,9 @@ "user": { "data": { "admin_password": "\u7ba1\u7406\u8005(Admin)\u30d1\u30b9\u30ef\u30fc\u30c9", + "admin_username": "\u7ba1\u7406\u8005\u30e6\u30fc\u30b6\u30fc\u540d", "surveillance_password": "\u76e3\u8996(Surveillance)\u30d1\u30b9\u30ef\u30fc\u30c9", + "surveillance_username": "\u76e3\u8996\u30e6\u30fc\u30b6\u30fc\u540d", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 9c74bf06f54..7a459d6bd17 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, @@ -25,7 +29,12 @@ }, "device_automation": { "trigger_subtype": { - "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" } }, "options": { diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json index 79235ee835e..b0e1e55764b 100644 --- a/homeassistant/components/mullvad/translations/ja.json +++ b/homeassistant/components/mullvad/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "description": "Mullvad VPN\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/mutesync/translations/ja.json b/homeassistant/components/mutesync/translations/ja.json index a42202307f2..8de0724a2fa 100644 --- a/homeassistant/components/mutesync/translations/ja.json +++ b/homeassistant/components/mutesync/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u8a2d\u5b9a\u3067\u3001m\u00fctesync\u306e\u8a8d\u8a3c\u3092\u6709\u52b9\u306b\u3059\u308b > \u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/myq/translations/ja.json b/homeassistant/components/myq/translations/ja.json index 7e44514d6fe..f95af61734c 100644 --- a/homeassistant/components/myq/translations/ja.json +++ b/homeassistant/components/myq/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index bc768f5db1c..d46cd61e0ae 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -1,23 +1,36 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", - "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", + "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "duplicate_topic": "\u30c8\u30d4\u30c3\u30af\u306f\u65e2\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "gw_mqtt": { "data": { + "retain": "mqtt retain(\u4fdd\u6301)", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, "description": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 8cd3da484e0..72e86724094 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -1,9 +1,16 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{host}", "step": { "confirm_discovery": { diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index b1dcabf9bfd..0b2f371d77e 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -1,7 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "error": { - "not_allowing_new_tokens": "Nanoleaf\u306f\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u8a31\u53ef\u3057\u3066\u3044\u306a\u3044\u306e\u3067\u3001\u4e0a\u8a18\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "not_allowing_new_tokens": "Nanoleaf\u306f\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u8a31\u53ef\u3057\u3066\u3044\u306a\u3044\u306e\u3067\u3001\u4e0a\u8a18\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/neato/translations/ja.json b/homeassistant/components/neato/translations/ja.json new file mode 100644 index 00000000000..0b41f69b65e --- /dev/null +++ b/homeassistant/components/neato/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "title": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index cb0ecea34d1..9b399384d39 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { "internal_error": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u4e2d\u306b\u5185\u90e8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", "invalid_pin": "\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9", - "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059" + "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "auth": { + "data": { + "code": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + }, "description": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001 [authorize your account]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u8a8d\u8a3c\u5f8c\u3001\u63d0\u4f9b\u3055\u308c\u305f\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u306e\u30b3\u30fc\u30c9\u3092\u4ee5\u4e0b\u306b\u30b3\u30d4\u30fc\u30da\u30fc\u30b9\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" }, @@ -14,6 +23,7 @@ "data": { "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" }, + "description": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e", "title": "\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" }, "link": { @@ -21,6 +31,9 @@ "code": "PIN\u30b3\u30fc\u30c9" }, "title": "Nest\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u30ea\u30f3\u30af" + }, + "reauth_confirm": { + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index e523422b9e8..8aa60160212 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -1,14 +1,38 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, "step": { "reauth_confirm": { - "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } }, "device_automation": { "trigger_subtype": { + "away": "\u7559\u5b88(away)", + "hg": "\u30d5\u30ed\u30b9\u30c8(frost)\u30ac\u30fc\u30c9", "schedule": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" + }, + "trigger_type": { + "alarm_started": "{entity_name} \u304c\u30a2\u30e9\u30fc\u30e0\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "animal": "{entity_name} \u304c\u52d5\u7269\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "cancel_set_point": "{entity_name} \u304c\u3001\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb\u3092\u518d\u958b\u3057\u307e\u3057\u305f\u3002", + "human": "{entity_name} \u304c\u3001\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "movement": "{entity_name} \u304c\u52d5\u304d\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "outdoor": "{entity_name} \u304c\u5c4b\u5916\u30a4\u30d9\u30f3\u30c8\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "person": "{entity_name} \u304c\u3001\u500b\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "person_away": "{entity_name} \u304c\u3001\u4eba\u304c\u53bb\u3063\u305f\u3053\u3068\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "set_point": "\u76ee\u6a19\u6e29\u5ea6 {entity_name} \u3092\u624b\u52d5\u3067\u8a2d\u5b9a", + "therm_mode": "{entity_name} \u306f \"{subtype}\" \u306b\u5207\u308a\u66ff\u308f\u308a\u307e\u3057\u305f\u3002", + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", + "vehicle": "{entity_name} \u304c\u8eca\u4e21\u3092\u691c\u51fa\u3057\u307e\u3057\u305f" } }, "options": { @@ -16,6 +40,10 @@ "public_weather": { "data": { "area_name": "\u30a8\u30ea\u30a2\u540d", + "lat_ne": "\u7def\u5ea6\u5317\u6771\u306e\u89d2", + "lat_sw": "\u7def\u5ea6\u5357\u897f\u306e\u89d2", + "lon_ne": "\u7d4c\u5ea6\u5317\u6771\u306e\u89d2", + "lon_sw": "\u7d4c\u5ea6 \u5357\u897f\u306e\u89d2", "mode": "\u8a08\u7b97", "show_on_map": "\u5730\u56f3\u306b\u8868\u793a" }, diff --git a/homeassistant/components/netgear/translations/id.json b/homeassistant/components/netgear/translations/id.json index a6a41a5023f..6ca86af8bbe 100644 --- a/homeassistant/components/netgear/translations/id.json +++ b/homeassistant/components/netgear/translations/id.json @@ -14,5 +14,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "Netgear" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index eb184ba0aa8..bd1eb2d6d15 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "config": "\u63a5\u7d9a\u307e\u305f\u306f\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc : \u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, @@ -9,6 +12,7 @@ "host": "\u30db\u30b9\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d (\u30aa\u30d7\u30b7\u30e7\u30f3)" }, "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}", diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index dd12e0e7086..85a319fc5bd 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index bbaa3ffa341..721cfa4e5f6 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "invalid_hosts": "\u7121\u52b9\u306a\u30db\u30b9\u30c8" }, @@ -16,13 +19,21 @@ } }, "options": { + "error": { + "invalid_hosts": "\u7121\u52b9\u306a\u30db\u30b9\u30c8" + }, "step": { "init": { "data": { "consider_home": "\u898b\u3089\u308c\u306a\u304b\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u3064\u79d2\u6570\u3002", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", + "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", + "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3", "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1" - } + }, + "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" } } }, diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index 403ac3e7b90..cb0a4e7d593 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -1,14 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { - "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index 5dc41a91227..71c68fa11af 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { + "reauth_confirm": { + "data": { + "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + }, + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" } } } diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 4b855e7ae53..c2d014e8614 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -23,6 +23,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json index 71e4d4d08f8..7b9f046afc7 100644 --- a/homeassistant/components/nws/translations/ja.json +++ b/homeassistant/components/nws/translations/ja.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" }, "title": "\u30a2\u30e1\u30ea\u30ab\u56fd\u7acb\u6c17\u8c61\u5c40\u306b\u63a5\u7d9a" } diff --git a/homeassistant/components/octoprint/translations/id.json b/homeassistant/components/octoprint/translations/id.json index a728c0c9401..7970b3f17fb 100644 --- a/homeassistant/components/octoprint/translations/id.json +++ b/homeassistant/components/octoprint/translations/id.json @@ -13,6 +13,7 @@ "user": { "data": { "host": "Host", + "ssl": "Gunakan SSL", "username": "Nama Pengguna" } } diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json index f42ab595ab6..891120b3600 100644 --- a/homeassistant/components/octoprint/translations/ja.json +++ b/homeassistant/components/octoprint/translations/ja.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "OctoPrint Printer: {host}", "progress": { diff --git a/homeassistant/components/ondilo_ico/translations/ja.json b/homeassistant/components/ondilo_ico/translations/ja.json new file mode 100644 index 00000000000..5b012e09846 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 1188b08dac5..70e653ebda9 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -8,6 +8,9 @@ } }, "user": { + "data": { + "type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + }, "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/opengarage/translations/ja.json b/homeassistant/components/opengarage/translations/ja.json index 91393016e27..ea25099aca4 100644 --- a/homeassistant/components/opengarage/translations/ja.json +++ b/homeassistant/components/opengarage/translations/ja.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "device_key": "\u30c7\u30d0\u30a4\u30b9\u30ad\u30fc", - "host": "\u30dd\u30fc\u30c8", - "port": "\u30dd\u30fc\u30c8" + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } } diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 45adb506487..f0986aaefc1 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" }, "step": { diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 3b5880a862e..0d1b8acc2fa 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -5,6 +5,8 @@ "data": { "api_key": "API\u30ad\u30fc", "language": "\u8a00\u8a9e", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json index 2b55e3aa88d..cc5852ca726 100644 --- a/homeassistant/components/ovo_energy/translations/ja.json +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -6,11 +6,13 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "OVO Energy\u306e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u73fe\u5728\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "OVO Energy\u306e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u73fe\u5728\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u518d\u8a8d\u8a3c" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "OVO Energy\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u8a2d\u5b9a\u3057\u3066\u3001\u30a8\u30cd\u30eb\u30ae\u30fc\u4f7f\u7528\u91cf\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u3002", "title": "OVO Energy\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3059\u308b" diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json index 84a0a8c5fb8..138920b69e4 100644 --- a/homeassistant/components/ozw/translations/ja.json +++ b/homeassistant/components/ozw/translations/ja.json @@ -1,13 +1,25 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + }, "step": { "hassio_confirm": { "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u3068OpenZWave\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, + "install_addon": { + "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f" + }, + "on_supervisor": { + "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" + }, "start_addon": { "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc" - } + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc", + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3059\u308b" } } } diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index 30ed51abe5a..984a87e34ef 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index fded9e5f9fd..a3a6323d005 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "invalid_pin": "\u7121\u52b9\u306aPIN" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_pin": "\u7121\u52b9\u306aPIN", + "pairing_failure": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f: {error_id}", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "pair": { @@ -12,11 +18,17 @@ }, "user": { "data": { + "api_version": "API\u30d0\u30fc\u30b8\u30e7\u30f3", "host": "\u30db\u30b9\u30c8" } } } }, + "device_automation": { + "trigger_type": { + "turn_on": "\u30c7\u30d0\u30a4\u30b9\u3092\u30aa\u30f3\u306b\u3059\u308b\u3088\u3046\u306b\u8981\u6c42\u3055\u308c\u307e\u3057\u305f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index 12a527f3c55..1032e9fb39b 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -11,7 +11,8 @@ "api_key": "API\u30ad\u30fc", "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "statistics_only": "\u7d71\u8a08\u306e\u307f" } } } diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index dbfec34c4ed..0a133c91e31 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 50d46d5dcce..62fea558e72 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "create_entry": { "default": "Plaato {device_type} \u540d\u524d **{device_name}** \u304c\u6b63\u5e38\u306b\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f\u3002" }, @@ -17,12 +20,27 @@ "title": "API\u65b9\u5f0f\u3092\u9078\u629e" }, "user": { + "data": { + "device_name": "\u30c7\u30d0\u30a4\u30b9\u306b\u540d\u524d\u3092\u4ed8\u3051\u308b", + "device_type": "Plaato\u30c7\u30d0\u30a4\u30b9\u306e\u30bf\u30a4\u30d7" + }, "title": "Plaato Wdebhookvices\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + }, + "webhook": { + "description": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Plaato Airlock\u3067webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u4f7f\u7528\u3059\u308bWebhook" } } }, "options": { "step": { + "user": { + "data": { + "update_interval": "\u66f4\u65b0\u9593\u9694(\u5206)" + }, + "description": "\u66f4\u65b0\u9593\u9694\u306e\u8a2d\u5b9a(\u5206)", + "title": "Plaato\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, "webhook": { "description": "Webhook\u60c5\u5831:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n", "title": "Plaato Airlock\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 13ab62c364e..0c4d7987c02 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "error": { "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "ssl_error": "SSL\u8a3c\u660e\u66f8\u306e\u554f\u984c" diff --git a/homeassistant/components/plum_lightpad/translations/ja.json b/homeassistant/components/plum_lightpad/translations/ja.json index 896966aee6c..bfe5b4dbc6c 100644 --- a/homeassistant/components/plum_lightpad/translations/ja.json +++ b/homeassistant/components/plum_lightpad/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" } } } diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index c2a9f098c33..2f77aacd5f7 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -1,10 +1,17 @@ { "config": { "abort": { - "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002" + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", + "no_flows": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" }, "error": { - "follow_link": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "follow_link": "\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u524d\u306b\u3001\u4e8b\u524d\u306b\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, "step": { "auth": { @@ -14,7 +21,9 @@ "user": { "data": { "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" - } + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } } diff --git a/homeassistant/components/poolsense/translations/ja.json b/homeassistant/components/poolsense/translations/ja.json index 896966aee6c..4b13a4f4c31 100644 --- a/homeassistant/components/poolsense/translations/ja.json +++ b/homeassistant/components/poolsense/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index e0f13949a5d..a27ebbb8072 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{ip_address}", "step": { "user": { diff --git a/homeassistant/components/prosegur/translations/ja.json b/homeassistant/components/prosegur/translations/ja.json index faa9fb6ea46..76dfa59ac95 100644 --- a/homeassistant/components/prosegur/translations/ja.json +++ b/homeassistant/components/prosegur/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index f774ad7d667..23a04b45bd8 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "credential_error": "\u8cc7\u683c\u60c5\u5831\u306e\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "port_987_bind_error": "\u30dd\u30fc\u30c8 987\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, @@ -17,6 +18,7 @@ "link": { "data": { "code": "PIN\u30b3\u30fc\u30c9", + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "name": "\u540d\u524d", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 6ef715d02c8..431fa71a74d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -13,7 +13,8 @@ "step": { "init": { "data": { - "power": "\u5951\u7d04\u96fb\u529b (kW)" + "power": "\u5951\u7d04\u96fb\u529b (kW)", + "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" }, "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/rachio/translations/ja.json b/homeassistant/components/rachio/translations/ja.json index c2ff6bbb145..46ff1326f82 100644 --- a/homeassistant/components/rachio/translations/ja.json +++ b/homeassistant/components/rachio/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rainforest_eagle/translations/ja.json b/homeassistant/components/rainforest_eagle/translations/ja.json index 0ddb56bb22a..e3ed5f9cff5 100644 --- a/homeassistant/components/rainforest_eagle/translations/ja.json +++ b/homeassistant/components/rainforest_eagle/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ja.json b/homeassistant/components/rainmachine/translations/ja.json index e5c92cd4a8f..1383afca891 100644 --- a/homeassistant/components/rainmachine/translations/ja.json +++ b/homeassistant/components/rainmachine/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{ip}", "step": { "user": { diff --git a/homeassistant/components/rdw/translations/ja.json b/homeassistant/components/rdw/translations/ja.json index 7114ff7d040..9909b76e8b7 100644 --- a/homeassistant/components/rdw/translations/ja.json +++ b/homeassistant/components/rdw/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "unknown_license_plate": "\u4e0d\u660e\u306a\u30e9\u30a4\u30bb\u30f3\u30b9\u30d7\u30ec\u30fc\u30c8" }, "step": { diff --git a/homeassistant/components/recollect_waste/translations/ja.json b/homeassistant/components/recollect_waste/translations/ja.json new file mode 100644 index 00000000000..f2d82cfe432 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_place_or_service_id": "\u7121\u52b9\u306a\u30d7\u30ec\u30a4\u30b9ID\u3001\u307e\u305f\u306f\u30b5\u30fc\u30d3\u30b9ID" + }, + "step": { + "user": { + "data": { + "place_id": "\u30d7\u30ec\u30a4\u30b9ID", + "service_id": "\u30b5\u30fc\u30d3\u30b9ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 4fc5fcc136d..743dfa36fb5 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "kamereon_no_account": "Kamereon\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "kamereon_no_account": "Kamereon\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_credentials": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "kamereon": { @@ -14,7 +19,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/id.json b/homeassistant/components/ridwell/translations/id.json index d19dabb2356..a3e90a11d51 100644 --- a/homeassistant/components/ridwell/translations/id.json +++ b/homeassistant/components/ridwell/translations/id.json @@ -19,7 +19,8 @@ "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Masukkan nama pengguna dan kata sandi Anda:" } } } diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json index c12ffee267d..33d7e12ebd4 100644 --- a/homeassistant/components/ridwell/translations/ja.json +++ b/homeassistant/components/ridwell/translations/ja.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ring/translations/ja.json b/homeassistant/components/ring/translations/ja.json index 513210e74b8..55dc9328e04 100644 --- a/homeassistant/components/ring/translations/ja.json +++ b/homeassistant/components/ring/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "2fa": { "data": { diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index 03d5baf2071..a20b33acd76 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "pin": "PIN\u30b3\u30fc\u30c9" + "pin": "PIN\u30b3\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/ja.json b/homeassistant/components/rituals_perfume_genie/translations/ja.json index 3cc00df0e04..ff8d819d5f0 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/ja.json +++ b/homeassistant/components/rituals_perfume_genie/translations/ja.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "title": "Rituals\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u63a5\u7d9a" diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index 02452958bdb..d54199c4691 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + }, "step": { "discovery_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index 7307245a9a8..2d1b41b1309 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "not_irobot_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fiRobot\u793e\u306e\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" + }, "flow_title": "{name} ({host})", "step": { "init": { "data": { "host": "\u30db\u30b9\u30c8" }, + "description": "\u30eb\u30f3\u30d0\u307e\u305f\u306f\u30d6\u30e9\u30fc\u30d0\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" }, "link": { @@ -21,8 +27,11 @@ }, "manual": { "data": { + "blid": "BLID", "host": "\u30db\u30b9\u30c8" - } + }, + "description": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u3001\u30eb\u30f3\u30d0\u3084\u30d6\u30e9\u30fc\u30d0\u304c\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "title": "\u30c7\u30d0\u30a4\u30b9\u306b\u624b\u52d5\u3067\u63a5\u7d9a\u3059\u308b" }, "user": { "data": { diff --git a/homeassistant/components/ruckus_unleashed/translations/ja.json b/homeassistant/components/ruckus_unleashed/translations/ja.json index 9f68231f0d2..2981d97e3c6 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ja.json +++ b/homeassistant/components/ruckus_unleashed/translations/ja.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index ff53e1b3cd1..75c6d8c2436 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -1,12 +1,23 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", + "id_missing": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002", - "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002" }, "flow_title": "{device}", "step": { + "confirm": { + "title": "Samsung TV" + }, "reauth_confirm": { "description": "\u9001\u4fe1(submit)\u3001\u8a8d\u8a3c\u3092\u8981\u6c42\u3059\u308b {device} \u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u300130\u79d2\u4ee5\u5185\u306b\u53d7\u3051\u5165\u308c\u307e\u3059\u3002" }, diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index 01672ec18e6..2dc4cd3354a 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "gateway_entry": { diff --git a/homeassistant/components/sense/translations/id.json b/homeassistant/components/sense/translations/id.json index 8d0d996e510..6767f1a54ca 100644 --- a/homeassistant/components/sense/translations/id.json +++ b/homeassistant/components/sense/translations/id.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Email", - "password": "Kata Sandi" + "password": "Kata Sandi", + "timeout": "Tenggang waktu" }, "title": "Hubungkan ke Sense Energy Monitor Anda" } diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 6099051cbae..e5df8e709bf 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 9c38c87bf66..9d002ebe8af 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,6 +1,8 @@ { "device_automation": { "condition_type": { + "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_frequency": "\u73fe\u5728\u306e {entity_name} \u983b\u5ea6(frequency)", "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -15,6 +17,8 @@ "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb" }, "trigger_type": { + "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", + "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "frequency": "{entity_name} \u983b\u5ea6(frequency)\u306e\u5909\u66f4", "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", diff --git a/homeassistant/components/sharkiq/translations/ja.json b/homeassistant/components/sharkiq/translations/ja.json index 0c16328bbfd..a68aa531e29 100644 --- a/homeassistant/components/sharkiq/translations/ja.json +++ b/homeassistant/components/sharkiq/translations/ja.json @@ -3,12 +3,14 @@ "step": { "reauth": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 3d020eb2865..bb77833d2a8 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -7,7 +7,8 @@ }, "credentials": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { @@ -19,15 +20,24 @@ }, "device_automation": { "trigger_subtype": { + "button": "\u30dc\u30bf\u30f3", + "button1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" }, "trigger_type": { "btn_down": "{subtype} button down", "btn_up": "{subtype} button up", + "double": "{subtype} \u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "double_push": "{subtype} double push", "long": "{subtype} \u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af", "long_push": "{subtype} long push", - "single_push": "{subtype} single push" + "long_single": "{subtype} \u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089\u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af", + "single": "{subtype} \u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af", + "single_long": "{subtype} \u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089\u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af", + "single_push": "{subtype} single push", + "triple": "{subtype} 3\u56de\u30af\u30ea\u30c3\u30af" } } } \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/ja.json b/homeassistant/components/shopping_list/translations/ja.json index 56b6469f63a..e48b00fd05b 100644 --- a/homeassistant/components/shopping_list/translations/ja.json +++ b/homeassistant/components/shopping_list/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "title": "\u30b7\u30e7\u30c3\u30d4\u30f3\u30b0\u30ea\u30b9\u30c8" diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index 7f253907705..796b0964b9b 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -1,12 +1,23 @@ { "config": { "error": { + "invalid_account_format": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u300116\u9032\u6570\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u30020\u301c9\u3068A\uff5eF\u306e\u307f\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_account_length": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u306f\u30013\uff5e16\u6587\u5b57\u304c\u5fc5\u8981\u306a\u306e\u3067\u9069\u5207\u306a\u9577\u3055\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "invalid_key_format": "\u30ad\u30fc\u304c\u300116\u9032\u6570\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u30020\u301c9\u3068A\uff5eF\u306e\u307f\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_key_length": "\u30ad\u30fc\u306e\u9577\u3055\u304c\u9069\u5207\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u300216\u300124\u3001\u307e\u305f\u306f32\u306e16\u9032\u6570\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "invalid_ping": "ping\u9593\u9694\u306f1\u301c1440\u5206\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "invalid_ping": "ping\u9593\u9694\u306f1\u301c1440\u5206\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "invalid_zones": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u30be\u30fc\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "additional_account": { + "data": { + "account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "additional_account": "\u8ffd\u52a0\u306e\u30a2\u30ab\u30a6\u30f3\u30c8", + "encryption_key": "\u6697\u53f7\u5316\u30ad\u30fc", + "ping_interval": "Ping\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u5206)", + "zones": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30be\u30fc\u30f3\u6570" + }, "title": "\u73fe\u5728\u306e\u30dd\u30fc\u30c8\u306b\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" }, "user": { @@ -26,7 +37,8 @@ "step": { "options": { "data": { - "ignore_timestamps": "SIA\u30a4\u30d9\u30f3\u30c8\u306e\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7 \u30c1\u30a7\u30c3\u30af\u3092\u7121\u8996\u3059\u308b" + "ignore_timestamps": "SIA\u30a4\u30d9\u30f3\u30c8\u306e\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7 \u30c1\u30a7\u30c3\u30af\u3092\u7121\u8996\u3059\u308b", + "zones": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30be\u30fc\u30f3\u6570" }, "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a: {account}", "title": "SIA\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3002" diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index c9ff0f96bb9..910798405a6 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -11,6 +11,12 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Kode Otorisasi" + }, + "title": "Selesaikan Otorisasi" + }, "mfa": { "description": "Periksa email Anda untuk mendapatkan tautan dari SimpliSafe. Setelah memverifikasi tautan, kembali ke sini untuk menyelesaikan instalasi integrasi.", "title": "Autentikasi Multi-Faktor SimpliSafe" diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 30405f4a319..c71fc1261ed 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -25,7 +25,8 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" }, "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json index cd624e3113f..2e9b67ef068 100644 --- a/homeassistant/components/sma/translations/ja.json +++ b/homeassistant/components/sma/translations/ja.json @@ -1,14 +1,23 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + }, "error": { - "cannot_retrieve_device_info": "\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_retrieve_device_info": "\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "group": "\u30b0\u30eb\u30fc\u30d7", "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "description": "SMA\u30c7\u30d0\u30a4\u30b9\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "SMA Solar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/smart_meter_texas/translations/ja.json b/homeassistant/components/smart_meter_texas/translations/ja.json index 896966aee6c..38abb3ce5b6 100644 --- a/homeassistant/components/smart_meter_texas/translations/ja.json +++ b/homeassistant/components/smart_meter_texas/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json index 896966aee6c..4b13a4f4c31 100644 --- a/homeassistant/components/smarthab/translations/ja.json +++ b/homeassistant/components/smarthab/translations/ja.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index cdf81a2bd91..dfe2ef1ab26 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { + "reauth_confirm": { + "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "SmartTub\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/smhi/translations/ja.json b/homeassistant/components/smhi/translations/ja.json index ceabfe8d7a6..31cf832abc6 100644 --- a/homeassistant/components/smhi/translations/ja.json +++ b/homeassistant/components/smhi/translations/ja.json @@ -7,6 +7,8 @@ "step": { "user": { "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "title": "\u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u3067\u306e\u4f4d\u7f6e" diff --git a/homeassistant/components/solarlog/translations/ja.json b/homeassistant/components/solarlog/translations/ja.json index a42202307f2..8275fdf956b 100644 --- a/homeassistant/components/solarlog/translations/ja.json +++ b/homeassistant/components/solarlog/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 3797126ff71..fa1507b91a1 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/somfy/translations/ja.json b/homeassistant/components/somfy/translations/ja.json new file mode 100644 index 00000000000..6aeb517fc16 --- /dev/null +++ b/homeassistant/components/somfy/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index b0c2f760a12..a5a69b2482c 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -1,29 +1,53 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{mac} ({ip})", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" - } + "port": "\u30dd\u30fc\u30c8", + "system_id": "\u30b7\u30b9\u30c6\u30e0ID" + }, + "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" } } }, "options": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "entity_config": { "data": { "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" }, + "description": "{{entity_id}} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a", "title": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u8a2d\u5b9a" }, + "init": { + "data": { + "default_reverse": "\u672a\u8a2d\u5b9a\u306e\u30ab\u30d0\u30fc\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u53cd\u8ee2\u72b6\u614b", + "entity_id": "\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "target_id": "\u30ab\u30d0\u30fc\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + }, + "title": "MyLink\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + }, "target_config": { "data": { "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" }, + "description": "`{target_name}` \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b", "title": "MyLink\u30ab\u30d0\u30fc\u306e\u8a2d\u5b9a" } } - } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json index 390f487e69e..009f19d22b0 100644 --- a/homeassistant/components/sonos/translations/ja.json +++ b/homeassistant/components/sonos/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fSonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fSonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 7ad990022dc..5e048f9ce9a 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "mijn.ithodaalderop.nl\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3" } diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index f81413cf319..de984e58986 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -5,6 +5,11 @@ }, "create_entry": { "default": "Spotify\u306e\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json index ea8623de542..f17835ca35a 100644 --- a/homeassistant/components/squeezebox/translations/ja.json +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -6,7 +6,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, "user": { diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index a4fccf05982..87d3ff85853 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID\u306f9\u6841\u306e\u6570\u5b57\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } diff --git a/homeassistant/components/stookalert/translations/ja.json b/homeassistant/components/stookalert/translations/ja.json index 297ea3baa71..f8318373bb9 100644 --- a/homeassistant/components/stookalert/translations/ja.json +++ b/homeassistant/components/stookalert/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index d48e1770c74..5e90ff79ec7 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -1,20 +1,31 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "error": { "bad_pin_format": "PIN\u306f4\u6841\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "incorrect_pin": "PIN\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "incorrect_pin": "PIN\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "pin": { "data": { "pin": "PIN" }, + "description": "MySubaru PIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\n\u6ce8: \u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u8eca\u4e21\u306f\u3001\u540c\u3058PIN\u3092\u6301\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "Subaru Starlink\u306e\u8a2d\u5b9a" }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + "country": "\u56fd\u3092\u9078\u629e", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "MySubaru\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u6ce8: \u521d\u671f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u306f\u6700\u5927 30\u79d2\u304b\u304b\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059", + "title": "Subaru Starlink\u306e\u8a2d\u5b9a" } } }, diff --git a/homeassistant/components/surepetcare/translations/ja.json b/homeassistant/components/surepetcare/translations/ja.json index 38abb3ce5b6..580a8bb3a3d 100644 --- a/homeassistant/components/surepetcare/translations/ja.json +++ b/homeassistant/components/surepetcare/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index f74956203c0..a2a7a100286 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", - "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot" + "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/switcher_kis/translations/ja.json b/homeassistant/components/switcher_kis/translations/ja.json new file mode 100644 index 00000000000..d1234b69652 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index a34853576cf..ff05e237fca 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", "token": "\u30c8\u30fc\u30af\u30f3", - "url": "URL" + "url": "URL", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } } diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 0609bbeeb52..024a2b786ad 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "reconfigure_successful": "\u518d\u8a2d\u5b9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "step": { @@ -23,7 +24,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index d6bd56bb379..99274e33a4e 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name}", "step": { "authenticate": { diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index 794e300a2ed..b8780258287 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "auth": { "description": "TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f:\n1. \u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n2. Telldus Live\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\n3. **{app_name}** \u3092\u627f\u8a8d\u3057\u307e\u3059(**Yes(\u306f\u3044)**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059)\n4. \u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u3001**\u9001\u4fe1(submit)** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n [TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30ea\u30f3\u30af]({auth_url})", diff --git a/homeassistant/components/tile/translations/ja.json b/homeassistant/components/tile/translations/ja.json index 896966aee6c..bfe5b4dbc6c 100644 --- a/homeassistant/components/tile/translations/ja.json +++ b/homeassistant/components/tile/translations/ja.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" } } } diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 1413e073362..2690f8bfccb 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_locations": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u5229\u7528\u3067\u304d\u308b\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u305b\u3093\u3002TotalConnect\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + "no_locations": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u5229\u7528\u3067\u304d\u308b\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u305b\u3093\u3002TotalConnect\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "usercode": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u3053\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u306b\u306f\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059" @@ -9,13 +10,20 @@ "step": { "locations": { "data": { + "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", "usercode": "\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" }, "description": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u5834\u6240 {location_id} \u306b\u5165\u529b\u3057\u307e\u3059", "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" }, "reauth_confirm": { - "description": "Total Connect\u306f\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "description": "Total Connect\u306f\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } } } } diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 1ef29de844c..615c0047a4b 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name} {model} ({host})", "step": { "confirm": { diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index eed21e239b6..7f97d4c23f4 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json index df071d8e5f7..a7d809235c9 100644 --- a/homeassistant/components/tradfri/translations/ja.json +++ b/homeassistant/components/tradfri/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "cannot_authenticate": "\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306f\u3001Homekit\u306a\u3069\u306e\u4ed6\u306e\u30b5\u30fc\u30d0\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 4e455012a4a..3ead1384575 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 2ed28caa2b1..0fe60e18781 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -34,14 +34,23 @@ } }, "options": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "error": { + "dev_multi_type": "\u69cb\u6210\u3059\u308b\u8907\u6570\u306e\u9078\u629e\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u540c\u3058\u30bf\u30a4\u30d7\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "dev_not_config": "\u30c7\u30d0\u30a4\u30b9\u30bf\u30a4\u30d7\u304c\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093", "dev_not_found": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "step": { "device": { "data": { "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", + "curr_temp_divider": "\u73fe\u5728\u306e\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", + "set_temp_divided": "\u8a2d\u5b9a\u6e29\u5ea6\u30b3\u30de\u30f3\u30c9\u306b\u533a\u5207\u3089\u308c\u305f\u6e29\u5ea6\u5024\u3092\u4f7f\u7528", "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8", + "temp_divider": "\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", + "temp_step_override": "\u76ee\u6a19\u6e29\u5ea6\u30b9\u30c6\u30c3\u30d7", "unit_of_measurement": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d" }, "title": "Tuya\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 53dfe543506..4336bbc01e7 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", "title": "Twilio Webhook\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index 687cb7a55c3..9e407f5dba8 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "device_exists": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 2c1eac4bbdb..a5d84529d21 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "configuration_updated": "\u8a2d\u5b9a\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + "already_configured": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u30b5\u30a4\u30c8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059", + "configuration_updated": "\u8a2d\u5b9a\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "faulty_credentials": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown_client_mac": "\u305d\u306eMAC\u30a2\u30c9\u30ec\u30b9\u3067\u4f7f\u7528\u53ef\u80fd\u306a\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093" }, "flow_title": "{site} ({host})", "step": { @@ -14,7 +18,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "site": "\u30b5\u30a4\u30c8ID", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "title": "UniFi\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index e42ef641473..06b788b86d5 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "incomplete_discovery": "\u4e0d\u5b8c\u5168\u306a\u691c\u51fa" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "incomplete_discovery": "\u4e0d\u5b8c\u5168\u306a\u691c\u51fa", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 713e6f6f91f..11d79efa137 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -1,17 +1,24 @@ { "config": { "abort": { - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" }, - "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/venstar/translations/ja.json b/homeassistant/components/venstar/translations/ja.json index 4417972a338..b699fcb4c3a 100644 --- a/homeassistant/components/venstar/translations/ja.json +++ b/homeassistant/components/venstar/translations/ja.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "pin": "PIN\u30b3\u30fc\u30c9", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Venstar\u793e\u306e\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306b\u63a5\u7d9a" diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 52d2993104e..5b8df8badba 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "installation": { "data": { @@ -8,14 +16,14 @@ }, "reauth_confirm": { "data": { - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, "user": { "data": { "description": "Verisure My Pages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3059\u3002", - "email": "Email", + "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } } diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 8c2f5c25764..738654030ad 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -4,7 +4,7 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "Email" + "username": "E\u30e1\u30fc\u30eb" }, "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json deleted file mode 100644 index b6ce73e1c2b..00000000000 --- a/homeassistant/components/vicare/translations/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "flow_title": "{name}", - "step": { - "user": { - "data": { - "password": "Password", - "client_id": "API Key", - "username": "Username", - "heating_type": "Heating type" - }, - "description": "Setup ViCare to control your Viessmann device.\nMinimum needed: username, password, API key.", - "title": "Setup ViCare" - } - }, - "error": { - "invalid_auth": "Invalid authentication" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 2dbb5827e78..6c7a89257bd 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "error": { "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, @@ -17,6 +20,7 @@ }, "user": { "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "device_class": "\u30c7\u30d0\u30a4\u30b9\u30bf\u30a4\u30d7", "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" @@ -30,8 +34,10 @@ "init": { "data": { "apps_to_include_or_exclude": "\u542b\u3081\u308b\u30a2\u30d7\u30ea\u3001\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea", - "include_or_exclude": "\u30a2\u30d7\u30ea\u3092\u542b\u3081\u308b\u304b\u3001\u9664\u5916\u3057\u307e\u3059\u304b\uff1f" - } + "include_or_exclude": "\u30a2\u30d7\u30ea\u3092\u542b\u3081\u308b\u304b\u3001\u9664\u5916\u3057\u307e\u3059\u304b\uff1f", + "volume_step": "\u30dc\u30ea\u30e5\u30fc\u30e0 \u30b9\u30c6\u30c3\u30d7\u30b5\u30a4\u30ba" + }, + "description": "\u30b9\u30de\u30fc\u30c8\u30c6\u30ec\u30d3\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea\u3092\u9078\u629e\u3057\u3066\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3067\u304d\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/vlc_telnet/translations/ja.json b/homeassistant/components/vlc_telnet/translations/ja.json index a10999772e5..233a0dede69 100644 --- a/homeassistant/components/vlc_telnet/translations/ja.json +++ b/homeassistant/components/vlc_telnet/translations/ja.json @@ -1,5 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{host}", "step": { "hassio_confirm": { diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index 7e26b2dadfd..504b319f5b4 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { - "reauth_invalid": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u30aa\u30ea\u30b8\u30ca\u30eb\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "reauth_invalid": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u30aa\u30ea\u30b8\u30ca\u30eb\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/water_heater/translations/ja.json b/homeassistant/components/water_heater/translations/ja.json index bfbcb3fb697..5401cf7248f 100644 --- a/homeassistant/components/water_heater/translations/ja.json +++ b/homeassistant/components/water_heater/translations/ja.json @@ -8,7 +8,9 @@ "state": { "_": { "eco": "\u30a8\u30b3", + "electric": "\u30a8\u30ec\u30af\u30c8\u30ea\u30c3\u30af", "gas": "\u30ac\u30b9", + "heat_pump": "\u30d2\u30fc\u30c8\u30dd\u30f3\u30d7", "off": "\u30aa\u30d5", "performance": "\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9" } diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 24251e77a48..87b71e0675f 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -1,20 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "unknown_coordinates": "\u7def\u5ea6/\u7d4c\u5ea6\u306e\u30c7\u30fc\u30bf\u306a\u3057" }, "step": { "coordinates": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + }, "description": "\u76e3\u8996\u3059\u308b\u7def\u5ea6(latitude)\u3068\u7d4c\u5ea6(longitude )\u3092\u5165\u529b:" }, "location": { + "data": { + "location_type": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" + }, "description": "\u76e3\u8996\u3059\u308b\u5834\u6240\u3092\u9078\u629e:" }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 8612554fb0c..55122a8bff4 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json index 38abb3ce5b6..1988d82cf8c 100644 --- a/homeassistant/components/whirlpool/translations/ja.json +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index f35fecd232d..c60d8562235 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + }, "create_entry": { "default": "\u9078\u629e\u3057\u305fWithings\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f\u3002" }, "flow_title": "{profile}", "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, "profile": { "data": { "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d" diff --git a/homeassistant/components/wled/translations/select.bg.json b/homeassistant/components/wled/translations/select.bg.json new file mode 100644 index 00000000000..66b7b19e292 --- /dev/null +++ b/homeassistant/components/wled/translations/select.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "1": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.id.json b/homeassistant/components/wled/translations/select.id.json new file mode 100644 index 00000000000..00ddc2878ff --- /dev/null +++ b/homeassistant/components/wled/translations/select.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Mati", + "1": "Nyala", + "2": "Hingga perangkat dimulai ulang" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.no.json b/homeassistant/components/wled/translations/select.no.json new file mode 100644 index 00000000000..c093835a76d --- /dev/null +++ b/homeassistant/components/wled/translations/select.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Av", + "1": "P\u00e5", + "2": "Inntil enheten starter p\u00e5 nytt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.tr.json b/homeassistant/components/wled/translations/select.tr.json new file mode 100644 index 00000000000..5191153eee1 --- /dev/null +++ b/homeassistant/components/wled/translations/select.tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Kapal\u0131", + "1": "A\u00e7\u0131k", + "2": "Cihaz yeniden ba\u015flat\u0131lana kadar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 91c53d294f2..2b57152bebb 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -2,7 +2,8 @@ "config": { "abort": { "incomplete_info": "\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u305f\u3081\u306e\u60c5\u5831\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30db\u30b9\u30c8\u307e\u305f\u306f\u30c8\u30fc\u30af\u30f3\u304c\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "not_xiaomi_miio": "\u30c7\u30d0\u30a4\u30b9\u306f(\u307e\u3060) Xiaomi Miio\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + "not_xiaomi_miio": "\u30c7\u30d0\u30a4\u30b9\u306f(\u307e\u3060) Xiaomi Miio\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", @@ -21,6 +22,7 @@ "cloud_username": "\u30af\u30e9\u30a6\u30c9\u306e\u30e6\u30fc\u30b6\u30fc\u540d", "manual": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b(\u975e\u63a8\u5968)" }, + "description": "Xiaomi Miio cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002\u4f7f\u7528\u3059\u308b\u30af\u30e9\u30a6\u30c9\u30b5\u30fc\u30d0\u30fc\u306b\u3064\u3044\u3066\u306f\u3001https://www.openhab.org/addons/bindings/miio/#country-servers \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "connect": { @@ -58,7 +60,8 @@ "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "reauth_confirm": { - "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8cc7\u683c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8cc7\u683c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "select": { "data": { diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json index f17f9d146c7..7c85312543f 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ja.json +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/ja.json b/homeassistant/components/yamaha_musiccast/translations/ja.json index 647878af0dd..3ddd882479e 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ja.json +++ b/homeassistant/components/yamaha_musiccast/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "yxc_control_url_missing": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebURL\u306f\u3001ssdp\u306e\u8a18\u8ff0\u306b\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { @@ -8,6 +9,9 @@ }, "flow_title": "MusicCast: {name}", "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/youless/translations/ja.json b/homeassistant/components/youless/translations/ja.json index 5021773b5ad..2557c6ee9d6 100644 --- a/homeassistant/components/youless/translations/ja.json +++ b/homeassistant/components/youless/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index f906597a8cb..8fa2a910d5f 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -2,8 +2,12 @@ "config": { "abort": { "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "confirm": { diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index 003efcd2fcc..fe7ecaaca20 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -1,12 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" }, "step": { "user": { "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)" + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306f\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002\u65b0\u898f\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3059\u308b\u5834\u5408\u306f\u3001\u4ee3\u308f\u308a\u306bZ-Wave JS\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u69cb\u6210\u5909\u6570\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/docs/z-wave/installation/ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index abedcd5a7b1..81d41422fa9 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -6,12 +6,17 @@ "addon_install_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "addon_set_config_failed": "Z-Wave JS\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002", "not_zwave_device": "\u767a\u898b\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Z-Wave\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { "addon_start_failed": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", "progress": { @@ -25,7 +30,8 @@ "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", - "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" + "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, @@ -67,14 +73,17 @@ }, "condition_type": { "config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024", - "node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9" + "node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9", + "value": "Z-Wave\u5024\u306e\u73fe\u5728\u306e\u5024" }, "trigger_type": { "event.notification.entry_control": "\u30a8\u30f3\u30c8\u30ea\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", "event.notification.notification": "\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", + "event.value_notification.central_scene": "{subtype} \u306e\u30bb\u30f3\u30c8\u30e9\u30eb \u30b7\u30fc\u30f3 \u30a2\u30af\u30b7\u30e7\u30f3", "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", - "zwave_js.value_updated.config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u306e\u5909\u66f4" + "zwave_js.value_updated.config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u306e\u5909\u66f4", + "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u306e\u5909\u66f4" } }, "options": { @@ -84,10 +93,14 @@ "addon_install_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "addon_set_config_failed": "Z-Wave JS\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3057\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "progress": { "install_addon": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002", @@ -102,7 +115,8 @@ "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", - "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc" + "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", + "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" }, From 5550b5445b826b2bb093233ab8a84d0e0a905194 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 23 Nov 2021 01:26:37 +0100 Subject: [PATCH 0742/1452] Add Button platform to Nanoleaf (#60169) Co-authored-by: J. Nick Koston Co-authored-by: Franck Nijhof --- .coveragerc | 2 ++ homeassistant/components/nanoleaf/__init__.py | 7 ++-- homeassistant/components/nanoleaf/button.py | 36 +++++++++++++++++++ homeassistant/components/nanoleaf/entity.py | 22 ++++++++++++ homeassistant/components/nanoleaf/light.py | 25 +++++-------- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/nanoleaf/button.py create mode 100644 homeassistant/components/nanoleaf/entity.py diff --git a/.coveragerc b/.coveragerc index bb7e429fa12..fbb761eef1f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -698,6 +698,8 @@ omit = homeassistant/components/myq/light.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py + homeassistant/components/nanoleaf/button.py + homeassistant/components/nanoleaf/entity.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index c706f52035f..79bb2577bb7 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -9,6 +9,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +PLATFORMS = ["button", "light"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nanoleaf from a config entry.""" @@ -24,7 +26,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = nanoleaf - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py new file mode 100644 index 00000000000..d85e61fc09c --- /dev/null +++ b/homeassistant/components/nanoleaf/button.py @@ -0,0 +1,36 @@ +"""Support for Nanoleaf buttons.""" + +from aionanoleaf import Nanoleaf + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import NanoleafEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Nanoleaf button.""" + nanoleaf: Nanoleaf = hass.data[DOMAIN][entry.entry_id] + async_add_entities([NanoleafIdentifyButton(nanoleaf)]) + + +class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): + """Representation of a Nanoleaf identify button.""" + + def __init__(self, nanoleaf: Nanoleaf) -> None: + """Initialize the Nanoleaf button.""" + super().__init__(nanoleaf) + self._attr_unique_id = f"{nanoleaf.serial_no}_identify" + self._attr_name = f"Identify {nanoleaf.name}" + self._attr_icon = "mdi:magnify" + self._attr_entity_category = ENTITY_CATEGORY_CONFIG + + async def async_press(self) -> None: + """Identify the Nanoleaf.""" + await self._nanoleaf.identify() diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py new file mode 100644 index 00000000000..d181aeb12d8 --- /dev/null +++ b/homeassistant/components/nanoleaf/entity.py @@ -0,0 +1,22 @@ +"""Base class for Nanoleaf entity.""" + +from aionanoleaf import Nanoleaf + +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN + + +class NanoleafEntity(Entity): + """Representation of a Nanoleaf entity.""" + + def __init__(self, nanoleaf: Nanoleaf) -> None: + """Initialize an Nanoleaf entity.""" + self._nanoleaf = nanoleaf + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, nanoleaf.serial_no)}, + manufacturer=nanoleaf.manufacturer, + model=nanoleaf.model, + name=nanoleaf.name, + sw_version=nanoleaf.firmware_version, + ) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 6203431d609..e6a602b51f3 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -26,7 +26,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.color import ( @@ -35,6 +34,7 @@ from homeassistant.util.color import ( ) from .const import DOMAIN +from .entity import NanoleafEntity RESERVED_EFFECTS = ("*Solid*", "*Static*", "*Dynamic*") DEFAULT_NAME = "Nanoleaf" @@ -74,25 +74,16 @@ async def async_setup_entry( async_add_entities([NanoleafLight(nanoleaf)]) -class NanoleafLight(LightEntity): +class NanoleafLight(NanoleafEntity, LightEntity): """Representation of a Nanoleaf Light.""" def __init__(self, nanoleaf: Nanoleaf) -> None: - """Initialize an Nanoleaf light.""" - self._nanoleaf = nanoleaf - self._attr_unique_id = self._nanoleaf.serial_no - self._attr_name = self._nanoleaf.name - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._nanoleaf.serial_no)}, - manufacturer=self._nanoleaf.manufacturer, - model=self._nanoleaf.model, - name=self._nanoleaf.name, - sw_version=self._nanoleaf.firmware_version, - ) - self._attr_min_mireds = math.ceil( - 1000000 / self._nanoleaf.color_temperature_max - ) - self._attr_max_mireds = kelvin_to_mired(self._nanoleaf.color_temperature_min) + """Initialize the Nanoleaf light.""" + super().__init__(nanoleaf) + self._attr_unique_id = nanoleaf.serial_no + self._attr_name = nanoleaf.name + self._attr_min_mireds = math.ceil(1000000 / nanoleaf.color_temperature_max) + self._attr_max_mireds = kelvin_to_mired(nanoleaf.color_temperature_min) @property def brightness(self) -> int: From cb3b19b0002e2aeb72738c90fd495cfaf5a3c5f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Nov 2021 18:52:06 -0600 Subject: [PATCH 0743/1452] Add support for adjusting flux_led effects speed (#59679) --- homeassistant/components/flux_led/__init__.py | 5 +- homeassistant/components/flux_led/const.py | 26 ++ homeassistant/components/flux_led/entity.py | 46 ++-- homeassistant/components/flux_led/light.py | 42 +--- .../components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/number.py | 79 ++++++ homeassistant/components/flux_led/switch.py | 4 +- homeassistant/components/flux_led/util.py | 27 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 13 + tests/components/flux_led/test_number.py | 227 ++++++++++++++++++ 12 files changed, 414 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/flux_led/number.py create mode 100644 homeassistant/components/flux_led/util.py create mode 100644 tests/components/flux_led/test_number.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 717a3c9e2b0..db73105e8ce 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -35,7 +35,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS_BY_TYPE: Final = {DeviceType.Bulb: ["light"], DeviceType.Switch: ["switch"]} +PLATFORMS_BY_TYPE: Final = { + DeviceType.Bulb: ["light", "number"], + DeviceType.Switch: ["switch"], +} DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index e7f9509c54b..1bc9bb6d227 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -4,8 +4,31 @@ import asyncio import socket from typing import Final +from flux_led.const import ( + COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, + COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, + COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, + COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, +) + +from homeassistant.components.light import ( + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, +) + DOMAIN: Final = "flux_led" + +FLUX_COLOR_MODE_TO_HASS: Final = { + FLUX_COLOR_MODE_RGB: COLOR_MODE_RGB, + FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, + FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, + FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, +} + + API: Final = "flux_api" SIGNAL_STATE_UPDATED = "flux_led_{}_state_updated" @@ -48,6 +71,9 @@ CONF_SPEED_PCT: Final = "speed_pct" CONF_TRANSITION: Final = "transition" +EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} + + CONF_CUSTOM_EFFECT_COLORS: Final = "custom_effect_colors" CONF_CUSTOM_EFFECT_SPEED_PCT: Final = "custom_effect_speed_pct" CONF_CUSTOM_EFFECT_TRANSITION: Final = "custom_effect_transition" diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 4183ccc14cd..bab9a24bfe5 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -42,32 +42,11 @@ class FluxEntity(CoordinatorEntity): sw_version=str(self._device.version_num), ) - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return cast(bool, self._device.is_on) - @property def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" return {"ip_address": self._device.ipaddr} - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the specified device on.""" - await self._async_turn_on(**kwargs) - self.async_write_ha_state() - await self.coordinator.async_request_refresh() - - @abstractmethod - async def _async_turn_on(self, **kwargs: Any) -> None: - """Turn the specified device on.""" - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the specified device off.""" - await self._device.async_turn_off() - self.async_write_ha_state() - await self.coordinator.async_request_refresh() - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -85,3 +64,28 @@ class FluxEntity(CoordinatorEntity): ) ) await super().async_added_to_hass() + + +class FluxOnOffEntity(FluxEntity): + """Representation of a Flux entity that supports on/off.""" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return cast(bool, self._device.is_on) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + await self._async_turn_on(**kwargs) + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @abstractmethod + async def _async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the specified device off.""" + await self._device.async_turn_off() + self.async_write_ha_state() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index fedf31743da..878c27aac8c 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -6,13 +6,6 @@ import logging import random from typing import Any, Final, cast -from flux_led.const import ( - COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, - COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, - COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, - COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, - COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, -) from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -33,7 +26,6 @@ from homeassistant.components.light import ( ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, - COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, @@ -78,6 +70,7 @@ from .const import ( CONF_TRANSITION, DEFAULT_EFFECT_SPEED, DOMAIN, + EFFECT_SUPPORT_MODES, FLUX_HOST, FLUX_LED_DISCOVERY, FLUX_MAC, @@ -89,22 +82,13 @@ from .const import ( TRANSITION_JUMP, TRANSITION_STROBE, ) -from .entity import FluxEntity +from .entity import FluxOnOffEntity +from .util import _flux_color_mode_to_hass, _hass_color_modes _LOGGER = logging.getLogger(__name__) SUPPORT_FLUX_LED: Final = SUPPORT_TRANSITION - -FLUX_COLOR_MODE_TO_HASS: Final = { - FLUX_COLOR_MODE_RGB: COLOR_MODE_RGB, - FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, - FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, - FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, -} - -EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} - # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 @@ -148,15 +132,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) -> str: - """Map the flux color mode to Home Assistant color mode.""" - if flux_color_mode == FLUX_COLOR_MODE_DIM: - if len(flux_color_modes) > 1: - return COLOR_MODE_WHITE - return COLOR_MODE_BRIGHTNESS - return FLUX_COLOR_MODE_TO_HASS.get(flux_color_mode, COLOR_MODE_ONOFF) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -242,7 +217,7 @@ async def async_setup_entry( ) -class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): +class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): """Representation of a Flux light.""" def __init__( @@ -261,10 +236,7 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): color_temperature_kelvin_to_mired(self._device.max_temp) + 1 ) # for rounding self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) - self._attr_supported_color_modes = { - _flux_color_mode_to_hass(mode, self._device.color_modes) - for mode in self._device.color_modes - } + self._attr_supported_color_modes = _hass_color_modes(self._device) if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): self._attr_supported_features |= SUPPORT_EFFECT self._attr_effect_list = [*self._device.effect_list, EFFECT_RANDOM] @@ -405,7 +377,9 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): self._custom_effect_transition, ) return - await self._device.async_set_effect(effect, DEFAULT_EFFECT_SPEED) + await self._device.async_set_effect( + effect, self._device.speed or DEFAULT_EFFECT_SPEED + ) return # Handle brightness adjustment in CCT Color Mode diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0b9b7f00e8c..d1af244ad83 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.24"], + "requirements": ["flux_led==0.24.27"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py new file mode 100644 index 00000000000..9ae581d8c88 --- /dev/null +++ b/homeassistant/components/flux_led/number.py @@ -0,0 +1,79 @@ +"""Support for LED numbers.""" +from __future__ import annotations + +from typing import cast + +from homeassistant import config_entries +from homeassistant.components.number import NumberEntity +from homeassistant.components.number.const import MODE_SLIDER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import FluxLedUpdateCoordinator +from .const import DOMAIN, EFFECT_SUPPORT_MODES +from .entity import FluxEntity +from .util import _hass_color_modes + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Flux lights.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + color_modes = _hass_color_modes(coordinator.device) + if not color_modes.intersection(EFFECT_SUPPORT_MODES): + return + + async_add_entities( + [ + FluxNumber( + coordinator, + entry.unique_id, + entry.data[CONF_NAME], + ) + ] + ) + + +class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): + """Defines a flux_led speed number.""" + + _attr_min_value = 1 + _attr_max_value = 100 + _attr_step = 1 + _attr_mode = MODE_SLIDER + _attr_icon = "mdi:speedometer" + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + ) -> None: + """Initialize the flux number.""" + super().__init__(coordinator, unique_id, name) + self._attr_name = f"{name} Effect Speed" + + @property + def value(self) -> float: + """Return the effect speed.""" + return cast(float, self._device.speed) + + async def async_set_value(self, value: float) -> None: + """Set the flux speed value.""" + current_effect = self._device.effect + new_speed = int(value) + if not current_effect: + raise HomeAssistantError( + "Speed can only be adjusted when an effect is active" + ) + if self._device.original_addressable and not self._device.is_on: + raise HomeAssistantError("Speed can only be adjusted when the light is on") + await self._device.async_set_effect(current_effect, new_speed) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 0ca7a771c78..d022acc1c74 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator from .const import DOMAIN -from .entity import FluxEntity +from .entity import FluxOnOffEntity async def async_setup_entry( @@ -33,7 +33,7 @@ async def async_setup_entry( ) -class FluxSwitch(FluxEntity, CoordinatorEntity, SwitchEntity): +class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity): """Representation of a Flux switch.""" async def _async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py new file mode 100644 index 00000000000..a564cb81bd0 --- /dev/null +++ b/homeassistant/components/flux_led/util.py @@ -0,0 +1,27 @@ +"""Utils for FluxLED/MagicHome.""" +from __future__ import annotations + +from flux_led.aio import AIOWifiLedBulb +from flux_led.const import COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM + +from homeassistant.components.light import ( + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, + COLOR_MODE_WHITE, +) + +from .const import FLUX_COLOR_MODE_TO_HASS + + +def _hass_color_modes(device: AIOWifiLedBulb) -> set[str]: + color_modes = device.color_modes + return {_flux_color_mode_to_hass(mode, color_modes) for mode in color_modes} + + +def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) -> str: + """Map the flux color mode to Home Assistant color mode.""" + if flux_color_mode == FLUX_COLOR_MODE_DIM: + if len(flux_color_modes) > 1: + return COLOR_MODE_WHITE + return COLOR_MODE_BRIGHTNESS + return FLUX_COLOR_MODE_TO_HASS.get(flux_color_mode, COLOR_MODE_ONOFF) diff --git a/requirements_all.txt b/requirements_all.txt index 5a6f14b3726..c337ff932eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.24 +flux_led==0.24.27 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 313ae45a336..8e93e6873f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.24 +flux_led==0.24.27 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 19e11e6085c..c04189312af 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -67,8 +67,11 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.brightness = 128 bulb.model_num = 0x35 bulb.effect = None + bulb.speed = 50 bulb.model = "Smart Bulb (0x35)" bulb.version_num = 8 + bulb.original_addressable = False + bulb.addressable = False bulb.rgbwcapable = True bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB @@ -115,6 +118,16 @@ async def async_mock_device_turn_on(hass: HomeAssistant, bulb: AIOWifiLedBulb) - await hass.async_block_till_done() +async def async_mock_effect_speed( + hass: HomeAssistant, bulb: AIOWifiLedBulb, effect: str, speed: int +) -> None: + """Mock the device being on with an effect.""" + bulb.speed = speed + bulb.effect = effect + bulb.data_receive_callback() + await hass.async_block_till_done() + + def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py new file mode 100644 index 00000000000..6e7f9e60de6 --- /dev/null +++ b/tests/components/flux_led/test_number.py @@ -0,0 +1,227 @@ +"""Tests for the flux_led number platform.""" + + +from flux_led.const import COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB +import pytest + +from homeassistant.components import flux_led +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_bulb, + _patch_discovery, + _patch_wifibulb, + async_mock_device_turn_off, + async_mock_device_turn_on, + async_mock_effect_speed, +) + +from tests.common import MockConfigEntry + + +async def test_number_unique_id(hass: HomeAssistant) -> None: + """Test a number unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.az120444_aabbccddeeff_effect_speed" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS + + +async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: + """Test an rgb light with an effect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + await async_mock_device_turn_on(hass, bulb) + + light_entity_id = "light.az120444_aabbccddeeff" + number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + + state = hass.states.get(light_entity_id) + assert state.state == STATE_ON + + bulb.effect = "colorloop" + bulb.speed = 50 + await async_mock_device_turn_off(hass, bulb) + state = hass.states.get(number_entity_id) + assert state.state == "50" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("colorloop", 100) + bulb.async_set_effect.reset_mock() + + await async_mock_effect_speed(hass, bulb, "red_fade", 50) + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 50}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("red_fade", 50) + bulb.async_set_effect.reset_mock() + + state = hass.states.get(number_entity_id) + assert state.state == "50" + + +async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> None: + """Test an original addressable light with an effect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.original_addressable = True + bulb.raw_state = bulb.raw_state._replace( + model_num=0xA1 + ) # Original addressable model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + bulb.effect = "7 colors change gradually" + bulb.speed = 50 + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + await async_mock_device_turn_on(hass, bulb) + + light_entity_id = "light.az120444_aabbccddeeff" + number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + + state = hass.states.get(light_entity_id) + assert state.state == STATE_ON + + state = hass.states.get(number_entity_id) + assert state.state == "50" + + await async_mock_device_turn_off(hass, bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + + await async_mock_device_turn_on(hass, bulb) + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("7 colors change gradually", 100) + bulb.async_set_effect.reset_mock() + await async_mock_effect_speed(hass, bulb, "7 colors run in olivary", 100) + + state = hass.states.get(number_entity_id) + assert state.state == "100" + + +async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: + """Test an addressable light with an effect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.addressable = True + bulb.raw_state = bulb.raw_state._replace( + model_num=0xA2 + ) # Original addressable model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + bulb.effect = "RBM 1" + bulb.speed = 50 + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + await async_mock_device_turn_on(hass, bulb) + + light_entity_id = "light.az120444_aabbccddeeff" + number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + + state = hass.states.get(light_entity_id) + assert state.state == STATE_ON + + state = hass.states.get(number_entity_id) + assert state.state == "50" + + await async_mock_device_turn_off(hass, bulb) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("RBM 1", 100) + bulb.async_set_effect.reset_mock() + + await async_mock_device_turn_on(hass, bulb) + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("RBM 1", 100) + bulb.async_set_effect.reset_mock() + await async_mock_effect_speed(hass, bulb, "RBM 2", 100) + + state = hass.states.get(number_entity_id) + assert state.state == "100" From 4af5cde73862e961f82811e1a4314f0957c81fa3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Nov 2021 20:21:46 -0600 Subject: [PATCH 0744/1452] Include model name in flux_led discovery when available (#60105) --- homeassistant/components/flux_led/__init__.py | 23 +++-- .../components/flux_led/config_flow.py | 53 +++++++----- homeassistant/components/flux_led/const.py | 4 - homeassistant/components/flux_led/light.py | 5 +- .../components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 23 ++++- tests/components/flux_led/test_config_flow.py | 86 ++++++++++++++++--- tests/components/flux_led/test_init.py | 19 +++- tests/components/flux_led/test_light.py | 26 +++--- tests/components/flux_led/test_number.py | 14 +-- tests/components/flux_led/test_switch.py | 2 +- 13 files changed, 184 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index db73105e8ce..b82fbe97913 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -3,11 +3,12 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Final +from typing import Any, Final, cast from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.aioscanner import AIOBulbScanner +from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry @@ -24,11 +25,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, - FLUX_HOST, FLUX_LED_DISCOVERY, FLUX_LED_EXCEPTIONS, - FLUX_MAC, - FLUX_MODEL, SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, ) @@ -49,17 +47,28 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: return AIOWifiLedBulb(host) +@callback +def async_name_from_discovery(device: dict[str, Any]) -> str: + """Convert a flux_led discovery to a human readable name.""" + if (mac := device.get(ATTR_ID)) is None: + return cast(str, device[ATTR_IPADDR]) + short_mac = mac[-6:] + if device.get(ATTR_MODEL_DESCRIPTION): + return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" + return f"{device[ATTR_MODEL]} {short_mac}" + + @callback def async_update_entry_from_discovery( hass: HomeAssistant, entry: config_entries.ConfigEntry, device: dict[str, Any] ) -> None: """Update a config entry from a flux_led discovery.""" - name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + name = async_name_from_discovery(device) hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_NAME: name}, title=name, - unique_id=dr.format_mac(device[FLUX_MAC]), + unique_id=dr.format_mac(device[ATTR_ID]), ) @@ -86,7 +95,7 @@ async def async_discover_device( # If we are missing the unique_id we should be able to fetch it # from the device by doing a directed discovery at the host only for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): - if device[FLUX_HOST] == host: + if device[ATTR_IPADDR] == host: return device return None diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 0a059abaf34..f33a623faa2 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any, Final +from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION import voluptuous as vol from homeassistant import config_entries @@ -17,6 +18,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import ( async_discover_device, async_discover_devices, + async_name_from_discovery, async_update_entry_from_discovery, async_wifi_bulb_for_host, ) @@ -27,10 +29,7 @@ from .const import ( DEFAULT_EFFECT_SPEED, DISCOVER_SCAN_TIMEOUT, DOMAIN, - FLUX_HOST, FLUX_LED_EXCEPTIONS, - FLUX_MAC, - FLUX_MODEL, TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE, @@ -38,6 +37,7 @@ from .const import ( CONF_DEVICE: Final = "device" + _LOGGER = logging.getLogger(__name__) @@ -85,9 +85,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" self._discovered_device = { - FLUX_HOST: discovery_info[dhcp.IP_ADDRESS], - FLUX_MODEL: discovery_info[dhcp.HOSTNAME], - FLUX_MAC: discovery_info[dhcp.MAC_ADDRESS].replace(":", ""), + ATTR_IPADDR: discovery_info[dhcp.IP_ADDRESS], + ATTR_MODEL: discovery_info[dhcp.HOSTNAME], + ATTR_ID: discovery_info[dhcp.MAC_ADDRESS].replace(":", ""), } return await self._async_handle_discovery() @@ -101,8 +101,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device - mac = dr.format_mac(device[FLUX_MAC]) - host = device[FLUX_HOST] + mac = dr.format_mac(device[ATTR_ID]) + host = device[ATTR_IPADDR] await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): @@ -113,6 +113,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == host: return self.async_abort(reason="already_in_progress") + if not device.get(ATTR_MODEL_DESCRIPTION): + try: + device = await self._async_try_connect(host) + except FLUX_LED_EXCEPTIONS: + return self.async_abort(reason="cannot_connect") + else: + if device.get(ATTR_MODEL_DESCRIPTION): + self._discovered_device = device return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( @@ -123,7 +131,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._async_create_entry_from_device(self._discovered_device) self._set_confirm_only() - placeholders = self._discovered_device + device = self._discovered_device + placeholders = { + "model": device.get(ATTR_MODEL_DESCRIPTION, device[ATTR_MODEL]), + "id": device[ATTR_ID][-6:], + "ipaddr": device[ATTR_IPADDR], + } self.context["title_placeholders"] = placeholders return self.async_show_form( step_id="discovery_confirm", description_placeholders=placeholders @@ -132,15 +145,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_create_entry_from_device(self, device: dict[str, Any]) -> FlowResult: """Create a config entry from a device.""" - self._async_abort_entries_match({CONF_HOST: device[FLUX_HOST]}) - if device.get(FLUX_MAC): - name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" - else: - name = device[FLUX_HOST] + self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]}) + name = async_name_from_discovery(device) return self.async_create_entry( title=name, data={ - CONF_HOST: device[FLUX_HOST], + CONF_HOST: device[ATTR_IPADDR], CONF_NAME: name, }, ) @@ -158,9 +168,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except FLUX_LED_EXCEPTIONS: errors["base"] = "cannot_connect" else: - if device[FLUX_MAC]: + if device[ATTR_ID]: await self.async_set_unique_id( - dr.format_mac(device[FLUX_MAC]), raise_on_progress=False + dr.format_mac(device[ATTR_ID]), raise_on_progress=False ) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self._async_create_entry_from_device(device) @@ -189,12 +199,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, DISCOVER_SCAN_TIMEOUT ) self._discovered_devices = { - dr.format_mac(device[FLUX_MAC]): device for device in discovered_devices + dr.format_mac(device[ATTR_ID]): device for device in discovered_devices } devices_name = { - mac: f"{device[FLUX_MODEL]} {mac} ({device[FLUX_HOST]})" + mac: f"{async_name_from_discovery(device)} ({device[ATTR_IPADDR]})" for mac, device in self._discovered_devices.items() - if mac not in current_unique_ids and device[FLUX_HOST] not in current_hosts + if mac not in current_unique_ids + and device[ATTR_IPADDR] not in current_hosts } # Check if there is at least one device if not devices_name: @@ -214,7 +225,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await bulb.async_setup(lambda: None) finally: await bulb.async_stop() - return {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} + return {ATTR_ID: None, ATTR_MODEL: None, ATTR_IPADDR: host} class OptionsFlow(config_entries.OptionsFlow): diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 1bc9bb6d227..188967dfdb9 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -77,7 +77,3 @@ EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} CONF_CUSTOM_EFFECT_COLORS: Final = "custom_effect_colors" CONF_CUSTOM_EFFECT_SPEED_PCT: Final = "custom_effect_speed_pct" CONF_CUSTOM_EFFECT_TRANSITION: Final = "custom_effect_transition" - -FLUX_HOST: Final = "ipaddr" -FLUX_MAC: Final = "id" -FLUX_MODEL: Final = "model" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 878c27aac8c..8c6da0583b4 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -6,6 +6,7 @@ import logging import random from typing import Any, Final, cast +from flux_led.const import ATTR_ID, ATTR_IPADDR from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -71,9 +72,7 @@ from .const import ( DEFAULT_EFFECT_SPEED, DOMAIN, EFFECT_SUPPORT_MODES, - FLUX_HOST, FLUX_LED_DISCOVERY, - FLUX_MAC, MODE_AUTO, MODE_RGB, MODE_RGBW, @@ -141,7 +140,7 @@ async def async_setup_platform( """Set up the flux led platform.""" domain_data = hass.data[DOMAIN] discovered_mac_by_host = { - device[FLUX_HOST]: device[FLUX_MAC] + device[ATTR_IPADDR]: device[ATTR_ID] for device in domain_data[FLUX_LED_DISCOVERY] } for host, device_config in config.get(CONF_DEVICES, {}).items(): diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d1af244ad83..202a6bc1584 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.27"], + "requirements": ["flux_led==0.24.28"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index c337ff932eb..7f06f2f77c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.27 +flux_led==0.24.28 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e93e6873f0..4b33ed1a238 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.27 +flux_led==0.24.28 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index c04189312af..ea02529d83c 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -8,30 +8,47 @@ from unittest.mock import AsyncMock, MagicMock, patch from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.const import ( + ATTR_ID, + ATTR_IPADDR, + ATTR_MODEL, + ATTR_MODEL_DESCRIPTION, COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, ) from flux_led.protocol import LEDENETRawState from homeassistant.components import dhcp -from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL from homeassistant.core import HomeAssistant MODULE = "homeassistant.components.flux_led" MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" IP_ADDRESS = "127.0.0.1" MODEL = "AZ120444" +MODEL_DESCRIPTION = "RGBW Controller" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" FLUX_MAC_ADDRESS = "aabbccddeeff" +SHORT_MAC_ADDRESS = "ddeeff" + +DEFAULT_ENTRY_TITLE = f"{MODEL_DESCRIPTION} {SHORT_MAC_ADDRESS}" +DEFAULT_ENTRY_TITLE_PARTIAL = f"{MODEL} {SHORT_MAC_ADDRESS}" -DEFAULT_ENTRY_TITLE = f"{MODEL} {FLUX_MAC_ADDRESS}" DHCP_DISCOVERY = dhcp.DhcpServiceInfo( hostname=MODEL, ip=IP_ADDRESS, macaddress=MAC_ADDRESS, ) -FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS} +FLUX_DISCOVERY_PARTIAL = { + ATTR_IPADDR: IP_ADDRESS, + ATTR_MODEL: MODEL, + ATTR_ID: FLUX_MAC_ADDRESS, +} +FLUX_DISCOVERY = { + ATTR_IPADDR: IP_ADDRESS, + ATTR_MODEL: MODEL, + ATTR_ID: FLUX_MAC_ADDRESS, + ATTR_MODEL_DESCRIPTION: MODEL_DESCRIPTION, +} def _mocked_bulb() -> AIOWifiLedBulb: diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index b9518e35cc0..06b47dd2788 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -29,6 +29,7 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( DEFAULT_ENTRY_TITLE, + DEFAULT_ENTRY_TITLE_PARTIAL, DHCP_DISCOVERY, FLUX_DISCOVERY, IP_ADDRESS, @@ -350,19 +351,14 @@ async def test_discovered_by_discovery_and_dhcp(hass): assert result3["reason"] == "already_in_progress" -@pytest.mark.parametrize( - "source, data", - [ - (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), - (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), - ], -) -async def test_discovered_by_dhcp_or_discovery(hass, source, data): - """Test we can setup when discovered from dhcp or discovery.""" +async def test_discovered_by_discovery(hass): + """Test we can setup when discovered from discovery.""" with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source}, data=data + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=FLUX_DISCOVERY, ) await hass.async_block_till_done() @@ -383,6 +379,74 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): assert mock_async_setup_entry.called +async def test_discovered_by_dhcp_udp_responds(hass): + """Test we can setup when discovered from dhcp but with udp response.""" + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_wifibulb(), 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_NAME: DEFAULT_ENTRY_TITLE} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +async def test_discovered_by_dhcp_no_udp_response(hass): + """Test we can setup when discovered from dhcp but no udp response.""" + + with _patch_discovery(no_device=True), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(no_device=True), _patch_wifibulb(), 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_NAME: DEFAULT_ENTRY_TITLE_PARTIAL, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): + """Test we can setup when discovered from dhcp but no udp response or tcp response.""" + + with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + @pytest.mark.parametrize( "source, data", [ @@ -445,4 +509,4 @@ async def test_options(hass: HomeAssistant): assert result2["type"] == "create_entry" assert result2["data"] == user_input assert result2["data"] == config_entry.options - assert hass.states.get("light.az120444_aabbccddeeff") is not None + assert hass.states.get("light.rgbw_controller_ddeeff") is not None diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index db4ddffbc3f..abb671da9c2 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -3,6 +3,8 @@ from __future__ import annotations from unittest.mock import patch +import pytest + from homeassistant.components import flux_led from homeassistant.components.flux_led.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -13,7 +15,9 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, + DEFAULT_ENTRY_TITLE_PARTIAL, FLUX_DISCOVERY, + FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, _patch_discovery, @@ -67,8 +71,15 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: assert config_entry.state == ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize( + "discovery,title", + [ + (FLUX_DISCOVERY, DEFAULT_ENTRY_TITLE), + (FLUX_DISCOVERY_PARTIAL, DEFAULT_ENTRY_TITLE_PARTIAL), + ], +) async def test_config_entry_fills_unique_id_with_directed_discovery( - hass: HomeAssistant, + hass: HomeAssistant, discovery: dict[str, str], title: str ) -> None: """Test that the unique id is added if its missing via directed (not broadcast) discovery.""" config_entry = MockConfigEntry( @@ -78,7 +89,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( async def _discovery(self, *args, address=None, **kwargs): # Only return discovery results when doing directed discovery - return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + return [discovery] if address == IP_ADDRESS else [] with patch( "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery @@ -88,5 +99,5 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( assert config_entry.state == ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS - assert config_entry.data[CONF_NAME] == DEFAULT_ENTRY_TITLE - assert config_entry.title == DEFAULT_ENTRY_TITLE + assert config_entry.data[CONF_NAME] == title + assert config_entry.title == title diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index ce0320f7717..1a9ce57a4f7 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -85,7 +85,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS state = hass.states.get(entity_id) @@ -105,7 +105,7 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS state = hass.states.get(entity_id) @@ -137,7 +137,7 @@ async def test_light_no_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id) is None state = hass.states.get(entity_id) @@ -195,7 +195,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -301,7 +301,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -420,7 +420,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -522,7 +522,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -633,7 +633,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -762,7 +762,7 @@ async def test_white_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -817,7 +817,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -899,7 +899,7 @@ async def test_rgb_light_custom_effects_invalid_colors( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -928,7 +928,7 @@ async def test_rgb_light_custom_effect_via_service( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -1073,7 +1073,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.az120444_aabbccddeeff" + entity_id = "light.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 6e7f9e60de6..115414b8201 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -45,7 +45,7 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "number.az120444_aabbccddeeff_effect_speed" + entity_id = "number.rgbw_controller_ddeeff_effect_speed" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS @@ -70,8 +70,8 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.az120444_aabbccddeeff" - number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + light_entity_id = "light.rgbw_controller_ddeeff" + number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" with pytest.raises(HomeAssistantError): await hass.services.async_call( NUMBER_DOMAIN, @@ -135,8 +135,8 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.az120444_aabbccddeeff" - number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + light_entity_id = "light.rgbw_controller_ddeeff" + number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" state = hass.states.get(light_entity_id) assert state.state == STATE_ON @@ -192,8 +192,8 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.az120444_aabbccddeeff" - number_entity_id = "number.az120444_aabbccddeeff_effect_speed" + light_entity_id = "light.rgbw_controller_ddeeff" + number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" state = hass.states.get(light_entity_id) assert state.state == STATE_ON diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index e41a10807c7..b3f27c28b5e 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -39,7 +39,7 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "switch.az120444_aabbccddeeff" + entity_id = "switch.rgbw_controller_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON From 4ff3b2e9a9bbca91254ce73def36cf7f91633469 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 22 Nov 2021 19:07:27 -0800 Subject: [PATCH 0745/1452] Bump frontend to 20211123.0 (#60184) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d662d3300ab..e507def0078 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211117.0" + "home-assistant-frontend==20211123.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4c6356fbe4..8774bdd947a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211117.0 +home-assistant-frontend==20211123.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7f06f2f77c0..4165f857b97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211117.0 +home-assistant-frontend==20211123.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b33ed1a238..2ab1792c8ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211117.0 +home-assistant-frontend==20211123.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0e4de4253971408f073cbad03983a7c3cba29069 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 22 Nov 2021 20:47:01 -0700 Subject: [PATCH 0746/1452] Alter RainMachine to enable/disable program/zones via separate switches (#59617) --- .../components/rainmachine/__init__.py | 45 ++- .../components/rainmachine/config_flow.py | 2 +- .../components/rainmachine/switch.py | 341 ++++++++++-------- .../rainmachine/test_config_flow.py | 65 +++- 4 files changed, 294 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 786895dc99b..76a02fa94a8 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -21,8 +21,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, config_validation as cv -import homeassistant.helpers.device_registry as dr +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -323,6 +327,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate an old config entry.""" + version = entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Update unique IDs to be consistent across platform (including removing + # the silly removal of colons in the MAC address that was added originally): + if version == 1: + version = entry.version = 2 + + ent_reg = er.async_get(hass) + for entity_entry in [ + e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id + ]: + unique_id_pieces = entity_entry.unique_id.split("_") + old_mac = unique_id_pieces[0] + new_mac = ":".join(old_mac[i : i + 2] for i in range(0, len(old_mac), 2)) + unique_id_pieces[0] = new_mac + + if entity_entry.entity_id.startswith("switch"): + unique_id_pieces[1] = unique_id_pieces[1][11:].lower() + + ent_reg.async_update_entity( + entity_entry.entity_id, new_unique_id="_".join(unique_id_pieces) + ) + + LOGGER.info("Migration to version %s successful", version) + + return True + + async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(entry.entry_id) @@ -355,10 +391,7 @@ class RainMachineEntity(CoordinatorEntity): ) self._attr_extra_state_attributes = {} self._attr_name = f"{controller.name} {description.name}" - # The colons are removed from the device MAC simply because that value - # (unnecessarily) makes up the existing unique ID formula and we want to avoid - # a breaking change: - self._attr_unique_id = f"{controller.mac.replace(':', '')}_{description.key}" + self._attr_unique_id = f"{controller.mac}_{description.key}" self._controller = controller self.entity_description = description diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index fc7c594f677..6b11e82d023 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -42,7 +42,7 @@ async def async_get_controller( class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" - VERSION = 1 + VERSION = 2 discovered_ip_address: str | None = None diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 345da380316..ab39ca1a669 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -1,6 +1,7 @@ """This component provides support for RainMachine programs and zones.""" from __future__ import annotations +import asyncio from collections.abc import Coroutine from dataclasses import dataclass from datetime import datetime @@ -12,8 +13,9 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ID +from homeassistant.const import ATTR_ID, ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -51,8 +53,6 @@ ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" -DEFAULT_ICON = "mdi:water" - DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} @@ -130,10 +130,6 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() for service_name, schema, method in ( - ("disable_program", {}, "async_disable_program"), - ("disable_zone", {}, "async_disable_zone"), - ("enable_program", {}, "async_enable_program"), - ("enable_zone", {}, "async_enable_zone"), ("start_program", {}, "async_start_program"), ( "start_zone", @@ -149,44 +145,55 @@ async def async_setup_entry( ): platform.async_register_entity_service(service_name, schema, method) - controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - programs_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - DATA_PROGRAMS - ] - zones_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][DATA_ZONES] + data = hass.data[DOMAIN][entry.entry_id] + controller = data[DATA_CONTROLLER] + program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS] + zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES] - entities: list[RainMachineProgram | RainMachineZone] = [ - RainMachineProgram( - entry, - programs_coordinator, - controller, - RainMachineSwitchDescription( - key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid - ), - ) - for uid, program in programs_coordinator.data.items() - ] - entities.extend( - [ - RainMachineZone( - entry, - zones_coordinator, - controller, - RainMachineSwitchDescription( - key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid - ), + entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = [] + + for kind, coordinator, switch_class, switch_enabled_class in ( + ("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled), + ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), + ): + for uid, data in coordinator.data.items(): + # Add a switch to start/stop the program or zone: + entities.append( + switch_class( + entry, + coordinator, + controller, + RainMachineSwitchDescription( + key=f"{kind}_{uid}", + name=data["name"], + icon="mdi:water", + uid=uid, + ), + ) + ) + + # Add a switch to enabled/disable the program or zone: + entities.append( + switch_enabled_class( + entry, + coordinator, + controller, + RainMachineSwitchDescription( + key=f"{kind}_{uid}_enabled", + name=f"{data['name']} Enabled", + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:cog", + uid=uid, + ), + ) ) - for uid, zone in zones_coordinator.data.items() - ] - ) async_add_entities(entities) -class RainMachineSwitch(RainMachineEntity, SwitchEntity): - """A class to represent a generic RainMachine switch.""" +class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): + """Define a base RainMachine switch.""" - _attr_icon = DEFAULT_ICON entity_description: RainMachineSwitchDescription def __init__( @@ -196,37 +203,30 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): controller: Controller, description: RainMachineSwitchDescription, ) -> None: - """Initialize a generic RainMachine switch.""" + """Initialize.""" super().__init__(entry, coordinator, controller, description) self._attr_is_on = False - self._data = coordinator.data[self.entity_description.uid] self._entry = entry - self._is_active = True - @property - def available(self) -> bool: - """Return True if entity is available.""" - return super().available and self._is_active - - async def _async_run_switch_coroutine(self, api_coro: Coroutine) -> None: - """Run a coroutine to toggle the switch.""" + async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None: + """Await an API coroutine, handle any errors, and update as appropriate.""" try: resp = await api_coro except RequestError as err: LOGGER.error( - 'Error while toggling %s "%s": %s', - self.entity_description.key, - self.unique_id, + 'Error while executing %s on "%s": %s', + api_coro.__name__, + self.name, err, ) return if resp["statusCode"] != 0: LOGGER.error( - 'Error while toggling %s "%s": %s', - self.entity_description.key, - self.unique_id, + 'Error while executing %s on "%s": %s', + api_coro.__name__, + self.name, resp["message"], ) return @@ -237,94 +237,102 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) - async def async_disable_program(self) -> None: - """Disable a program.""" - raise NotImplementedError("Service not implemented for this entity") - - async def async_disable_zone(self) -> None: - """Disable a zone.""" - raise NotImplementedError("Service not implemented for this entity") - - async def async_enable_program(self) -> None: - """Enable a program.""" - raise NotImplementedError("Service not implemented for this entity") - - async def async_enable_zone(self) -> None: - """Enable a zone.""" - raise NotImplementedError("Service not implemented for this entity") - async def async_start_program(self) -> None: - """Start a program.""" + """Execute the start_program entity service.""" raise NotImplementedError("Service not implemented for this entity") async def async_start_zone(self, *, zone_run_time: int) -> None: - """Start a zone.""" + """Execute the start_zone entity service.""" raise NotImplementedError("Service not implemented for this entity") async def async_stop_program(self) -> None: - """Stop a program.""" + """Execute the stop_program entity service.""" raise NotImplementedError("Service not implemented for this entity") async def async_stop_zone(self) -> None: - """Stop a zone.""" + """Execute the stop_zone entity service.""" raise NotImplementedError("Service not implemented for this entity") + +class RainMachineActivitySwitch(RainMachineBaseSwitch): + """Define a RainMachine switch to start/stop an activity (program or zone).""" + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off. + + The only way this could occur is if someone rapidly turns a disabled activity + off right after turning it on. + """ + if not self.coordinator.data[self.entity_description.uid]["active"]: + raise HomeAssistantError( + f"Cannot turn off an inactive program/zone: {self.name}" + ) + + await self.async_turn_off_when_active(**kwargs) + + async def async_turn_off_when_active(self, **kwargs: Any) -> None: + """Turn the switch off when its associated activity is active.""" + raise NotImplementedError + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + if not self.coordinator.data[self.entity_description.uid]["active"]: + raise HomeAssistantError( + f"Cannot turn on an inactive program/zone: {self.name}" + ) + + await self.async_turn_on_when_active(**kwargs) + + async def async_turn_on_when_active(self, **kwargs: Any) -> None: + """Turn the switch on when its associated activity is active.""" + raise NotImplementedError + + +class RainMachineEnabledSwitch(RainMachineBaseSwitch): + """Define a RainMachine switch to enable/disable an activity (program or zone).""" + @callback def update_from_latest_data(self) -> None: - """Update the state.""" - self._data = self.coordinator.data[self.entity_description.uid] - self._is_active = self._data["active"] + """Update the entity when new data is received.""" + self._attr_is_on = self.coordinator.data[self.entity_description.uid]["active"] -class RainMachineProgram(RainMachineSwitch): - """A RainMachine program.""" - - @property - def zones(self) -> list: - """Return a list of active zones associated with this program.""" - return [z for z in self._data["wateringTimes"] if z["active"]] - - async def async_disable_program(self) -> None: - """Disable a program.""" - await self._controller.programs.disable(self.entity_description.uid) - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_enable_program(self) -> None: - """Enable a program.""" - await self._controller.programs.enable(self.entity_description.uid) - await async_update_programs_and_zones(self.hass, self._entry) +class RainMachineProgram(RainMachineActivitySwitch): + """Define a RainMachine program.""" async def async_start_program(self) -> None: - """Start a program.""" + """Start the program.""" await self.async_turn_on() async def async_stop_program(self) -> None: - """Stop a program.""" + """Stop the program.""" await self.async_turn_off() - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the program off.""" - await self._async_run_switch_coroutine( + async def async_turn_off_when_active(self, **kwargs: Any) -> None: + """Turn the switch off when its associated activity is active.""" + await self._async_run_api_coroutine( self._controller.programs.stop(self.entity_description.uid) ) - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the program on.""" - await self._async_run_switch_coroutine( + async def async_turn_on_when_active(self, **kwargs: Any) -> None: + """Turn the switch on when its associated activity is active.""" + await self._async_run_api_coroutine( self._controller.programs.start(self.entity_description.uid) ) @callback def update_from_latest_data(self) -> None: - """Update the state.""" - super().update_from_latest_data() + """Update the entity when new data is received.""" + data = self.coordinator.data[self.entity_description.uid] - self._attr_is_on = bool(self._data["status"]) + self._attr_is_on = bool(data["status"]) - next_run: str | None = None - if self._data.get("nextRun") is not None: + next_run: str | None + if data.get("nextRun") is None: + next_run = None + else: next_run = datetime.strptime( - f"{self._data['nextRun']} {self._data['startTime']}", + f"{data['nextRun']} {data['startTime']}", "%Y-%m-%d %H:%M", ).isoformat() @@ -332,76 +340,107 @@ class RainMachineProgram(RainMachineSwitch): { ATTR_ID: self.entity_description.uid, ATTR_NEXT_RUN: next_run, - ATTR_SOAK: self.coordinator.data[self.entity_description.uid].get( - "soak" - ), - ATTR_STATUS: RUN_STATUS_MAP[ - self.coordinator.data[self.entity_description.uid]["status"] - ], - ATTR_ZONES: ", ".join(z["name"] for z in self.zones), + ATTR_SOAK: data.get("soak"), + ATTR_STATUS: RUN_STATUS_MAP[data["status"]], + ATTR_ZONES: [z for z in data["wateringTimes"] if z["active"]], } ) -class RainMachineZone(RainMachineSwitch): - """A RainMachine zone.""" +class RainMachineProgramEnabled(RainMachineEnabledSwitch): + """Define a switch to enable/disable a RainMachine program.""" - async def async_disable_zone(self) -> None: - """Disable a zone.""" - await self._controller.zones.disable(self.entity_description.uid) - await async_update_programs_and_zones(self.hass, self._entry) + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable the program.""" + tasks = [ + self._async_run_api_coroutine( + self._controller.programs.stop(self.entity_description.uid) + ), + self._async_run_api_coroutine( + self._controller.programs.disable(self.entity_description.uid) + ), + ] - async def async_enable_zone(self) -> None: - """Enable a zone.""" - await self._controller.zones.enable(self.entity_description.uid) - await async_update_programs_and_zones(self.hass, self._entry) + await asyncio.gather(*tasks) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable the program.""" + await self._async_run_api_coroutine( + self._controller.programs.enable(self.entity_description.uid) + ) + + +class RainMachineZone(RainMachineActivitySwitch): + """Define a RainMachine zone.""" async def async_start_zone(self, *, zone_run_time: int) -> None: """Start a particular zone for a certain amount of time.""" - await self._controller.zones.start(self.entity_description.uid, zone_run_time) - await async_update_programs_and_zones(self.hass, self._entry) + await self.async_turn_off(duration=zone_run_time) async def async_stop_zone(self) -> None: """Stop a zone.""" await self.async_turn_off() - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the zone off.""" - await self._async_run_switch_coroutine( + async def async_turn_off_when_active(self, **kwargs: Any) -> None: + """Turn the switch off when its associated activity is active.""" + await self._async_run_api_coroutine( self._controller.zones.stop(self.entity_description.uid) ) - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the zone on.""" - await self._async_run_switch_coroutine( + async def async_turn_on_when_active(self, **kwargs: Any) -> None: + """Turn the switch on when its associated activity is active.""" + await self._async_run_api_coroutine( self._controller.zones.start( self.entity_description.uid, - self._entry.options[CONF_ZONE_RUN_TIME], + kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), ) ) @callback def update_from_latest_data(self) -> None: - """Update the state.""" - super().update_from_latest_data() + """Update the entity when new data is received.""" + data = self.coordinator.data[self.entity_description.uid] - self._attr_is_on = bool(self._data["state"]) + self._attr_is_on = bool(data["state"]) self._attr_extra_state_attributes.update( { - ATTR_STATUS: RUN_STATUS_MAP[self._data["state"]], - ATTR_AREA: self._data.get("waterSense").get("area"), - ATTR_CURRENT_CYCLE: self._data.get("cycle"), - ATTR_FIELD_CAPACITY: self._data.get("waterSense").get("fieldCapacity"), - ATTR_ID: self._data["uid"], - ATTR_NO_CYCLES: self._data.get("noOfCycles"), - ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"), - ATTR_RESTRICTIONS: self._data.get("restriction"), - ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")), - ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("soil")), - ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")), - ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")), - ATTR_TIME_REMAINING: self._data.get("remaining"), - ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._data.get("type")), + ATTR_AREA: data.get("waterSense").get("area"), + ATTR_CURRENT_CYCLE: data.get("cycle"), + ATTR_FIELD_CAPACITY: data.get("waterSense").get("fieldCapacity"), + ATTR_ID: data["uid"], + ATTR_NO_CYCLES: data.get("noOfCycles"), + ATTR_PRECIP_RATE: data.get("waterSense").get("precipitationRate"), + ATTR_RESTRICTIONS: data.get("restriction"), + ATTR_SLOPE: SLOPE_TYPE_MAP.get(data.get("slope")), + ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data.get("soil")), + ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data.get("group_id")), + ATTR_STATUS: RUN_STATUS_MAP[data["state"]], + ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")), + ATTR_TIME_REMAINING: data.get("remaining"), + ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data.get("type")), } ) + + +class RainMachineZoneEnabled(RainMachineEnabledSwitch): + """Define a switch to enable/disable a RainMachine zone.""" + + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable the zone.""" + tasks = [ + self._async_run_api_coroutine( + self._controller.zones.stop(self.entity_description.uid) + ), + self._async_run_api_coroutine( + self._controller.zones.disable(self.entity_description.uid) + ), + ] + + await asyncio.gather(*tasks) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable the zone.""" + await self._async_run_api_coroutine( + self._controller.zones.enable(self.entity_description.uid) + ) diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index d7925c2a0ae..1ce95f76ac5 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from regenmaschine.errors import RainMachineError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import zeroconf from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -70,6 +71,68 @@ async def test_invalid_password(hass): assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} +@pytest.mark.parametrize( + "platform,entity_name,entity_id,old_unique_id,new_unique_id", + [ + ( + "binary_sensor", + "Home Flow Sensor", + "binary_sensor.home_flow_sensor", + "60e32719b6cf_flow_sensor", + "60:e3:27:19:b6:cf_flow_sensor", + ), + ( + "switch", + "Home Landscaping", + "switch.home_landscaping", + "60e32719b6cf_RainMachineZone_1", + "60:e3:27:19:b6:cf_zone_1", + ), + ], +) +async def test_migrate_1_2( + hass, platform, entity_name, entity_id, old_unique_id, new_unique_id +): + """Test migration from version 1 to 2 (consistent unique IDs).""" + conf = { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + + entry = MockConfigEntry(domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf) + entry.add_to_hass(hass) + + ent_reg = er.async_get(hass) + + # Create entity RegistryEntry using old unique ID format: + entity_entry = ent_reg.async_get_or_create( + platform, + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + assert entity_entry.entity_id == entity_id + assert entity_entry.unique_id == old_unique_id + + with patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id) is None + + async def test_options_flow(hass): """Test config flow options.""" conf = { From 7359083e9820339da22c9de8219dda26cfea7e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Nov 2021 22:37:28 -0600 Subject: [PATCH 0747/1452] Remove legacy executor usage from HomeKit tests (#60180) --- tests/components/homekit/test_type_covers.py | 32 ++++---- tests/components/homekit/test_type_fans.py | 8 +- tests/components/homekit/test_type_locks.py | 6 +- .../components/homekit/test_type_switches.py | 28 +++---- .../homekit/test_type_thermostats.py | 76 +++++++------------ 5 files changed, 66 insertions(+), 84 deletions(-) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index ab85147475b..c357598a3df 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -92,7 +92,7 @@ async def test_garage_door_open_close(hass, hk_driver, events): call_close_cover = async_mock_service(hass, DOMAIN, "close_cover") call_open_cover = async_mock_service(hass, DOMAIN, "open_cover") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert call_close_cover assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -104,14 +104,14 @@ async def test_garage_door_open_close(hass, hk_driver, events): hass.states.async_set(entity_id, STATE_CLOSED) await hass.async_block_till_done() - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_CLOSED assert acc.char_target_state.value == HK_DOOR_CLOSED assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_open_cover assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -123,7 +123,7 @@ async def test_garage_door_open_close(hass, hk_driver, events): hass.states.async_set(entity_id, STATE_OPEN) await hass.async_block_till_done() - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN @@ -202,7 +202,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): # Set from HomeKit call_set_cover_position = async_mock_service(hass, DOMAIN, "set_cover_position") - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 25) + acc.char_target_position.client_update_value(25) await hass.async_block_till_done() assert call_set_cover_position[0] assert call_set_cover_position[0].data[ATTR_ENTITY_ID] == entity_id @@ -212,7 +212,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == 25 - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 75) + acc.char_target_position.client_update_value(75) await hass.async_block_till_done() assert call_set_cover_position[1] assert call_set_cover_position[1].data[ATTR_ENTITY_ID] == entity_id @@ -286,7 +286,7 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): # HomeKit sets tilts between -90 and 90 (degrees), whereas # Homeassistant expects a % between 0 and 100. Keep that in mind # when comparing - await hass.async_add_executor_job(acc.char_target_tilt.client_update_value, 90) + acc.char_target_tilt.client_update_value(90) await hass.async_block_till_done() assert call_set_tilt_position[0] assert call_set_tilt_position[0].data[ATTR_ENTITY_ID] == entity_id @@ -296,7 +296,7 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == 100 - await hass.async_add_executor_job(acc.char_target_tilt.client_update_value, 45) + acc.char_target_tilt.client_update_value(45) await hass.async_block_till_done() assert call_set_tilt_position[1] assert call_set_tilt_position[1].data[ATTR_ENTITY_ID] == entity_id @@ -378,7 +378,7 @@ async def test_windowcovering_open_close(hass, hk_driver, events): call_close_cover = async_mock_service(hass, DOMAIN, "close_cover") call_open_cover = async_mock_service(hass, DOMAIN, "open_cover") - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 25) + acc.char_target_position.client_update_value(25) await hass.async_block_till_done() assert call_close_cover assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -388,7 +388,7 @@ async def test_windowcovering_open_close(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 90) + acc.char_target_position.client_update_value(90) await hass.async_block_till_done() assert call_open_cover[0] assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -398,7 +398,7 @@ async def test_windowcovering_open_close(hass, hk_driver, events): assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 55) + acc.char_target_position.client_update_value(55) await hass.async_block_till_done() assert call_open_cover[1] assert call_open_cover[1].data[ATTR_ENTITY_ID] == entity_id @@ -425,7 +425,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, events): call_open_cover = async_mock_service(hass, DOMAIN, "open_cover") call_stop_cover = async_mock_service(hass, DOMAIN, "stop_cover") - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 25) + acc.char_target_position.client_update_value(25) await hass.async_block_till_done() assert call_close_cover assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -435,7 +435,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 90) + acc.char_target_position.client_update_value(90) await hass.async_block_till_done() assert call_open_cover assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -445,7 +445,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, events): assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_position.client_update_value, 55) + acc.char_target_position.client_update_value(55) await hass.async_block_till_done() assert call_stop_cover assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id @@ -474,11 +474,11 @@ async def test_windowcovering_open_close_with_position_and_stop( # Set from HomeKit call_stop_cover = async_mock_service(hass, DOMAIN, "stop_cover") - await hass.async_add_executor_job(acc.char_hold_position.client_update_value, 0) + acc.char_hold_position.client_update_value(0) await hass.async_block_till_done() assert not call_stop_cover - await hass.async_add_executor_job(acc.char_hold_position.client_update_value, 1) + acc.char_hold_position.client_update_value(1) await hass.async_block_till_done() assert call_stop_cover assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index c8990169741..c1ce1ffaddb 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -170,7 +170,7 @@ async def test_fan_direction(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_direction.client_update_value, 1) + acc.char_direction.client_update_value(1) await hass.async_block_till_done() assert call_set_direction[1] assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id @@ -219,7 +219,7 @@ async def test_fan_oscillate(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_swing.client_update_value, 0) + acc.char_swing.client_update_value(0) await hass.async_block_till_done() assert call_oscillate[0] assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id @@ -239,7 +239,7 @@ async def test_fan_oscillate(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_swing.client_update_value, 1) + acc.char_swing.client_update_value(1) await hass.async_block_till_done() assert call_oscillate[1] assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id @@ -295,7 +295,7 @@ async def test_fan_speed(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_speed.client_update_value, 42) + acc.char_speed.client_update_value(42) await hass.async_block_till_done() assert acc.char_speed.value == 50 assert acc.char_active.value == 1 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index e47f4dfac71..1106699909b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -76,7 +76,7 @@ async def test_lock_unlock(hass, hk_driver, events): call_lock = async_mock_service(hass, DOMAIN, "lock") call_unlock = async_mock_service(hass, DOMAIN, "unlock") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert call_lock assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id @@ -85,7 +85,7 @@ async def test_lock_unlock(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_unlock assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id @@ -107,7 +107,7 @@ async def test_no_code(hass, hk_driver, config, events): # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, "lock") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert call_lock assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 59f871f31ac..54eae42ca1d 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -71,14 +71,14 @@ async def test_outlet_set_state(hass, hk_driver, events): call_turn_on = async_mock_service(hass, "switch", "turn_on") call_turn_off = async_mock_service(hass, "switch", "turn_off") - await hass.async_add_executor_job(acc.char_on.client_update_value, True) + acc.char_on.client_update_value(True) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_on.client_update_value, False) + acc.char_on.client_update_value(False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id @@ -123,14 +123,14 @@ async def test_switch_set_state(hass, hk_driver, entity_id, attrs, events): call_turn_on = async_mock_service(hass, domain, "turn_on") call_turn_off = async_mock_service(hass, domain, "turn_off") - await hass.async_add_executor_job(acc.char_on.client_update_value, True) + acc.char_on.client_update_value(True) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_on.client_update_value, False) + acc.char_on.client_update_value(False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id @@ -188,7 +188,7 @@ async def test_valve_set_state(hass, hk_driver, events): call_turn_on = async_mock_service(hass, "switch", "turn_on") call_turn_off = async_mock_service(hass, "switch", "turn_off") - await hass.async_add_executor_job(acc.char_active.client_update_value, 1) + acc.char_active.client_update_value(1) await hass.async_block_till_done() assert acc.char_in_use.value == 1 assert call_turn_on @@ -196,7 +196,7 @@ async def test_valve_set_state(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_active.client_update_value, 0) + acc.char_active.client_update_value(0) await hass.async_block_till_done() assert acc.char_in_use.value == 0 assert call_turn_off @@ -246,7 +246,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass, VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE ) - await hass.async_add_executor_job(acc.char_on.client_update_value, 1) + acc.char_on.client_update_value(1) await hass.async_block_till_done() assert acc.char_on.value == 1 assert call_start @@ -254,7 +254,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_on.client_update_value, 0) + acc.char_on.client_update_value(0) await hass.async_block_till_done() assert acc.char_on.value == 0 assert call_return_to_base @@ -292,7 +292,7 @@ async def test_vacuum_set_state_without_returnhome_and_start_support( call_turn_on = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_ON) call_turn_off = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_OFF) - await hass.async_add_executor_job(acc.char_on.client_update_value, 1) + acc.char_on.client_update_value(1) await hass.async_block_till_done() assert acc.char_on.value == 1 assert call_turn_on @@ -300,7 +300,7 @@ async def test_vacuum_set_state_without_returnhome_and_start_support( assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_on.client_update_value, 0) + acc.char_on.client_update_value(0) await hass.async_block_till_done() assert acc.char_on.value == 0 assert call_turn_off @@ -326,7 +326,7 @@ async def test_reset_switch(hass, hk_driver, events): call_turn_on = async_mock_service(hass, domain, "turn_on") call_turn_off = async_mock_service(hass, domain, "turn_off") - await hass.async_add_executor_job(acc.char_on.client_update_value, True) + acc.char_on.client_update_value(True) await hass.async_block_till_done() assert acc.char_on.value is True assert call_turn_on @@ -347,7 +347,7 @@ async def test_reset_switch(hass, hk_driver, events): assert len(events) == 1 assert not call_turn_off - await hass.async_add_executor_job(acc.char_on.client_update_value, False) + acc.char_on.client_update_value(False) await hass.async_block_till_done() assert acc.char_on.value is False assert len(events) == 1 @@ -370,7 +370,7 @@ async def test_script_switch(hass, hk_driver, events): call_turn_on = async_mock_service(hass, domain, "test") call_turn_off = async_mock_service(hass, domain, "turn_off") - await hass.async_add_executor_job(acc.char_on.client_update_value, True) + acc.char_on.client_update_value(True) await hass.async_block_till_done() assert acc.char_on.value is True assert call_turn_on @@ -391,7 +391,7 @@ async def test_script_switch(hass, hk_driver, events): assert len(events) == 1 assert not call_turn_off - await hass.async_add_executor_job(acc.char_on.client_update_value, False) + acc.char_on.client_update_value(False) await hass.async_block_till_done() assert acc.char_on.value is False assert len(events) == 1 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 9d2b5066ce4..20ed225552c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1062,16 +1062,16 @@ async def test_thermostat_hvac_modes(hass, hk_driver): assert acc.char_target_heat_cool.value == 0 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 0 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 1) + acc.char_target_heat_cool.set_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 2) + acc.char_target_heat_cool.set_value(2) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1104,16 +1104,16 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): assert hap["valid-values"] == [0, 1, 3] assert acc.char_target_heat_cool.value == 0 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 1) + acc.char_target_heat_cool.set_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 2) + acc.char_target_heat_cool.set_value(2) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1160,16 +1160,16 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): assert hap["valid-values"] == [0, 1, 3] assert acc.char_target_heat_cool.value == 1 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 1) + acc.char_target_heat_cool.set_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 2) + acc.char_target_heat_cool.set_value(2) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1214,17 +1214,17 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): assert hap["valid-values"] == [0, 3] assert acc.char_target_heat_cool.value == 3 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 1) + acc.char_target_heat_cool.set_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 2) + acc.char_target_heat_cool.set_value(2) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 @@ -1268,23 +1268,17 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT] assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_HEAT) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT with pytest.raises(ValueError): - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_COOL) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT with pytest.raises(ValueError): - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_AUTO) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT @@ -1328,23 +1322,17 @@ async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_COOL] assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_COOL) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL with pytest.raises(ValueError): - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_AUTO) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL with pytest.raises(ValueError): - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_HEAT) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL @@ -1396,22 +1384,16 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): ] assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_COOL) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL with pytest.raises(ValueError): - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_AUTO) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL - await hass.async_add_executor_job( - acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT - ) + acc.char_target_heat_cool.set_value(HC_HEAT_COOL_HEAT) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] @@ -1481,21 +1463,21 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver): assert hap["valid-values"] == [1, 3] assert acc.char_target_heat_cool.value == 3 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 1) + acc.char_target_heat_cool.set_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 2) + acc.char_target_heat_cool.set_value(2) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 0) + acc.char_target_heat_cool.set_value(0) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1737,7 +1719,7 @@ async def test_water_heater(hass, hk_driver, events): hass, DOMAIN_WATER_HEATER, "set_temperature" ) - await hass.async_add_executor_job(acc.char_target_temp.client_update_value, 52.0) + acc.char_target_temp.client_update_value(52.0) await hass.async_block_till_done() assert call_set_temperature assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id @@ -1746,12 +1728,12 @@ async def test_water_heater(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}" - await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 1) + acc.char_target_heat_cool.client_update_value(1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) + acc.char_target_heat_cool.set_value(3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1778,7 +1760,7 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events): hass, DOMAIN_WATER_HEATER, "set_temperature" ) - await hass.async_add_executor_job(acc.char_target_temp.client_update_value, 60) + acc.char_target_temp.client_update_value(60) await hass.async_block_till_done() assert call_set_temperature assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id From 42ed6ddba3e0cba0974d7a008a7ee2f178cec1f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 05:50:21 +0100 Subject: [PATCH 0748/1452] Add button support to Google Assistant (#60158) --- .../components/google_assistant/const.py | 2 ++ .../components/google_assistant/trait.py | 15 ++++++++----- script/hassfest/dependencies.py | 1 + .../components/google_assistant/test_trait.py | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index d23560b85c1..269c0aafea1 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -2,6 +2,7 @@ from homeassistant.components import ( alarm_control_panel, binary_sensor, + button, camera, climate, cover, @@ -120,6 +121,7 @@ EVENT_SYNC_RECEIVED = "google_assistant_sync" DOMAIN_TO_GOOGLE_TYPES = { alarm_control_panel.DOMAIN: TYPE_ALARM, + button.DOMAIN: TYPE_SCENE, camera.DOMAIN: TYPE_CAMERA, climate.DOMAIN: TYPE_THERMOSTAT, cover.DOMAIN: TYPE_BLINDS, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 66df9315220..5801ae6811b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -6,6 +6,7 @@ import logging from homeassistant.components import ( alarm_control_panel, binary_sensor, + button, camera, cover, fan, @@ -513,11 +514,11 @@ class SceneTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - return domain in (scene.DOMAIN, script.DOMAIN) + return domain in (button.DOMAIN, scene.DOMAIN, script.DOMAIN) def sync_attributes(self): """Return scene attributes for a sync request.""" - # Neither supported domain can support sceneReversible + # None of the supported domains can support sceneReversible return {} def query_attributes(self): @@ -526,12 +527,16 @@ class SceneTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a scene command.""" - # Don't block for scripts as they can be slow. + service = SERVICE_TURN_ON + if self.state.domain == button.DOMAIN: + service = button.SERVICE_PRESS + + # Don't block for scripts or buttons, as they can be slow. await self.hass.services.async_call( self.state.domain, - SERVICE_TURN_ON, + service, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=self.state.domain != script.DOMAIN, + blocking=self.state.domain not in (button.DOMAIN, script.DOMAIN), context=data.context, ) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 9e66e05899c..3f0bd9c1236 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -94,6 +94,7 @@ ALLOWED_USED_COMPONENTS = { # Internal integrations "alert", "automation", + "button", "conversation", "button", "device_automation", diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 62a9d7a0120..a396c1bc91d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components import ( alarm_control_panel, binary_sensor, + button, camera, cover, fan, @@ -767,6 +768,26 @@ async def test_light_modes(hass): } +async def test_scene_button(hass): + """Test Scene trait support for the button domain.""" + assert helpers.get_google_type(button.DOMAIN, None) is not None + assert trait.SceneTrait.supported(button.DOMAIN, 0, None, None) + + trt = trait.SceneTrait(hass, State("button.bla", STATE_UNKNOWN), BASIC_CONFIG) + assert trt.sync_attributes() == {} + assert trt.query_attributes() == {} + assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) + + calls = async_mock_service(hass, button.DOMAIN, button.SERVICE_PRESS) + await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {}) + + # We don't wait till button press is done. + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "button.bla"} + + async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" assert helpers.get_google_type(scene.DOMAIN, None) is not None From 3b0d984959beef06a29a620815de4f3e810c73e5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 09:01:40 +0100 Subject: [PATCH 0749/1452] Use dataclass for MqttServiceInfo (#60191) * Use dataclass for MqttServiceInfo * Drop test exception Co-authored-by: epenet --- homeassistant/components/mqtt/discovery.py | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 7aec80b2e9c..8ccfca96c26 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,13 +1,14 @@ """Support for MQTT discovery.""" import asyncio from collections import deque +from dataclasses import dataclass import datetime as dt import functools import json import logging import re import time -from typing import TypedDict +from typing import Any from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -17,6 +18,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.frame import report from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -89,7 +91,8 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -class MqttServiceInfo(TypedDict): +@dataclass +class MqttServiceInfo: """Prepared info from mqtt entries.""" topic: str @@ -99,6 +102,24 @@ class MqttServiceInfo(TypedDict): subscribed_topic: str timestamp: dt.datetime + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"mqtt"}, + error_if_core=False, + ) + self._warning_logged = True + return getattr(self, name) + async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None From 8ece8d124dec2fc59061995c4aef870d4e65b171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Nov 2021 09:29:22 +0100 Subject: [PATCH 0750/1452] Bump actions/cache from 2.1.6 to 2.1.7 (#60189) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c35aea46f91..e7f44c7441e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: >- @@ -65,7 +65,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -92,7 +92,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -104,7 +104,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -132,7 +132,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -144,7 +144,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -172,7 +172,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -184,7 +184,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -234,7 +234,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -246,7 +246,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -277,7 +277,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -289,7 +289,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -320,7 +320,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -332,7 +332,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -360,7 +360,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -372,7 +372,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -403,7 +403,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -415,7 +415,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -454,7 +454,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -466,7 +466,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -496,7 +496,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -525,7 +525,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -561,7 +561,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: >- @@ -598,7 +598,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -629,7 +629,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -663,7 +663,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -721,7 +721,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From ca20fc857fbe98b2cedb5b38100052b8a64f58a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Nov 2021 09:36:03 +0100 Subject: [PATCH 0751/1452] Remove via_device links when a device is removed (#60153) * Remove via_device links when a device is removed * Update test --- homeassistant/helpers/device_registry.py | 3 +++ tests/helpers/test_device_registry.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5395218b48e..65df557b582 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -501,6 +501,9 @@ class DeviceRegistry: orphaned_timestamp=None, ) ) + for other_device in list(self.devices.values()): + if other_device.via_device_id == device_id: + self._async_update_device(other_device.id, via_device_id=None) self.hass.bus.async_fire( EVENT_DEVICE_REGISTRY_UPDATED, {"action": "remove", "device_id": device_id} ) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 8c311d44588..939387f411a 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -420,7 +420,7 @@ async def test_deleted_device_removing_area_id(registry): async def test_specifying_via_device_create(registry): - """Test specifying a via_device and updating.""" + """Test specifying a via_device and removal of the hub device.""" via = registry.async_get_or_create( config_entry_id="123", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, @@ -440,6 +440,10 @@ async def test_specifying_via_device_create(registry): assert light.via_device_id == via.id + registry.async_remove_device(via.id) + light = registry.async_get_device({("hue", "456")}) + assert light.via_device_id is None + async def test_specifying_via_device_update(registry): """Test specifying a via_device and updating.""" From 39691faccc6f15fc7a957f3981824a8e80fffa67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 23 Nov 2021 11:04:33 +0200 Subject: [PATCH 0752/1452] Use DeviceEntryType in non-typechecked code too (#58646) --- homeassistant/components/climacell/__init__.py | 3 ++- homeassistant/components/coinbase/sensor.py | 5 +++-- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/eafm/sensor.py | 3 ++- homeassistant/components/hassio/__init__.py | 11 +++++++---- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/met_eireann/weather.py | 3 ++- homeassistant/components/meteo_france/sensor.py | 3 ++- homeassistant/components/meteo_france/weather.py | 3 ++- homeassistant/components/meteoclimatic/sensor.py | 3 ++- homeassistant/components/meteoclimatic/weather.py | 3 ++- homeassistant/components/nightscout/__init__.py | 2 +- homeassistant/components/nws/__init__.py | 3 ++- homeassistant/components/ovo_energy/__init__.py | 3 ++- homeassistant/components/plex/media_player.py | 3 ++- homeassistant/components/spotify/media_player.py | 3 ++- homeassistant/components/unifi/switch.py | 3 ++- homeassistant/components/xbox/base_sensor.py | 3 ++- tests/components/config/test_device_registry.py | 4 ++-- tests/components/deconz/test_gateway.py | 2 +- tests/components/kraken/test_sensor.py | 3 ++- tests/components/picnic/test_sensor.py | 3 ++- tests/helpers/test_device_registry.py | 4 ++-- tests/helpers/test_entity_platform.py | 4 ++-- 24 files changed, 50 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index c6a40b839f2..bd22e8f92aa 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -358,7 +359,7 @@ class ClimaCellEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device registry information.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, manufacturer="ClimaCell", name="ClimaCell", diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 817a99d9eb1..f2d450463f2 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from .const import ( @@ -112,7 +113,7 @@ class AccountSensor(SensorEntity): self._attr_state_class = STATE_CLASS_TOTAL self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._coinbase_data.user_id)}, manufacturer="Coinbase.com", name=f"Coinbase {self._coinbase_data.user_id[-4:]}", @@ -185,7 +186,7 @@ class ExchangeRateSensor(SensorEntity): self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._coinbase_data.user_id)}, manufacturer="Coinbase.com", name=f"Coinbase {self._coinbase_data.user_id[-4:]}", diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 679c1498e44..8742cd087d4 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -156,7 +156,7 @@ class DeconzGateway: device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, configuration_url=configuration_url, - entry_type="service", + entry_type=dr.DeviceEntryType.SERVICE, identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)}, manufacturer="Dresden Elektronik", model=self.api.config.model_id, diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index e5edf8e0b99..40059f71f1a 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -8,6 +8,7 @@ import async_timeout from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -123,7 +124,7 @@ class Measurement(CoordinatorEntity, SensorEntity): def device_info(self): """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, "measure-id", self.station_id)}, manufacturer="https://environment.data.gov.uk/", model=self.parameter_name, diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e05491b5ae1..6b61881b47b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -19,7 +19,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_NAME, - ATTR_SERVICE, EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, @@ -27,7 +26,11 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, recorder -from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.device_registry import ( + DeviceEntryType, + DeviceRegistry, + async_get_registry, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -660,7 +663,7 @@ def async_register_addons_in_dev_reg( model=SupervisorEntityModel.ADDON, sw_version=addon[ATTR_VERSION], name=addon[ATTR_NAME], - entry_type=ATTR_SERVICE, + entry_type=DeviceEntryType.SERVICE, configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", ) if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): @@ -679,7 +682,7 @@ def async_register_os_in_dev_reg( model=SupervisorEntityModel.OS, sw_version=os_dict[ATTR_VERSION], name="Home Assistant Operating System", - entry_type=ATTR_SERVICE, + entry_type=DeviceEntryType.SERVICE, ) dev_reg.async_get_or_create(config_entry_id=entry_id, **params) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 050f0b3db12..b372a863532 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -766,7 +766,7 @@ class HomeKit: manufacturer=MANUFACTURER, name=accessory_friendly_name(self._entry_title, self.driver.accessory), model=f"HomeKit {hk_mode_name}", - entry_type="service", + entry_type=device_registry.DeviceEntryType.SERVICE, ) @callback diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index ddd39d82c3b..51bfa5b360a 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -20,6 +20,7 @@ from homeassistant.const import ( SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util @@ -187,7 +188,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Device info.""" return DeviceInfo( default_name="Forecast", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN,)}, manufacturer="Met Éireann", model="Forecast", diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 3cbd56bf94e..ac1ccc13009 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -96,7 +97,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): def device_info(self) -> DeviceInfo: """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, manufacturer=MANUFACTURER, model=MODEL, diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 5892944cf6a..8158d6564cf 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -15,6 +15,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -90,7 +91,7 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): def device_info(self) -> DeviceInfo: """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, manufacturer=MANUFACTURER, model=MODEL, diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 8e4e9f8fe13..603940b4cc8 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -43,7 +44,7 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): def device_info(self): """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, manufacturer=MANUFACTURER, model=MODEL, diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 36c3229777f..4faecdaa3ac 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -5,6 +5,7 @@ from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -57,7 +58,7 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): def device_info(self): """Return the device info.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, manufacturer=MANUFACTURER, model=MODEL, diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 6bf1a7f1929..28906ce6e88 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Nightscout Foundation", name=status.name, sw_version=status.version, - entry_type="service", + entry_type=dr.DeviceEntryType.SERVICE, ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 6f5bdd145c9..02327336035 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -172,7 +173,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def device_info(latitude, longitude) -> DeviceInfo: """Return device registry information.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, base_unique_id(latitude, longitude))}, manufacturer="National Weather Service", name=f"NWS: {latitude}, {longitude}", diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index cb3fba80378..2b69ca0ade7 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( @@ -112,7 +113,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): def device_info(self) -> DeviceInfo: """Return device information about this OVO Energy instance.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self._client.account_id)}, manufacturer="OVO Energy", name=self._client.username, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 3bc0a080f8c..6a48a427519 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -23,6 +23,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -533,7 +534,7 @@ class PlexMediaPlayer(MediaPlayerEntity): name="Plex Client Service", manufacturer="Plex", model="Plex Clients", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, ) return DeviceInfo( diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 029f7dc9e8b..efb5e0f6b4a 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -53,6 +53,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp @@ -267,7 +268,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): manufacturer="Spotify AB", model=model, name=self._name, - entry_type="service", + entry_type=DeviceEntryType.SERVICE, configuration_url="https://open.spotify.com", ) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 3c002642564..370d48d5adf 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -17,6 +17,7 @@ from aiounifi.events import ( from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -370,7 +371,7 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): def device_info(self) -> DeviceInfo: """Return a service description for device registry.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"unifi_controller_{self._item.site_id}")}, manufacturer=ATTR_MANUFACTURER, model="UniFi Controller", diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 8ef0928f903..100685b6c34 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from yarl import URL +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -70,7 +71,7 @@ class XboxBaseSensorEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, "xbox_live")}, manufacturer="Microsoft", model="Xbox Live", diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 0d9170f0a83..edc167405ac 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -36,7 +36,7 @@ async def test_list_devices(hass, client, registry): manufacturer="manufacturer", model="model", via_device=("bridgeid", "0123"), - entry_type="service", + entry_type=helpers_dr.DeviceEntryType.SERVICE, ) await client.send_json({"id": 5, "type": "config/device_registry/list"}) @@ -68,7 +68,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, - "entry_type": "service", + "entry_type": helpers_dr.DeviceEntryType.SERVICE, "via_device_id": dev1, "area_id": None, "name_by_user": None, diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 8016d2327ef..9457c2e988b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -176,7 +176,7 @@ async def test_gateway_setup(hass, aioclient_mock): ) assert gateway_entry.configuration_url == f"http://{HOST}:{PORT}" - assert gateway_entry.entry_type == "service" + assert gateway_entry.entry_type is dr.DeviceEntryType.SERVICE async def test_gateway_device_configuration_url_when_addon(hass, aioclient_mock): diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index ca98251f157..bc37b67e676 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.kraken.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START +from homeassistant.helpers.device_registry import DeviceEntryType import homeassistant.util.dt as dt_util from .const import ( @@ -253,7 +254,7 @@ async def test_sensors_available_after_restart(hass): identifiers={(DOMAIN, "XBT_USD")}, name="XBT USD", manufacturer="Kraken.com", - entry_type="service", + entry_type=DeviceEntryType.SERVICE, ) entry.add_to_hass(hass) diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 4edf2c3ec98..b0d12f1f080 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, STATE_UNAVAILABLE, ) +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.util import dt from tests.common import ( @@ -390,7 +391,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): ) assert picnic_service.model == DEFAULT_USER_RESPONSE["user_id"] assert picnic_service.name == "Picnic: Commonstreet 123a" - assert picnic_service.entry_type == "service" + assert picnic_service.entry_type is DeviceEntryType.SERVICE async def test_auth_token_is_saved_on_update(self): """Test that auth-token changes in the session object are reflected by the config entry.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 939387f411a..5d3736fd060 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -179,7 +179,7 @@ async def test_loading_from_storage(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", - "entry_type": "service", + "entry_type": device_registry.DeviceEntryType.SERVICE, "area_id": "12345A", "name_by_user": "Test Friendly Name", "disabled_by": device_registry.DISABLED_USER, @@ -212,7 +212,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.id == "abcdefghijklm" assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" - assert entry.entry_type == "service" + assert entry.entry_type is device_registry.DeviceEntryType.SERVICE assert entry.disabled_by == device_registry.DISABLED_USER assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 8573e90f592..11cbc78ed71 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -839,7 +839,7 @@ async def test_device_info_called(hass): "name": "test-name", "sw_version": "test-sw", "suggested_area": "Heliport", - "entry_type": "service", + "entry_type": dr.DeviceEntryType.SERVICE, "via_device": ("hue", "via-id"), }, ), @@ -863,7 +863,7 @@ async def test_device_info_called(hass): assert device.identifiers == {("hue", "1234")} assert device.configuration_url == "http://192.168.0.100/config" assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "abcd")} - assert device.entry_type == "service" + assert device.entry_type is dr.DeviceEntryType.SERVICE assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" From 4aae0885129f3f628fc140c388827972b2df3f3a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 10:05:48 +0100 Subject: [PATCH 0753/1452] Add GitHub Actions concurrency limits (#60194) --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e7f44c7441e..62e14b2f129 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,10 @@ env: PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: # Separate job to pre-populate the base dependency cache # This prevent upcoming jobs to do the same individually From 83aff48db952d4c5584d534911f4f2817a77ca7c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 11:21:55 +0100 Subject: [PATCH 0754/1452] GitHub Actions: Don't run test suite if basic linters fails (#60197) --- .github/workflows/ci.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 62e14b2f129..e95bd9034f8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -653,7 +653,20 @@ jobs: pytest: runs-on: ubuntu-latest - needs: prepare-tests + needs: + - gen-requirements-all + - hassfest + - lint-bandit + - lint-black + - lint-codespell + - lint-dockerfile + - lint-executable-shebangs + - lint-isort + - lint-json + - lint-pyupgrade + - lint-yaml + - mypy + - prepare-tests strategy: fail-fast: false matrix: From e673d9dbd044b870e37102f96e6b831a5fa03e0f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 23 Nov 2021 11:56:16 +0100 Subject: [PATCH 0755/1452] Fix missing mocking (#60181) --- .../components/samsungtv/test_config_flow.py | 189 +++++++++++------- 1 file changed, 112 insertions(+), 77 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index adf23b4f2da..432596eeca2 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -124,6 +124,14 @@ MOCK_DEVICE_INFO = { }, "id": "123", } +MOCK_DEVICE_INFO_2 = { + "device": { + "type": "Samsung SmartTV", + "name": "fake2_name", + "modelName": "fake2_model", + }, + "id": "345", +} AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -208,7 +216,9 @@ async def test_user_websocket(hass: HomeAssistant, remotews: Mock): assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_user_legacy_missing_auth(hass: HomeAssistant, remote: Mock): +async def test_user_legacy_missing_auth( + hass: HomeAssistant, remote: Mock, remotews: Mock +): """Test starting a flow by user with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -288,57 +298,67 @@ async def test_user_not_successful_2(hass: HomeAssistant, remotews: Mock): async def test_ssdp(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery.""" - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - - # entry was added - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "create_entry" - assert result["title"] == "fake_model" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_model" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" - - -async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): - """Test starting a flow from discovery without prefixes.""" - - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_SSDP_DATA_NOPREFIX, - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - with patch( - "homeassistant.components.samsungtv.bridge.Remote.__enter__", - return_value=True, - ): + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO, + ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "fake2_model" - assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_model" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" - assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + assert result["title"] == "fake_name (fake_model)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): +async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): + """Test starting a flow from discovery without prefixes.""" + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO_2, + ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_NOPREFIX, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + with patch( + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, + ): + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake2_name (fake2_model)" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "fake2_name" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" + assert result["data"][CONF_MODEL] == "fake2_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + + +async def test_ssdp_legacy_missing_auth( + hass: HomeAssistant, remote: Mock, remotews: Mock +): """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -365,7 +385,9 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_AUTH_MISSING -async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock): +async def test_ssdp_legacy_not_supported( + hass: HomeAssistant, remote: Mock, remotews: Mock +): """Test starting a flow from discovery for not supported device.""" # confirm to add the entry @@ -505,43 +527,53 @@ async def test_ssdp_not_successful_2( async def test_ssdp_already_in_progress(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery twice.""" - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO, + ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): - # failed as already in progress - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_ALREADY_IN_PROGRESS + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # failed as already in progress + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_ALREADY_IN_PROGRESS async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery when already configured.""" - # entry was added - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "create_entry" - entry = result["result"] - assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER - assert entry.data[CONF_MODEL] is None - assert entry.unique_id is None + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO, + ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): - # failed as already configured - result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result2["type"] == "abort" - assert result2["reason"] == RESULT_ALREADY_CONFIGURED + # entry was added + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + entry = result["result"] + assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER + assert entry.data[CONF_MODEL] is None + assert entry.unique_id is None - # check updated device info - assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + # failed as already configured + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result2["type"] == "abort" + assert result2["reason"] == RESULT_ALREADY_CONFIGURED + + # check updated device info + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" async def test_import_legacy(hass: HomeAssistant, remote: Mock): @@ -549,7 +581,7 @@ async def test_import_legacy(hass: HomeAssistant, remote: Mock): with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", return_value="fake_host", - ): + ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -570,7 +602,10 @@ async def test_import_legacy(hass: HomeAssistant, remote: Mock): async def test_import_legacy_without_name( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock + hass: HomeAssistant, + remote: Mock, + remotews_no_device_info: Mock, + no_mac_address: Mock, ): """Test importing from yaml without a name.""" with patch( @@ -595,7 +630,7 @@ async def test_import_legacy_without_name( assert entries[0].data[CONF_PORT] == LEGACY_PORT -async def test_import_websocket(hass: HomeAssistant): +async def test_import_websocket(hass: HomeAssistant, remotews: Mock): """Test importing from yaml with hostname.""" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", @@ -929,7 +964,7 @@ async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock ] -async def test_update_old_entry(hass: HomeAssistant, remote: Mock): +async def test_update_old_entry(hass: HomeAssistant, remote: Mock, remotews: Mock): """Test update of old entry.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: remote().rest_device_info.return_value = { From 560546f65e81fb8cf9ffab6496445007b852f3a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 13:35:53 +0100 Subject: [PATCH 0756/1452] Use dataclass for DhcpServiceInfo (#60136) Co-authored-by: epenet --- homeassistant/components/dhcp/__init__.py | 29 +++++++++++++++++-- homeassistant/data_entry_flow.py | 8 ++++- homeassistant/helpers/frame.py | 14 ++++++--- .../components/gogogate2/test_config_flow.py | 12 ++++++-- .../test_config_flow.py | 4 ++- .../components/samsungtv/test_config_flow.py | 12 ++++++-- .../screenlogic/test_config_flow.py | 1 + tests/components/tplink/test_config_flow.py | 8 +++-- tests/components/verisure/test_config_flow.py | 4 ++- tests/components/yeelight/test_config_flow.py | 20 +++++++++---- 10 files changed, 89 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index be6aa5072d8..a45de7b0c16 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,12 +1,13 @@ """The dhcp integration.""" +from dataclasses import dataclass from datetime import timedelta import fnmatch from ipaddress import ip_address as make_ip_address import logging import os import threading -from typing import Final, TypedDict +from typing import Any, Final from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( @@ -32,12 +33,14 @@ from homeassistant.const import ( STATE_HOME, ) from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, ) +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_dhcp from homeassistant.util.async_ import run_callback_threadsafe @@ -55,13 +58,33 @@ SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) -class DhcpServiceInfo(TypedDict): +@dataclass +class DhcpServiceInfo(BaseServiceInfo): """Prepared info from dhcp entries.""" - ip: str + ip: str # pylint: disable=invalid-name hostname: str macaddress: str + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"dhcp"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + return getattr(self, name) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ae5feb3d198..e84689ee269 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import abc import asyncio from collections.abc import Iterable, Mapping +from dataclasses import dataclass from types import MappingProxyType from typing import Any, TypedDict import uuid @@ -25,6 +26,11 @@ RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" +@dataclass +class BaseServiceInfo: + """Base class for discovery ServiceInfo.""" + + class FlowError(HomeAssistantError): """Error while configuring an account.""" @@ -301,7 +307,7 @@ class FlowManager(abc.ABC): self, flow: Any, step_id: str, - user_input: dict | None, + user_input: dict | BaseServiceInfo | None, step_done: asyncio.Future | None = None, ) -> FlowResult: """Handle a step of a flow.""" diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 8105c4f6c0e..3995f24102d 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -51,7 +51,10 @@ class MissingIntegrationFrame(HomeAssistantError): def report( - what: str, exclude_integrations: set | None = None, error_if_core: bool = True + what: str, + exclude_integrations: set | None = None, + error_if_core: bool = True, + level: int = logging.WARNING, ) -> None: """Report incorrect usage. @@ -68,11 +71,13 @@ def report( _LOGGER.warning(msg, stack_info=True) return - report_integration(what, integration_frame) + report_integration(what, integration_frame, level) def report_integration( - what: str, integration_frame: tuple[FrameSummary, str, str] + what: str, + integration_frame: tuple[FrameSummary, str, str], + level: int = logging.WARNING, ) -> None: """Report incorrect usage in an integration. @@ -86,7 +91,8 @@ def report_integration( else: extra = "" - _LOGGER.warning( + _LOGGER.log( + level, "Detected integration that %s. " "Please report issue%s for %s using this method at %s, line %s: %s", what, diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index a70959cfd09..9b4f64fa903 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -191,7 +191,9 @@ async def test_discovered_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress=MOCK_MAC_ADDR), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" + ), ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} @@ -246,7 +248,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress=MOCK_MAC_ADDR), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" + ), ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" @@ -254,7 +258,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip="1.2.3.4", macaddress="00:00:00:00:00:00"), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), ) assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 1631cd2cb60..ec82cc0c4d1 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -23,7 +23,9 @@ ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ) DHCP_DISCOVERY_INFO = dhcp.DhcpServiceInfo( - hostname="Hunter Douglas Powerview Hub", ip="1.2.3.4" + hostname="Hunter Douglas Powerview Hub", + ip="1.2.3.4", + macaddress="AA:BB:CC:DD:EE:FF", ) DISCOVERY_DATA = [ diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 432596eeca2..79420d6f303 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -84,7 +84,9 @@ MOCK_SSDP_DATA_WRONGMODEL = { ATTR_UPNP_MODEL_NAME: "HW-Qfake", ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", } -MOCK_DHCP_DATA = dhcp.DhcpServiceInfo(ip="fake_host", macaddress="aa:bb:cc:dd:ee:ff") +MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( + ip="fake_host", macaddress="aa:bb:cc:dd:ee:ff", hostname="fake_hostname" +) EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host="fake_host", @@ -1131,7 +1133,9 @@ async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff"), + data=dhcp.DhcpServiceInfo( + ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff", hostname="fake_hostname" + ), ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -1165,7 +1169,9 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mo result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff"), + data=dhcp.DhcpServiceInfo( + ip=EXISTING_IP, macaddress="aa:bb:cc:dd:ee:ff", hostname="fake_hostname" + ), ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index 9a6715bc401..375bcddac24 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -141,6 +141,7 @@ async def test_dhcp(hass): data=dhcp.DhcpServiceInfo( hostname="Pentair: 01-01-01", ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", ), ) diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index d19b47ffd86..5a4e672a384 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -321,7 +321,9 @@ async def test_discovered_by_discovery_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="00:00:00:00:00:00"), + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -331,7 +333,9 @@ async def test_discovered_by_discovery_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip="1.2.3.5", macaddress="00:00:00:00:00:01"), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 41cabb976c9..356cd8fd63c 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -176,7 +176,9 @@ async def test_dhcp(hass: HomeAssistant) -> None: """Test that DHCP discovery works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo(macaddress="01:23:45:67:89:ab"), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", macaddress="01:23:45:67:89:ab", hostname="mock_hostname" + ), context={"source": config_entries.SOURCE_DHCP}, ) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index b3466fb2525..cc2a3600337 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -471,7 +471,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff", hostname="mock_hostname" + ), ) await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT @@ -483,7 +485,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="00:00:00:00:00:00"), + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -497,7 +501,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo(ip="1.2.3.5", macaddress="00:00:00:00:00:01"), + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ), ) await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT @@ -509,7 +515,9 @@ async def test_discovered_by_homekit_and_dhcp(hass): [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff", hostname="mock_hostname" + ), ), ( config_entries.SOURCE_HOMEKIT, @@ -570,7 +578,9 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff"), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff", hostname="mock_hostname" + ), ), ( config_entries.SOURCE_HOMEKIT, From e615e70e4945e4a42f040dfb717bbc46025e278f Mon Sep 17 00:00:00 2001 From: micha91 Date: Tue, 23 Nov 2021 16:29:34 +0100 Subject: [PATCH 0757/1452] Add Yamaha MusicCast zone specific devices (#58285) --- .../components/yamaha_musiccast/__init__.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index a8ef373e3dc..2245dd952a9 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import BRAND, CONF_SERIAL, CONF_UPNP_DESC, DOMAIN +from .const import BRAND, CONF_SERIAL, CONF_UPNP_DESC, DEFAULT_ZONE, DOMAIN PLATFORMS = ["media_player"] @@ -150,22 +150,43 @@ class MusicCastEntity(CoordinatorEntity): class MusicCastDeviceEntity(MusicCastEntity): """Defines a MusicCast device entity.""" + _zone_id: str = DEFAULT_ZONE + + @property + def device_id(self): + """Return the ID of the current device.""" + if self._zone_id == DEFAULT_ZONE: + return self.coordinator.data.device_id + return f"{self.coordinator.data.device_id}_{self._zone_id}" + + @property + def device_name(self): + """Return the name of the current device.""" + return self.coordinator.data.zones[self._zone_id].name + @property def device_info(self) -> DeviceInfo: """Return device information about this MusicCast device.""" - return DeviceInfo( - connections={ - (CONNECTION_NETWORK_MAC, format_mac(mac)) - for mac in self.coordinator.data.mac_addresses.values() - }, + + device_info = DeviceInfo( + name=self.device_name, identifiers={ ( DOMAIN, - self.coordinator.data.device_id, + self.device_id, ) }, - name=self.coordinator.data.network_name, manufacturer=BRAND, model=self.coordinator.data.model_name, sw_version=self.coordinator.data.system_version, ) + + if self._zone_id == DEFAULT_ZONE: + device_info["connections"] = { + (CONNECTION_NETWORK_MAC, format_mac(mac)) + for mac in self.coordinator.data.mac_addresses.values() + } + else: + device_info["via_device"] = (DOMAIN, self.coordinator.data.device_id) + + return device_info From 1d3fbc93a0af6436f1779b344a321792c016d082 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 16:32:58 +0100 Subject: [PATCH 0758/1452] Update ZeroconfServiceInfo in tests (a-f) (#60212) Co-authored-by: epenet --- tests/components/apple_tv/test_config_flow.py | 17 +++++-- tests/components/axis/test_config_flow.py | 16 +++++-- tests/components/bond/test_config_flow.py | 45 ++++++++++++++++--- .../components/bosch_shc/test_config_flow.py | 19 +++++--- tests/components/brother/test_config_flow.py | 31 +++++++++++-- tests/components/daikin/test_config_flow.py | 13 +++++- tests/components/devolo_home_control/const.py | 16 ++++++- tests/components/devolo_home_network/const.py | 9 +++- tests/components/doorbird/test_config_flow.py | 24 +++++++--- tests/components/elgato/test_config_flow.py | 31 +++++++++++-- .../enphase_envoy/test_config_flow.py | 18 ++++++-- tests/components/esphome/test_config_flow.py | 24 +++++++--- .../forked_daapd/test_config_flow.py | 22 ++++++++- 13 files changed, 239 insertions(+), 46 deletions(-) diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index a2823ac2271..a99df9ad856 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -13,9 +13,12 @@ from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN from tests.common import MockConfigEntry DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( - type="_touch-able._tcp.local.", + host="mock_host", + hostname="mock_hostname", name="dmapid.something", + port=None, properties={"CtlN": "Apple TV"}, + type="_touch-able._tcp.local.", ) @@ -401,8 +404,12 @@ async def test_zeroconf_unsupported_service_aborts(hass): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - type="_dummy._tcp.local.", + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, properties={}, + type="_dummy._tcp.local.", ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -415,8 +422,12 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - type="_mediaremotetv._tcp.local.", + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + type="_mediaremotetv._tcp.local.", ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 744868e7911..2261a462113 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -364,9 +364,11 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host=DEFAULT_HOST, - port=80, + hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + port=80, properties={"macaddress": MAC}, + type="mock_type", ), ), ], @@ -412,9 +414,11 @@ async def test_discovered_device_already_configured( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="2.3.4.5", - port=8080, + hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + port=8080, properties={"macaddress": MAC}, + type="mock_type", ), 8080, ), @@ -480,9 +484,11 @@ async def test_discovery_flow_updated_configuration( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="", - port=0, + hostname="mock_hostname", name="", + port=0, properties={"macaddress": "01234567890"}, + type="mock_type", ), ), ], @@ -522,9 +528,11 @@ async def test_discovery_flow_ignore_non_axis_device( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="169.254.3.4", - port=80, + hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + port=80, properties={"macaddress": MAC}, + type="mock_type", ), ), ], diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 286e9fff9bf..e3f441ab56f 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -198,7 +198,12 @@ async def test_zeroconf_form(hass: core.HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="test-bond-id.some-other-tail-info", host="test-host" + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) assert result["type"] == "form" @@ -230,7 +235,12 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="test-bond-id.some-other-tail-info", host="test-host" + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -265,7 +275,12 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="test-bond-id.some-other-tail-info", host="test-host" + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -303,8 +318,12 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="already-registered-bond-id.some-other-tail-info", host="updated-host", + hostname="mock_hostname", + name="already-registered-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) @@ -344,8 +363,12 @@ async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistan DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="already-registered-bond-id.some-other-tail-info", host="updated-host", + hostname="mock_hostname", + name="already-registered-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -377,8 +400,12 @@ async def test_zeroconf_already_configured_no_reload_same_host( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - name="already-registered-bond-id.some-other-tail-info", host="stored-host", + hostname="mock_hostname", + name="already-registered-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -394,8 +421,12 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): hass, source=config_entries.SOURCE_ZEROCONF, initial_input=zeroconf.ZeroconfServiceInfo( - name="test-bond-id.some-other-tail-info", host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", ), user_input={CONF_ACCESS_TOKEN: "test-token"}, error=Exception(), diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 3e52772b30f..b10b76b1042 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -22,10 +22,11 @@ MOCK_SETTINGS = { } DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=["169.1.1.1", "1.1.1.1"], - port=0, hostname="shc012345.local.", - type="_http._tcp.local.", name="Bosch SHC [test-mac]._http._tcp.local.", + port=0, + properties={}, + type="_http._tcp.local.", ) @@ -530,10 +531,11 @@ async def test_zeroconf_link_local(hass, mock_zeroconf): """Test we get the form.""" DISCOVERY_INFO_LINK_LOCAL = zeroconf.ZeroconfServiceInfo( host=["169.1.1.1"], - port=0, hostname="shc012345.local.", - type="_http._tcp.local.", name="Bosch SHC [test-mac]._http._tcp.local.", + port=0, + properties={}, + type="_http._tcp.local.", ) with patch( @@ -552,7 +554,14 @@ async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=zeroconf.ZeroconfServiceInfo(host="1.1.1.1", name="notboschshc"), + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + hostname="mock_hostname", + name="notboschshc", + port=None, + properties={}, + type="mock_type", + ), context={"source": config_entries.SOURCE_ZEROCONF}, ) assert result["type"] == "abort" diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 82757fa425a..3bc7738bb8d 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -145,7 +145,12 @@ async def test_zeroconf_snmp_error(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer", properties={} + host="mock_host", + hostname="example.local.", + name="Brother Printer", + port=None, + properties={}, + type="mock_type", ), ) @@ -160,9 +165,12 @@ async def test_zeroconf_unsupported_model(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( + host="mock_host", hostname="example.local.", name="Brother Printer", + port=None, properties={"product": "MFC-8660DN"}, + type="mock_type", ), ) @@ -185,7 +193,12 @@ async def test_zeroconf_device_exists_abort(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer", properties={} + host="mock_host", + hostname="example.local.", + name="Brother Printer", + port=None, + properties={}, + type="mock_type", ), ) @@ -202,7 +215,12 @@ async def test_zeroconf_no_probe_existing_device(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="localhost", name="Brother Printer" + host="mock_host", + hostname="localhost", + name="Brother Printer", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -223,7 +241,12 @@ async def test_zeroconf_confirm_create_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - hostname="example.local.", name="Brother Printer", properties={} + host="mock_host", + hostname="example.local.", + name="Brother Printer", + port=None, + properties={}, + type="mock_type", ), ) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 56463022597..5421f6e1d52 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -120,7 +120,18 @@ async def test_api_password_abort(hass): @pytest.mark.parametrize( "source, data, unique_id", [ - (SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo(host=HOST), MAC), + ( + SOURCE_ZEROCONF, + zeroconf.ZeroconfServiceInfo( + host=HOST, + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), + MAC, + ), ], ) async def test_discovery_zeroconf( diff --git a/tests/components/devolo_home_control/const.py b/tests/components/devolo_home_control/const.py index 819f1c3e005..96686e204eb 100644 --- a/tests/components/devolo_home_control/const.py +++ b/tests/components/devolo_home_control/const.py @@ -20,7 +20,19 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ) DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = zeroconf.ZeroconfServiceInfo( - properties={"MT": "2700"} + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"MT": "2700"}, + type="mock_type", ) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo(properties={"Features": ""}) +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"Features": ""}, + type="mock_type", +) diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 44baab0e85c..681c2673dff 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -36,7 +36,14 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( }, ) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo(properties={"MT": "2600"}) +DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"MT": "2600"}, + type="mock_type", +) NEIGHBOR_ACCESS_POINTS = { "neighbor_aps": [ diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 9d579e4bbf5..ccb05b327ac 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -83,9 +83,12 @@ async def test_form_zeroconf_wrong_oui(hass): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"macaddress": "notdoorbirdoui"}, host="192.168.1.8", + hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "notdoorbirdoui"}, + type="mock_type", ), ) assert result["type"] == "abort" @@ -99,9 +102,12 @@ async def test_form_zeroconf_link_local_ignored(hass): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"macaddress": "1CCAE3DOORBIRD"}, host="169.254.103.61", + hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", ), ) assert result["type"] == "abort" @@ -122,9 +128,12 @@ async def test_form_zeroconf_correct_oui(hass): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"macaddress": "1CCAE3DOORBIRD"}, - name="Doorstation - abc123._axis-video._tcp.local.", host="192.168.1.5", + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -181,9 +190,12 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"macaddress": "1CCAE3DOORBIRD"}, - name="Doorstation - abc123._axis-video._tcp.local.", host="192.168.1.5", + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", ), ) await hass.async_block_till_done() diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 5a7487b5995..2ea2dab6acf 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -31,8 +31,10 @@ async def test_full_user_flow_implementation( data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", hostname="example.local.", + name="mock_name", port=9123, properties={}, + type="mock_type", ), ) @@ -74,8 +76,10 @@ async def test_full_zeroconf_flow_implementation( data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", hostname="example.local.", + name="mock_name", port=9123, properties={}, + type="mock_type", ), ) @@ -128,7 +132,14 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=9123), + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", + port=9123, + properties={}, + type="mock_type", + ), ) assert result["reason"] == "cannot_connect" @@ -159,7 +170,14 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host="127.0.0.1", port=9123), + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", + port=9123, + properties={}, + type="mock_type", + ), ) assert result["reason"] == "already_configured" @@ -168,7 +186,14 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host="127.0.0.2", port=9123), + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.2", + hostname="mock_hostname", + name="mock_name", + port=9123, + properties={}, + type="mock_type", + ), ) assert result["reason"] == "already_configured" diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 812ab87e848..fc9a7de188e 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -159,8 +159,12 @@ async def test_zeroconf(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"serialnum": "1234"}, host="1.1.1.1", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234"}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -255,8 +259,12 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"serialnum": "1234"}, host="1.1.1.1", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234"}, + type="mock_type", ), ) @@ -290,8 +298,12 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - properties={"serialnum": "1234"}, host="1.1.1.1", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234"}, + type="mock_type", ), ) await hass.async_block_till_done() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 1ae332142a0..25103c4fe2a 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -218,9 +218,11 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={}, + type="mock_type", ) flow = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info @@ -250,9 +252,11 @@ async def test_discovery_already_configured_hostname(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info @@ -275,9 +279,11 @@ async def test_discovery_already_configured_ip(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={"address": "192.168.43.183"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info @@ -304,9 +310,11 @@ async def test_discovery_already_configured_name(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.184", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={"address": "test8266.local"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info @@ -323,9 +331,11 @@ async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={"address": "test8266.local"}, + type="mock_type", ) mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) @@ -354,9 +364,11 @@ async def test_discovery_updates_unique_id(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", - port=6053, hostname="test8266.local.", + name="mock_name", + port=6053, properties={"address": "test8266.local"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 5c7bf8db97f..abd2f1ab3a2 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -101,8 +101,11 @@ async def test_zeroconf_updates_title(hass, config_entry): assert len(hass.config_entries.async_entries(DOMAIN)) == 2 discovery_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.1", + hostname="mock_hostname", + name="mock_name", port=23, properties={"mtd-version": "27.0", "Machine Name": "zeroconf_test"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info @@ -131,7 +134,12 @@ async def test_config_flow_zeroconf_invalid(hass): """Test that an invalid zeroconf entry doesn't work.""" # test with no discovery properties discovery_info = zeroconf.ZeroconfServiceInfo( - host="127.0.0.1", port=23, properties={} + host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", + port=23, + properties={}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info @@ -141,8 +149,11 @@ async def test_config_flow_zeroconf_invalid(hass): # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", port=23, properties={"mtd-version": "26.3", "Machine Name": "forked-daapd"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info @@ -152,8 +163,11 @@ async def test_config_flow_zeroconf_invalid(hass): # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", port=23, properties={"mtd-version": "0.2.4.1", "Machine Name": "firefly"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info @@ -163,8 +177,11 @@ async def test_config_flow_zeroconf_invalid(hass): # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + hostname="mock_hostname", + name="mock_name", port=23, properties={"mtd-version": "svn-1676", "Machine Name": "firefly"}, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info @@ -177,12 +194,15 @@ async def test_config_flow_zeroconf_valid(hass): """Test that a valid zeroconf entry works.""" discovery_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.1", + hostname="mock_hostname", + name="mock_name", port=23, properties={ "mtd-version": "27.0", "Machine Name": "zeroconf_test", "Machine ID": "5E55EEFF", }, + type="mock_type", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info From e7b05ef452dc574a209bbbb79d008e1c85a807a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 16:34:38 +0100 Subject: [PATCH 0759/1452] Adjust CI to split tests into 6 groups (#60198) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e95bd9034f8..8b02dd8a63f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -670,7 +670,7 @@ jobs: strategy: fail-fast: false matrix: - group: [1, 2, 3, 4] + group: [1, 2, 3, 4, 5, 6] python-version: [3.8, 3.9] name: >- Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) @@ -709,7 +709,7 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count 4 \ + --test-group-count 6 \ --test-group=${{ matrix.group }} \ --cov homeassistant \ --cov-report= \ From 7e08238cd87e02ec014e2777abdf3065eab7118c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Nov 2021 16:38:31 +0100 Subject: [PATCH 0760/1452] Bump pychromecast to 10.1.1 (#60214) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 9e79e395fde..3f3c31b8d3d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.1.0"], + "requirements": ["pychromecast==10.1.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 4165f857b97..5716e9a4d6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1393,7 +1393,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.1.0 +pychromecast==10.1.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ab1792c8ee..04551e57bf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -847,7 +847,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.1.0 +pychromecast==10.1.1 # homeassistant.components.climacell pyclimacell==0.18.2 From 31b033ac25e3540bbf1d92188a3275affb728480 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 16:42:38 +0100 Subject: [PATCH 0761/1452] Update ZeroconfServiceInfo in tests (g-m) (#60217) Co-authored-by: epenet --- .../components/gogogate2/test_config_flow.py | 35 ++++++++++++++++--- .../homekit_controller/test_config_flow.py | 4 +-- tests/components/hue/test_config_flow.py | 8 +++++ .../test_config_flow.py | 13 +++++-- .../lutron_caseta/test_config_flow.py | 16 +++++++++ .../modern_forms/test_config_flow.py | 24 +++++++++++-- 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 9b4f64fa903..a20687c5f3d 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -108,7 +108,12 @@ async def test_form_homekit_unique_id_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR}, + type="mock_type", ), ) assert result["type"] == RESULT_TYPE_FORM @@ -130,7 +135,12 @@ async def test_form_homekit_unique_id_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR}, + type="mock_type", ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -149,7 +159,12 @@ async def test_form_homekit_ip_address_already_setup(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR}, + type="mock_type", ), ) assert result["type"] == RESULT_TYPE_ABORT @@ -162,7 +177,12 @@ async def test_form_homekit_ip_address(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR}, + type="mock_type", ), ) assert result["type"] == RESULT_TYPE_FORM @@ -239,7 +259,12 @@ async def test_discovered_by_homekit_and_dhcp(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="1.2.3.4", properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR} + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC_ADDR}, + type="mock_type", ), ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index c375dc3dd4d..6c030a3c573 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -141,10 +141,9 @@ def get_device_discovery_info( record = device.info result = zeroconf.ZeroconfServiceInfo( host=record["address"], - port=record["port"], hostname=record["name"], - type="_hap._tcp.local.", name=record["name"], + port=record["port"], properties={ "md": record["md"], "pv": record["pv"], @@ -156,6 +155,7 @@ def get_device_discovery_info( "sf": 0x01, # record["sf"], "sh": "", }, + type="_hap._tcp.local.", ) if missing_csharp: diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 3079875eff1..9d5fc280ff6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -517,7 +517,11 @@ async def test_bridge_homekit(hass, aioclient_mock): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ) @@ -560,7 +564,11 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ) diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index ec82cc0c4d1..7f5d4a569ed 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -12,14 +12,21 @@ from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from tests.common import MockConfigEntry, load_fixture HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( - name="Hunter Douglas Powerview Hub._hap._tcp.local.", host="1.2.3.4", - properties={"id": "AA::BB::CC::DD::EE::FF"}, + hostname="mock_hostname", + name="Hunter Douglas Powerview Hub._hap._tcp.local.", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA::BB::CC::DD::EE::FF"}, + type="mock_type", ) ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( - name="Hunter Douglas Powerview Hub._powerview._tcp.local.", host="1.2.3.4", + hostname="mock_hostname", + name="Hunter Douglas Powerview Hub._powerview._tcp.local.", + port=None, + properties={}, + type="mock_type", ) DHCP_DISCOVERY_INFO = dhcp.DhcpServiceInfo( diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index ee9403bad50..e22d759c1b3 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -428,6 +428,10 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", hostname="lutron-abc.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -451,6 +455,10 @@ async def test_zeroconf_lutron_id_already_configured(hass): data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", hostname="lutron-abc.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -469,6 +477,10 @@ async def test_zeroconf_not_lutron_device(hass): data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", hostname="notlutron-abc.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -493,6 +505,10 @@ async def test_zeroconf(hass, source, tmpdir): data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", hostname="lutron-abc.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) await hass.async_block_till_done() diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 39db815064b..b1b2eb618af 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -70,7 +70,12 @@ async def test_full_zeroconf_flow_implementation( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.local.", properties={} + host="192.168.1.123", + hostname="example.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -134,7 +139,12 @@ async def test_zeroconf_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.local.", properties={} + host="192.168.1.123", + hostname="example.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -160,7 +170,12 @@ async def test_zeroconf_confirm_connection_error( CONF_NAME: "test", }, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.com.", properties={} + host="192.168.1.123", + hostname="example.com.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -226,7 +241,10 @@ async def test_zeroconf_with_mac_device_exists_abort( data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", hostname="example.local.", + name="mock_name", + port=None, properties={CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", ), ) From f6bbdec6cb1d7ce941fe7729efd5c54124e8621c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 16:52:33 +0100 Subject: [PATCH 0762/1452] Use BaseServiceInfo inheritance in MqttServiceInfo (#60207) Co-authored-by: epenet --- homeassistant/components/mqtt/discovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 8ccfca96c26..3826ce81d92 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -12,7 +12,7 @@ from typing import Any from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -92,7 +92,7 @@ class MQTTConfig(dict): @dataclass -class MqttServiceInfo: +class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" topic: str From d249743ccf29efc8dbfaacdabfa79ce015f6ae07 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 23 Nov 2021 10:53:46 -0500 Subject: [PATCH 0763/1452] Bump zwave-js-server-python to 0.33.0 (#60213) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index a9fe3d6e4fb..4e65a9fe093 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.32.0"], + "requirements": ["zwave-js-server-python==0.33.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5716e9a4d6c..34978289ce9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,4 +2504,4 @@ zigpy==0.41.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.32.0 +zwave-js-server-python==0.33.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04551e57bf6..6846e9e9f21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1478,4 +1478,4 @@ zigpy-znp==0.6.1 zigpy==0.41.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.32.0 +zwave-js-server-python==0.33.0 From e3910856ad64cf8189262f4eac4fa5f6609689bf Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:18:58 +0100 Subject: [PATCH 0764/1452] Use native datetime value in Shelly sensors (#60179) Co-authored-by: Franck Nijhof --- homeassistant/components/shelly/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 783153e2746..d61140f56b9 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -153,16 +153,15 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool: return button_type in ["momentary", "momentary_on_release"] -def get_device_uptime(uptime: float, last_uptime: str | None) -> str: +def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime: """Return device uptime string, tolerate up to 5 seconds deviation.""" delta_uptime = utcnow() - timedelta(seconds=uptime) if ( not last_uptime - or abs((delta_uptime - datetime.fromisoformat(last_uptime)).total_seconds()) - > UPTIME_DEVIATION + or abs((delta_uptime - last_uptime).total_seconds()) > UPTIME_DEVIATION ): - return delta_uptime.replace(microsecond=0).isoformat() + return delta_uptime return last_uptime From 7a0ff4e2e11e641bd864988110c9c88709d3df3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 17:19:13 +0100 Subject: [PATCH 0765/1452] Add button support to Alexa (#60163) --- homeassistant/components/alexa/entities.py | 17 +++++++++++++++++ homeassistant/components/alexa/handlers.py | 7 ++++++- tests/components/alexa/test_smart_home.py | 22 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d74f9329812..2f7f6dc996b 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -9,6 +9,7 @@ from homeassistant.components import ( alert, automation, binary_sensor, + button, camera, cover, fan, @@ -424,6 +425,22 @@ class SwitchCapabilities(AlexaEntity): ] +@ENTITY_ADAPTERS.register(button.DOMAIN) +class ButtonCapabilities(AlexaEntity): + """Class to represent Button capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.ACTIVITY_TRIGGER] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaSceneController(self.entity, supports_deactivation=False), + Alexa(self.hass), + ] + + @ENTITY_ADAPTERS.register(climate.DOMAIN) class ClimateCapabilities(AlexaEntity): """Class to represent Climate capabilities.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index edf900bb18f..edd510f7844 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -4,6 +4,7 @@ import math from homeassistant import core as ha from homeassistant.components import ( + button, camera, cover, fan, @@ -313,9 +314,13 @@ async def async_api_activate(hass, config, directive, context): entity = directive.entity domain = entity.domain + service = SERVICE_TURN_ON + if domain == button.DOMAIN: + service = button.SERVICE_PRESS + await hass.services.async_call( domain, - SERVICE_TURN_ON, + service, {ATTR_ENTITY_ID: entity.entity_id}, blocking=False, context=context, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 99d43816050..12708c9b55a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -25,7 +25,7 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.vacuum as vacuum from homeassistant.config import async_process_ha_core_config -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component @@ -3937,3 +3937,23 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): "https://mycamerastream.test/api/camera_proxy/camera.demo_camera?token=" in response["payload"]["imageUri"] ) + + +async def test_button(hass): + """Test button discovery.""" + device = ("button.ring_doorbell", STATE_UNKNOWN, {"friendly_name": "Ring Doorbell"}) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "button#ring_doorbell" + assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" + assert appliance["friendlyName"] == "Ring Doorbell" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert scene_capability["supportsDeactivation"] is False + + await assert_scene_controller_works( + "button#ring_doorbell", "button.press", False, hass + ) From 77dfeb062f902f4e25450e791160fdf7ce515695 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:35:44 +0100 Subject: [PATCH 0766/1452] Update ZeroconfServiceInfo in tests (n-t) (#60219) Co-authored-by: epenet --- tests/components/nam/test_config_flow.py | 9 +++- tests/components/nanoleaf/test_config_flow.py | 8 +++- tests/components/netatmo/test_config_flow.py | 4 ++ tests/components/nut/test_config_flow.py | 9 +++- .../components/octoprint/test_config_flow.py | 8 +++- tests/components/plugwise/test_config_flow.py | 10 +++-- tests/components/rachio/test_config_flow.py | 14 +++++- .../rainmachine/test_config_flow.py | 45 ++++++++++++++++--- tests/components/roku/__init__.py | 5 ++- .../components/samsungtv/test_config_flow.py | 3 ++ tests/components/shelly/test_config_flow.py | 5 ++- tests/components/sonos/test_config_flow.py | 7 ++- tests/components/tado/test_config_flow.py | 14 +++++- 13 files changed, 119 insertions(+), 22 deletions(-) diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 5a3e46cedb9..015c645a3e7 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -12,7 +12,14 @@ from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZERO from tests.common import MockConfigEntry -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(host="10.10.2.3") +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + host="10.10.2.3", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", +) VALID_CONFIG = {"host": "10.10.2.3"} VALID_AUTH = {"username": "fake_username", "password": "fake_password"} diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 5e781499325..9faf48b625c 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -238,9 +238,11 @@ async def test_discovery_link_unavailable( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery_info}", - type=type_in_discovery_info, + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + type=type_in_discovery_info, ), ) assert result["type"] == "form" @@ -420,9 +422,11 @@ async def test_import_discovery_integration( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery}", - type=type_in_discovery, + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + type=type_in_discovery, ), ) assert result["type"] == "create_entry" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index b38c59055b6..b97f4c8b4ec 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -42,7 +42,11 @@ async def test_abort_if_existing_entry(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 892d74b4713..733d5807c5f 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -35,7 +35,14 @@ async def test_form_zeroconf(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.5", port=1234), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.5", + hostname="mock_hostname", + name="mock_name", + port=1234, + properties={}, + type="mock_type", + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 2c8143fb78c..b55d43fda9e 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -172,9 +172,11 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", - port=80, hostname="example.local.", + name="mock_name", + port=80, properties={"uuid": "83747482", "path": "/foo/"}, + type="mock_type", ), ) assert result["type"] == "form" @@ -487,9 +489,11 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", - port=80, hostname="example.local.", + name="mock_name", + port=80, properties={"uuid": "83747482", "path": "/foo/"}, + type="mock_type", ), ) assert result["type"] == "abort" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 72e695a4e9c..9f9be299f84 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -42,25 +42,27 @@ TEST_USERNAME2 = "stretch" TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( host=TEST_HOST, - port=DEFAULT_PORT, hostname=f"{TEST_HOSTNAME}.local.", - server=f"{TEST_HOSTNAME}.local.", + name="mock_name", + port=DEFAULT_PORT, properties={ "product": "smile", "version": "1.2.3", "hostname": f"{TEST_HOSTNAME}.local.", }, + type="mock_type", ) TEST_DISCOVERY2 = zeroconf.ZeroconfServiceInfo( host=TEST_HOST, - port=DEFAULT_PORT, hostname=f"{TEST_HOSTNAME2}.local.", - server=f"{TEST_HOSTNAME2}.local.", + name="mock_name", + port=DEFAULT_PORT, properties={ "product": "stretch", "version": "1.2.3", "hostname": f"{TEST_HOSTNAME2}.local.", }, + type="mock_type", ) diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 8afcaca1886..cf9e811ed5a 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -113,7 +113,12 @@ async def test_form_homekit(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", ), ) assert result["type"] == "form" @@ -132,7 +137,12 @@ async def test_form_homekit(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", ), ) assert result["type"] == "abort" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 1ce95f76ac5..35824296cc6 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -235,7 +235,14 @@ async def test_step_homekit_zeroconf_ip_already_exists(hass, source): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -264,7 +271,14 @@ async def test_step_homekit_zeroconf_ip_change(hass, source): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.2"), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.2", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -295,7 +309,14 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist(hass, source result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -339,7 +360,14 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -352,7 +380,14 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo(host="192.168.1.100"), + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.100", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", + ), ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index d97d30e66ce..ece58de763a 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -33,11 +33,14 @@ MOCK_SSDP_DISCOVERY_INFO = { HOMEKIT_HOST = "192.168.1.161" MOCK_HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( - name="onn._hap._tcp.local.", host=HOMEKIT_HOST, + hostname="mock_hostname", + name="onn._hap._tcp.local.", + port=None, properties={ zeroconf.ATTR_PROPERTIES_ID: "2d:97:da:ee:dc:99", }, + type="mock_type", ) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 79420d6f303..4c5d77fcc1f 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -90,6 +90,8 @@ MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host="fake_host", + hostname="mock_hostname", + name="mock_name", port=1234, properties={ "deviceid": "aa:bb:cc:dd:ee:ff", @@ -97,6 +99,7 @@ MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( "model": "fake_model", "serialNumber": "fake_serial", }, + type="mock_type", ) MOCK_OLD_ENTRY = { CONF_HOST: "fake_host", diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index ec88856603a..2d9cb20342f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -19,8 +19,11 @@ MOCK_SETTINGS = { } DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + hostname="mock_hostname", name="shelly1pm-12345", - properties={"id": "shelly1pm-12345"}, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + type="mock_type", ) MOCK_CONFIG = { "wifi": {"ap": {"ssid": "Test name"}}, diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 4d4e0cc4cad..9677ee73759 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -46,9 +46,11 @@ async def test_zeroconf_form(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.4.2", - name="Sonos-aaa@Living Room._sonos._tcp.local.", hostname="Sonos-aaa", + name="Sonos-aaa@Living Room._sonos._tcp.local.", + port=None, properties={"bootseq": "1234"}, + type="mock_type", ), ) assert result["type"] == "form" @@ -136,7 +138,10 @@ async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): data=zeroconf.ZeroconfServiceInfo( host="192.168.4.2", hostname="not-aaa", + name="mock_name", + port=None, properties={"bootseq": "1234"}, + type="mock_type", ), ) assert result["type"] == "abort" diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 327747cb841..4c234fee248 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -128,7 +128,12 @@ async def test_form_homekit(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", ), ) assert result["type"] == "form" @@ -149,7 +154,12 @@ async def test_form_homekit(hass): DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"} + host="mock_host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", ), ) assert result["type"] == "abort" From cbbf22db52f19b8ba77769352fc020460eb358ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:51:54 +0100 Subject: [PATCH 0767/1452] Use dataclass for UsbServiceInfo (#60140) Co-authored-by: epenet --- homeassistant/components/usb/__init__.py | 26 ++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 355b60906b3..ba63e59e8f1 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import TypedDict +from typing import Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -17,8 +17,10 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow, system_info from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_usb @@ -31,7 +33,8 @@ _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown -class UsbServiceInfo(TypedDict): +@dataclasses.dataclass +class UsbServiceInfo(BaseServiceInfo): """Prepared info from usb entries.""" device: str @@ -41,6 +44,25 @@ class UsbServiceInfo(TypedDict): manufacturer: str | None description: str | None + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"usb"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + return getattr(self, name) + def human_readable_device_name( device: str, From 9f74ad06d67bea1835fc175e2f74d918fadd0758 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 18:28:50 +0100 Subject: [PATCH 0768/1452] Update ZeroconfServiceInfo in tests (t-z) (#60221) Co-authored-by: epenet --- tests/components/tradfri/test_config_flow.py | 26 ++++++++++++++-- tests/components/volumio/test_config_flow.py | 3 ++ tests/components/wled/test_config_flow.py | 31 ++++++++++++++++--- .../xiaomi_aqara/test_config_flow.py | 13 +++++++- .../xiaomi_miio/test_config_flow.py | 25 +++++++++++++-- tests/components/yeelight/test_config_flow.py | 12 +++++++ 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index b4d80343238..12726bc553a 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -106,7 +106,11 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + type="mock_type", ), ) @@ -256,7 +260,12 @@ async def test_discovery_duplicate_aborted(hass): "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="new-host", properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"} + host="new-host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + type="mock_type", ), ) @@ -287,7 +296,11 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + type="mock_type", ), ) @@ -298,7 +311,11 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + type="mock_type", ), ) @@ -317,7 +334,12 @@ async def test_discovery_updates_unique_id(hass): "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( - host="some-host", properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"} + host="some-host", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + type="mock_type", ), ) diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index f217ed9919b..53eaef2ddf0 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -19,8 +19,11 @@ TEST_CONNECTION = { TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + hostname="mock_hostname", + name="mock_name", port=3000, properties={"volumioName": "discovered", "UUID": "2222-2222-2222-2222"}, + type="mock_type", ) TEST_DISCOVERY_RESULT = { diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 5fc735b4742..770d6abd2f8 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -49,7 +49,12 @@ async def test_full_zeroconf_flow_implementation( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.local.", properties={} + host="192.168.1.123", + hostname="example.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -104,7 +109,12 @@ async def test_zeroconf_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.local.", properties={} + host="192.168.1.123", + hostname="example.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -126,7 +136,12 @@ async def test_zeroconf_confirm_connection_error( CONF_NAME: "test", }, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.com.", properties={} + host="192.168.1.123", + hostname="example.com.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -160,7 +175,12 @@ async def test_zeroconf_device_exists_abort( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="192.168.1.123", hostname="example.local.", properties={} + host="192.168.1.123", + hostname="example.local.", + name="mock_name", + port=None, + properties={}, + type="mock_type", ), ) @@ -180,7 +200,10 @@ async def test_zeroconf_with_mac_device_exists_abort( data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", hostname="example.local.", + name="mock_name", + port=None, properties={CONF_MAC: "aabbccddeeff"}, + type="mock_type", ), ) diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 27000af8a05..554e0460443 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -403,8 +403,11 @@ async def test_zeroconf_success(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name=TEST_ZEROCONF_NAME, + port=None, properties={ZEROCONF_MAC: TEST_MAC}, + type="mock_type", ), ) @@ -445,7 +448,12 @@ async def test_zeroconf_missing_data(hass): const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host=TEST_HOST, name=TEST_ZEROCONF_NAME, properties={} + host=TEST_HOST, + hostname="mock_hostname", + name=TEST_ZEROCONF_NAME, + port=None, + properties={}, + type="mock_type", ), ) @@ -460,8 +468,11 @@ async def test_zeroconf_unknown_device(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name="not-a-xiaomi-aqara-gateway", + port=None, properties={ZEROCONF_MAC: TEST_MAC}, + type="mock_type", ), ) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 16c9057379b..3be52e83237 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -394,8 +394,11 @@ async def test_zeroconf_gateway_success(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name=TEST_ZEROCONF_NAME, + port=None, properties={ZEROCONF_MAC: TEST_MAC}, + type="mock_type", ), ) @@ -433,8 +436,11 @@ async def test_zeroconf_unknown_device(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name="not-a-xiaomi-miio-device", + port=None, properties={ZEROCONF_MAC: TEST_MAC}, + type="mock_type", ), ) @@ -447,7 +453,14 @@ async def test_zeroconf_no_data(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo(host=None, name=None, properties={}), + data=zeroconf.ZeroconfServiceInfo( + host=None, + hostname="mock_hostname", + name=None, + port=None, + properties={}, + type="mock_type", + ), ) assert result["type"] == "abort" @@ -460,7 +473,12 @@ async def test_zeroconf_missing_data(hass): const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host=TEST_HOST, name=TEST_ZEROCONF_NAME, properties={} + host=TEST_HOST, + hostname="mock_hostname", + name=TEST_ZEROCONF_NAME, + port=None, + properties={}, + type="mock_type", ), ) @@ -753,8 +771,11 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + hostname="mock_hostname", name=zeroconf_name_to_test, + port=None, properties={"poch": f"0:mac={TEST_MAC_DEVICE}\x00"}, + type="mock_type", ), ) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index cc2a3600337..a983c4a2127 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -458,7 +458,11 @@ async def test_discovered_by_homekit_and_dhcp(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ) await hass.async_block_till_done() @@ -523,7 +527,11 @@ async def test_discovered_by_homekit_and_dhcp(hass): config_entries.SOURCE_HOMEKIT, zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ), ], @@ -586,7 +594,11 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): config_entries.SOURCE_HOMEKIT, zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + hostname="mock_hostname", + name="mock_name", + port=None, properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", ), ), ], From 400aaeaa9120e491657a24707faa72e68bfb5bee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 11:40:20 -0600 Subject: [PATCH 0769/1452] Ensure homekit setup messages run in event loop (#60226) --- homeassistant/components/homekit/__init__.py | 8 +++--- .../components/homekit/accessories.py | 11 +++++--- homeassistant/components/homekit/util.py | 10 ++++--- tests/components/homekit/test_accessories.py | 4 +-- tests/components/homekit/test_homekit.py | 26 +++++++++---------- tests/components/homekit/test_util.py | 22 +++++++++------- 6 files changed, 44 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b372a863532..cfa734559fc 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -102,11 +102,11 @@ from .const import ( from .type_triggers import DeviceTriggerAccessory from .util import ( accessory_friendly_name, + async_dismiss_setup_message, async_port_is_available, - dismiss_setup_message, + async_show_setup_message, get_persist_fullpath_for_entry_id, remove_state_files_for_entry_id, - show_setup_message, state_needs_accessory_mode, validate_entity_config, ) @@ -321,7 +321,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - dismiss_setup_message(hass, entry.entry_id) + async_dismiss_setup_message(hass, entry.entry_id) homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] if homekit.status == STATUS_RUNNING: @@ -719,7 +719,7 @@ class HomeKit: @callback def _async_show_setup_message(self): """Show the pairing setup message.""" - show_setup_message( + async_show_setup_message( self.hass, self._entry_id, accessory_friendly_name(self._entry_title, self.driver.accessory), diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 06c70c09f2e..ca12daa33b3 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,6 +4,7 @@ import logging from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER +from pyhap.util import callback as pyhap_callback from homeassistant.components import cover from homeassistant.components.cover import ( @@ -80,10 +81,10 @@ from .const import ( ) from .util import ( accessory_friendly_name, + async_dismiss_setup_message, + async_show_setup_message, convert_to_float, - dismiss_setup_message, format_sw_version, - show_setup_message, validate_media_player_features, ) @@ -550,13 +551,15 @@ class HomeDriver(AccessoryDriver): self._bridge_name = bridge_name self._entry_title = entry_title + @pyhap_callback def pair(self, client_uuid, client_public, client_permissions): """Override super function to dismiss setup message if paired.""" success = super().pair(client_uuid, client_public, client_permissions) if success: - dismiss_setup_message(self.hass, self._entry_id) + async_dismiss_setup_message(self.hass, self._entry_id) return success + @pyhap_callback def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" super().unpair(client_uuid) @@ -564,7 +567,7 @@ class HomeDriver(AccessoryDriver): if self.state.paired: return - show_setup_message( + async_show_setup_message( self.hass, self._entry_id, accessory_friendly_name(self._entry_title, self.accessory), diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 7782c389e56..11b0f6cf925 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -315,7 +315,7 @@ def validate_media_player_features(state, feature_list): return True -def show_setup_message(hass, entry_id, bridge_name, pincode, uri): +def async_show_setup_message(hass, entry_id, bridge_name, pincode, uri): """Display persistent notification with setup information.""" pin = pincode.decode() _LOGGER.info("Pincode: %s", pin) @@ -334,12 +334,14 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri): f"### {pin}\n" f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})" ) - hass.components.persistent_notification.create(message, "HomeKit Pairing", entry_id) + hass.components.persistent_notification.async_create( + message, "HomeKit Pairing", entry_id + ) -def dismiss_setup_message(hass, entry_id): +def async_dismiss_setup_message(hass, entry_id): """Dismiss persistent notification and remove QR code.""" - hass.components.persistent_notification.dismiss(entry_id) + hass.components.persistent_notification.async_dismiss(entry_id) def convert_to_float(state): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 60c7e5ac8e2..b47ab223be8 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -694,7 +694,7 @@ def test_home_driver(): # pair with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( - "homeassistant.components.homekit.accessories.dismiss_setup_message" + "homeassistant.components.homekit.accessories.async_dismiss_setup_message" ) as mock_dissmiss_msg: driver.pair("client_uuid", "client_public", b"1") @@ -703,7 +703,7 @@ def test_home_driver(): # unpair with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( - "homeassistant.components.homekit.accessories.show_setup_message" + "homeassistant.components.homekit.accessories.async_show_setup_message" ) as mock_show_msg: driver.unpair("client_uuid") diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index deff07ecdb4..0b1d2cc8535 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -459,7 +459,7 @@ async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): state = hass.states.async_all()[0] with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: @@ -491,7 +491,7 @@ async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): # Start again to make sure the registry entry is kept homekit.status = STATUS_READY with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: @@ -529,7 +529,7 @@ async def test_homekit_start_with_a_broken_accessory( hass.states.async_set("light.broken", "on") with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: @@ -568,7 +568,7 @@ async def test_homekit_start_with_a_device( homekit.driver = hk_driver with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg: await homekit.async_start() @@ -1085,7 +1085,7 @@ async def test_homekit_too_many_accessories( hass.states.async_set("light.demo3", "on") with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch(f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge): await homekit.async_start() await hass.async_block_till_done() @@ -1141,7 +1141,7 @@ async def test_homekit_finds_linked_batteries( ) hass.states.async_set(light.entity_id, STATE_ON) - with patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( + with patch(f"{PATH_HOMEKIT}.async_show_setup_message"), patch( f"{PATH_HOMEKIT}.get_accessory" ) as mock_get_acc, patch("pyhap.accessory_driver.AccessoryDriver.async_start"): await homekit.async_start() @@ -1211,7 +1211,7 @@ async def test_homekit_async_get_integration_fails( hass.states.async_set(light.entity_id, STATE_ON) with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ): @@ -1418,7 +1418,7 @@ async def test_homekit_finds_linked_motion_sensors( hass.states.async_set(camera.entity_id, STATE_ON) with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ): @@ -1483,7 +1483,7 @@ async def test_homekit_finds_linked_humidity_sensors( hass.states.async_set(humidifier.entity_id, STATE_ON) with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ): @@ -1543,7 +1543,7 @@ async def test_reload(hass, mock_async_zeroconf): with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( f"{PATH_HOMEKIT}.HomeKit" ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch( f"{PATH_HOMEKIT}.get_accessory" ), patch( @@ -1592,7 +1592,7 @@ async def test_homekit_start_in_accessory_mode( hass.states.async_set("light.demo", "on") with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: @@ -1623,7 +1623,7 @@ async def test_homekit_start_in_accessory_mode_unsupported_entity( hass.states.async_set("notsupported.demo", "on") with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: @@ -1651,7 +1651,7 @@ async def test_homekit_start_in_accessory_mode_missing_entity( homekit.driver.accessory = Accessory(hk_driver, "any") with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.show_setup_message" + f"{PATH_HOMEKIT}.async_show_setup_message" ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"): await homekit.async_start() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0bb3d2053d4..31efcc0b948 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -25,21 +25,21 @@ from homeassistant.components.homekit.const import ( ) from homeassistant.components.homekit.util import ( accessory_friendly_name, + async_dismiss_setup_message, async_find_next_available_port, async_port_is_available, + async_show_setup_message, cleanup_name_for_homekit, convert_to_float, density_to_air_quality, - dismiss_setup_message, format_sw_version, - show_setup_message, state_needs_accessory_mode, temperature_to_homekit, temperature_to_states, validate_entity_config as vec, validate_media_player_features, ) -from homeassistant.components.persistent_notification import create, dismiss +from homeassistant.components.persistent_notification import async_create, async_dismiss from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, @@ -231,7 +231,7 @@ def test_density_to_air_quality(): assert density_to_air_quality(300) == 5 -async def test_show_setup_msg(hass, hk_driver, mock_get_source_ip): +async def test_async_show_setup_msg(hass, hk_driver, mock_get_source_ip): """Test show setup message as persistence notification.""" pincode = b"123-45-678" @@ -239,10 +239,11 @@ async def test_show_setup_msg(hass, hk_driver, mock_get_source_ip): assert entry with patch( - "homeassistant.components.persistent_notification.create", side_effect=create + "homeassistant.components.persistent_notification.async_create", + side_effect=async_create, ) as mock_create: - await hass.async_add_executor_job( - show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" + async_show_setup_message( + hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" ) await hass.async_block_till_done() assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET] @@ -253,12 +254,13 @@ async def test_show_setup_msg(hass, hk_driver, mock_get_source_ip): assert pincode.decode() in mock_create.mock_calls[0][1][1] -async def test_dismiss_setup_msg(hass): +async def test_async_dismiss_setup_msg(hass): """Test dismiss setup message.""" with patch( - "homeassistant.components.persistent_notification.dismiss", side_effect=dismiss + "homeassistant.components.persistent_notification.async_dismiss", + side_effect=async_dismiss, ) as mock_dismiss: - await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id") + async_dismiss_setup_message(hass, "entry_id") await hass.async_block_till_done() assert len(mock_dismiss.mock_calls) == 1 From 6524cd4eb234c925e77d1bd3b03eff71caec464e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 11:46:54 -0600 Subject: [PATCH 0770/1452] Fix user input malformed with deleted entities in HomeKit exclude flow (#60061) --- .../components/homekit/config_flow.py | 17 +++-- tests/components/homekit/test_config_flow.py | 63 ++++++++++++++++++- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index dc7f7b9013e..0d8bf967c5b 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -453,12 +453,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): data_schema = {} entity_schema = vol.In - # Strip out entities that no longer exist to prevent error in the UI - entities = [ - entity_id - for entity_id in entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if entity_id in all_supported_entities - ] + entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: include_exclude_mode = MODE_INCLUDE if not entities: @@ -469,9 +464,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ] = vol.In(INCLUDE_EXCLUDE_MODES) entity_schema = cv.multi_select - data_schema[vol.Optional(CONF_ENTITIES, default=entities)] = entity_schema( - all_supported_entities - ) + # Strip out entities that no longer exist to prevent error in the UI + valid_entities = [ + entity_id for entity_id in entities if entity_id in all_supported_entities + ] + data_schema[ + vol.Optional(CONF_ENTITIES, default=valid_entities) + ] = entity_schema(all_supported_entities) return self.async_show_form( step_id="include_exclude", data_schema=vol.Schema(data_schema) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index fb65895604e..f076d8e00ae 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -514,8 +514,10 @@ async def test_options_flow_devices_preserved_when_advanced_off( } -async def test_options_flow_with_non_existant_entity(hass, mock_get_source_ip): - """Test config flow options in include mode.""" +async def test_options_flow_include_mode_with_non_existant_entity( + hass, mock_get_source_ip +): + """Test config flow options in include mode with a non-existent entity.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, @@ -568,6 +570,63 @@ async def test_options_flow_with_non_existant_entity(hass, mock_get_source_ip): } +async def test_options_flow_exclude_mode_with_non_existant_entity( + hass, mock_get_source_ip +): + """Test config flow options in exclude mode with a non-existent entity.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "filter": { + "include_domains": ["climate"], + "exclude_entities": ["climate.not_exist", "climate.front_gate"], + }, + }, + ) + config_entry.add_to_hass(hass) + hass.states.async_set("climate.front_gate", "off") + hass.states.async_set("climate.new", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + + entities = result["data_schema"]({})["entities"] + assert "climate.not_exist" not in entities + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.new", "climate.front_gate"], + "include_exclude_mode": "exclude", + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.new", "climate.front_gate"], + "include_domains": ["climate"], + "include_entities": [], + }, + } + + async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): """Test config flow options in include mode.""" From 881d35ab171007696f819d3a3e84499d0cbf6728 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 11:56:07 -0600 Subject: [PATCH 0771/1452] Split up yeelight code into multiple files (#59990) --- homeassistant/components/yeelight/__init__.py | 540 ++---------------- .../components/yeelight/binary_sensor.py | 3 +- .../components/yeelight/config_flow.py | 6 +- homeassistant/components/yeelight/const.py | 103 ++++ homeassistant/components/yeelight/device.py | 233 ++++++++ homeassistant/components/yeelight/entity.py | 40 ++ homeassistant/components/yeelight/light.py | 6 +- homeassistant/components/yeelight/scanner.py | 185 ++++++ tests/components/yeelight/__init__.py | 11 +- tests/components/yeelight/test_config_flow.py | 4 +- tests/components/yeelight/test_init.py | 2 +- tests/components/yeelight/test_light.py | 2 +- 12 files changed, 615 insertions(+), 520 deletions(-) create mode 100644 homeassistant/components/yeelight/const.py create mode 100644 homeassistant/components/yeelight/device.py create mode 100644 homeassistant/components/yeelight/entity.py create mode 100644 homeassistant/components/yeelight/scanner.py diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 1408cb56709..79249ae8c44 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -2,19 +2,12 @@ from __future__ import annotations import asyncio -import contextlib -from datetime import timedelta -from ipaddress import IPv4Address, IPv6Address import logging -from urllib.parse import urlparse -from async_upnp_client.search import SsdpSearchListener import voluptuous as vol from yeelight import BulbException -from yeelight.aio import KEY_CONNECTED, AsyncBulb +from yeelight.aio import AsyncBulb -from homeassistant import config_entries -from homeassistant.components import network from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_DEVICES, @@ -25,82 +18,45 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType +from .const import ( + ACTION_OFF, + ACTION_RECOVER, + ACTION_STAY, + ATTR_ACTION, + ATTR_COUNT, + ATTR_TRANSITIONS, + CONF_CUSTOM_EFFECTS, + CONF_DETECTED_MODEL, + CONF_FLOW_PARAMS, + CONF_MODE_MUSIC, + CONF_MODEL, + CONF_NIGHTLIGHT_SWITCH, + CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + DATA_CONFIG_ENTRIES, + DATA_CUSTOM_EFFECTS, + DATA_DEVICE, + DEFAULT_MODE_MUSIC, + DEFAULT_NAME, + DEFAULT_NIGHTLIGHT_SWITCH, + DEFAULT_SAVE_ON_CHANGE, + DEFAULT_TRANSITION, + DOMAIN, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, + PLATFORMS, + YEELIGHT_HSV_TRANSACTION, + YEELIGHT_RGB_TRANSITION, + YEELIGHT_SLEEP_TRANSACTION, + YEELIGHT_TEMPERATURE_TRANSACTION, +) +from .device import YeelightDevice, async_format_id +from .scanner import YeelightScanner + _LOGGER = logging.getLogger(__name__) -STATE_CHANGE_TIME = 0.40 # seconds -POWER_STATE_CHANGE_TIME = 1 # seconds - -# -# These models do not transition correctly when turning on, and -# yeelight is no longer updating the firmware on older devices -# -# https://github.com/home-assistant/core/issues/58315 -# -# The problem can be worked around by always setting the brightness -# even when the bulb is reporting the brightness is already at the -# desired level. -# -MODELS_WITH_DELAYED_ON_TRANSITION = { - "color", # YLDP02YL -} - -DOMAIN = "yeelight" -DATA_YEELIGHT = DOMAIN -DATA_UPDATED = "yeelight_{}_data_updated" - -DEFAULT_NAME = "Yeelight" -DEFAULT_TRANSITION = 350 -DEFAULT_MODE_MUSIC = False -DEFAULT_SAVE_ON_CHANGE = False -DEFAULT_NIGHTLIGHT_SWITCH = False - -CONF_MODEL = "model" -CONF_DETECTED_MODEL = "detected_model" -CONF_TRANSITION = "transition" -CONF_SAVE_ON_CHANGE = "save_on_change" -CONF_MODE_MUSIC = "use_music_mode" -CONF_FLOW_PARAMS = "flow_params" -CONF_CUSTOM_EFFECTS = "custom_effects" -CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" -CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" - -DATA_CONFIG_ENTRIES = "config_entries" -DATA_CUSTOM_EFFECTS = "custom_effects" -DATA_DEVICE = "device" -DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" -DATA_PLATFORMS_LOADED = "platforms_loaded" - -ATTR_COUNT = "count" -ATTR_ACTION = "action" -ATTR_TRANSITIONS = "transitions" -ATTR_MODE_MUSIC = "music_mode" - -ACTION_RECOVER = "recover" -ACTION_STAY = "stay" -ACTION_OFF = "off" - -ACTIVE_MODE_NIGHTLIGHT = 1 -ACTIVE_COLOR_FLOWING = 1 - -NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" - -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 = 8 - - -YEELIGHT_RGB_TRANSITION = "RGBTransition" -YEELIGHT_HSV_TRANSACTION = "HSVTransition" -YEELIGHT_TEMPERATURE_TRANSACTION = "TemperatureTransition" -YEELIGHT_SLEEP_TRANSACTION = "SleepTransition" YEELIGHT_FLOW_TRANSITION_SCHEMA = { vol.Optional(ATTR_COUNT, default=0): cv.positive_int, @@ -155,31 +111,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -UPDATE_REQUEST_PROPERTIES = [ - "power", - "main_power", - "bright", - "ct", - "rgb", - "hue", - "sat", - "color_mode", - "flowing", - "bg_power", - "bg_lmode", - "bg_flowing", - "bg_ct", - "bg_bright", - "bg_hue", - "bg_sat", - "bg_rgb", - "nl_br", - "active_mode", -] - - -PLATFORMS = ["binary_sensor", "light"] - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Yeelight bulbs.""" @@ -299,410 +230,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -@callback -def async_format_model(model: str) -> str: - """Generate a more human readable model.""" - return model.replace("_", " ").title() - - -@callback -def async_format_id(id_: str) -> str: - """Generate a more human readable id.""" - return hex(int(id_, 16)) if id_ else "None" - - -@callback -def async_format_model_id(model: str, id_: str) -> str: - """Generate a more human readable name.""" - return f"{async_format_model(model)} {async_format_id(id_)}" - - -@callback -def _async_unique_name(capabilities: dict) -> str: - """Generate name from capabilities.""" - model_id = async_format_model_id(capabilities["model"], capabilities["id"]) - return f"Yeelight {model_id}" - - async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -class YeelightScanner: - """Scan for Yeelight devices.""" - - _scanner = None - - @classmethod - @callback - def async_get(cls, hass: HomeAssistant): - """Get scanner instance.""" - if cls._scanner is None: - cls._scanner = cls(hass) - return cls._scanner - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize class.""" - self._hass = hass - self._host_discovered_events = {} - self._unique_id_capabilities = {} - self._host_capabilities = {} - self._track_interval = None - self._listeners = [] - self._connected_events = [] - - async def async_setup(self): - """Set up the scanner.""" - if self._connected_events: - await self._async_wait_connected() - return - - for idx, source_ip in enumerate(await self._async_build_source_set()): - self._connected_events.append(asyncio.Event()) - - def _wrap_async_connected_idx(idx): - """Create a function to capture the idx cell variable.""" - - async def _async_connected(): - self._connected_events[idx].set() - - return _async_connected - - self._listeners.append( - SsdpSearchListener( - async_callback=self._async_process_entry, - service_type=SSDP_ST, - target=SSDP_TARGET, - source_ip=source_ip, - async_connect_callback=_wrap_async_connected_idx(idx), - ) - ) - - results = await asyncio.gather( - *(listener.async_start() for listener in self._listeners), - return_exceptions=True, - ) - failed_listeners = [] - for idx, result in enumerate(results): - if not isinstance(result, Exception): - continue - _LOGGER.warning( - "Failed to setup listener for %s: %s", - self._listeners[idx].source_ip, - result, - ) - failed_listeners.append(self._listeners[idx]) - self._connected_events[idx].set() - - for listener in failed_listeners: - self._listeners.remove(listener) - - await self._async_wait_connected() - self._track_interval = async_track_time_interval( - self._hass, self.async_scan, DISCOVERY_INTERVAL - ) - self.async_scan() - - async def _async_wait_connected(self): - """Wait for the listeners to be up and connected.""" - await asyncio.gather(*(event.wait() for event in self._connected_events)) - - async def _async_build_source_set(self) -> set[IPv4Address]: - """Build the list of ssdp sources.""" - adapters = await network.async_get_adapters(self._hass) - sources: set[IPv4Address] = set() - if network.async_only_default_interface_enabled(adapters): - sources.add(IPv4Address("0.0.0.0")) - return sources - - 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) - } - - async def async_discover(self): - """Discover bulbs.""" - _LOGGER.debug("Yeelight discover with interval %s", DISCOVERY_SEARCH_INTERVAL) - await self.async_setup() - for _ in range(DISCOVERY_ATTEMPTS): - self.async_scan() - await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds()) - return self._unique_id_capabilities.values() - - @callback - def async_scan(self, *_): - """Send discovery packets.""" - _LOGGER.debug("Yeelight scanning") - for listener in self._listeners: - 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() - - for listener in self._listeners: - 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 - current_entry = self._unique_id_capabilities.get(unique_id) - # Make sure we handle ip changes - if not current_entry or host != urlparse(current_entry["location"]).hostname: - _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() - - -def update_needs_bg_power_workaround(data): - """Check if a push update needs the bg_power workaround. - - Some devices will push the incorrect state for bg_power. - - To work around this any time we are pushed an update - with bg_power, we force poll state which will be correct. - """ - return "bg_power" in data - - -class YeelightDevice: - """Represents single Yeelight device.""" - - def __init__(self, hass, host, config, bulb): - """Initialize device.""" - self._hass = hass - self._config = config - self._host = host - self._bulb_device = bulb - self.capabilities = {} - self._device_type = None - self._available = True - self._initialized = False - self._name = None - - @property - def bulb(self): - """Return bulb device.""" - return self._bulb_device - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def config(self): - """Return device config.""" - return self._config - - @property - def host(self): - """Return hostname.""" - return self._host - - @property - def available(self): - """Return true is device is available.""" - return self._available - - @callback - def async_mark_unavailable(self): - """Set unavailable on api call failure due to a network issue.""" - self._available = False - - @property - def model(self): - """Return configured/autodetected device model.""" - return self._bulb_device.model or self.capabilities.get("model") - - @property - def fw_version(self): - """Return the firmware version.""" - return self.capabilities.get("fw_ver") - - @property - def is_nightlight_supported(self) -> bool: - """ - Return true / false if nightlight is supported. - - Uses brightness as it appears to be supported in both ceiling and other lights. - """ - return self._nightlight_brightness is not None - - @property - def is_nightlight_enabled(self) -> bool: - """Return true / false if nightlight is currently enabled.""" - # Only ceiling lights have active_mode, from SDK docs: - # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only) - if self._active_mode is not None: - return int(self._active_mode) == ACTIVE_MODE_NIGHTLIGHT - - if self._nightlight_brightness is not None: - return int(self._nightlight_brightness) > 0 - - return False - - @property - def is_color_flow_enabled(self) -> bool: - """Return true / false if color flow is currently running.""" - return self._color_flow and int(self._color_flow) == ACTIVE_COLOR_FLOWING - - @property - def _active_mode(self): - return self.bulb.last_properties.get("active_mode") - - @property - def _color_flow(self): - return self.bulb.last_properties.get("flowing") - - @property - def _nightlight_brightness(self): - return self.bulb.last_properties.get("nl_br") - - @property - def type(self): - """Return bulb type.""" - if not self._device_type: - self._device_type = self.bulb.bulb_type - - return self._device_type - - async def _async_update_properties(self): - """Read new properties from the device.""" - try: - await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) - self._available = True - if not self._initialized: - self._initialized = True - except OSError as ex: - if self._available: # just inform once - _LOGGER.error( - "Unable to update device %s, %s: %s", self._host, self.name, ex - ) - self._available = False - except asyncio.TimeoutError as ex: - _LOGGER.debug( - "timed out while trying to update device %s, %s: %s", - self._host, - self.name, - ex, - ) - except BulbException as ex: - _LOGGER.debug( - "Unable to update device %s, %s: %s", self._host, self.name, ex - ) - - async def async_setup(self): - """Fetch capabilities and setup name if available.""" - scanner = YeelightScanner.async_get(self._hass) - self.capabilities = await scanner.async_get_capabilities(self._host) or {} - if self.capabilities: - self._bulb_device.set_capabilities(self.capabilities) - 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(self, force=False): - """Update device properties and send data updated signal.""" - if not force and self._initialized and self._available: - # No need to poll unless force, already connected - return - await self._async_update_properties() - async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) - - async def _async_forced_update(self, _now): - """Call a forced update.""" - await self.async_update(True) - - @callback - def async_update_callback(self, data): - """Update push from device.""" - was_available = self._available - self._available = data.get(KEY_CONNECTED, True) - if update_needs_bg_power_workaround(data) or ( - not was_available and self._available - ): - # On reconnect the properties may be out of sync - # - # If the device drops the connection right away, we do not want to - # do a property resync via async_update since its about - # to be called when async_setup_entry reaches the end of the - # function - # - async_call_later(self._hass, STATE_CHANGE_TIME, self._async_forced_update) - async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) - - -class YeelightEntity(Entity): - """Represents single Yeelight entity.""" - - _attr_should_poll = False - - def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: - """Initialize the entity.""" - self._device = device - self._unique_id = entry.unique_id or entry.entry_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._unique_id)}, - name=self._device.name, - manufacturer="Yeelight", - model=self._device.model, - sw_version=self._device.fw_version, - ) - - @property - def unique_id(self) -> str: - """Return the unique ID.""" - return self._unique_id - - @property - def available(self) -> bool: - """Return if bulb is available.""" - return self._device.available - - async def async_update(self) -> None: - """Update the entity.""" - await self._device.async_update() - - async def _async_get_device( hass: HomeAssistant, host: str, entry: ConfigEntry ) -> YeelightDevice: diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 185bb504a1b..89eb910f942 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -6,7 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_CONFIG_ENTRIES, DATA_DEVICE, DATA_UPDATED, DOMAIN, YeelightEntity +from .const import DATA_CONFIG_ENTRIES, DATA_DEVICE, DATA_UPDATED, DOMAIN +from .entity import YeelightEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index d876f35a2dc..ba1ab4d24c7 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import ( +from .const import ( CONF_DETECTED_MODEL, CONF_MODE_MUSIC, CONF_MODEL, @@ -25,12 +25,14 @@ from . import ( CONF_TRANSITION, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, - YeelightScanner, +) +from .device import ( _async_unique_name, async_format_id, async_format_model, async_format_model_id, ) +from .scanner import YeelightScanner MODEL_UNKNOWN = "unknown" diff --git a/homeassistant/components/yeelight/const.py b/homeassistant/components/yeelight/const.py new file mode 100644 index 00000000000..2b494bf9ef2 --- /dev/null +++ b/homeassistant/components/yeelight/const.py @@ -0,0 +1,103 @@ +"""Support for Xiaomi Yeelight WiFi color bulb.""" + +from datetime import timedelta + +DOMAIN = "yeelight" + + +STATE_CHANGE_TIME = 0.40 # seconds +POWER_STATE_CHANGE_TIME = 1 # seconds + + +# +# These models do not transition correctly when turning on, and +# yeelight is no longer updating the firmware on older devices +# +# https://github.com/home-assistant/core/issues/58315 +# +# The problem can be worked around by always setting the brightness +# even when the bulb is reporting the brightness is already at the +# desired level. +# +MODELS_WITH_DELAYED_ON_TRANSITION = { + "color", # YLDP02YL +} + +DATA_UPDATED = "yeelight_{}_data_updated" + +DEFAULT_NAME = "Yeelight" +DEFAULT_TRANSITION = 350 +DEFAULT_MODE_MUSIC = False +DEFAULT_SAVE_ON_CHANGE = False +DEFAULT_NIGHTLIGHT_SWITCH = False + +CONF_MODEL = "model" +CONF_DETECTED_MODEL = "detected_model" +CONF_TRANSITION = "transition" + +CONF_SAVE_ON_CHANGE = "save_on_change" +CONF_MODE_MUSIC = "use_music_mode" +CONF_FLOW_PARAMS = "flow_params" +CONF_CUSTOM_EFFECTS = "custom_effects" +CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" +CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" + +DATA_CONFIG_ENTRIES = "config_entries" +DATA_CUSTOM_EFFECTS = "custom_effects" +DATA_DEVICE = "device" +DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" +DATA_PLATFORMS_LOADED = "platforms_loaded" + +ATTR_COUNT = "count" +ATTR_ACTION = "action" +ATTR_TRANSITIONS = "transitions" +ATTR_MODE_MUSIC = "music_mode" + +ACTION_RECOVER = "recover" +ACTION_STAY = "stay" +ACTION_OFF = "off" + +ACTIVE_MODE_NIGHTLIGHT = 1 +ACTIVE_COLOR_FLOWING = 1 + + +NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" + +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 = 8 + + +YEELIGHT_RGB_TRANSITION = "RGBTransition" +YEELIGHT_HSV_TRANSACTION = "HSVTransition" +YEELIGHT_TEMPERATURE_TRANSACTION = "TemperatureTransition" +YEELIGHT_SLEEP_TRANSACTION = "SleepTransition" + + +UPDATE_REQUEST_PROPERTIES = [ + "power", + "main_power", + "bright", + "ct", + "rgb", + "hue", + "sat", + "color_mode", + "flowing", + "bg_power", + "bg_lmode", + "bg_flowing", + "bg_ct", + "bg_bright", + "bg_hue", + "bg_sat", + "bg_rgb", + "nl_br", + "active_mode", +] + + +PLATFORMS = ["binary_sensor", "light"] diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py new file mode 100644 index 00000000000..5f70866b229 --- /dev/null +++ b/homeassistant/components/yeelight/device.py @@ -0,0 +1,233 @@ +"""Support for Xiaomi Yeelight WiFi color bulb.""" +from __future__ import annotations + +import asyncio +import logging + +from yeelight import BulbException +from yeelight.aio import KEY_CONNECTED + +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later + +from .const import ( + ACTIVE_COLOR_FLOWING, + ACTIVE_MODE_NIGHTLIGHT, + DATA_UPDATED, + STATE_CHANGE_TIME, + UPDATE_REQUEST_PROPERTIES, +) +from .scanner import YeelightScanner + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_format_model(model: str) -> str: + """Generate a more human readable model.""" + return model.replace("_", " ").title() + + +@callback +def async_format_id(id_: str) -> str: + """Generate a more human readable id.""" + return hex(int(id_, 16)) if id_ else "None" + + +@callback +def async_format_model_id(model: str, id_: str) -> str: + """Generate a more human readable name.""" + return f"{async_format_model(model)} {async_format_id(id_)}" + + +@callback +def _async_unique_name(capabilities: dict) -> str: + """Generate name from capabilities.""" + model_id = async_format_model_id(capabilities["model"], capabilities["id"]) + return f"Yeelight {model_id}" + + +def update_needs_bg_power_workaround(data): + """Check if a push update needs the bg_power workaround. + + Some devices will push the incorrect state for bg_power. + + To work around this any time we are pushed an update + with bg_power, we force poll state which will be correct. + """ + return "bg_power" in data + + +class YeelightDevice: + """Represents single Yeelight device.""" + + def __init__(self, hass, host, config, bulb): + """Initialize device.""" + self._hass = hass + self._config = config + self._host = host + self._bulb_device = bulb + self.capabilities = {} + self._device_type = None + self._available = True + self._initialized = False + self._name = None + + @property + def bulb(self): + """Return bulb device.""" + return self._bulb_device + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def config(self): + """Return device config.""" + return self._config + + @property + def host(self): + """Return hostname.""" + return self._host + + @property + def available(self): + """Return true is device is available.""" + return self._available + + @callback + def async_mark_unavailable(self): + """Set unavailable on api call failure due to a network issue.""" + self._available = False + + @property + def model(self): + """Return configured/autodetected device model.""" + return self._bulb_device.model or self.capabilities.get("model") + + @property + def fw_version(self): + """Return the firmware version.""" + return self.capabilities.get("fw_ver") + + @property + def is_nightlight_supported(self) -> bool: + """ + Return true / false if nightlight is supported. + + Uses brightness as it appears to be supported in both ceiling and other lights. + """ + return self._nightlight_brightness is not None + + @property + def is_nightlight_enabled(self) -> bool: + """Return true / false if nightlight is currently enabled.""" + # Only ceiling lights have active_mode, from SDK docs: + # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only) + if self._active_mode is not None: + return int(self._active_mode) == ACTIVE_MODE_NIGHTLIGHT + + if self._nightlight_brightness is not None: + return int(self._nightlight_brightness) > 0 + + return False + + @property + def is_color_flow_enabled(self) -> bool: + """Return true / false if color flow is currently running.""" + return self._color_flow and int(self._color_flow) == ACTIVE_COLOR_FLOWING + + @property + def _active_mode(self): + return self.bulb.last_properties.get("active_mode") + + @property + def _color_flow(self): + return self.bulb.last_properties.get("flowing") + + @property + def _nightlight_brightness(self): + return self.bulb.last_properties.get("nl_br") + + @property + def type(self): + """Return bulb type.""" + if not self._device_type: + self._device_type = self.bulb.bulb_type + + return self._device_type + + async def _async_update_properties(self): + """Read new properties from the device.""" + try: + await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) + self._available = True + if not self._initialized: + self._initialized = True + except OSError as ex: + if self._available: # just inform once + _LOGGER.error( + "Unable to update device %s, %s: %s", self._host, self.name, ex + ) + self._available = False + except asyncio.TimeoutError as ex: + _LOGGER.debug( + "timed out while trying to update device %s, %s: %s", + self._host, + self.name, + ex, + ) + except BulbException as ex: + _LOGGER.debug( + "Unable to update device %s, %s: %s", self._host, self.name, ex + ) + + async def async_setup(self): + """Fetch capabilities and setup name if available.""" + scanner = YeelightScanner.async_get(self._hass) + self.capabilities = await scanner.async_get_capabilities(self._host) or {} + if self.capabilities: + self._bulb_device.set_capabilities(self.capabilities) + 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(self, force=False): + """Update device properties and send data updated signal.""" + if not force and self._initialized and self._available: + # No need to poll unless force, already connected + return + await self._async_update_properties() + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) + + async def _async_forced_update(self, _now): + """Call a forced update.""" + await self.async_update(True) + + @callback + def async_update_callback(self, data): + """Update push from device.""" + was_available = self._available + self._available = data.get(KEY_CONNECTED, True) + if update_needs_bg_power_workaround(data) or ( + not was_available and self._available + ): + # On reconnect the properties may be out of sync + # + # If the device drops the connection right away, we do not want to + # do a property resync via async_update since its about + # to be called when async_setup_entry reaches the end of the + # function + # + async_call_later(self._hass, STATE_CHANGE_TIME, self._async_forced_update) + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) diff --git a/homeassistant/components/yeelight/entity.py b/homeassistant/components/yeelight/entity.py new file mode 100644 index 00000000000..53211115dd6 --- /dev/null +++ b/homeassistant/components/yeelight/entity.py @@ -0,0 +1,40 @@ +"""Support for Xiaomi Yeelight WiFi color bulb.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN +from .device import YeelightDevice + + +class YeelightEntity(Entity): + """Represents single Yeelight entity.""" + + _attr_should_poll = False + + def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: + """Initialize the entity.""" + self._device = device + self._unique_id = entry.unique_id or entry.entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + name=self._device.name, + manufacturer="Yeelight", + model=self._device.model, + sw_version=self._device.fw_version, + ) + + @property + def unique_id(self) -> str: + """Return the unique ID.""" + return self._unique_id + + @property + def available(self) -> bool: + """Return if bulb is available.""" + return self._device.available + + async def async_update(self) -> None: + """Update the entity.""" + await self._device.async_update() diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 838e56f5d99..75735beed34 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -47,7 +47,8 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) -from . import ( +from . import YEELIGHT_FLOW_TRANSITION_SCHEMA +from .const import ( ACTION_RECOVER, ATTR_ACTION, ATTR_COUNT, @@ -65,9 +66,8 @@ from . import ( DOMAIN, MODELS_WITH_DELAYED_ON_TRANSITION, POWER_STATE_CHANGE_TIME, - YEELIGHT_FLOW_TRANSITION_SCHEMA, - YeelightEntity, ) +from .entity import YeelightEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py new file mode 100644 index 00000000000..a331db8d4ed --- /dev/null +++ b/homeassistant/components/yeelight/scanner.py @@ -0,0 +1,185 @@ +"""Support for Xiaomi Yeelight WiFi color bulb.""" +from __future__ import annotations + +import asyncio +import contextlib +from ipaddress import IPv4Address, IPv6Address +import logging +from urllib.parse import urlparse + +from async_upnp_client.search import SsdpSearchListener + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .const import ( + DISCOVERY_ATTEMPTS, + DISCOVERY_INTERVAL, + DISCOVERY_SEARCH_INTERVAL, + DISCOVERY_TIMEOUT, + DOMAIN, + SSDP_ST, + SSDP_TARGET, +) + +_LOGGER = logging.getLogger(__name__) + + +class YeelightScanner: + """Scan for Yeelight devices.""" + + _scanner = None + + @classmethod + @callback + def async_get(cls, hass: HomeAssistant): + """Get scanner instance.""" + if cls._scanner is None: + cls._scanner = cls(hass) + return cls._scanner + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize class.""" + self._hass = hass + self._host_discovered_events = {} + self._unique_id_capabilities = {} + self._host_capabilities = {} + self._track_interval = None + self._listeners = [] + self._connected_events = [] + + async def async_setup(self): + """Set up the scanner.""" + if self._connected_events: + await self._async_wait_connected() + return + + for idx, source_ip in enumerate(await self._async_build_source_set()): + self._connected_events.append(asyncio.Event()) + + def _wrap_async_connected_idx(idx): + """Create a function to capture the idx cell variable.""" + + async def _async_connected(): + self._connected_events[idx].set() + + return _async_connected + + self._listeners.append( + SsdpSearchListener( + async_callback=self._async_process_entry, + service_type=SSDP_ST, + target=SSDP_TARGET, + source_ip=source_ip, + async_connect_callback=_wrap_async_connected_idx(idx), + ) + ) + + results = await asyncio.gather( + *(listener.async_start() for listener in self._listeners), + return_exceptions=True, + ) + failed_listeners = [] + for idx, result in enumerate(results): + if not isinstance(result, Exception): + continue + _LOGGER.warning( + "Failed to setup listener for %s: %s", + self._listeners[idx].source_ip, + result, + ) + failed_listeners.append(self._listeners[idx]) + self._connected_events[idx].set() + + for listener in failed_listeners: + self._listeners.remove(listener) + + await self._async_wait_connected() + self._track_interval = async_track_time_interval( + self._hass, self.async_scan, DISCOVERY_INTERVAL + ) + self.async_scan() + + async def _async_wait_connected(self): + """Wait for the listeners to be up and connected.""" + await asyncio.gather(*(event.wait() for event in self._connected_events)) + + async def _async_build_source_set(self) -> set[IPv4Address]: + """Build the list of ssdp sources.""" + adapters = await network.async_get_adapters(self._hass) + sources: set[IPv4Address] = set() + if network.async_only_default_interface_enabled(adapters): + sources.add(IPv4Address("0.0.0.0")) + return sources + + 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) + } + + async def async_discover(self): + """Discover bulbs.""" + _LOGGER.debug("Yeelight discover with interval %s", DISCOVERY_SEARCH_INTERVAL) + await self.async_setup() + for _ in range(DISCOVERY_ATTEMPTS): + self.async_scan() + await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds()) + return self._unique_id_capabilities.values() + + @callback + def async_scan(self, *_): + """Send discovery packets.""" + _LOGGER.debug("Yeelight scanning") + for listener in self._listeners: + 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() + + for listener in self._listeners: + 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 + current_entry = self._unique_id_capabilities.get(unique_id) + # Make sure we handle ip changes + if not current_entry or host != urlparse(current_entry["location"]).hostname: + _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() diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 2fa9c029d92..a93cea7a94c 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -8,7 +8,7 @@ from async_upnp_client.search import SsdpSearchListener from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS -from homeassistant.components import yeelight as hass_yeelight, zeroconf +from homeassistant.components import zeroconf from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -16,6 +16,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, YeelightScanner, + scanner, ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME from homeassistant.core import callback @@ -185,16 +186,14 @@ def _patch_discovery(no_device=False, capabilities=None): ) return patch( - "homeassistant.components.yeelight.SsdpSearchListener", + "homeassistant.components.yeelight.scanner.SsdpSearchListener", new=_generate_fake_ssdp_listener, ) def _patch_discovery_interval(): - return patch.object( - hass_yeelight, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0) - ) + return patch.object(scanner, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0)) def _patch_discovery_timeout(): - return patch.object(hass_yeelight, "DISCOVERY_TIMEOUT", 0.0001) + return patch.object(scanner, "DISCOVERY_TIMEOUT", 0.0001) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index a983c4a2127..8628e9620e9 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -5,7 +5,8 @@ import pytest from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf -from homeassistant.components.yeelight import ( +from homeassistant.components.yeelight.config_flow import MODEL_UNKNOWN, CannotConnect +from homeassistant.components.yeelight.const import ( CONF_DETECTED_MODEL, CONF_MODE_MUSIC, CONF_MODEL, @@ -21,7 +22,6 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.components.yeelight.config_flow import MODEL_UNKNOWN, CannotConnect from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 39e31aa91cf..13c71d656bb 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -7,7 +7,7 @@ import pytest from yeelight import BulbException, BulbType from yeelight.aio import KEY_CONNECTED -from homeassistant.components.yeelight import ( +from homeassistant.components.yeelight.const import ( CONF_DETECTED_MODEL, CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 4377efe129f..059a47b53a1 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -36,7 +36,7 @@ from homeassistant.components.light import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.components.yeelight import ( +from homeassistant.components.yeelight.const import ( ATTR_COUNT, ATTR_MODE_MUSIC, ATTR_TRANSITIONS, From c27948a82a02572ab528cc8c95e8934a44d60a92 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 23 Nov 2021 19:10:03 +0100 Subject: [PATCH 0772/1452] Fully migrate to attribute shorthand in velbus (#59797) * Move velbus completly to _attr instead of propertys * Commit all sugestions * One more sugestion * Fixed light.py --- homeassistant/components/velbus/__init__.py | 44 +++++-------- homeassistant/components/velbus/light.py | 5 -- homeassistant/components/velbus/sensor.py | 68 +++++++-------------- 3 files changed, 37 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index be5ce04051c..a63a533e487 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -169,26 +169,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class VelbusEntity(Entity): """Representation of a Velbus entity.""" + _attr_should_poll: bool = False + def __init__(self, channel: VelbusChannel) -> None: """Initialize a Velbus entity.""" self._channel = channel - - @property - def unique_id(self) -> str: - """Get unique ID.""" - if (serial := self._channel.get_module_serial()) == "": - serial = str(self._channel.get_module_address()) - return f"{serial}-{self._channel.get_channel_number()}" - - @property - def name(self) -> str: - """Return the display name of this entity.""" - return self._channel.get_name() - - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False + self._attr_name = channel.get_name() + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, str(channel.get_module_address())), + }, + manufacturer="Velleman", + model=channel.get_module_type_name(), + name=channel.get_full_name(), + sw_version=channel.get_module_sw_version(), + ) + serial = channel.get_module_serial() or str(channel.get_module_address()) + self._attr_unique_id = f"{serial}-{channel.get_channel_number()}" async def async_added_to_hass(self) -> None: """Add listener for state changes.""" @@ -196,16 +193,3 @@ class VelbusEntity(Entity): async def _on_update(self) -> None: self.async_write_ha_state() - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return DeviceInfo( - identifiers={ - (DOMAIN, str(self._channel.get_module_address())), - }, - manufacturer="Velleman", - model=self._channel.get_module_type_name(), - name=self._channel.get_full_name(), - sw_version=self._channel.get_module_sw_version(), - ) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index ad0b150bf22..bd903c76790 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -51,11 +51,6 @@ class VelbusLight(VelbusEntity, LightEntity): _channel: VelbusDimmer _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - def __init__(self, channel: VelbusDimmer) -> None: - """Initialize the dimmer.""" - super().__init__(channel) - self._attr_name = self._channel.get_name() - @property def is_on(self) -> bool: """Return true if the light is on.""" diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 8534a32080e..34642dd3bf1 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -50,33 +50,32 @@ class VelbusSensor(VelbusEntity, SensorEntity): """Initialize a sensor Velbus entity.""" super().__init__(channel) self._is_counter: bool = counter - - @property - def unique_id(self) -> str: - """Return unique ID for counter sensors.""" - unique_id = super().unique_id + # define the unique id if self._is_counter: - unique_id = f"{unique_id}-counter" - return unique_id - - @property - def name(self) -> str: - """Return the name for the sensor.""" - name = super().name + self._attr_unique_id = f"{self._attr_unique_id}-counter" + # define the name if self._is_counter: - name = f"{name}-counter" - return name - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" + self._attr_name = f"{self._attr_name}-counter" + # define the device class if self._is_counter: - return DEVICE_CLASS_ENERGY - if self._channel.is_counter_channel(): - return DEVICE_CLASS_POWER - if self._channel.is_temperature(): - return DEVICE_CLASS_TEMPERATURE - return None + self._attr_device_class = DEVICE_CLASS_ENERGY + elif channel.is_counter_channel(): + self._attr_device_class = DEVICE_CLASS_POWER + elif channel.is_temperature(): + self._attr_device_class = DEVICE_CLASS_TEMPERATURE + # define the icon + if self._is_counter: + self._attr_icon = "mdi:counter" + # the state class + if self._is_counter: + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + else: + self._attr_state_class = STATE_CLASS_MEASUREMENT + # unit + if self._is_counter: + self._attr_native_unit_of_measurement = channel.get_counter_unit() + else: + self._attr_native_unit_of_measurement = channel.get_unit() @property def native_value(self) -> float | int | None: @@ -84,24 +83,3 @@ class VelbusSensor(VelbusEntity, SensorEntity): if self._is_counter: return float(self._channel.get_counter_state()) return float(self._channel.get_state()) - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - if self._is_counter: - return str(self._channel.get_counter_unit()) - return str(self._channel.get_unit()) - - @property - def icon(self) -> str | None: - """Icon to use in the frontend.""" - if self._is_counter: - return "mdi:counter" - return None - - @property - def state_class(self) -> str: - """Return the state class of this device.""" - if self._is_counter: - return STATE_CLASS_TOTAL_INCREASING - return STATE_CLASS_MEASUREMENT From 9a328eae67345ea603c2471c0b1dd9d8496e7993 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 20:03:22 +0100 Subject: [PATCH 0773/1452] Use native datetime value in Synology DSM sensors (#60176) --- homeassistant/components/synology_dsm/const.py | 4 ++-- homeassistant/components/synology_dsm/sensor.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 5bae7426769..9bd4728643a 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -353,7 +353,7 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( SynologyDSMSensorEntityDescription( api_key=SynoDSMInformation.API_KEY, key="temperature", - name="temperature", + name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -362,7 +362,7 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( SynologyDSMSensorEntityDescription( api_key=SynoDSMInformation.API_KEY, key="uptime", - name="last boot", + name="Last boot", device_class=DEVICE_CLASS_TIMESTAMP, entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 1aa7e35d992..305e6dda4e7 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,7 +1,7 @@ """Support for Synology DSM sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any from homeassistant.components.sensor import SensorEntity @@ -164,7 +164,7 @@ class SynoDSMInfoSensor(SynoDSMSensor): """Initialize the Synology SynoDSMInfoSensor entity.""" super().__init__(api, coordinator, description) self._previous_uptime: str | None = None - self._last_boot: str | None = None + self._last_boot: datetime | None = None @property def native_value(self) -> Any | None: @@ -176,8 +176,7 @@ class SynoDSMInfoSensor(SynoDSMSensor): if self.entity_description.key == "uptime": # reboot happened or entity creation if self._previous_uptime is None or self._previous_uptime > attr: - last_boot = utcnow() - timedelta(seconds=attr) - self._last_boot = last_boot.replace(microsecond=0).isoformat() + self._last_boot = utcnow() - timedelta(seconds=attr) self._previous_uptime = attr return self._last_boot From 6588879b6ec91fbfe527d78a48e3add2aaade2d5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 21:20:51 +0100 Subject: [PATCH 0774/1452] Use native datetime value in AVM Fritz!Tools sensors (#60233) --- homeassistant/components/fritz/sensor.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 809af534f0e..951085dbdbc 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -import datetime +from datetime import datetime, timedelta import logging from typing import Any, Callable, Literal @@ -40,30 +40,29 @@ from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) -def _uptime_calculation(seconds_uptime: float, last_value: str | None) -> str: +def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime: """Calculate uptime with deviation.""" - delta_uptime = utcnow() - datetime.timedelta(seconds=seconds_uptime) + delta_uptime = utcnow() - timedelta(seconds=seconds_uptime) if ( not last_value - or abs( - (delta_uptime - datetime.datetime.fromisoformat(last_value)).total_seconds() - ) - > UPTIME_DEVIATION + or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION ): - return delta_uptime.replace(microsecond=0).isoformat() + return delta_uptime return last_value -def _retrieve_device_uptime_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_device_uptime_state( + status: FritzStatus, last_value: datetime +) -> datetime: """Return uptime from device.""" return _uptime_calculation(status.device_uptime, last_value) def _retrieve_connection_uptime_state( - status: FritzStatus, last_value: str | None -) -> str: + status: FritzStatus, last_value: datetime | None +) -> datetime: """Return uptime from connection.""" return _uptime_calculation(status.connection_uptime, last_value) From 7c6a72b086a2dd6f0fc7f75a9cf591efdad1234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 23 Nov 2021 22:40:53 +0200 Subject: [PATCH 0775/1452] Remove unneeded pylint disables (#60235) --- homeassistant/components/environment_canada/__init__.py | 2 +- homeassistant/components/knx/light.py | 2 -- homeassistant/helpers/entity.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 01d74179f41..5e52d3631f6 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -78,7 +78,7 @@ def trigger_import(hass, config): CONF_LATITUDE, CONF_LONGITUDE, CONF_LANGUAGE, - ): # pylint: disable=consider-using-tuple + ): if config.get(key): data[key] = config[key] diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 0403b658221..580293a15d8 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -219,7 +219,6 @@ class KNXLight(KnxEntity, LightEntity): if rgb is not None: if not self._device.supports_brightness: # brightness will be calculated from color so color must not hold brightness again - # pylint: disable=protected-access return cast( Tuple[int, int, int], color_util.match_max_scale((255,), rgb) ) @@ -234,7 +233,6 @@ class KNXLight(KnxEntity, LightEntity): if rgb is not None and white is not None: if not self._device.supports_brightness: # brightness will be calculated from color so color must not hold brightness again - # pylint: disable=protected-access return cast( Tuple[int, int, int, int], color_util.match_max_scale((255,), (*rgb, white)), diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a26da695e27..4d301e7d1b6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -534,7 +534,7 @@ class Entity(ABC): attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement entry = self.registry_entry - # pylint: disable=consider-using-ternary + if assumed_state := self.assumed_state: attr[ATTR_ASSUMED_STATE] = assumed_state From 0a0928b9f5404c8debb15b4715d1354ec0a0bb21 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 23 Nov 2021 21:46:10 +0100 Subject: [PATCH 0776/1452] Add daikin sensor state class (#60145) Co-authored-by: Martin Hjelmare --- homeassistant/components/daikin/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 62d3e8e1f7e..2ca91aa2780 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -6,7 +6,11 @@ from dataclasses import dataclass from pydaikin.daikin_base import Appliance -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -49,6 +53,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_INSIDE_TEMPERATURE, name="Inside Temperature", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value_func=lambda device: device.inside_temperature, ), @@ -56,6 +61,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_OUTSIDE_TEMPERATURE, name="Outside Temperature", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value_func=lambda device: device.outside_temperature, ), @@ -63,6 +69,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_HUMIDITY, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_func=lambda device: device.humidity, ), @@ -70,6 +77,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_TARGET_HUMIDITY, name="Target Humidity", device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_func=lambda device: device.humidity, ), From a3bf56c11d536b945e665ee806a071307126509a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 21:46:31 +0100 Subject: [PATCH 0777/1452] Use native datetime value in Systemmonitor sensors (#60236) --- homeassistant/components/systemmonitor/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index f630e4215f7..8bbdae820e3 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -import datetime +from datetime import datetime, timedelta from functools import lru_cache import logging import os @@ -315,9 +315,9 @@ class SensorData: """Data for a sensor.""" argument: Any - state: str | None + state: str | datetime | None value: Any | None - update_time: datetime.datetime | None + update_time: datetime | None last_exception: BaseException | None @@ -367,7 +367,7 @@ async def async_setup_platform( async def async_setup_sensor_registry_updates( hass: HomeAssistant, sensor_registry: dict[tuple[str, str], SensorData], - scan_interval: datetime.timedelta, + scan_interval: timedelta, ) -> None: """Update the registry and create polling.""" @@ -439,7 +439,7 @@ class SystemMonitorSensor(SensorEntity): self._argument: str = argument @property - def native_value(self) -> str | None: + def native_value(self) -> str | datetime | None: """Return the state of the device.""" return self.data.state @@ -465,7 +465,7 @@ class SystemMonitorSensor(SensorEntity): def _update( # noqa: C901 type_: str, data: SensorData -) -> tuple[str | None, str | None, datetime.datetime | None]: +) -> tuple[str | datetime | None, str | None, datetime | None]: """Get the latest system information.""" state = None value = None @@ -549,7 +549,7 @@ def _update( # noqa: C901 elif type_ == "last_boot": # Only update on initial setup if data.state is None: - state = dt_util.utc_from_timestamp(psutil.boot_time()).isoformat() + state = dt_util.utc_from_timestamp(psutil.boot_time()) else: state = data.state elif type_ == "load_1m": From 2d29ef9b632800d299d9be694e07ba578f29ee09 Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Tue, 23 Nov 2021 12:54:53 -0800 Subject: [PATCH 0778/1452] Avoid errors when AirNow API does not return all expected pollutants (#60232) Co-authored-by: Aaron Bach --- homeassistant/components/airnow/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index b0d8c69cff2..c56530613a2 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -86,7 +86,8 @@ class AirNowSensor(CoordinatorEntity, SensorEntity): @property def native_value(self): """Return the state.""" - self._state = self.coordinator.data[self.entity_description.key] + self._state = self.coordinator.data.get(self.entity_description.key) + return self._state @property From df5d678858dbb27d22b5566886260644e4fd5b7c Mon Sep 17 00:00:00 2001 From: bashworth <53146061+bash-worth@users.noreply.github.com> Date: Tue, 23 Nov 2021 21:00:42 +0000 Subject: [PATCH 0779/1452] Tado ignore fix (#60011) Co-authored-by: J. Nick Koston --- homeassistant/components/tado/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index a1b7661b0f6..779d330992e 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -92,6 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() } await self.async_set_unique_id(properties[zeroconf.ATTR_PROPERTIES_ID]) + self._abort_if_unique_id_configured() return await self.async_step_user() def _username_already_configured(self, user_input): From a5dc0e37b8dfba2cb1a7a3a892ddc718afd34eb7 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 23 Nov 2021 22:06:05 +0100 Subject: [PATCH 0780/1452] Consistent capitalization for Synology DSM entity names (#60237) --- homeassistant/components/synology_dsm/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 9bd4728643a..b1afe37b765 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -108,7 +108,7 @@ UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( SynologyDSMBinarySensorEntityDescription( api_key=SynoCoreUpgrade.API_KEY, key="update_available", - name="Update available", + name="Update Available", device_class=DEVICE_CLASS_UPDATE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), @@ -118,7 +118,7 @@ SECURITY_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = SynologyDSMBinarySensorEntityDescription( api_key=SynoCoreSecurity.API_KEY, key="status", - name="Security status", + name="Security Status", device_class=DEVICE_CLASS_SAFETY, ), ) @@ -362,7 +362,7 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( SynologyDSMSensorEntityDescription( api_key=SynoDSMInformation.API_KEY, key="uptime", - name="Last boot", + name="Last Boot", device_class=DEVICE_CLASS_TIMESTAMP, entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -374,7 +374,7 @@ SURVEILLANCE_SWITCH: tuple[SynologyDSMSwitchEntityDescription, ...] = ( SynologyDSMSwitchEntityDescription( api_key=SynoSurveillanceStation.HOME_MODE_API_KEY, key="home_mode", - name="home mode", + name="Home Mode", icon="mdi:home-account", entity_category=ENTITY_CATEGORY_CONFIG, ), From 4480e1255ac71622e6762d6400bd72448293c6c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:07:10 +0100 Subject: [PATCH 0781/1452] Use MqttServiceInfo in tasmota (#60113) Co-authored-by: epenet --- .../components/tasmota/config_flow.py | 6 +- tests/components/tasmota/test_config_flow.py | 65 ++++++++++--------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index 9c22934678e..df1c5f17de2 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -29,15 +29,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) # Validate the message, abort if it fails - if not discovery_info["topic"].endswith("/config"): + if not discovery_info.topic.endswith("/config"): # Not a Tasmota discovery message return self.async_abort(reason="invalid_discovery_info") - if not discovery_info["payload"]: + if not discovery_info.payload: # Empty payload, the Tasmota is not configured for native discovery return self.async_abort(reason="invalid_discovery_info") # "tasmota/discovery/#" is hardcoded in Tasmota's manifest - assert discovery_info["subscribed_topic"] == "tasmota/discovery/#" + assert discovery_info.subscribed_topic == "tasmota/discovery/#" self._prefix = "tasmota/discovery" return await self.async_step_confirm() diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 7d6d0628de1..5f1c45ce138 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" from homeassistant import config_entries +from homeassistant.components.mqtt import discovery as mqtt from tests.common import MockConfigEntry @@ -18,9 +19,9 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" - discovery_info = { - "topic": "tasmota/discovery/DC4F220848A2/bla", - "payload": ( + discovery_info = mqtt.MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/bla", + payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' 'null,null,null],"hn":"tasmota_0848A2","mac":"DC4F220848A2","md":"Sonoff Basic",' '"ty":0,"if":0,"ofln":"Offline","onln":"Online","state":["OFF","ON",' @@ -30,34 +31,34 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): '"so":{"4":0,"11":0,"13":0,"17":1,"20":0,"30":0,"68":0,"73":0,"82":0,"114":1,"117":0},' '"lk":1,"lt_st":0,"sho":[0,0,0,0],"ver":1}' ), - "qos": 0, - "retain": False, - "subscribed_topic": "tasmota/discovery/#", - "timestamp": None, - } + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = { - "topic": "tasmota/discovery/DC4F220848A2/config", - "payload": "", - "qos": 0, - "retain": False, - "subscribed_topic": "tasmota/discovery/#", - "timestamp": None, - } + discovery_info = mqtt.MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/config", + payload="", + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = { - "topic": "tasmota/discovery/DC4F220848A2/config", - "payload": ( + discovery_info = mqtt.MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/config", + payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' 'null,null,null],"hn":"tasmota_0848A2","mac":"DC4F220848A2","md":"Sonoff Basic",' '"ty":0,"if":0,"ofln":"Offline","onln":"Online","state":["OFF","ON",' @@ -67,11 +68,11 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): '"so":{"4":0,"11":0,"13":0,"17":1,"20":0,"30":0,"68":0,"73":0,"82":0,"114":1,"117":0},' '"lk":1,"lt_st":0,"sho":[0,0,0,0],"ver":1}' ), - "qos": 0, - "retain": False, - "subscribed_topic": "tasmota/discovery/#", - "timestamp": None, - } + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) @@ -80,9 +81,9 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" - discovery_info = { - "topic": "tasmota/discovery/DC4F220848A2/config", - "payload": ( + discovery_info = mqtt.MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/config", + payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' 'null,null,null],"hn":"tasmota_0848A2","mac":"DC4F220848A2","md":"Sonoff Basic",' '"ty":0,"if":0,"ofln":"Offline","onln":"Online","state":["OFF","ON",' @@ -92,11 +93,11 @@ async def test_mqtt_setup(hass, mqtt_mock) -> None: '"so":{"4":0,"11":0,"13":0,"17":1,"20":0,"30":0,"68":0,"73":0,"82":0,"114":1,"117":0},' '"lk":1,"lt_st":0,"sho":[0,0,0,0],"ver":1}' ), - "qos": 0, - "retain": False, - "subscribed_topic": "tasmota/discovery/#", - "timestamp": None, - } + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) From 5c443b626ad7513794093ab482ddc404b75ca1b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 22:10:14 +0100 Subject: [PATCH 0782/1452] Use native datetime value in MQTT sensors (#59923) --- homeassistant/components/mqtt/sensor.py | 14 +++++++ tests/components/mqtt/test_sensor.py | 54 ++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4d0f610300f..7ddc8ec467a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -21,6 +21,8 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + DEVICE_CLASS_DATE, + DEVICE_CLASS_TIMESTAMP, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -196,6 +198,18 @@ class MqttSensor(MqttEntity, SensorEntity): self._state, variables=variables, ) + + if payload is not None and self.device_class in ( + DEVICE_CLASS_DATE, + DEVICE_CLASS_TIMESTAMP, + ): + if (payload := dt_util.parse_datetime(payload)) is None: + _LOGGER.warning( + "Invalid state message '%s' from '%s'", msg.payload, msg.topic + ) + elif self.device_class == DEVICE_CLASS_DATE: + payload = payload.date() + self._state = payload def _update_last_reset(msg): diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 752b1cfa48a..680bafb3b2b 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -80,6 +80,58 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.attributes.get("unit_of_measurement") == "fav unit" +@pytest.mark.parametrize( + "device_class,native_value,state_value,log", + [ + (sensor.DEVICE_CLASS_DATE, "2021-11-18", "2021-11-18", False), + (sensor.DEVICE_CLASS_DATE, "invalid", STATE_UNKNOWN, True), + ( + sensor.DEVICE_CLASS_TIMESTAMP, + "2021-11-18T20:25:00+00:00", + "2021-11-18T20:25:00+00:00", + False, + ), + ( + sensor.DEVICE_CLASS_TIMESTAMP, + "2021-11-18 20:25:00+00:00", + "2021-11-18T20:25:00+00:00", + False, + ), + ( + sensor.DEVICE_CLASS_TIMESTAMP, + "2021-11-18 20:25:00+01:00", + "2021-11-18T19:25:00+00:00", + False, + ), + (sensor.DEVICE_CLASS_TIMESTAMP, "invalid", STATE_UNKNOWN, True), + ], +) +async def test_setting_sensor_native_value_handling_via_mqtt_message( + hass, mqtt_mock, caplog, device_class, native_value, state_value, log +): + """Test the setting of the value via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "device_class": device_class, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", native_value) + state = hass.states.get("sensor.test") + + assert state.state == state_value + assert state.attributes.get("device_class") == device_class + assert log == ("Invalid state message" in caplog.text) + + async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): """Test the expiration of the value.""" assert await async_setup_component( From 52e0027fad055e6be81f197be172d7379f141029 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 15:15:08 -0600 Subject: [PATCH 0783/1452] Limit homekit sources to prevent exceeding 100 limit (#59743) --- .../components/homekit/type_remotes.py | 35 ++++-- .../homekit/test_type_media_players.py | 117 ++++++++++++------ 2 files changed, 106 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 41a76ca7fed..69267c733d2 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -52,6 +52,10 @@ from .const import ( SERV_TELEVISION, ) +MAXIMUM_SOURCES = ( + 90 # Maximum services per accessory is 100. The base acccessory uses 9 +) + _LOGGER = logging.getLogger(__name__) REMOTE_KEYS = { @@ -92,7 +96,14 @@ class RemoteInputSelectAccessory(HomeAccessory): self.sources = [] self.support_select_source = False if features & required_feature: - self.sources = state.attributes.get(source_list_key, []) + sources = state.attributes.get(source_list_key, []) + if len(sources) > MAXIMUM_SOURCES: + _LOGGER.warning( + "%s: Reached maximum number of sources (%s)", + self.entity_id, + MAXIMUM_SOURCES, + ) + self.sources = sources[:MAXIMUM_SOURCES] if self.sources: self.support_select_source = True @@ -159,13 +170,21 @@ class RemoteInputSelectAccessory(HomeAccessory): possible_sources = new_state.attributes.get(self.source_list_key, []) if source_name in possible_sources: - _LOGGER.debug( - "%s: Sources out of sync. Rebuilding Accessory", - self.entity_id, - ) - # Sources are out of sync, recreate the accessory - self.async_reset() - return + index = possible_sources.index(source_name) + if index >= MAXIMUM_SOURCES: + _LOGGER.debug( + "%s: Source %s and above are not supported", + self.entity_id, + MAXIMUM_SOURCES, + ) + else: + _LOGGER.debug( + "%s: Sources out of sync. Rebuilding Accessory", + self.entity_id, + ) + # Sources are out of sync, recreate the accessory + self.async_reset() + return _LOGGER.debug( "%s: Source %s does not exist the source list: %s", diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 46787c98a3c..6b24c731fab 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -112,63 +112,49 @@ async def test_media_player_set_state(hass, hk_driver, events): call_media_stop = async_mock_service(hass, DOMAIN, "media_stop") call_toggle_mute = async_mock_service(hass, DOMAIN, "volume_mute") - await hass.async_add_executor_job( - acc.chars[FEATURE_ON_OFF].client_update_value, True - ) + acc.chars[FEATURE_ON_OFF].client_update_value(True) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_ON_OFF].client_update_value, False - ) + acc.chars[FEATURE_ON_OFF].client_update_value(False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_PLAY_PAUSE].client_update_value, True - ) + acc.chars[FEATURE_PLAY_PAUSE].client_update_value(True) await hass.async_block_till_done() assert call_media_play assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_PLAY_PAUSE].client_update_value, False - ) + acc.chars[FEATURE_PLAY_PAUSE].client_update_value(False) await hass.async_block_till_done() assert call_media_pause assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 4 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_PLAY_STOP].client_update_value, True - ) + acc.chars[FEATURE_PLAY_STOP].client_update_value(True) await hass.async_block_till_done() assert call_media_play assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 5 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_PLAY_STOP].client_update_value, False - ) + acc.chars[FEATURE_PLAY_STOP].client_update_value(False) await hass.async_block_till_done() assert call_media_stop assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 6 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_TOGGLE_MUTE].client_update_value, True - ) + acc.chars[FEATURE_TOGGLE_MUTE].client_update_value(True) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id @@ -176,9 +162,7 @@ async def test_media_player_set_state(hass, hk_driver, events): assert len(events) == 7 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job( - acc.chars[FEATURE_TOGGLE_MUTE].client_update_value, False - ) + acc.chars[FEATURE_TOGGLE_MUTE].client_update_value(False) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id @@ -258,21 +242,21 @@ async def test_media_player_television(hass, hk_driver, events, caplog): call_volume_down = async_mock_service(hass, DOMAIN, "volume_down") call_volume_set = async_mock_service(hass, DOMAIN, "volume_set") - await hass.async_add_executor_job(acc.char_active.client_update_value, 1) + acc.char_active.client_update_value(1) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_active.client_update_value, 0) + acc.char_active.client_update_value(0) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 11) + acc.char_remote_key.client_update_value(11) await hass.async_block_till_done() assert call_media_play_pause assert call_media_play_pause[0].data[ATTR_ENTITY_ID] == entity_id @@ -281,28 +265,28 @@ async def test_media_player_television(hass, hk_driver, events, caplog): hass.states.async_set(entity_id, STATE_PLAYING) await hass.async_block_till_done() - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 11) + acc.char_remote_key.client_update_value(11) await hass.async_block_till_done() assert call_media_pause assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 4 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 10) + acc.char_remote_key.client_update_value(10) await hass.async_block_till_done() assert len(events) == 4 assert events[-1].data[ATTR_VALUE] is None hass.states.async_set(entity_id, STATE_PAUSED) await hass.async_block_till_done() - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 11) + acc.char_remote_key.client_update_value(11) await hass.async_block_till_done() assert call_media_play assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 5 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_mute.client_update_value, True) + acc.char_mute.client_update_value(True) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id @@ -310,7 +294,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog): assert len(events) == 6 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_mute.client_update_value, False) + acc.char_mute.client_update_value(False) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id @@ -318,7 +302,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog): assert len(events) == 7 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_input_source.client_update_value, 1) + acc.char_input_source.client_update_value(1) await hass.async_block_till_done() assert call_select_source assert call_select_source[0].data[ATTR_ENTITY_ID] == entity_id @@ -326,21 +310,21 @@ async def test_media_player_television(hass, hk_driver, events, caplog): assert len(events) == 8 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_volume_selector.client_update_value, 0) + acc.char_volume_selector.client_update_value(0) await hass.async_block_till_done() assert call_volume_up assert call_volume_up[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 9 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_volume_selector.client_update_value, 1) + acc.char_volume_selector.client_update_value(1) await hass.async_block_till_done() assert call_volume_down assert call_volume_down[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 10 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_volume.client_update_value, 20) + acc.char_volume.client_update_value(20) await hass.async_block_till_done() assert call_volume_set[0] assert call_volume_set[0].data[ATTR_ENTITY_ID] == entity_id @@ -356,10 +340,10 @@ async def test_media_player_television(hass, hk_driver, events, caplog): hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) with pytest.raises(ValueError): - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) + acc.char_remote_key.client_update_value(20) await hass.async_block_till_done() - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7) + acc.char_remote_key.client_update_value(7) await hass.async_block_till_done() assert len(events) == 1 @@ -471,3 +455,60 @@ async def test_tv_restore(hass, hk_driver, events): ] assert acc.support_select_source is True assert acc.char_input_source is not None + + +async def test_media_player_television_max_sources(hass, hk_driver, events, caplog): + """Test if television accessory that reaches the maximum number of sources.""" + entity_id = "media_player.television" + sources = [f"HDMI {i}" for i in range(1, 101)] + hass.states.async_set( + entity_id, + None, + { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_SUPPORTED_FEATURES: 3469, + ATTR_MEDIA_VOLUME_MUTED: False, + ATTR_INPUT_SOURCE: "HDMI 3", + ATTR_INPUT_SOURCE_LIST: sources, + }, + ) + await hass.async_block_till_done() + acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 31 # Television + + assert acc.char_active.value == 0 + assert acc.char_remote_key.value == 0 + assert acc.char_input_source.value == 2 + assert acc.char_mute.value is False + + hass.states.async_set( + entity_id, + None, + { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_SUPPORTED_FEATURES: 3469, + ATTR_MEDIA_VOLUME_MUTED: False, + ATTR_INPUT_SOURCE: "HDMI 90", + ATTR_INPUT_SOURCE_LIST: sources, + }, + ) + await hass.async_block_till_done() + assert acc.char_input_source.value == 89 + + hass.states.async_set( + entity_id, + None, + { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_SUPPORTED_FEATURES: 3469, + ATTR_MEDIA_VOLUME_MUTED: False, + ATTR_INPUT_SOURCE: "HDMI 91", + ATTR_INPUT_SOURCE_LIST: sources, + }, + ) + await hass.async_block_till_done() + assert acc.char_input_source.value == 0 From fd8229f4f4f6509f5783c89b06e492bc9b98967a Mon Sep 17 00:00:00 2001 From: Robert Dunmire III Date: Tue, 23 Nov 2021 16:18:19 -0500 Subject: [PATCH 0784/1452] Bump librouteros version to 3.2.0 (#60066) --- homeassistant/components/mikrotik/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index fdb6774f4b6..eff9d26103d 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -3,7 +3,7 @@ "name": "Mikrotik", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mikrotik", - "requirements": ["librouteros==3.0.0"], + "requirements": ["librouteros==3.2.0"], "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 34978289ce9..fa77ae9c9cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -933,7 +933,7 @@ libpyfoscam==1.0 libpyvivotek==0.4.0 # homeassistant.components.mikrotik -librouteros==3.0.0 +librouteros==3.2.0 # homeassistant.components.soundtouch libsoundtouch==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6846e9e9f21..25b782d9e01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -582,7 +582,7 @@ krakenex==2.1.0 libpyfoscam==1.0 # homeassistant.components.mikrotik -librouteros==3.0.0 +librouteros==3.2.0 # homeassistant.components.soundtouch libsoundtouch==0.8 From 73d4445f80673e1fe7e7d947644cd871ac06381a Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Tue, 23 Nov 2021 23:21:10 +0200 Subject: [PATCH 0785/1452] Bumped version of wirelesstagpy to 0.8.0 (#60125) --- homeassistant/components/wirelesstag/__init__.py | 3 ++- homeassistant/components/wirelesstag/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index b7de56d8eff..a7555d15bb9 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -3,7 +3,8 @@ import logging from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from wirelesstagpy import WirelessTags, WirelessTagsException +from wirelesstagpy import WirelessTags +from wirelesstagpy.exceptions import WirelessTagsException from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index 37c1b82cba9..2772b117e07 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -2,7 +2,7 @@ "domain": "wirelesstag", "name": "Wireless Sensor Tags", "documentation": "https://www.home-assistant.io/integrations/wirelesstag", - "requirements": ["wirelesstagpy==0.5.0"], + "requirements": ["wirelesstagpy==0.8.0"], "codeowners": ["@sergeymaysak"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index fa77ae9c9cf..42a59d20a56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2418,7 +2418,7 @@ whirlpool-sixth-sense==0.15.1 wiffi==1.0.1 # homeassistant.components.wirelesstag -wirelesstagpy==0.5.0 +wirelesstagpy==0.8.0 # homeassistant.components.withings withings-api==2.3.2 From 24779dea3b14a6de1fa01b63620222596b46d502 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Nov 2021 22:22:15 +0100 Subject: [PATCH 0786/1452] Bump device registry version to 1.2 (#60199) --- homeassistant/helpers/device_registry.py | 79 ++++++++++++++------- tests/helpers/test_device_registry.py | 88 ++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 29 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 65df557b582..b99e80e197a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,6 +11,7 @@ import attr from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import RequiredParameterMissing +from homeassistant.helpers import storage from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass from homeassistant.util.enum import StrEnum @@ -31,7 +32,8 @@ _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" -STORAGE_VERSION = 1 +STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 2 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -159,6 +161,41 @@ def _async_get_device_id_from_index( return None +class DeviceRegistryStore(storage.Store): + """Store entity registry data.""" + + async def _async_migrate_func( + self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any] + ) -> dict[str, Any]: + """Migrate to the new version.""" + if old_major_version < 2 and old_minor_version < 2: + # From version 1.1 + for device in old_data["devices"]: + # Introduced in 0.110 + device["entry_type"] = device.get("entry_type") + # Introduced in 0.79 + # renamed in 0.95 + device["via_device_id"] = device.get("via_device_id") or device.get( + "hub_device_id" + ) + # Introduced in 0.87 + device["area_id"] = device.get("area_id") + device["name_by_user"] = device.get("name_by_user") + # Introduced in 0.119 + device["disabled_by"] = device.get("disabled_by") + # Introduced in 2021.11 + device["configuration_url"] = device.get("configuration_url") + # Introduced in 0.111 + old_data["deleted_devices"] = old_data.get("deleted_devices", []) + for device in old_data["deleted_devices"]: + # Introduced in 2021.2 + device["orphaned_timestamp"] = device.get("orphaned_timestamp") + + if old_major_version > 1: + raise NotImplementedError + return old_data + + class DeviceRegistry: """Class to hold a registry of devices.""" @@ -170,8 +207,12 @@ class DeviceRegistry: def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" self.hass = hass - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + self._store = DeviceRegistryStore( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, ) self._clear_index() @@ -519,44 +560,36 @@ class DeviceRegistry: deleted_devices = OrderedDict() if data is not None: + data = cast("dict[str, Any]", data) for device in data["devices"]: devices[device["id"]] = DeviceEntry( + area_id=device["area_id"], config_entries=set(device["config_entries"]), + configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + disabled_by=device["disabled_by"], + entry_type=DeviceEntryType(device["entry_type"]) + if device["entry_type"] + else None, + id=device["id"], identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] manufacturer=device["manufacturer"], model=device["model"], + name_by_user=device["name_by_user"], name=device["name"], sw_version=device["sw_version"], - # Introduced in 0.110 - entry_type=DeviceEntryType(device["entry_type"]) - if device.get("entry_type") - else None, - id=device["id"], - # Introduced in 0.79 - # renamed in 0.95 - via_device_id=( - device.get("via_device_id") or device.get("hub_device_id") - ), - # Introduced in 0.87 - area_id=device.get("area_id"), - name_by_user=device.get("name_by_user"), - # Introduced in 0.119 - disabled_by=device.get("disabled_by"), - # Introduced in 2021.11 - configuration_url=device.get("configuration_url"), + via_device_id=device["via_device_id"], ) # Introduced in 0.111 - for device in data.get("deleted_devices", []): + for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] id=device["id"], - # Introduced in 2021.2 - orphaned_timestamp=device.get("orphaned_timestamp"), + orphaned_timestamp=device["orphaned_timestamp"], ) self.devices = devices diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 5d3736fd060..2955c02345f 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -167,23 +167,25 @@ async def test_multiple_config_entries(registry): async def test_loading_from_storage(hass, hass_storage): """Test loading stored devices on start.""" hass_storage[device_registry.STORAGE_KEY] = { - "version": device_registry.STORAGE_VERSION, + "version": device_registry.STORAGE_VERSION_MAJOR, + "minor_version": device_registry.STORAGE_VERSION_MINOR, "data": { "devices": [ { + "area_id": "12345A", "config_entries": ["1234"], + "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": device_registry.DISABLED_USER, + "entry_type": device_registry.DeviceEntryType.SERVICE, "id": "abcdefghijklm", "identifiers": [["serial", "12:34:56:AB:CD:EF"]], "manufacturer": "manufacturer", "model": "model", + "name_by_user": "Test Friendly Name", "name": "name", "sw_version": "version", - "entry_type": device_registry.DeviceEntryType.SERVICE, - "area_id": "12345A", - "name_by_user": "Test Friendly Name", - "disabled_by": device_registry.DISABLED_USER, - "suggested_area": "Kitchen", + "via_device_id": None, } ], "deleted_devices": [ @@ -192,6 +194,7 @@ async def test_loading_from_storage(hass, hass_storage): "connections": [["Zigbee", "23.45.67.89.01"]], "id": "bcdefghijklmn", "identifiers": [["serial", "34:56:AB:CD:EF:12"]], + "orphaned_timestamp": None, } ], }, @@ -231,6 +234,79 @@ async def test_loading_from_storage(hass, hass_storage): assert isinstance(entry.identifiers, set) +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_1_to_1_2(hass, hass_storage): + """Test migration from version 1.1 to 1.2.""" + hass_storage[device_registry.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "data": { + "devices": [ + { + "config_entries": ["1234"], + "connections": [["Zigbee", "01.23.45.67.89"]], + "entry_type": "service", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "sw_version": "version", + } + ], + }, + } + + await device_registry.async_load(hass) + registry = device_registry.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + assert hass_storage[device_registry.STORAGE_KEY] == { + "version": device_registry.STORAGE_VERSION_MAJOR, + "minor_version": device_registry.STORAGE_VERSION_MINOR, + "key": device_registry.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "sw_version": "new_version", + "via_device_id": None, + } + ], + "deleted_devices": [], + }, + } + + async def test_removing_config_entries(hass, registry, update_events): """Make sure we do not get duplicate entries.""" entry = registry.async_get_or_create( From ce369bb3362b651b7ad3fbde4bcb0c45f9243ea5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 15:23:38 -0600 Subject: [PATCH 0787/1452] Reduce flux_led light turn on complexity (#60139) --- homeassistant/components/flux_led/light.py | 69 +++++++++++----------- tests/components/flux_led/test_light.py | 8 --- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 8c6da0583b4..0a0bccd4e6f 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -299,21 +299,48 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" - if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: - brightness = self.brightness - if not self.is_on: await self._device.async_turn_on() - if not kwargs: - return + if not kwargs: + return + if effect := kwargs.get(ATTR_EFFECT): + await self._async_set_effect(effect) + return + await self._async_set_colors(**kwargs) + + async def _async_set_effect(self, effect: str) -> None: + """Set an effect.""" + # Random color effect + if effect == EFFECT_RANDOM: + await self._device.async_set_levels( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + return + # Custom effect + if effect == EFFECT_CUSTOM: + if self._custom_effect_colors: + await self._device.async_set_custom_pattern( + self._custom_effect_colors, + self._custom_effect_speed_pct, + self._custom_effect_transition, + ) + return + await self._device.async_set_effect( + effect, self._device.speed or DEFAULT_EFFECT_SPEED + ) + + async def _async_set_colors(self, **kwargs: Any) -> None: + """Set color (can be done before turning on).""" + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: + brightness = self.brightness + if not brightness: # If the brightness was previously 0, the light # will not turn on unless brightness is at least 1 - if not brightness: - brightness = 1 - elif not brightness: # If the device was on and brightness was not # set, it means it was masked by an effect - brightness = 255 + brightness = 255 if self.is_on else 1 # Handle switch to CCT Color Mode if ATTR_COLOR_TEMP in kwargs: @@ -357,29 +384,6 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): if ATTR_WHITE in kwargs: await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) return - if ATTR_EFFECT in kwargs: - effect = kwargs[ATTR_EFFECT] - # Random color effect - if effect == EFFECT_RANDOM: - await self._device.async_set_levels( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), - ) - return - # Custom effect - if effect == EFFECT_CUSTOM: - if self._custom_effect_colors: - await self._device.async_set_custom_pattern( - self._custom_effect_colors, - self._custom_effect_speed_pct, - self._custom_effect_transition, - ) - return - await self._device.async_set_effect( - effect, self._device.speed or DEFAULT_EFFECT_SPEED - ) - return # Handle brightness adjustment in CCT Color Mode if self.color_mode == COLOR_MODE_COLOR_TEMP: @@ -404,7 +408,6 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): if self.color_mode in {COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS}: await self._device.async_set_levels(w=brightness) return - raise ValueError(f"Unsupported color mode {self.color_mode}") async def async_set_custom_effect( self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 1a9ce57a4f7..9a89632513c 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1098,11 +1098,3 @@ async def test_addressable_light(hass: HomeAssistant) -> None: bulb.async_turn_on.assert_called_once() bulb.async_turn_on.reset_mock() await async_mock_device_turn_on(hass, bulb) - - with pytest.raises(ValueError): - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, - blocking=True, - ) From 6089aef0726da47d039ca4eb04de80ffa38c33d3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:30:22 +0100 Subject: [PATCH 0788/1452] Enable strict typing - wallbox (#59301) --- .strict-typing | 1 + homeassistant/components/wallbox/__init__.py | 28 +++++++-------- .../components/wallbox/config_flow.py | 21 ++++++++--- homeassistant/components/wallbox/number.py | 36 ++++++++++++------- homeassistant/components/wallbox/sensor.py | 35 ++++++++++++------ mypy.ini | 11 ++++++ 6 files changed, 91 insertions(+), 41 deletions(-) diff --git a/.strict-typing b/.strict-typing index daed6f3434a..318a1367bef 100644 --- a/.strict-typing +++ b/.strict-typing @@ -141,6 +141,7 @@ homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.velbus.* homeassistant.components.vlc_telnet.* +homeassistant.components.wallbox.* homeassistant.components.water_heater.* homeassistant.components.watttime.* homeassistant.components.weather.* diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index b1604a37c6f..c40d3fb37a8 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -1,7 +1,10 @@ """The Wallbox integration.""" +from __future__ import annotations + from datetime import timedelta from http import HTTPStatus import logging +from typing import Any, Dict import requests from wallbox import Wallbox @@ -20,10 +23,10 @@ PLATFORMS = ["sensor", "number"] UPDATE_INTERVAL = 30 -class WallboxCoordinator(DataUpdateCoordinator): +class WallboxCoordinator(DataUpdateCoordinator[Dict[str, Any]]): """Wallbox Coordinator class.""" - def __init__(self, station, wallbox, hass): + def __init__(self, station: str, wallbox: Wallbox, hass: HomeAssistant) -> None: """Initialize.""" self._station = station self._wallbox = wallbox @@ -35,31 +38,29 @@ class WallboxCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - def _authenticate(self): + def _authenticate(self) -> None: """Authenticate using Wallbox API.""" try: self._wallbox.authenticate() - return True except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: raise ConfigEntryAuthFailed from wallbox_connection_error raise ConnectionError from wallbox_connection_error - def _validate(self): + def _validate(self) -> None: """Authenticate using Wallbox API.""" try: self._wallbox.authenticate() - return True except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error - def _get_data(self): + def _get_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" try: self._authenticate() - data = self._wallbox.getChargerStatus(self._station) + data: dict[str, Any] = self._wallbox.getChargerStatus(self._station) data[CONF_MAX_CHARGING_CURRENT_KEY] = data[CONF_DATA_KEY][ CONF_MAX_CHARGING_CURRENT_KEY ] @@ -69,7 +70,7 @@ class WallboxCoordinator(DataUpdateCoordinator): except requests.exceptions.HTTPError as wallbox_connection_error: raise ConnectionError from wallbox_connection_error - def _set_charging_current(self, charging_current): + def _set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" try: self._authenticate() @@ -79,22 +80,21 @@ class WallboxCoordinator(DataUpdateCoordinator): raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error - async def async_set_charging_current(self, charging_current): + async def async_set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" await self.hass.async_add_executor_job( self._set_charging_current, charging_current ) await self.async_request_refresh() - async def _async_update_data(self) -> bool: + async def _async_update_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" data = await self.hass.async_add_executor_job(self._get_data) return data - async def async_validate_input(self) -> bool: + async def async_validate_input(self) -> None: """Get new sensor data for Wallbox component.""" - data = await self.hass.async_add_executor_job(self._validate) - return data + await self.hass.async_add_executor_job(self._validate) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index 8559c29c9aa..d2c0a048fa1 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -1,9 +1,14 @@ """Config flow for Wallbox integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from wallbox import Wallbox from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from . import InvalidAuth, WallboxCoordinator from .const import CONF_STATION, DOMAIN @@ -19,7 +24,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. @@ -36,11 +43,13 @@ async def validate_input(hass: core.HomeAssistant, data): class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): """Handle a config flow for Wallbox.""" - def __init__(self): + def __init__(self) -> None: """Start the Wallbox config flow.""" - self._reauth_entry = None + self._reauth_entry: config_entries.ConfigEntry | None = None - async def async_step_reauth(self, user_input=None): + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -48,7 +57,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index b94351f4353..64c1f1e1abb 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -2,12 +2,16 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Optional, cast from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_CURRENT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import InvalidAuth +from . import InvalidAuth, WallboxCoordinator from .const import CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, DOMAIN @@ -28,9 +32,11 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { } -async def async_setup_entry(hass, config, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Create wallbox sensor entities in HASS.""" - coordinator = hass.data[DOMAIN][config.entry_id] + coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] # Check if the user is authorized to change current, if so, add number component: try: await coordinator.async_set_charging_current( @@ -41,7 +47,7 @@ async def async_setup_entry(hass, config, async_add_entities): async_add_entities( [ - WallboxNumber(coordinator, config, description) + WallboxNumber(coordinator, entry, description) for ent in coordinator.data if (description := NUMBER_TYPES.get(ent)) ] @@ -52,27 +58,33 @@ class WallboxNumber(CoordinatorEntity, NumberEntity): """Representation of the Wallbox portal.""" entity_description: WallboxNumberEntityDescription + coordinator: WallboxCoordinator def __init__( - self, coordinator, config, description: WallboxNumberEntityDescription - ): + self, + coordinator: WallboxCoordinator, + entry: ConfigEntry, + description: WallboxNumberEntityDescription, + ) -> None: """Initialize a Wallbox sensor.""" super().__init__(coordinator) self.entity_description = description self._coordinator = coordinator - self._attr_name = f"{config.title} {description.name}" + self._attr_name = f"{entry.title} {description.name}" self._attr_min_value = description.min_value @property - def max_value(self): + def max_value(self) -> float: """Return the maximum available current.""" - return self._coordinator.data[CONF_MAX_AVAILABLE_POWER_KEY] + return cast(float, self._coordinator.data[CONF_MAX_AVAILABLE_POWER_KEY]) @property - def value(self): + def value(self) -> float | None: """Return the state of the sensor.""" - return self._coordinator.data[CONF_MAX_CHARGING_CURRENT_KEY] + return cast( + Optional[float], self._coordinator.data[CONF_MAX_CHARGING_CURRENT_KEY] + ) - async def async_set_value(self, value: float): + async def async_set_value(self, value: float) -> None: """Set the value of the entity.""" await self._coordinator.async_set_charging_current(value) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 4571ed08725..835a8c405da 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging +from typing import cast from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -10,6 +11,8 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.components.wallbox import WallboxCoordinator +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, @@ -21,6 +24,9 @@ from homeassistant.const import ( PERCENTAGE, POWER_KILO_WATT, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -130,13 +136,15 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { } -async def async_setup_entry(hass, config, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Create wallbox sensor entities in HASS.""" - coordinator = hass.data[DOMAIN][config.entry_id] + coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - WallboxSensor(coordinator, config, description) + WallboxSensor(coordinator, entry, description) for ent in coordinator.data if (description := SENSOR_TYPES.get(ent)) ] @@ -147,24 +155,31 @@ class WallboxSensor(CoordinatorEntity, SensorEntity): """Representation of the Wallbox portal.""" entity_description: WallboxSensorEntityDescription + coordinator: WallboxCoordinator def __init__( - self, coordinator, config, description: WallboxSensorEntityDescription - ): + self, + coordinator: WallboxCoordinator, + entry: ConfigEntry, + description: WallboxSensorEntityDescription, + ) -> None: """Initialize a Wallbox sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{config.title} {description.name}" + self._attr_name = f"{entry.title} {description.name}" @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" if (sensor_round := self.entity_description.precision) is not None: try: - return round( - self.coordinator.data[self.entity_description.key], sensor_round + return cast( + StateType, + round( + self.coordinator.data[self.entity_description.key], sensor_round + ), ) except TypeError: _LOGGER.debug("Cannot format %s", self._attr_name) return None - return self.coordinator.data[self.entity_description.key] + return cast(StateType, self.coordinator.data[self.entity_description.key]) diff --git a/mypy.ini b/mypy.ini index 9c7c481e986..88b10fe06ab 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1562,6 +1562,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wallbox.*] +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.water_heater.*] check_untyped_defs = true disallow_incomplete_defs = true From 9088a6a13816652aabf7337a924dfdbc387d745e Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Tue, 23 Nov 2021 21:45:23 +0000 Subject: [PATCH 0789/1452] Add device registry entry for MetOffice (#58683) --- .../components/metoffice/__init__.py | 11 ++++++++++ homeassistant/components/metoffice/sensor.py | 4 ++++ homeassistant/components/metoffice/weather.py | 20 +++++++------------ tests/components/metoffice/const.py | 8 ++++++++ tests/components/metoffice/test_sensor.py | 15 ++++++++++++++ tests/components/metoffice/test_weather.py | 18 +++++++++++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 4c187a606c7..2d762eb7c3b 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -98,3 +99,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) return unload_ok + + +def get_device_info(coordinates: str, name: str) -> DeviceInfo: + """Return device registry information.""" + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, coordinates)}, + manufacturer="Met Office", + name=f"Met Office {name}", + ) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 4919e36bd58..4f2ed842086 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import get_device_info from .const import ( ATTRIBUTION, CONDITION_CLASSES, @@ -181,6 +182,9 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._attr_device_info = get_device_info( + coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] + ) self._attr_name = f"{hass_data[METOFFICE_NAME]} {description.name} {mode_label}" self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index b02539f0e31..56d94409db5 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import get_device_info from .const import ( ATTRIBUTION, CONDITION_CLASSES, @@ -76,20 +77,13 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): super().__init__(coordinator) mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL - self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" - self._unique_id = hass_data[METOFFICE_COORDINATES] + self._attr_device_info = get_device_info( + coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] + ) + self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" + self._attr_unique_id = hass_data[METOFFICE_COORDINATES] if not use_3hourly: - self._unique_id = f"{self._unique_id}_{MODE_DAILY}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique of the sensor.""" - return self._unique_id + self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @property def condition(self): diff --git a/tests/components/metoffice/const.py b/tests/components/metoffice/const.py index c9a173e3f12..56383764d08 100644 --- a/tests/components/metoffice/const.py +++ b/tests/components/metoffice/const.py @@ -1,5 +1,6 @@ """Helpers for testing Met Office DataPoint.""" +from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME TEST_DATETIME_STRING = "2020-04-25T12:00:00+00:00" @@ -55,3 +56,10 @@ WAVERTREE_SENSOR_RESULTS = { "wind_speed": ("wind_speed", "9"), "humidity": ("humidity", "50"), } + +DEVICE_KEY_KINGSLYNN = { + (DOMAIN, f"{TEST_LATITUDE_KINGSLYNN}_{TEST_LONGITUDE_KINGSLYNN}") +} +DEVICE_KEY_WAVERTREE = { + (DOMAIN, f"{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}") +} diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 201c5922d33..b8b2458dc5b 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -3,9 +3,12 @@ import json from unittest.mock import patch from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN +from homeassistant.helpers.device_registry import async_get as get_dev_reg from . import NewDateTime from .const import ( + DEVICE_KEY_KINGSLYNN, + DEVICE_KEY_WAVERTREE, KINGSLYNN_SENSOR_RESULTS, METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, @@ -48,6 +51,11 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + dev_reg = get_dev_reg(hass) + assert len(dev_reg.devices) == 1 + device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert device_wavertree.name == "Met Office Wavertree" + running_sensor_ids = hass.states.async_entity_ids("sensor") assert len(running_sensor_ids) > 0 for running_id in running_sensor_ids: @@ -105,6 +113,13 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() + dev_reg = get_dev_reg(hass) + assert len(dev_reg.devices) == 2 + device_kingslynn = dev_reg.async_get_device(identifiers=DEVICE_KEY_KINGSLYNN) + assert device_kingslynn.name == "Met Office King's Lynn" + device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert device_wavertree.name == "Met Office Wavertree" + running_sensor_ids = hass.states.async_entity_ids("sensor") assert len(running_sensor_ids) > 0 for running_id in running_sensor_ids: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 76e01b638c3..4fdd80f8277 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -5,10 +5,13 @@ from unittest.mock import patch from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers.device_registry import async_get as get_dev_reg from homeassistant.util import utcnow from . import NewDateTime from .const import ( + DEVICE_KEY_KINGSLYNN, + DEVICE_KEY_WAVERTREE, METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, WAVERTREE_SENSOR_RESULTS, @@ -36,6 +39,9 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + dev_reg = get_dev_reg(hass) + assert len(dev_reg.devices) == 0 + assert hass.states.get("weather.met_office_wavertree_3hourly") is None assert hass.states.get("weather.met_office_wavertree_daily") is None for sensor_id in WAVERTREE_SENSOR_RESULTS: @@ -124,6 +130,11 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + dev_reg = get_dev_reg(hass) + assert len(dev_reg.devices) == 1 + device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert device_wavertree.name == "Met Office Wavertree" + # Wavertree 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_wavertree_3_hourly") assert weather @@ -213,6 +224,13 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() + dev_reg = get_dev_reg(hass) + assert len(dev_reg.devices) == 2 + device_kingslynn = dev_reg.async_get_device(identifiers=DEVICE_KEY_KINGSLYNN) + assert device_kingslynn.name == "Met Office King's Lynn" + device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert device_wavertree.name == "Met Office Wavertree" + # Wavertree 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_wavertree_3_hourly") assert weather From 135778fe914a59fd6c1a573251cec6f0449fa6b3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Nov 2021 22:49:42 +0100 Subject: [PATCH 0790/1452] Enable basic type checking for awair (#55046) --- homeassistant/components/awair/config_flow.py | 1 + homeassistant/components/awair/const.py | 3 ++- homeassistant/components/awair/sensor.py | 16 +++++++++++----- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 2214fc30519..4c4ccad8f52 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -69,6 +69,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): if error is None: entry = await self.async_set_unique_id(self.unique_id) + assert entry self.hass.config_entries.async_update_entry(entry, data=user_input) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 4968e86bcf5..68ca3335d97 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import logging +from python_awair.air_data import AirData from python_awair.devices import AwairDevice from homeassistant.components.sensor import SensorEntityDescription @@ -134,4 +135,4 @@ class AwairResult: """Wrapper class to hold an awair device and set of air data.""" device: AwairDevice - air_data: dict + air_data: AirData diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 1ff1b6e0efb..4e67e56cfe3 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -1,12 +1,13 @@ """Support for Awair sensors.""" from __future__ import annotations +from python_awair.air_data import AirData from python_awair.devices import AwairDevice import voluptuous as vol from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_CONNECTIONS, @@ -18,7 +19,6 @@ from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ): """Set up Awair sensor entity based on a config entry.""" @@ -131,6 +131,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): # for users with first-gen devices that are upgrading. if ( self.entity_description.key == API_PM25 + and self._air_data and API_DUST in self._air_data.sensors ): unique_id_tag = "DUST" @@ -161,8 +162,11 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return False @property - def native_value(self) -> float: + def native_value(self) -> float | None: """Return the state, rounding off to reasonable values.""" + if not self._air_data: + return None + state: float sensor_type = self.entity_description.key @@ -206,6 +210,8 @@ class AwairSensor(CoordinatorEntity, SensorEntity): """ sensor_type = self.entity_description.key attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + if not self._air_data: + return attrs if sensor_type in self._air_data.indices: attrs["awair_index"] = abs(self._air_data.indices[sensor_type]) elif sensor_type in DUST_ALIASES and API_DUST in self._air_data.indices: @@ -233,7 +239,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return info @property - def _air_data(self) -> AwairResult | None: + def _air_data(self) -> AirData | None: """Return the latest data for our device, or None.""" result: AwairResult | None = self.coordinator.data.get(self._device.uuid) if result: diff --git a/mypy.ini b/mypy.ini index 88b10fe06ab..6e9cc991f7f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1672,9 +1672,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.awair.*] -ignore_errors = true - [mypy-homeassistant.components.blueprint.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index cf1faadc72e..41dfa1ad05e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -14,7 +14,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.awair.*", "homeassistant.components.blueprint.*", "homeassistant.components.climacell.*", "homeassistant.components.config.*", From af51aeb6dc000579e7e12ba43a6516be2650890e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 15:50:54 -0600 Subject: [PATCH 0791/1452] Show how user input is malformed in the UI on error (#60057) --- homeassistant/helpers/data_entry_flow.py | 6 +- .../components/config/test_config_entries.py | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 787041700f4..061233c0e1a 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -110,8 +110,10 @@ class FlowManagerResourceView(_BaseFlowManagerView): result = await self._flow_mgr.async_configure(flow_id, data) except data_entry_flow.UnknownFlow: return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) - except vol.Invalid: - return self.json_message("User input malformed", HTTPStatus.BAD_REQUEST) + except vol.Invalid as ex: + return self.json_message( + f"User input malformed: {ex}", HTTPStatus.BAD_REQUEST + ) result = self._prepare_result_json(result) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 04444e40f5d..8b890148d51 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -12,6 +12,7 @@ from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS from homeassistant.core import callback from homeassistant.generated import config_flows +import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component from tests.common import ( @@ -689,6 +690,81 @@ async def test_two_step_options_flow(hass, client): } +async def test_options_flow_with_invalid_data(hass, client): + """Test an options flow with invalid_data.""" + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config_entry): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + return self.async_show_form( + step_id="finish", + data_schema=vol.Schema( + { + vol.Required( + "choices", default=["invalid", "valid"] + ): cv.multi_select({"valid": "Valid"}) + } + ), + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title="Enable disable", data=user_input + ) + + return OptionsFlowHandler() + + MockConfigEntry( + domain="test", + entry_id="test1", + source="bla", + ).add_to_hass(hass) + entry = hass.config_entries.async_entries()[0] + + with patch.dict(HANDLERS, {"test": TestFlow}): + url = "/api/config/config_entries/options/flow" + resp = await client.post(url, json={"handler": entry.entry_id}) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data.pop("flow_id") + assert data == { + "type": "form", + "handler": "test1", + "step_id": "finish", + "data_schema": [ + { + "default": ["invalid", "valid"], + "name": "choices", + "options": {"valid": "Valid"}, + "required": True, + "type": "multi_select", + } + ], + "description_placeholders": None, + "errors": None, + "last_step": None, + } + + with patch.dict(HANDLERS, {"test": TestFlow}): + resp = await client.post( + f"/api/config/config_entries/options/flow/{flow_id}", + json={"choices": ["valid", "invalid"]}, + ) + assert resp.status == HTTPStatus.BAD_REQUEST + data = await resp.json() + assert data == { + "message": "User input malformed: invalid is not a valid option for " + "dictionary value @ data['choices']" + } + + async def test_update_prefrences(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) From 2de0a14db0d52af3b909ec1341519e1b458aeae0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:55:32 +0100 Subject: [PATCH 0792/1452] Use native datetime value in IPP sensors (#60234) --- homeassistant/components/ipp/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 02e2082bbd3..2cc7d00a633 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -1,7 +1,7 @@ """Support for IPP sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any from homeassistant.components.sensor import SensorEntity @@ -187,7 +187,6 @@ class IPPUptimeSensor(IPPSensor): ) @property - def native_value(self) -> str: + def native_value(self) -> datetime: """Return the state of the sensor.""" - uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) - return uptime.replace(microsecond=0).isoformat() + return utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) From 44611d7e26f204ca9e0f45af4a8e1a676538efb4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:59:36 +0100 Subject: [PATCH 0793/1452] Use dataclass for ZeroconfServiceInfo (#60206) Co-authored-by: epenet --- .../components/bosch_shc/config_flow.py | 2 +- .../components/modern_forms/config_flow.py | 4 +- homeassistant/components/nut/config_flow.py | 2 +- .../components/shelly/config_flow.py | 4 +- homeassistant/components/wled/config_flow.py | 4 +- homeassistant/components/zeroconf/__init__.py | 47 ++++++++++++++----- .../homekit_controller/test_config_flow.py | 4 +- tests/components/ipp/test_config_flow.py | 44 +++++++++-------- tests/components/lookin/test_config_flow.py | 6 +-- tests/components/roku/test_config_flow.py | 9 ++-- 10 files changed, 72 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 4c6070b41eb..c642df2a619 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -186,7 +186,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - if not discovery_info.get(zeroconf.ATTR_NAME, "").startswith("Bosch SHC"): + if not discovery_info.name.startswith("Bosch SHC"): return self.async_abort(reason="not_bosch_shc") try: diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index bd52ae6f5a2..6a1059a0387 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Modern Forms.""" from __future__ import annotations -from typing import Any, cast +from typing import Any from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice import voluptuous as vol @@ -43,7 +43,7 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): ) # Prepare configuration flow - return await self._handle_config_flow(cast(dict, discovery_info), True) + return await self._handle_config_flow({}, True) async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 9a8fc704886..120c3754eca 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -107,7 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input.update( { CONF_HOST: self.discovery_info[CONF_HOST], - CONF_PORT: self.discovery_info.get(CONF_PORT, DEFAULT_PORT), + CONF_PORT: self.discovery_info.port or DEFAULT_PORT, } ) info, errors = await self._async_validate_or_error(user_input) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 0f744fad4c7..ee24a302923 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -201,9 +201,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: host}) self.host = host - self.context["title_placeholders"] = { - "name": discovery_info.get("name", "").split(".")[0] - } + self.context["title_placeholders"] = {"name": discovery_info.name.split(".")[0]} if get_info_auth(self.info): return await self.async_step_credentials() diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index fa02c14ed27..b2112e8962b 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the WLED integration.""" from __future__ import annotations -from typing import Any, cast +from typing import Any import voluptuous as vol from wled import WLED, WLEDConnectionError @@ -57,7 +57,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): ) # Prepare configuration flow - return await self._handle_config_flow(cast(dict, discovery_info), True) + return await self._handle_config_flow({}, True) async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 8735dc47b83..dd42b2146d7 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -3,12 +3,13 @@ from __future__ import annotations import asyncio from contextlib import suppress +from dataclasses import dataclass import fnmatch from ipaddress import IPv4Address, IPv6Address, ip_address import logging import socket import sys -from typing import Any, Final, TypedDict, cast +from typing import Any, Final, cast import voluptuous as vol from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange @@ -25,8 +26,10 @@ from homeassistant.const import ( __version__, ) from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.frame import report 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 @@ -89,7 +92,8 @@ CONFIG_SCHEMA = vol.Schema( ) -class ZeroconfServiceInfo(TypedDict): +@dataclass +class ZeroconfServiceInfo(BaseServiceInfo): """Prepared info from mDNS entries.""" host: str @@ -99,6 +103,25 @@ class ZeroconfServiceInfo(TypedDict): name: str properties: dict[str, Any] + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"zeroconf"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + return getattr(self, name) + @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: @@ -360,7 +383,7 @@ class ZeroconfDiscovery: # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - props = info[ATTR_PROPERTIES] + props = info.properties if domain := async_get_homekit_discovery_domain(self.homekit_models, props): discovery_flow.async_create_flow( self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT}, info @@ -382,25 +405,23 @@ class ZeroconfDiscovery: # likely bad homekit data return - if ATTR_NAME in info: - lowercase_name: str | None = info[ATTR_NAME].lower() + if info.name: + lowercase_name: str | None = info.name.lower() else: lowercase_name = None - if "macaddress" in info[ATTR_PROPERTIES]: - uppercase_mac: str | None = info[ATTR_PROPERTIES]["macaddress"].upper() + if "macaddress" in info.properties: + uppercase_mac: str | None = info.properties["macaddress"].upper() else: uppercase_mac = None - if "manufacturer" in info[ATTR_PROPERTIES]: - lowercase_manufacturer: str | None = info[ATTR_PROPERTIES][ - "manufacturer" - ].lower() + if "manufacturer" in info.properties: + lowercase_manufacturer: str | None = info.properties["manufacturer"].lower() else: lowercase_manufacturer = None - if "model" in info[ATTR_PROPERTIES]: - lowercase_model: str | None = info[ATTR_PROPERTIES]["model"].lower() + if "model" in info.properties: + lowercase_model: str | None = info.properties["model"].lower() else: lowercase_model = None diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 6c030a3c573..5026840a4e1 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -162,8 +162,8 @@ def get_device_discovery_info( del result["properties"]["c#"] if upper_case_props: - result["properties"] = { - key.upper(): val for (key, val) in result["properties"].items() + result.properties = { + key.upper(): val for (key, val) in result.properties.items() } return result diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index b08150ba9bd..1f24d9fd7cd 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the IPP config flow.""" +import dataclasses from unittest.mock import patch from homeassistant.components import zeroconf @@ -40,7 +41,7 @@ async def test_show_zeroconf_form( """Test that the zeroconf confirmation form is served.""" mock_connection(aioclient_mock) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -76,7 +77,7 @@ async def test_zeroconf_connection_error( """Test we abort zeroconf flow on IPP connection error.""" mock_connection(aioclient_mock, conn_error=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -93,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( """Test we abort zeroconf flow on IPP connection error.""" mock_connection(aioclient_mock, conn_error=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -126,7 +127,7 @@ async def test_zeroconf_connection_upgrade_required( """Test we abort zeroconf flow on IPP connection error.""" mock_connection(aioclient_mock, conn_upgrade_error=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -160,7 +161,7 @@ async def test_zeroconf_parse_error( """Test we abort zeroconf flow on IPP parse error.""" mock_connection(aioclient_mock, parse_error=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -194,7 +195,7 @@ async def test_zeroconf_ipp_error( """Test we abort zeroconf flow on IPP error.""" mock_connection(aioclient_mock, ipp_error=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -228,7 +229,7 @@ async def test_zeroconf_ipp_version_error( """Test we abort zeroconf flow on IPP version not supported error.""" mock_connection(aioclient_mock, version_not_supported=True) - discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO} + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -262,7 +263,7 @@ async def test_zeroconf_device_exists_abort( """Test we abort zeroconf flow if printer already configured.""" await init_integration(hass, aioclient_mock, skip_setup=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -279,13 +280,12 @@ async def test_zeroconf_with_uuid_device_exists_abort( """Test we abort zeroconf flow if printer already configured.""" await init_integration(hass, aioclient_mock, skip_setup=True) - discovery_info = { - **MOCK_ZEROCONF_IPP_SERVICE_INFO, - "properties": { - **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], - "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", - }, + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) + discovery_info.properties = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], + "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", } + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -302,12 +302,10 @@ async def test_zeroconf_empty_unique_id( """Test zeroconf flow if printer lacks (empty) unique identification.""" mock_connection(aioclient_mock, no_unique_id=True) - discovery_info = { - **MOCK_ZEROCONF_IPP_SERVICE_INFO, - "properties": { - **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], - "UUID": "", - }, + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) + discovery_info.properties = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], + "UUID": "", } result = await hass.config_entries.flow.async_init( DOMAIN, @@ -324,7 +322,7 @@ async def test_zeroconf_no_unique_id( """Test zeroconf flow if printer lacks unique identification.""" mock_connection(aioclient_mock, no_unique_id=True) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -371,7 +369,7 @@ async def test_full_zeroconf_flow_implementation( """Test the full manual user flow from start to finish.""" mock_connection(aioclient_mock) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -405,7 +403,7 @@ async def test_full_zeroconf_tls_flow_implementation( """Test the full manual user flow from start to finish.""" mock_connection(aioclient_mock, ssl=True) - discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPPS_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index 2050e80392d..e24b9c92221 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -1,12 +1,12 @@ """Define tests for the lookin config flow.""" from __future__ import annotations +import dataclasses from unittest.mock import patch from aiolookin import NoUsableService from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.lookin.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -138,8 +138,8 @@ async def test_discovered_zeroconf(hass): assert mock_async_setup_entry.called entry = hass.config_entries.async_entries(DOMAIN)[0] - zc_data_new_ip = ZEROCONF_DATA.copy() - zc_data_new_ip[zeroconf.ATTR_HOST] = "127.0.0.2" + zc_data_new_ip = dataclasses.replace(ZEROCONF_DATA) + zc_data_new_ip.host = "127.0.0.2" with _patch_get_info(), patch( f"{MODULE}.async_setup_entry", return_value=True diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 743d69167fe..768f42548c0 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Roku config flow.""" +import dataclasses from unittest.mock import patch from homeassistant.components.roku.const import DOMAIN @@ -136,7 +137,7 @@ async def test_homekit_cannot_connect( error=True, ) - discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, @@ -151,7 +152,7 @@ async def test_homekit_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort homekit flow on unknown error.""" - discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) with patch( "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, @@ -172,7 +173,7 @@ async def test_homekit_discovery( """Test the homekit discovery flow.""" mock_connection(aioclient_mock, device="rokutv", host=HOMEKIT_HOST) - discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) @@ -200,7 +201,7 @@ async def test_homekit_discovery( assert len(mock_setup_entry.mock_calls) == 1 # test abort on existing host - discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) From cee5595ba7f7dfcfc678ce49ba75971b78fd1197 Mon Sep 17 00:00:00 2001 From: Andreas Brett Date: Tue, 23 Nov 2021 23:05:27 +0100 Subject: [PATCH 0794/1452] Add pi_hole entity "available_updates" (#56181) Co-authored-by: Martin Hjelmare --- homeassistant/components/pi_hole/__init__.py | 7 +- .../components/pi_hole/binary_sensor.py | 67 +++++++++++++++--- homeassistant/components/pi_hole/const.py | 68 +++++++++++++++++++ .../components/pi_hole/manifest.json | 2 +- homeassistant/components/pi_hole/sensor.py | 6 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pi_hole/__init__.py | 16 +++++ tests/components/pi_hole/test_init.py | 54 +++++++++++++++ 9 files changed, 207 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 5e960a1f70d..420cc51373c 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -109,6 +109,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_token=api_key, ) await api.get_data() + await api.get_versions() + except HoleError as ex: _LOGGER.warning("Failed to connect: %s", ex) raise ConfigEntryNotReady from ex @@ -117,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" try: await api.get_data() + await api.get_versions() except HoleError as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err @@ -150,11 +153,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_platforms(entry: ConfigEntry) -> list[str]: """Return platforms to be loaded / unloaded.""" - platforms = ["sensor"] + platforms = ["binary_sensor", "sensor"] if not entry.data[CONF_STATISTICS_ONLY]: platforms.append("switch") - else: - platforms.append("binary_sensor") return platforms diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 5758c0e4145..e887f2ea12f 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -1,12 +1,27 @@ """Support for getting status from a Pi-hole system.""" +from __future__ import annotations + +from typing import Any + +from hole import Hole + from homeassistant.components.binary_sensor import BinarySensorEntity 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 PiHoleEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN +from .const import ( + BINARY_SENSOR_TYPES, + BINARY_SENSOR_TYPES_STATISTICS_ONLY, + CONF_STATISTICS_ONLY, + DATA_KEY_API, + DATA_KEY_COORDINATOR, + DOMAIN as PIHOLE_DOMAIN, + PiHoleBinarySensorEntityDescription, +) async def async_setup_entry( @@ -15,33 +30,63 @@ async def async_setup_entry( """Set up the Pi-hole binary sensor.""" name = entry.data[CONF_NAME] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] + binary_sensors = [ PiHoleBinarySensor( hole_data[DATA_KEY_API], hole_data[DATA_KEY_COORDINATOR], name, entry.entry_id, + description, ) + for description in BINARY_SENSOR_TYPES ] + + if entry.data[CONF_STATISTICS_ONLY]: + binary_sensors += [ + PiHoleBinarySensor( + hole_data[DATA_KEY_API], + hole_data[DATA_KEY_COORDINATOR], + name, + entry.entry_id, + description, + ) + for description in BINARY_SENSOR_TYPES_STATISTICS_ONLY + ] + async_add_entities(binary_sensors, True) class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity): """Representation of a Pi-hole binary sensor.""" - _attr_icon = "mdi:pi-hole" + entity_description: PiHoleBinarySensorEntityDescription - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name + def __init__( + self, + api: Hole, + coordinator: DataUpdateCoordinator, + name: str, + server_unique_id: str, + description: PiHoleBinarySensorEntityDescription, + ) -> None: + """Initialize a Pi-hole sensor.""" + super().__init__(api, coordinator, name, server_unique_id) + self.entity_description = description - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return f"{self._server_unique_id}/Status" + if description.key == "status": + self._attr_name = f"{name}" + else: + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{self._server_unique_id}/{description.name}" @property def is_on(self) -> bool: """Return if the service is on.""" - return self.api.data.get("status") == "enabled" # type: ignore[no-any-return] + + return self.entity_description.state_value(self.api) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes of the Pi-hole.""" + return self.entity_description.extra_value(self.api) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 37167cb873a..d13c83c7b28 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,9 +1,17 @@ """Constants for the pi_hole integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta +from typing import Any +from hole import Hole + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntityDescription, +) from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import PERCENTAGE @@ -22,6 +30,7 @@ DEFAULT_STATISTICS_ONLY = True SERVICE_DISABLE = "disable" SERVICE_DISABLE_ATTR_DURATION = "duration" +ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" @@ -91,3 +100,62 @@ SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( icon="mdi:domain", ), ) + + +@dataclass +class RequiredPiHoleBinaryDescription: + """Represent the required attributes of the PiHole binary description.""" + + state_value: Callable[[Hole], bool] + + +@dataclass +class PiHoleBinarySensorEntityDescription( + BinarySensorEntityDescription, RequiredPiHoleBinaryDescription +): + """Describes PiHole binary sensor entity.""" + + extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None + + +BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( + PiHoleBinarySensorEntityDescription( + key="core_update_available", + name="Core Update Available", + device_class=DEVICE_CLASS_UPDATE, + extra_value=lambda api: { + "current_version": api.versions["core_current"], + "latest_version": api.versions["core_latest"], + }, + state_value=lambda api: bool(api.versions["core_update"]), + ), + PiHoleBinarySensorEntityDescription( + key="web_update_available", + name="Web Update Available", + device_class=DEVICE_CLASS_UPDATE, + extra_value=lambda api: { + "current_version": api.versions["web_current"], + "latest_version": api.versions["web_latest"], + }, + state_value=lambda api: bool(api.versions["web_update"]), + ), + PiHoleBinarySensorEntityDescription( + key="ftl_update_available", + name="FTL Update Available", + device_class=DEVICE_CLASS_UPDATE, + extra_value=lambda api: { + "current_version": api.versions["FTL_current"], + "latest_version": api.versions["FTL_latest"], + }, + state_value=lambda api: bool(api.versions["FTL_update"]), + ), +) + +BINARY_SENSOR_TYPES_STATISTICS_ONLY: tuple[PiHoleBinarySensorEntityDescription, ...] = ( + PiHoleBinarySensorEntityDescription( + key="status", + name="Status", + icon="mdi:pi-hole", + state_value=lambda api: bool(api.data.get("status") == "enabled"), + ), +) diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index a96cae8b22b..415075549bb 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -2,7 +2,7 @@ "domain": "pi_hole", "name": "Pi-hole", "documentation": "https://www.home-assistant.io/integrations/pi_hole", - "requirements": ["hole==0.5.1"], + "requirements": ["hole==0.6.0"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 656bd8a652b..0e231868647 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -14,6 +14,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity from .const import ( + ATTR_BLOCKED_DOMAINS, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, @@ -68,3 +69,8 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): return round(self.api.data[self.entity_description.key], 2) except TypeError: return self.api.data[self.entity_description.key] + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the Pi-hole.""" + return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} diff --git a/requirements_all.txt b/requirements_all.txt index 42a59d20a56..154c62074c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hkavr==0.0.5 hlk-sw16==0.0.9 # homeassistant.components.pi_hole -hole==0.5.1 +hole==0.6.0 # homeassistant.components.workday holidays==0.11.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25b782d9e01..2277a6cdf49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -509,7 +509,7 @@ herepy==2.0.0 hlk-sw16==0.0.9 # homeassistant.components.pi_hole -hole==0.5.1 +hole==0.6.0 # homeassistant.components.workday holidays==0.11.3.1 diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index f02cd0c8a7a..235cce92a4b 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -26,6 +26,18 @@ ZERO_DATA = { "unique_domains": 0, } +SAMPLE_VERSIONS = { + "core_current": "v5.5", + "core_latest": "v5.6", + "core_update": True, + "web_current": "v5.7", + "web_latest": "v5.8", + "web_update": True, + "FTL_current": "v5.10", + "FTL_latest": "v5.11", + "FTL_update": True, +} + HOST = "1.2.3.4" PORT = 80 LOCATION = "location" @@ -75,9 +87,13 @@ def _create_mocked_hole(raise_exception=False): type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None ) + type(mocked_hole).get_versions = AsyncMock( + side_effect=HoleError("") if raise_exception else None + ) type(mocked_hole).enable = AsyncMock() type(mocked_hole).disable = AsyncMock() mocked_hole.data = ZERO_DATA + mocked_hole.versions = SAMPLE_VERSIONS return mocked_hole diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index a14e155b3da..e96f0d7b33f 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -94,6 +94,60 @@ async def test_setup_minimal_config(hass): assert hass.states.get("binary_sensor.pi_hole").name == "Pi-Hole" assert hass.states.get("binary_sensor.pi_hole").state == "off" + assert ( + hass.states.get("binary_sensor.pi_hole_core_update_available").name + == "Pi-Hole Core Update Available" + ) + assert hass.states.get("binary_sensor.pi_hole_core_update_available").state == "on" + assert ( + hass.states.get("binary_sensor.pi_hole_core_update_available").attributes[ + "current_version" + ] + == "v5.5" + ) + assert ( + hass.states.get("binary_sensor.pi_hole_core_update_available").attributes[ + "latest_version" + ] + == "v5.6" + ) + + assert ( + hass.states.get("binary_sensor.pi_hole_ftl_update_available").name + == "Pi-Hole FTL Update Available" + ) + assert hass.states.get("binary_sensor.pi_hole_ftl_update_available").state == "on" + assert ( + hass.states.get("binary_sensor.pi_hole_ftl_update_available").attributes[ + "current_version" + ] + == "v5.10" + ) + assert ( + hass.states.get("binary_sensor.pi_hole_ftl_update_available").attributes[ + "latest_version" + ] + == "v5.11" + ) + + assert ( + hass.states.get("binary_sensor.pi_hole_web_update_available").name + == "Pi-Hole Web Update Available" + ) + assert hass.states.get("binary_sensor.pi_hole_web_update_available").state == "on" + assert ( + hass.states.get("binary_sensor.pi_hole_web_update_available").attributes[ + "current_version" + ] + == "v5.7" + ) + assert ( + hass.states.get("binary_sensor.pi_hole_web_update_available").attributes[ + "latest_version" + ] + == "v5.8" + ) + async def test_setup_name_config(hass): """Tests component setup with a custom name.""" From 027577805e2b6ea9167e8ceb12093b7b52ffcedf Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Nov 2021 23:15:05 +0100 Subject: [PATCH 0795/1452] Use attribute shortcuts in Samsung TV integration (#60171) Co-authored-by: Paulus Schoutsen --- .../components/samsungtv/media_player.py | 89 ++++++------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cab6435af95..8bbb97925f5 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -99,16 +99,34 @@ class SamsungTVDevice(MediaPlayerEntity): self._config_entry = config_entry self._host: str | None = config_entry.data[CONF_HOST] self._mac: str | None = config_entry.data.get(CONF_MAC) - self._manufacturer: str | None = config_entry.data.get(CONF_MANUFACTURER) - self._model: str | None = config_entry.data.get(CONF_MODEL) - self._name: str | None = config_entry.data.get(CONF_NAME) self._on_script = on_script - self._uuid = config_entry.unique_id - # Assume that the TV is not muted - self._muted: bool = False # Assume that the TV is in Play mode self._playing: bool = True - self._state: str | None = None + + self._attr_name: str | None = config_entry.data.get(CONF_NAME) + self._attr_state: str | None = None + self._attr_unique_id = config_entry.unique_id + self._attr_is_volume_muted: bool = False + self._attr_device_class = DEVICE_CLASS_TV + self._attr_source_list = list(SOURCES) + + if self._on_script or self._mac: + self._attr_supported_features = SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + else: + self._attr_supported_features = SUPPORT_SAMSUNGTV + + self._attr_device_info = DeviceInfo( + name=self.name, + manufacturer=config_entry.data.get(CONF_MANUFACTURER), + model=config_entry.data.get(CONF_MODEL), + ) + if self.unique_id: + self._attr_device_info["identifiers"] = {(DOMAIN, self.unique_id)} + if self._mac: + self._attr_device_info["connections"] = { + (CONNECTION_NETWORK_MAC, self._mac) + } + # Mark the end of a shutdown command (need to wait 15 seconds before # sending the next command to avoid turning the TV back ON). self._end_of_power_off: datetime | None = None @@ -136,9 +154,9 @@ class SamsungTVDevice(MediaPlayerEntity): if self._auth_failed or self.hass.is_stopping: return if self._power_off_in_progress(): - self._state = STATE_OFF + self._attr_state = STATE_OFF else: - self._state = STATE_ON if self._bridge.is_on() else STATE_OFF + self._attr_state = STATE_ON if self._bridge.is_on() else STATE_OFF def send_key(self, key: str) -> None: """Send a key to the tv and handles exceptions.""" @@ -153,69 +171,18 @@ class SamsungTVDevice(MediaPlayerEntity): and self._end_of_power_off > dt_util.utcnow() ) - @property - def unique_id(self) -> str | None: - """Return the unique ID of the device.""" - return self._uuid - - @property - def name(self) -> str | None: - """Return the name of the device.""" - return self._name - - @property - def state(self) -> str | None: - """Return the state of the device.""" - return self._state - @property def available(self) -> bool: """Return the availability of the device.""" if self._auth_failed: return False return ( - self._state == STATE_ON + self._attr_state == STATE_ON or self._on_script is not None or self._mac is not None or self._power_off_in_progress() ) - @property - def device_info(self) -> DeviceInfo | None: - """Return device specific attributes.""" - info: DeviceInfo = { - "name": self.name, - "manufacturer": self._manufacturer, - "model": self._model, - } - if self.unique_id: - info["identifiers"] = {(DOMAIN, self.unique_id)} - if self._mac: - info["connections"] = {(CONNECTION_NETWORK_MAC, self._mac)} - return info - - @property - def is_volume_muted(self) -> bool: - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source_list(self) -> list: - """List of available input sources.""" - return list(SOURCES) - - @property - def supported_features(self) -> int: - """Flag media player features that are supported.""" - if self._on_script or self._mac: - return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON - return SUPPORT_SAMSUNGTV - - @property - def device_class(self) -> str: - """Set the device class to TV.""" - return DEVICE_CLASS_TV - def turn_off(self) -> None: """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME From 615198a58f242919a2369083e608617611d54cf3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Nov 2021 23:17:58 +0100 Subject: [PATCH 0796/1452] Use pychromecast CastInfo type in cast integration (#60205) --- homeassistant/components/cast/discovery.py | 14 +- homeassistant/components/cast/helpers.py | 31 +-- homeassistant/components/cast/media_player.py | 40 +-- tests/components/cast/conftest.py | 21 +- tests/components/cast/test_media_player.py | 237 ++++++++---------- 5 files changed, 149 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 01b00c82f64..e76302fefbc 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -26,12 +26,7 @@ def discover_chromecast( """Discover a Chromecast.""" info = ChromecastInfo( - services=cast_info.services, - uuid=cast_info.uuid, - model_name=cast_info.model_name, - friendly_name=cast_info.friendly_name, - cast_type=cast_info.cast_type, - manufacturer=cast_info.manufacturer, + cast_info=cast_info, ) if info.uuid is None: @@ -76,12 +71,7 @@ def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None: _remove_chromecast( hass, ChromecastInfo( - services=cast_info.services, - uuid=cast_info.uuid, - model_name=cast_info.model_name, - friendly_name=cast_info.friendly_name, - cast_type=cast_info.cast_type, - manufacturer=cast_info.manufacturer, + cast_info=cast_info, ), ) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index a3bf34ab3ae..ba7380bcaa2 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -6,6 +6,7 @@ from typing import Optional import attr from pychromecast import dial from pychromecast.const import CAST_TYPE_GROUP +from pychromecast.models import CastInfo @attr.s(slots=True, frozen=True) @@ -15,18 +16,23 @@ class ChromecastInfo: This also has the same attributes as the mDNS fields by zeroconf. """ - services: set | None = attr.ib() - uuid: str = attr.ib(converter=attr.converters.optional(str)) - model_name: str = attr.ib() - friendly_name: str = attr.ib() - cast_type: str = attr.ib() - manufacturer: str = attr.ib() + cast_info: CastInfo = attr.ib() is_dynamic_group = attr.ib(type=Optional[bool], default=None) + @property + def friendly_name(self) -> str: + """Return the UUID.""" + return self.cast_info.friendly_name + @property def is_audio_group(self) -> bool: """Return if the cast is an audio group.""" - return self.cast_type == CAST_TYPE_GROUP + return self.cast_info.cast_type == CAST_TYPE_GROUP + + @property + def uuid(self) -> bool: + """Return the UUID.""" + return self.cast_info.uuid def fill_out_missing_chromecast_info(self) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. @@ -42,21 +48,16 @@ class ChromecastInfo: http_group_status = None http_group_status = dial.get_multizone_status( None, - services=self.services, + services=self.cast_info.services, zconf=ChromeCastZeroconf.get_zeroconf(), ) if http_group_status is not None: is_dynamic_group = any( - str(g.uuid) == self.uuid for g in http_group_status.dynamic_groups + g.uuid == self.cast_info.uuid for g in http_group_status.dynamic_groups ) return ChromecastInfo( - services=self.services, - uuid=self.uuid, - friendly_name=self.friendly_name, - model_name=self.model_name, - cast_type=self.cast_type, - manufacturer=self.manufacturer, + cast_info=self.cast_info, is_dynamic_group=is_dynamic_group, ) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 32c303dfd8b..46c25501f3a 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -134,7 +134,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Handle discovery of a new chromecast.""" # If wanted_uuids is set, we're only accepting specific cast devices identified # by UUID - if wanted_uuids is not None and discover.uuid not in wanted_uuids: + if wanted_uuids is not None and str(discover.uuid) not in wanted_uuids: # UUID not matching, ignore. return @@ -162,7 +162,6 @@ class CastDevice(MediaPlayerEntity): """Initialize the cast device.""" self._cast_info = cast_info - self.services = cast_info.services self._chromecast: pychromecast.Chromecast | None = None self.cast_status = None self.media_status = None @@ -176,13 +175,13 @@ class CastDevice(MediaPlayerEntity): self._add_remove_handler = None self._cast_view_remove_handler = None - self._attr_unique_id = cast_info.uuid + self._attr_unique_id = str(cast_info.uuid) self._attr_name = cast_info.friendly_name - if cast_info.model_name != "Google Cast Group": + if cast_info.cast_info.model_name != "Google Cast Group": self._attr_device_info = DeviceInfo( identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, - manufacturer=str(cast_info.manufacturer), - model=cast_info.model_name, + manufacturer=str(cast_info.cast_info.manufacturer), + model=cast_info.cast_info.model_name, name=str(cast_info.friendly_name), ) @@ -224,20 +223,11 @@ class CastDevice(MediaPlayerEntity): "[%s %s] Connecting to cast device by service %s", self.entity_id, self._cast_info.friendly_name, - self.services, + self._cast_info.cast_info.services, ) chromecast = await self.hass.async_add_executor_job( pychromecast.get_chromecast_from_cast_info, - pychromecast.discovery.CastInfo( - self.services, - self._cast_info.uuid, - self._cast_info.model_name, - self._cast_info.friendly_name, - None, - None, - self._cast_info.cast_type, - self._cast_info.manufacturer, - ), + self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA @@ -776,7 +766,6 @@ class DynamicCastGroup: self.hass = hass self._cast_info = cast_info - self.services = cast_info.services self._chromecast: pychromecast.Chromecast | None = None self.mz_mgr = None self._status_listener: CastStatusListener | None = None @@ -824,20 +813,11 @@ class DynamicCastGroup: "[%s %s] Connecting to cast device by service %s", "Dynamic group", self._cast_info.friendly_name, - self.services, + self._cast_info.cast_info.services, ) chromecast = await self.hass.async_add_executor_job( pychromecast.get_chromecast_from_cast_info, - pychromecast.discovery.CastInfo( - self.services, - self._cast_info.uuid, - self._cast_info.model_name, - self._cast_info.friendly_name, - None, - None, - self._cast_info.cast_type, - self._cast_info.manufacturer, - ), + self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA @@ -889,7 +869,7 @@ class DynamicCastGroup: # Removed is not our device. return - if not discover.services: + if not discover.cast_info.services: # Clean up the dynamic group _LOGGER.debug("Clean up dynamic group: %s", discover) await self.async_tear_down() diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 901f7fb74fb..3b96f378906 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -7,11 +7,11 @@ import pytest @pytest.fixture() -def dial_mock(): +def get_multizone_status_mock(): """Mock pychromecast dial.""" - dial_mock = MagicMock() - dial_mock.get_multizone_status.return_value.dynamic_groups = [] - return dial_mock + mock = MagicMock(spec_set=pychromecast.dial.get_multizone_status) + mock.return_value.dynamic_groups = [] + return mock @pytest.fixture() @@ -23,7 +23,7 @@ def castbrowser_mock(): @pytest.fixture() def mz_mock(): """Mock pychromecast MultizoneManager.""" - return MagicMock() + return MagicMock(spec_set=pychromecast.controllers.multizone.MultizoneManager) @pytest.fixture() @@ -40,7 +40,11 @@ def get_chromecast_mock(): @pytest.fixture(autouse=True) def cast_mock( - dial_mock, mz_mock, quick_play_mock, castbrowser_mock, get_chromecast_mock + mz_mock, + quick_play_mock, + castbrowser_mock, + get_chromecast_mock, + get_multizone_status_mock, ): """Mock pychromecast.""" ignore_cec_orig = list(pychromecast.IGNORE_CEC) @@ -48,7 +52,10 @@ def cast_mock( with patch( "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", castbrowser_mock, - ), patch("homeassistant.components.cast.helpers.dial", dial_mock), patch( + ), patch( + "homeassistant.components.cast.helpers.dial.get_multizone_status", + get_multizone_status_mock, + ), patch( "homeassistant.components.cast.media_player.MultizoneManager", return_value=mz_mock, ), patch( diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 3690cb7a2e0..adab55c50df 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -67,40 +67,21 @@ def get_fake_chromecast_info( ): """Generate a Fake ChromecastInfo with the specified arguments.""" - @attr.s(slots=True, frozen=True, eq=False) - class ExtendedChromecastInfo(ChromecastInfo): - host: str | None = attr.ib(default=None) - port: int | None = attr.ib(default=0) - - def __eq__(self, other): - if isinstance(other, ChromecastInfo): - return ( - ChromecastInfo( - services=self.services, - uuid=self.uuid, - model_name=self.model_name, - friendly_name=self.friendly_name, - cast_type=self.cast_type, - manufacturer=self.manufacturer, - is_dynamic_group=self.is_dynamic_group, - ) - == other - ) - return super().__eq__(other) - if service is None: service = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_HOST, (host, port) ) - return ExtendedChromecastInfo( - host=host, - port=port, - services={service}, - uuid=uuid, - model_name="Chromecast", - friendly_name="Speaker", - cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, - manufacturer="Nabu Casa", + return ChromecastInfo( + cast_info=pychromecast.models.CastInfo( + services={service}, + uuid=uuid, + model_name="Chromecast", + friendly_name="Speaker", + host=host, + port=port, + cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, + manufacturer="Nabu Casa", + ) ) @@ -154,12 +135,12 @@ async def async_setup_cast_internal_discovery(hass, config=None): browser.devices[info.uuid] = pychromecast.discovery.CastInfo( {service}, info.uuid, - info.model_name, + info.cast_info.model_name, info.friendly_name, - info.host, - info.port, - info.cast_type, - info.manufacturer, + info.cast_info.host, + info.cast_info.port, + info.cast_info.cast_type, + info.cast_info.manufacturer, ) discovery_callback(info.uuid, "") @@ -168,15 +149,15 @@ async def async_setup_cast_internal_discovery(hass, config=None): remove_callback( info.uuid, service_name, - pychromecast.discovery.CastInfo( + pychromecast.models.CastInfo( set(), info.uuid, - info.model_name, - info.friendly_name, - info.host, - info.port, - info.cast_type, - info.manufacturer, + info.cast_info.model_name, + info.cast_info.friendly_name, + info.cast_info.host, + info.cast_info.port, + info.cast_info.cast_type, + info.cast_info.manufacturer, ), ) @@ -187,7 +168,7 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf """Set up the cast platform with async_setup_component.""" browser = MagicMock(devices={}, zc={}) chromecast = get_fake_chromecast(info) - zconf = get_fake_zconf(host=info.host, port=info.port) + zconf = get_fake_zconf(host=info.cast_info.host, port=info.cast_info.port) with patch( "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_cast_info", @@ -210,12 +191,12 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf browser.devices[info.uuid] = pychromecast.discovery.CastInfo( {FAKE_MDNS_SERVICE}, info.uuid, - info.model_name, + info.cast_info.model_name, info.friendly_name, - info.host, - info.port, - info.cast_type, - info.manufacturer, + info.cast_info.host, + info.cast_info.port, + info.cast_info.cast_type, + info.cast_info.manufacturer, ) discovery_callback(info.uuid, FAKE_MDNS_SERVICE[1]) @@ -228,12 +209,12 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf browser.devices[info.uuid] = pychromecast.discovery.CastInfo( {FAKE_MDNS_SERVICE}, info.uuid, - info.model_name, + info.cast_info.model_name, info.friendly_name, - info.host, - info.port, - info.cast_type, - info.manufacturer, + info.cast_info.host, + info.cast_info.port, + info.cast_info.cast_type, + info.cast_info.manufacturer, ) discovery_callback(info.uuid, FAKE_MDNS_SERVICE[1]) @@ -268,50 +249,32 @@ async def test_start_discovery_called_once(hass, castbrowser_mock): assert castbrowser_mock.return_value.start_discovery.call_count == 1 -async def test_internal_discovery_callback_fill_out_fail(hass): - """Test internal discovery automatically filling out information.""" - discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info(host="host1", service=FAKE_MDNS_SERVICE) - zconf = get_fake_zconf(host="host1", port=8009) - full_info = ( - info # attr.evolve(info, model_name="", friendly_name="Speaker", uuid=FakeUUID) - ) - - with patch( - "homeassistant.components.cast.helpers.dial.get_device_status", - return_value=None, - ), patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf, - ): - signal = MagicMock() - - async_dispatcher_connect(hass, "cast_discovered", signal) - discover_cast(FAKE_MDNS_SERVICE, info) - await hass.async_block_till_done() - - # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] - assert discover == full_info - - -async def test_internal_discovery_callback_fill_out_group(hass): +async def test_internal_discovery_callback_fill_out_group_fail( + hass, get_multizone_status_mock +): """Test internal discovery automatically filling out information.""" discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) info = get_fake_chromecast_info(host="host1", port=12345, service=FAKE_MDNS_SERVICE) zconf = get_fake_zconf(host="host1", port=12345) full_info = attr.evolve( info, - model_name="Chromecast", - friendly_name="Speaker", - uuid=FakeUUID, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Chromecast", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type=info.cast_info.cast_type, + manufacturer=info.cast_info.manufacturer, + ), is_dynamic_group=False, ) + get_multizone_status_mock.assert_not_called() + get_multizone_status_mock.return_value = None + with patch( - "homeassistant.components.cast.helpers.dial.get_device_status", - return_value=full_info, - ), patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf, ): @@ -324,6 +287,48 @@ async def test_internal_discovery_callback_fill_out_group(hass): # when called with incomplete info, it should use HTTP to get missing discover = signal.mock_calls[0][1][0] assert discover == full_info + get_multizone_status_mock.assert_called_once() + + +async def test_internal_discovery_callback_fill_out_group( + hass, get_multizone_status_mock +): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1", port=12345, service=FAKE_MDNS_SERVICE) + zconf = get_fake_zconf(host="host1", port=12345) + full_info = attr.evolve( + info, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Chromecast", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type=info.cast_info.cast_type, + manufacturer=info.cast_info.manufacturer, + ), + is_dynamic_group=False, + ) + + get_multizone_status_mock.assert_not_called() + get_multizone_status_mock.return_value = None + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + get_multizone_status_mock.assert_called_once() async def test_stop_discovery_called_on_stop(hass, castbrowser_mock): @@ -437,7 +442,9 @@ async def test_auto_cast_chromecasts(hass): assert add_dev1.call_count == 2 -async def test_discover_dynamic_group(hass, dial_mock, get_chromecast_mock, caplog): +async def test_discover_dynamic_group( + hass, get_multizone_status_mock, get_chromecast_mock, caplog +): """Test dynamic group does not create device or entity.""" cast_1 = get_fake_chromecast_info(host="host_1", port=23456, uuid=FakeUUID) cast_2 = get_fake_chromecast_info(host="host_2", port=34567, uuid=FakeUUID2) @@ -451,7 +458,7 @@ async def test_discover_dynamic_group(hass, dial_mock, get_chromecast_mock, capl tmp1.uuid = FakeUUID tmp2 = MagicMock() tmp2.uuid = FakeUUID2 - dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2] + get_multizone_status_mock.return_value.dynamic_groups = [tmp1, tmp2] get_chromecast_mock.assert_not_called() discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery( @@ -492,7 +499,7 @@ async def test_discover_dynamic_group(hass, dial_mock, get_chromecast_mock, capl get_chromecast_mock.assert_called() get_chromecast_mock.reset_mock() assert add_dev1.call_count == 0 - assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + assert reg.async_get_entity_id("media_player", "cast", cast_2.uuid) is None # Get update for cast service with patch( @@ -601,9 +608,6 @@ async def test_entity_cast_status(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST @@ -618,7 +622,7 @@ async def test_entity_cast_status(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # No media status, pause, play, stop not supported assert state.attributes.get("supported_features") == ( @@ -756,9 +760,6 @@ async def test_entity_play_media(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) @@ -772,7 +773,7 @@ async def test_entity_play_media(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media await common.async_play_media(hass, "audio", "best.mp3", entity_id) @@ -785,9 +786,6 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) @@ -801,7 +799,7 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media - cast with app ID await common.async_play_media(hass, "cast", '{"app_id": "abc123"}', entity_id) @@ -830,9 +828,6 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) @@ -846,7 +841,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # play_media - media_type cast with invalid JSON with pytest.raises(json.decoder.JSONDecodeError): @@ -903,9 +898,6 @@ async def test_entity_media_content_type(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) @@ -919,7 +911,7 @@ async def test_entity_media_content_type(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) media_status = MagicMock(images=None) media_status.media_is_movie = False @@ -957,9 +949,6 @@ async def test_entity_control(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST @@ -982,7 +971,7 @@ async def test_entity_control(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "playing" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) assert state.attributes.get("supported_features") == ( SUPPORT_PAUSE @@ -1075,9 +1064,6 @@ async def test_entity_media_states(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) @@ -1091,7 +1077,7 @@ async def test_entity_media_states(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) media_status = MagicMock(images=None) media_status.player_is_playing = True @@ -1134,9 +1120,6 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) cast_status_cb, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) @@ -1150,7 +1133,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) chromecast.app_id = CAST_APP_ID_HOMEASSISTANT_LOVELACE cast_status = MagicMock() @@ -1204,9 +1187,6 @@ async def test_group_media_states(hass, mz_mock): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks( @@ -1222,7 +1202,7 @@ async def test_group_media_states(hass, mz_mock): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) group_media_status = MagicMock(images=None) player_media_status = MagicMock(images=None) @@ -1257,9 +1237,6 @@ async def test_group_media_control(hass, mz_mock): reg = er.async_get(hass) info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) chromecast, _ = await async_setup_media_player_cast(hass, info) @@ -1276,7 +1253,7 @@ async def test_group_media_control(hass, mz_mock): assert state is not None assert state.name == "Speaker" assert state.state == "off" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) group_media_status = MagicMock(images=None) player_media_status = MagicMock(images=None) From 4649bc3c115c8c2544dde257e901e1a840400d54 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 23 Nov 2021 23:21:07 +0100 Subject: [PATCH 0797/1452] Add LED brightness for Xiaomi Miio ZA5 fan (#60134) --- .../components/xiaomi_miio/number.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 87ccdd63d0f..2823c6c2582 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -33,6 +33,7 @@ from .const import ( FEATURE_SET_FAN_LEVEL, FEATURE_SET_FAVORITE_LEVEL, FEATURE_SET_FAVORITE_RPM, + FEATURE_SET_LED_BRIGHTNESS, FEATURE_SET_LED_BRIGHTNESS_LEVEL, FEATURE_SET_MOTOR_SPEED, FEATURE_SET_OSCILLATION_ANGLE, @@ -70,6 +71,7 @@ ATTR_DELAY_OFF_COUNTDOWN = "delay_off_countdown" ATTR_FAN_LEVEL = "fan_level" ATTR_FAVORITE_LEVEL = "favorite_level" ATTR_FAVORITE_RPM = "favorite_rpm" +ATTR_LED_BRIGHTNESS = "led_brightness" ATTR_LED_BRIGHTNESS_LEVEL = "led_brightness_level" ATTR_MOTOR_SPEED = "motor_speed" ATTR_OSCILLATION_ANGLE = "angle" @@ -161,6 +163,16 @@ NUMBER_TYPES = { method="async_set_delay_off_countdown", entity_category=ENTITY_CATEGORY_CONFIG, ), + FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioNumberDescription( + key=ATTR_LED_BRIGHTNESS, + name="Led Brightness", + icon="mdi:brightness-6", + min_value=0, + max_value=100, + step=1, + method="async_set_led_brightness", + entity_category=ENTITY_CATEGORY_CONFIG, + ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, name="Led Brightness", @@ -244,6 +256,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): description.max_value = OSCILLATION_ANGLE_VALUES[model].max_value description.min_value = OSCILLATION_ANGLE_VALUES[model].min_value description.step = OSCILLATION_ANGLE_VALUES[model].step + entities.append( XiaomiNumberEntity( f"{config_entry.title} {description.name}", @@ -354,6 +367,14 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): level, ) + async def async_set_led_brightness(self, level: int): + """Set the led brightness level.""" + return await self._try_command( + "Setting the led brightness level of the miio device failed.", + self._device.set_led_brightness, + level, + ) + async def async_set_favorite_rpm(self, rpm: int): """Set the target motor speed.""" return await self._try_command( From 9fa6daf47a5d60e1584bff50c3a1d42da1b6a6f7 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 24 Nov 2021 09:22:34 +1100 Subject: [PATCH 0798/1452] dlna_dmr: Ignore philips tv (#60204) --- homeassistant/components/dlna_dmr/config_flow.py | 9 +++++++++ tests/components/dlna_dmr/test_config_flow.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index bfc5cb27129..2b38cf4e56d 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -236,6 +236,10 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="already_in_progress") + # Abort if another config entry has the same location, in case the + # device doesn't have a static and unique UDN (breaking the UPnP spec). + self._async_abort_entries_match({CONF_URL: self._location}) + self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() @@ -478,6 +482,11 @@ def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: if manufacturer.startswith("xbmc") or model == "kodi": # kodi return True + if "philips" in manufacturer and "tv" in model: + # philips_js + # These TVs don't have a stable UDN, so also get discovered as a new + # device every time they are turned on. + return True if manufacturer.startswith("samsung") and "tv" in model: # samsungtv return True diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index e2d82d5b559..6ff718290b2 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -558,6 +558,21 @@ async def test_ssdp_flow_existing( assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION +async def test_ssdp_flow_duplicate_location( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test that discovery of device with URL matching existing entry gets aborted.""" + config_entry_mock.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION + + async def test_ssdp_flow_upnp_udn( hass: HomeAssistant, config_entry_mock: MockConfigEntry ) -> None: @@ -635,6 +650,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: ("XBMC Foundation", "Kodi"), ("Samsung", "Smart TV"), ("LG Electronics.", "LG TV"), + ("Royal Philips Electronics", "Philips TV DMR"), ]: discovery = dict(MOCK_DISCOVERY) discovery[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer From ac3dc0b090a3d3aeffc3d5d3dcf4e6cdcea5496f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 23 Nov 2021 23:25:48 +0100 Subject: [PATCH 0799/1452] Use native datetime value in NAM uptime sensor (#60241) --- homeassistant/components/nam/sensor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index c5c9c9f2e77..88f6008b45f 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -1,7 +1,7 @@ """Support for the Nettigo Air Monitor service.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import cast @@ -75,7 +75,7 @@ class NAMSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state.""" return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) @@ -99,11 +99,7 @@ class NAMSensorUptime(NAMSensor): """Define an Nettigo Air Monitor uptime sensor.""" @property - def native_value(self) -> str: + def native_value(self) -> datetime: """Return the state.""" uptime_sec = getattr(self.coordinator.data, self.entity_description.key) - return ( - (utcnow() - timedelta(seconds=uptime_sec)) - .replace(microsecond=0) - .isoformat() - ) + return utcnow() - timedelta(seconds=uptime_sec) From 28da8c474cf6802f1bc8628d5bd20f15407440d2 Mon Sep 17 00:00:00 2001 From: cvroque <65680394+cvroque@users.noreply.github.com> Date: Tue, 23 Nov 2021 19:32:03 -0300 Subject: [PATCH 0800/1452] Check if Tuya Vacuum (sd) is able to report status directly before using pause switch (#59888) --- homeassistant/components/tuya/vacuum.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 25596da01b5..5602413615c 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -131,7 +131,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): @property def state(self) -> str | None: """Return Tuya vacuum device state.""" - if self.device.status.get(DPCode.PAUSE): + if self.device.status.get(DPCode.PAUSE) and not ( + self.device.status.get(DPCode.STATUS) + ): return STATE_PAUSED if not (status := self.device.status.get(DPCode.STATUS)): return None From a7c44d89e1aee08239f2da95ccd59643a3896092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Doleg=C5=82o?= <9080183+kamildoleglo@users.noreply.github.com> Date: Tue, 23 Nov 2021 23:33:36 +0100 Subject: [PATCH 0801/1452] Fix Tuya integration for climate devices (#60229) --- homeassistant/components/tuya/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index f4bc0dc561f..f7b4453fea8 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -26,6 +26,7 @@ class IntegerTypeData: scale: float step: float unit: str | None = None + type: str | None = None @property def max_scaled(self) -> float: From d677baba366b97bd9742a1b0077cadb1383c91b1 Mon Sep 17 00:00:00 2001 From: Oleksandr Kapshuk Date: Wed, 24 Nov 2021 00:42:21 +0200 Subject: [PATCH 0802/1452] Add hjjcy device category to Tuya integration (#60224) --- homeassistant/components/tuya/sensor.py | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 4abd77fb7bd..e95ebc2982e 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -138,6 +138,45 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Air Quality Monitor + # No specification on Tuya portal + "hjjcy": ( + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CH2O_VALUE, + name="Formaldehyde", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VOC_VALUE, + name="Volatile Organic Compound", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.PM25_VALUE, + name="Particulate Matter 2.5 µm", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + ), # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( From 6b59e305cb6a867a253f9e9102118d8d498e62ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Nov 2021 23:57:45 +0100 Subject: [PATCH 0803/1452] Run partial test suite in CI if core untouched (#60230) Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .core_files.yaml | 120 ++++++++++++++++++++++++++++ .github/workflows/ci.yaml | 160 +++++++++++++++++++++++++++++++++++++- codecov.yml | 6 ++ 3 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 .core_files.yaml diff --git a/.core_files.yaml b/.core_files.yaml new file mode 100644 index 00000000000..27daff11f35 --- /dev/null +++ b/.core_files.yaml @@ -0,0 +1,120 @@ +# Defines a list of files that are part of main core of Home Assistant. +# Changes to these files/filters define how our CI test suite is ran. +core: &core + - homeassistant/*.py + - homeassistant/auth/** + - homeassistant/helpers/* + - homeassistant/package_constraints.txt + - homeassistant/util/* + - pyproject.yaml + - requirements.txt + - setup.cfg + +# Our base platforms, that are used by other integrations +base_platforms: &base_platforms + - homeassistant/components/air_quality/* + - homeassistant/components/alarm_control_panel/* + - homeassistant/components/binary_sensor/* + - homeassistant/components/button/* + - homeassistant/components/calendar/* + - homeassistant/components/camera/* + - homeassistant/components/climate/* + - homeassistant/components/cover/* + - homeassistant/components/device_tracker/* + - homeassistant/components/fan/* + - homeassistant/components/geo_location/* + - homeassistant/components/humidifier/* + - homeassistant/components/image_processing/* + - homeassistant/components/light/* + - homeassistant/components/lock/* + - homeassistant/components/media_player/* + - homeassistant/components/notify/* + - homeassistant/components/number/* + - homeassistant/components/remote/* + - homeassistant/components/scene/* + - homeassistant/components/select/* + - homeassistant/components/sensor/* + - homeassistant/components/siren/* + - homeassistant/components/stt/* + - homeassistant/components/switch/* + - homeassistant/components/tts/* + - homeassistant/components/vacuum/* + - homeassistant/components/water_heater/* + - homeassistant/components/weather/* + +# Extra components that trigger the full suite +components: &components + - homeassistant/components/alert/* + - homeassistant/components/alexa/* + - homeassistant/components/auth/* + - homeassistant/components/automation/* + - homeassistant/components/cloud/* + - homeassistant/components/config/* + - homeassistant/components/configurator/* + - homeassistant/components/conversation/* + - homeassistant/components/demo/* + - homeassistant/components/device_automation/* + - homeassistant/components/dhcp/* + - homeassistant/components/discovery/* + - homeassistant/components/energy/* + - homeassistant/components/ffmpeg/* + - homeassistant/components/frontend/* + - homeassistant/components/google_assistant/* + - homeassistant/components/group/* + - homeassistant/components/hassio/* + - homeassistant/components/homeassistant/** + - homeassistant/components/image/* + - homeassistant/components/input_boolean/* + - homeassistant/components/input_datetime/* + - homeassistant/components/input_number/* + - homeassistant/components/input_select/* + - homeassistant/components/input_text/* + - homeassistant/components/logbook/* + - homeassistant/components/logger/* + - homeassistant/components/lovelace/* + - homeassistant/components/media_source/* + - homeassistant/components/mqtt/* + - homeassistant/components/network/* + - homeassistant/components/onboarding/* + - homeassistant/components/otp/* + - homeassistant/components/persistent_notification/* + - homeassistant/components/person/* + - homeassistant/components/recorder/* + - homeassistant/components/safe_mode/* + - homeassistant/components/script/* + - homeassistant/components/shopping_list/* + - homeassistant/components/ssdp/* + - homeassistant/components/stream/* + - homeassistant/components/sun/* + - homeassistant/components/system_health/* + - homeassistant/components/tag/* + - homeassistant/components/template/* + - homeassistant/components/timer/* + - homeassistant/components/usb/* + - homeassistant/components/webhook/* + - homeassistant/components/websocket_api/* + - homeassistant/components/zeroconf/* + - homeassistant/components/zone/* + +# Testing related files that affect the whole test/linting suite +tests: &tests + - codecov.yaml + - requirements_test_pre_commit.txt + - requirements_test.txt + - tests/common.py + - tests/conftest.py + - tests/ignore_uncaught_exceptions.py + - tests/mock/* + - tests/test_util/* + - tests/testing_config/** + +other: &other + - .github/workflows/* + - homeassistant/scripts/** + +any: + - *base_platforms + - *components + - *core + - *other + - *tests diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8b02dd8a63f..29e0a7951e6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -651,9 +651,57 @@ jobs: . venv/bin/activate mypy homeassistant - pytest: + changes: + name: Determine what has changed + outputs: + core: ${{ steps.core.outputs.any }} + integrations: ${{ steps.integrations.outputs.changes }} + tests: ${{ steps.tests.outputs.integrations }} + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Filter for core changes + uses: dorny/paths-filter@v2.10.2 + id: core + with: + filters: .core_files.yaml + - name: Create a list of integrations to filter for changes + id: integration-filters + run: | + integrations=$(ls -Ad ./homeassistant/components/[!_]* | xargs -n 1 basename) + touch .integration_paths.yaml + for integration in $integrations; do + echo "${integration}: [homeassistant/components/${integration}/*, tests/components/${integration}/*]" \ + >> .integration_paths.yaml; + done + echo "Result:" + cat .integration_paths.yaml + - name: Filter for integration changes + uses: dorny/paths-filter@v2.10.2 + id: integrations + with: + filters: .integration_paths.yaml + - name: Determine integration tests to run + if: ${{ steps.integrations.outputs.changes }} + id: tests + run: | + possible_integrations=$(echo '${{ steps.integrations.outputs.changes }}' | jq -cSr '. | join(" ")') + integrations=$(for integration in $possible_integrations; do [[ -d "tests/components/${integration}" ]] && echo -n "${integration},"; done) + integrations="${integrations::-1}" + + # If more than one, add brackets to it + if [[ "${integrations}" == *","* ]]; then + integrations="{${integrations}}" + fi + + echo "::set-output name=integrations::${integrations}" + + pytest-full: + if: ${{ needs.changes.outputs.core == 'true' }} runs-on: ubuntu-latest needs: + - changes - gen-requirements-all - hassfest - lint-bandit @@ -725,10 +773,83 @@ jobs: run: | ./script/check_dirty - coverage: + pytest-partial: + if: ${{ needs.changes.outputs.core == 'false' }} + runs-on: ubuntu-latest + needs: + - changes + - gen-requirements-all + - hassfest + - lint-bandit + - lint-black + - lint-codespell + - lint-dockerfile + - lint-executable-shebangs + - lint-isort + - lint-json + - lint-pyupgrade + - lint-yaml + - mypy + - prepare-tests + strategy: + fail-fast: false + matrix: + python-version: [3.8, 3.9] + name: >- + Run partial tests Python ${{ matrix.python-version }} + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Install Pytest Annotation plugin + run: | + . venv/bin/activate + # Ideally this should be part of our dependencies + # However this plugin is fairly new and doesn't run correctly + # on a non-GitHub environment. + pip install pytest-github-actions-annotate-failures==0.1.3 + - name: Run pytest + run: | + . venv/bin/activate + python3 -X dev -m pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + -n auto \ + --dist=loadfile \ + --cov homeassistant \ + --cov-report= \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/${{ needs.changes.outputs.tests }} + - name: Upload coverage artifact + uses: actions/upload-artifact@v2.2.4 + with: + name: coverage-${{ matrix.python-version }} + path: .coverage + - name: Check dirty + run: | + ./script/check_dirty + + coverage-full: name: Process test coverage runs-on: ubuntu-latest - needs: ["prepare-tests", "pytest"] + needs: ["prepare-tests", "pytest-full"] strategy: matrix: python-version: [3.8] @@ -758,3 +879,36 @@ jobs: coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2.1.0 + + coverage-partial: + name: Process partial test coverage + runs-on: ubuntu-latest + needs: ["prepare-tests", "pytest-partial"] + strategy: + matrix: + python-version: [3.8] + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2 + - name: Combine coverage results + run: | + . venv/bin/activate + coverage combine coverage*/.coverage* + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2.1.0 diff --git a/codecov.yml b/codecov.yml index 7a9eea730d8..5c0750e659f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,3 +7,9 @@ coverage: target: 90 threshold: 0.09 comment: false + +# To make partial tests possible, +# we need to carry forward. +flag_management: + default_rules: + carryforward: true From ec44a55b2ca72498e7a9df9a959e54657fa98d4e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 01:09:55 +0100 Subject: [PATCH 0804/1452] Make partial test suite not waiting for linters (#60254) --- .github/workflows/ci.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 29e0a7951e6..7d9b17f4f42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -778,18 +778,6 @@ jobs: runs-on: ubuntu-latest needs: - changes - - gen-requirements-all - - hassfest - - lint-bandit - - lint-black - - lint-codespell - - lint-dockerfile - - lint-executable-shebangs - - lint-isort - - lint-json - - lint-pyupgrade - - lint-yaml - - mypy - prepare-tests strategy: fail-fast: false From 7e9ff26b4c2d6e96412437c4b4f9a759f5ead1f5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Nov 2021 01:13:44 +0100 Subject: [PATCH 0805/1452] Add slow tests list to VScode task code coverage (#60183) --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5488c3472de..1aea23fc781 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -64,7 +64,7 @@ "label": "Code Coverage", "detail": "Generate code coverage report for a given integration.", "type": "shell", - "command": "pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing", + "command": "pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0", "group": { "kind": "test", "isDefault": true From 314f59306629c0ab6b6a181077912749bf632a0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 01:34:03 +0100 Subject: [PATCH 0806/1452] Add partial codecov flag to CI to trigger carryforward coverage (#60256) --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d9b17f4f42..6af4eb444fe 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -900,3 +900,5 @@ jobs: coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2.1.0 + with: + flags: partial From 3dac6614807808efe2b8a5464232fe5350a11e99 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Nov 2021 02:04:36 +0100 Subject: [PATCH 0807/1452] Rewrite Fronius integration (#59686) * Add unique_id and use DataUpdateCoordinator in Fronius (#57879) * initial refactoring commit - meters - config_flow (no strings, no tests yet) - import yaml config - FroniusSolarNet class for holding Fronius object , coordinators and some common data - meter descriptions - update coordinator - entities (including devices) * storage controllers * error handling on init; inverter unique_id * inverters * power_flow * fix VA, var, varh not valid for device_class power/energy and add custom icons * add SolarNet device for system wide values * cleanup * config_flow strings * test config_flow * use pyfronius 0.7.0 * enable strict typing * remove TODO comments * fix lint errors; move FroniusSensorEntity to sensor.py * power_flow as optional coordinator API V0 doesn't support power_flow endpoint * show error message in logs * prevent parallel requests to one host * logger_info coordinator * store FroniusSolarNet reference directly in coordinator * cleanup coordinators when unloading entry * round floats returned by Fronius API * default icons for grid im/export tariffs * small typing fix * Update homeassistant/components/fronius/sensor.py Co-authored-by: Brett Adams * DC icons * prepend names with "Fronius" and device type to get more reasonable default entity_ids (eg. have them next to each other when alphabetically sorted) * remove config_flow and devices * rename _FroniusUpdateCoordinator to FroniusCoordinatorBase and mark ABC * move SensorEntityDescriptions to sensor.py * Revert "move SensorEntityDescriptions to sensor.py" This reverts commit 2e5a726eb65854f236a0c72f3f67f04a6f8a2eff. * Don't raise ConfigEntryNotReady and use regular refresh method * move bridge initialization out of helper class * no coverage tests * power_flow update interval 10 seconds * move SensorEntityDescriptions to sensor.py without introducing a circular dependency * deprecation warning for CONF_MONITORED_CONDITIONS * remove extra_state_attributes form meter sensor entities * readd diagnostic entities * decouple default entity_id from default name * use key instead of name for entity_id and make deprecated config key optional * adjust tests * use old entity_ids these changes are now backwards compatible * check coverage * simplify entity description definitions * restore entity names of previous implementation Co-authored-by: Brett Adams * Add config_flow for Fronius integration (#59677) * Cleanup Fronius config_flow and tests (#60094) * Add devices to Fronius integration (#60104) * New entity names for Fronius entities (#60215) * Adaptive update interval for Fronius coordinators (#60192) Co-authored-by: Brett Adams --- .coveragerc | 1 - .strict-typing | 1 + CODEOWNERS | 2 +- homeassistant/components/fronius/__init__.py | 205 +++- .../components/fronius/config_flow.py | 109 ++ homeassistant/components/fronius/const.py | 25 + .../components/fronius/coordinator.py | 184 ++++ .../components/fronius/manifest.json | 3 +- homeassistant/components/fronius/sensor.py | 996 +++++++++++++----- homeassistant/components/fronius/strings.json | 20 + .../components/fronius/translations/en.json | 20 + homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + tests/components/fronius/__init__.py | 77 +- tests/components/fronius/const.py | 4 - tests/components/fronius/test_config_flow.py | 263 +++++ tests/components/fronius/test_coordinator.py | 55 + tests/components/fronius/test_init.py | 23 + tests/components/fronius/test_sensor.py | 128 +-- 19 files changed, 1721 insertions(+), 407 deletions(-) create mode 100644 homeassistant/components/fronius/config_flow.py create mode 100644 homeassistant/components/fronius/const.py create mode 100644 homeassistant/components/fronius/coordinator.py create mode 100644 homeassistant/components/fronius/strings.json create mode 100644 homeassistant/components/fronius/translations/en.json delete mode 100644 tests/components/fronius/const.py create mode 100644 tests/components/fronius/test_config_flow.py create mode 100644 tests/components/fronius/test_coordinator.py create mode 100644 tests/components/fronius/test_init.py diff --git a/.coveragerc b/.coveragerc index fbb761eef1f..f03ad280be6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -372,7 +372,6 @@ omit = homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py - homeassistant/components/fronius/sensor.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py diff --git a/.strict-typing b/.strict-typing index 318a1367bef..e8941c307e6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -49,6 +49,7 @@ homeassistant.components.flunearyou.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* +homeassistant.components.fronius.* homeassistant.components.frontend.* homeassistant.components.fritz.* homeassistant.components.geo_location.* diff --git a/CODEOWNERS b/CODEOWNERS index 7fe3842761d..242ffa1ea03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -186,7 +186,7 @@ homeassistant/components/freebox/* @hacf-fr @Quentame homeassistant/components/freedompro/* @stefano055415 homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fritzbox/* @mib1185 @flabbamann -homeassistant/components/fronius/* @nielstron +homeassistant/components/fronius/* @nielstron @farmio homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/gdacs/* @exxamalte diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 2b4d968feca..9afd34ddc4a 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -1 +1,204 @@ -"""The Fronius component.""" +"""The Fronius integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Callable, TypeVar + +from pyfronius import Fronius, FroniusError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN, SOLAR_NET_ID_SYSTEM, FroniusDeviceInfo +from .coordinator import ( + FroniusCoordinatorBase, + FroniusInverterUpdateCoordinator, + FroniusLoggerUpdateCoordinator, + FroniusMeterUpdateCoordinator, + FroniusPowerFlowUpdateCoordinator, + FroniusStorageUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) +PLATFORMS: list[str] = ["sensor"] + +FroniusCoordinatorType = TypeVar("FroniusCoordinatorType", bound=FroniusCoordinatorBase) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up fronius from a config entry.""" + host = entry.data[CONF_HOST] + fronius = Fronius(async_get_clientsession(hass), host) + solar_net = FroniusSolarNet(hass, entry, fronius) + await solar_net.init_devices() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # reload on config_entry update + entry.async_on_unload(entry.add_update_listener(async_update_entry)) + 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: + solar_net = hass.data[DOMAIN].pop(entry.entry_id) + while solar_net.cleanup_callbacks: + solar_net.cleanup_callbacks.pop()() + + return unload_ok + + +async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update a given config entry.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class FroniusSolarNet: + """The FroniusSolarNet class routes received values to sensor entities.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, fronius: Fronius + ) -> None: + """Initialize FroniusSolarNet class.""" + self.hass = hass + self.cleanup_callbacks: list[Callable[[], None]] = [] + self.config_entry = entry + self.coordinator_lock = asyncio.Lock() + self.fronius = fronius + self.host: str = entry.data[CONF_HOST] + # entry.unique_id is either logger uid or first inverter uid if no logger available + # prepended by "solar_net_" to have individual device for whole system (power_flow) + self.solar_net_device_id = f"solar_net_{entry.unique_id}" + self.system_device_info: DeviceInfo | None = None + + self.inverter_coordinators: list[FroniusInverterUpdateCoordinator] = [] + self.logger_coordinator: FroniusLoggerUpdateCoordinator | None = None + self.meter_coordinator: FroniusMeterUpdateCoordinator | None = None + self.power_flow_coordinator: FroniusPowerFlowUpdateCoordinator | None = None + self.storage_coordinator: FroniusStorageUpdateCoordinator | None = None + + async def init_devices(self) -> None: + """Initialize DataUpdateCoordinators for SolarNet devices.""" + if self.config_entry.data["is_logger"]: + self.logger_coordinator = FroniusLoggerUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_logger_{self.host}", + ) + await self.logger_coordinator.async_config_entry_first_refresh() + + # _create_solar_net_device uses data from self.logger_coordinator when available + self.system_device_info = await self._create_solar_net_device() + + _inverter_infos = await self._get_inverter_infos() + for inverter_info in _inverter_infos: + coordinator = FroniusInverterUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_inverter_{inverter_info.solar_net_id}_{self.host}", + inverter_info=inverter_info, + ) + await coordinator.async_config_entry_first_refresh() + self.inverter_coordinators.append(coordinator) + + self.meter_coordinator = await self._init_optional_coordinator( + FroniusMeterUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_meters_{self.host}", + ) + ) + + self.power_flow_coordinator = await self._init_optional_coordinator( + FroniusPowerFlowUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_power_flow_{self.host}", + ) + ) + + self.storage_coordinator = await self._init_optional_coordinator( + FroniusStorageUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_storages_{self.host}", + ) + ) + + async def _create_solar_net_device(self) -> DeviceInfo: + """Create a device for the Fronius SolarNet system.""" + solar_net_device: DeviceInfo = DeviceInfo( + configuration_url=self.host, + identifiers={(DOMAIN, self.solar_net_device_id)}, + manufacturer="Fronius", + name="SolarNet", + ) + if self.logger_coordinator: + _logger_info = self.logger_coordinator.data[SOLAR_NET_ID_SYSTEM] + solar_net_device[ATTR_MODEL] = _logger_info["product_type"]["value"] + solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][ + "value" + ] + + device_registry = await dr.async_get_registry(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + **solar_net_device, + ) + return solar_net_device + + async def _get_inverter_infos(self) -> list[FroniusDeviceInfo]: + """Get information about the inverters in the SolarNet system.""" + try: + _inverter_info = await self.fronius.inverter_info() + except FroniusError as err: + raise ConfigEntryNotReady from err + + inverter_infos: list[FroniusDeviceInfo] = [] + for inverter in _inverter_info["inverters"]: + solar_net_id = inverter["device_id"]["value"] + unique_id = inverter["unique_id"]["value"] + device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=inverter["device_type"].get("manufacturer", "Fronius"), + model=inverter["device_type"].get( + "model", inverter["device_type"]["value"] + ), + name=inverter.get("custom_name", {}).get("value"), + via_device=(DOMAIN, self.solar_net_device_id), + ) + inverter_infos.append( + FroniusDeviceInfo( + device_info=device_info, + solar_net_id=solar_net_id, + unique_id=unique_id, + ) + ) + return inverter_infos + + @staticmethod + async def _init_optional_coordinator( + coordinator: FroniusCoordinatorType, + ) -> FroniusCoordinatorType | None: + """Initialize an update coordinator and return it if devices are found.""" + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + return None + # keep coordinator only if devices are found + # else ConfigEntryNotReady raised form KeyError + # in FroniusMeterUpdateCoordinator._get_fronius_device_data + return coordinator diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py new file mode 100644 index 00000000000..fdcd5301830 --- /dev/null +++ b/homeassistant/components/fronius/config_flow.py @@ -0,0 +1,109 @@ +"""Config flow for Fronius integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyfronius import Fronius, FroniusError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, FroniusConfigEntryData + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + } +) + + +async def validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, FroniusConfigEntryData]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + host = data[CONF_HOST] + fronius = Fronius(async_get_clientsession(hass), host) + + try: + datalogger_info: dict[str, Any] + datalogger_info = await fronius.current_logger_info() + except FroniusError as err: + _LOGGER.debug(err) + else: + logger_uid: str = datalogger_info["unique_identifier"]["value"] + return logger_uid, FroniusConfigEntryData( + host=host, + is_logger=True, + ) + # Gen24 devices don't provide GetLoggerInfo + try: + inverter_info = await fronius.inverter_info() + first_inverter = next(inverter for inverter in inverter_info["inverters"]) + except FroniusError as err: + _LOGGER.debug(err) + raise CannotConnect from err + except StopIteration as err: + raise CannotConnect("No supported Fronius SolarNet device found.") from err + first_inverter_uid: str = first_inverter["unique_id"]["value"] + return first_inverter_uid, FroniusConfigEntryData( + host=host, + is_logger=False, + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fronius.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + unique_id, info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(unique_id, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates=dict(info), reload_on_update=False + ) + title = ( + f"SolarNet {'Datalogger' if info['is_logger'] else 'Inverter'}" + f" at {info['host']}" + ) + return self.async_create_entry(title=title, data=info) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, conf: dict) -> FlowResult: + """Import a configuration from config.yaml.""" + return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]}) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py new file mode 100644 index 00000000000..de3e0cc9563 --- /dev/null +++ b/homeassistant/components/fronius/const.py @@ -0,0 +1,25 @@ +"""Constants for the Fronius integration.""" +from typing import Final, NamedTuple, TypedDict + +from homeassistant.helpers.entity import DeviceInfo + +DOMAIN: Final = "fronius" + +SolarNetId = str +SOLAR_NET_ID_POWER_FLOW: SolarNetId = "power_flow" +SOLAR_NET_ID_SYSTEM: SolarNetId = "system" + + +class FroniusConfigEntryData(TypedDict): + """ConfigEntry for the Fronius integration.""" + + host: str + is_logger: bool + + +class FroniusDeviceInfo(NamedTuple): + """Information about a Fronius inverter device.""" + + device_info: DeviceInfo + solar_net_id: SolarNetId + unique_id: str diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py new file mode 100644 index 00000000000..7a8d156dd65 --- /dev/null +++ b/homeassistant/components/fronius/coordinator.py @@ -0,0 +1,184 @@ +"""DataUpdateCoordinators for the Fronius integration.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Dict, TypeVar + +from pyfronius import FroniusError + +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.core import callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + SOLAR_NET_ID_POWER_FLOW, + SOLAR_NET_ID_SYSTEM, + FroniusDeviceInfo, + SolarNetId, +) +from .sensor import ( + INVERTER_ENTITY_DESCRIPTIONS, + LOGGER_ENTITY_DESCRIPTIONS, + METER_ENTITY_DESCRIPTIONS, + POWER_FLOW_ENTITY_DESCRIPTIONS, + STORAGE_ENTITY_DESCRIPTIONS, +) + +if TYPE_CHECKING: + from . import FroniusSolarNet + from .sensor import _FroniusSensorEntity + + FroniusEntityType = TypeVar("FroniusEntityType", bound=_FroniusSensorEntity) + + +class FroniusCoordinatorBase( + ABC, DataUpdateCoordinator[Dict[SolarNetId, Dict[str, Any]]] +): + """Query Fronius endpoint and keep track of seen conditions.""" + + default_interval: timedelta + error_interval: timedelta + valid_descriptions: list[SensorEntityDescription] + + def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None: + """Set up the FroniusCoordinatorBase class.""" + self._failed_update_count = 0 + self.solar_net = solar_net + # unregistered_keys are used to create entities in platform module + self.unregistered_keys: dict[SolarNetId, set[str]] = {} + super().__init__(*args, update_interval=self.default_interval, **kwargs) + + @abstractmethod + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + + async def _async_update_data(self) -> dict[SolarNetId, Any]: + """Fetch the latest data from the source.""" + async with self.solar_net.coordinator_lock: + try: + data = await self._update_method() + except FroniusError as err: + self._failed_update_count += 1 + if self._failed_update_count == 3: + self.update_interval = self.error_interval + raise UpdateFailed(err) from err + + if self._failed_update_count != 0: + self._failed_update_count = 0 + self.update_interval = self.default_interval + + for solar_net_id in data: + if solar_net_id not in self.unregistered_keys: + # id seen for the first time + self.unregistered_keys[solar_net_id] = { + desc.key for desc in self.valid_descriptions + } + return data + + @callback + def add_entities_for_seen_keys( + self, + async_add_entities: AddEntitiesCallback, + entity_constructor: type[FroniusEntityType], + ) -> None: + """ + Add entities for received keys and registers listener for future seen keys. + + Called from a platforms `async_setup_entry`. + """ + + @callback + def _add_entities_for_unregistered_keys() -> None: + """Add entities for keys seen for the first time.""" + new_entities: list = [] + for solar_net_id, device_data in self.data.items(): + for key in self.unregistered_keys[solar_net_id].intersection( + device_data + ): + new_entities.append(entity_constructor(self, key, solar_net_id)) + self.unregistered_keys[solar_net_id].remove(key) + if new_entities: + async_add_entities(new_entities) + + _add_entities_for_unregistered_keys() + self.solar_net.cleanup_callbacks.append( + self.async_add_listener(_add_entities_for_unregistered_keys) + ) + + +class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius device inverter endpoint and keep track of seen conditions.""" + + default_interval = timedelta(minutes=1) + error_interval = timedelta(minutes=10) + valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS + + def __init__( + self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any + ) -> None: + """Set up a Fronius inverter device scope coordinator.""" + super().__init__(*args, **kwargs) + self.inverter_info = inverter_info + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_inverter_data( + self.inverter_info.solar_net_id + ) + # wrap a single devices data in a dict with solar_net_id key for + # FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys + return {self.inverter_info.solar_net_id: data} + + +class FroniusLoggerUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius logger info endpoint and keep track of seen conditions.""" + + default_interval = timedelta(hours=1) + error_interval = timedelta(hours=1) + valid_descriptions = LOGGER_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_logger_info() + return {SOLAR_NET_ID_SYSTEM: data} + + +class FroniusMeterUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius system meter endpoint and keep track of seen conditions.""" + + default_interval = timedelta(minutes=1) + error_interval = timedelta(minutes=10) + valid_descriptions = METER_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_system_meter_data() + return data["meters"] # type: ignore[no-any-return] + + +class FroniusPowerFlowUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius power flow endpoint and keep track of seen conditions.""" + + default_interval = timedelta(seconds=10) + error_interval = timedelta(minutes=3) + valid_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_power_flow() + return {SOLAR_NET_ID_POWER_FLOW: data} + + +class FroniusStorageUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius system storage endpoint and keep track of seen conditions.""" + + default_interval = timedelta(minutes=1) + error_interval = timedelta(minutes=10) + valid_descriptions = STORAGE_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_system_storage_data() + return data["storages"] # type: ignore[no-any-return] diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index e40b5303eca..217598aaed4 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,8 +1,9 @@ { "domain": "fronius", "name": "Fronius", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.7.0"], - "codeowners": ["@nielstron"], + "codeowners": ["@nielstron", "@farmio"], "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 0ad172c2ab0..7c2a1bf6c09 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,348 +1,766 @@ """Support for Fronius devices.""" from __future__ import annotations -import copy -from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any -from pyfronius import Fronius, FroniusError import voluptuous as vol from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - CONF_DEVICE, CONF_MONITORED_CONDITIONS, 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, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_VOLT_AMPERE, + POWER_WATT, + TEMP_CELSIUS, ) -from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +if TYPE_CHECKING: + from . import FroniusSolarNet + from .coordinator import ( + FroniusCoordinatorBase, + FroniusInverterUpdateCoordinator, + FroniusLoggerUpdateCoordinator, + FroniusMeterUpdateCoordinator, + FroniusPowerFlowUpdateCoordinator, + FroniusStorageUpdateCoordinator, + ) _LOGGER = logging.getLogger(__name__) -CONF_SCOPE = "scope" +ELECTRIC_CHARGE_AMPERE_HOURS = "Ah" +ENERGY_VOLT_AMPERE_REACTIVE_HOUR = "varh" +POWER_VOLT_AMPERE_REACTIVE = "var" -TYPE_INVERTER = "inverter" -TYPE_STORAGE = "storage" -TYPE_METER = "meter" -TYPE_POWER_FLOW = "power_flow" -TYPE_LOGGER_INFO = "logger_info" -SCOPE_DEVICE = "device" -SCOPE_SYSTEM = "system" - -DEFAULT_SCOPE = SCOPE_DEVICE -DEFAULT_DEVICE = 0 -DEFAULT_INVERTER = 1 -DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) - -SENSOR_TYPES = [ - TYPE_INVERTER, - TYPE_STORAGE, - TYPE_METER, - TYPE_POWER_FLOW, - TYPE_LOGGER_INFO, -] -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.""" - config = copy.deepcopy(config) - for cond in config[CONF_MONITORED_CONDITIONS]: - if CONF_DEVICE not in cond: - if cond[CONF_SENSOR_TYPE] == TYPE_INVERTER: - cond[CONF_DEVICE] = DEFAULT_INVERTER - else: - cond[CONF_DEVICE] = DEFAULT_DEVICE - return config - - -PLATFORM_SCHEMA = vol.Schema( - vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_RESOURCE): cv.url, - vol.Required(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Optional(CONF_SCOPE, default=DEFAULT_SCOPE): vol.In( - SCOPE_TYPES - ), - vol.Optional(CONF_DEVICE): cv.positive_int, - } - ], - ), - } - ), - _device_id_validator, - ) +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_MONITORED_CONDITIONS): object, + } + ), ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up of Fronius platform.""" - session = async_get_clientsession(hass) - fronius = Fronius(session, config[CONF_RESOURCE]) - - scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - adapters = [] - # Creates all adapters for monitored conditions - for condition in config[CONF_MONITORED_CONDITIONS]: - - device = condition[CONF_DEVICE] - sensor_type = condition[CONF_SENSOR_TYPE] - scope = condition[CONF_SCOPE] - name = f"Fronius {condition[CONF_SENSOR_TYPE].replace('_', ' ').capitalize()} {device if scope == SCOPE_DEVICE else SCOPE_SYSTEM} {config[CONF_RESOURCE]}" - if sensor_type == TYPE_INVERTER: - if scope == SCOPE_SYSTEM: - adapter_cls = FroniusInverterSystem - else: - adapter_cls = FroniusInverterDevice - elif sensor_type == TYPE_METER: - if scope == SCOPE_SYSTEM: - adapter_cls = FroniusMeterSystem - else: - adapter_cls = FroniusMeterDevice - elif sensor_type == TYPE_POWER_FLOW: - adapter_cls = FroniusPowerFlow - elif sensor_type == TYPE_LOGGER_INFO: - adapter_cls = FroniusLoggerInfo - else: - adapter_cls = FroniusStorage - - adapters.append(adapter_cls(fronius, name, device, async_add_entities)) - - # Creates a lamdba that fetches an update when called - def adapter_data_fetcher(data_adapter): - async def fetch_data(*_): - await data_adapter.async_update() - - return fetch_data - - # Set up the fetching in a fixed interval for each adapter - for adapter in adapters: - fetch = adapter_data_fetcher(adapter) - # fetch data once at set-up - await fetch() - async_track_time_interval(hass, fetch, scan_interval) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: None = None, +) -> None: + """Import Fronius configuration from yaml.""" + _LOGGER.warning( + "Loading Fronius via platform setup is deprecated. Please remove it from your yaml configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class FroniusAdapter: - """The Fronius sensor fetching component.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Fronius sensor entities based on a config entry.""" + solar_net: FroniusSolarNet = hass.data[DOMAIN][config_entry.entry_id] + for inverter_coordinator in solar_net.inverter_coordinators: + inverter_coordinator.add_entities_for_seen_keys( + async_add_entities, InverterSensor + ) + if solar_net.logger_coordinator is not None: + solar_net.logger_coordinator.add_entities_for_seen_keys( + async_add_entities, LoggerSensor + ) + if solar_net.meter_coordinator is not None: + solar_net.meter_coordinator.add_entities_for_seen_keys( + async_add_entities, MeterSensor + ) + if solar_net.power_flow_coordinator is not None: + solar_net.power_flow_coordinator.add_entities_for_seen_keys( + async_add_entities, PowerFlowSensor + ) + if solar_net.storage_coordinator is not None: + solar_net.storage_coordinator.add_entities_for_seen_keys( + async_add_entities, StorageSensor + ) + + +INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="energy_day", + name="Energy day", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_year", + name="Energy year", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_total", + name="Energy total", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="frequency_ac", + name="Frequency AC", + native_unit_of_measurement=FREQUENCY_HERTZ, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current_ac", + name="AC Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current_dc", + name="DC current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="current_dc_2", + name="DC Current 2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="power_ac", + name="AC power", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac", + name="AC voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_dc", + name="DC voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="voltage_dc_2", + name="DC voltage 2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + # device status entities + SensorEntityDescription( + key="inverter_state", + name="Inverter state", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="error_code", + name="Error code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="status_code", + name="Status code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="led_state", + name="LED state", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="led_color", + name="LED color", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +] + +LOGGER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="co2_factor", + name="CO₂ factor", + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:molecule-co2", + ), + SensorEntityDescription( + key="cash_factor", + name="Grid export tariff", + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:cash-plus", + ), + SensorEntityDescription( + key="delivery_factor", + name="Grid import tariff", + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:cash-minus", + ), +] + +METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="current_ac_phase_1", + name="Current AC phase 1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current_ac_phase_2", + name="Current AC phase 2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current_ac_phase_3", + name="Current AC phase 3", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="energy_reactive_ac_consumed", + name="Energy reactive AC consumed", + native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + icon="mdi:lightning-bolt-outline", + ), + SensorEntityDescription( + key="energy_reactive_ac_produced", + name="Energy reactive AC produced", + native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + icon="mdi:lightning-bolt-outline", + ), + SensorEntityDescription( + key="energy_real_ac_minus", + name="Energy real AC minus", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_real_ac_plus", + name="Energy real AC plus", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_real_consumed", + name="Energy real consumed", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_real_produced", + name="Energy real produced", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="frequency_phase_average", + name="Frequency phase average", + native_unit_of_measurement=FREQUENCY_HERTZ, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="meter_location", + name="Meter location", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_apparent_phase_1", + name="Power apparent phase 1", + native_unit_of_measurement=POWER_VOLT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_apparent_phase_2", + name="Power apparent phase 2", + native_unit_of_measurement=POWER_VOLT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_apparent_phase_3", + name="Power apparent phase 3", + native_unit_of_measurement=POWER_VOLT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_apparent", + name="Power apparent", + native_unit_of_measurement=POWER_VOLT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_factor_phase_1", + name="Power factor phase 1", + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_factor_phase_2", + name="Power factor phase 2", + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_factor_phase_3", + name="Power factor phase 3", + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_factor", + name="Power factor", + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_reactive_phase_1", + name="Power reactive phase 1", + native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_reactive_phase_2", + name="Power reactive phase 2", + native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_reactive_phase_3", + name="Power reactive phase 3", + native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_reactive", + name="Power reactive", + native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash-outline", + ), + SensorEntityDescription( + key="power_real_phase_1", + name="Power real phase 1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_real_phase_2", + name="Power real phase 2", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_real_phase_3", + name="Power real phase 3", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_real", + name="Power real", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_1", + name="Voltage AC phase 1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_2", + name="Voltage AC phase 2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_3", + name="Voltage AC phase 3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_to_phase_12", + name="Voltage AC phase 1-2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_to_phase_23", + name="Voltage AC phase 2-3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_ac_phase_to_phase_31", + name="Voltage AC phase 3-1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), +] + +POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="energy_day", + name="Energy day", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_year", + name="Energy year", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_total", + name="Energy total", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="meter_mode", + name="Mode", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_battery", + name="Power battery", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_grid", + name="Power grid", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_load", + name="Power load", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="power_photovoltaics", + name="Power photovoltaics", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="relative_autonomy", + name="Relative autonomy", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:home-circle-outline", + ), + SensorEntityDescription( + key="relative_self_consumption", + name="Relative self consumption", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:solar-power", + ), +] + +STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="capacity_maximum", + name="Capacity maximum", + native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + ), + SensorEntityDescription( + key="capacity_designed", + name="Capacity designed", + native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + ), + SensorEntityDescription( + key="current_dc", + name="Current DC", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="voltage_dc", + name="Voltage DC", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="voltage_dc_maximum_cell", + name="Voltage DC maximum cell", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="voltage_dc_minimum_cell", + name="Voltage DC minimum cell", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:current-dc", + ), + SensorEntityDescription( + key="state_of_charge", + name="State of charge", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="temperature_cell", + name="Temperature cell", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), +] + + +class _FroniusSensorEntity(CoordinatorEntity, SensorEntity): + """Defines a Fronius coordinator entity.""" + + coordinator: FroniusCoordinatorBase + entity_descriptions: list[SensorEntityDescription] + _entity_id_prefix: str def __init__( - self, bridge: Fronius, name: str, device: int, add_entities: AddEntitiesCallback + self, + coordinator: FroniusCoordinatorBase, + key: str, + solar_net_id: str, ) -> None: - """Initialize the sensor.""" - self.bridge = bridge - self._name = name - self._device = device - self._fetched: dict[str, Any] = {} - self._available = True + """Set up an individual Fronius meter sensor.""" + super().__init__(coordinator) + self.entity_description = next( + desc for desc in self.entity_descriptions if desc.key == key + ) + # default entity_id added 2021.12 + # used for migration from non-unique_id entities of previous integration implementation + # when removed after migration period `_entity_id_prefix` will also no longer be needed + self.entity_id = f"{SENSOR_DOMAIN}.{key}_{DOMAIN}_{self._entity_id_prefix}_{coordinator.solar_net.host}" + self.solar_net_id = solar_net_id + self._attr_native_value = self._get_entity_value() - self.sensors: set[str] = set() - self._registered_sensors: set[SensorEntity] = set() - self._add_entities = add_entities + def _device_data(self) -> dict[str, Any]: + """Extract information for SolarNet device from coordinator data.""" + return self.coordinator.data[self.solar_net_id] - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def data(self): - """Return the state attributes.""" - return self._fetched - - @property - def available(self): - """Whether the fronius device is active.""" - return self._available - - async def async_update(self): - """Retrieve and update latest state.""" - try: - values = await self._update() - except FroniusError as err: - # fronius devices are often powered by self-produced solar energy - # and henced turned off at night. - # Therefore we will not print multiple errors when connection fails - if self._available: - self._available = False - _LOGGER.error("Failed to update: %s", err) - return - - self._available = True # reset connection failure - - attributes = self._fetched - # Copy data of current fronius device - for key, entry in values.items(): - # If the data is directly a sensor - if "value" in entry: - attributes[key] = entry - self._fetched = attributes - - # Add discovered value fields as sensors - # because some fields are only sent temporarily - new_sensors = [] - for key in attributes: - if key not in self.sensors: - self.sensors.add(key) - _LOGGER.info("Discovered %s, adding as sensor", key) - new_sensors.append(FroniusTemplateSensor(self, key)) - self._add_entities(new_sensors, True) - - # Schedule an update for all included sensors - for sensor in self._registered_sensors: - sensor.async_schedule_update_ha_state(True) - - async def _update(self) -> dict: - """Return values of interest.""" + def _get_entity_value(self) -> Any: + """Extract entity value from coordinator. Raises KeyError if not included in latest update.""" + new_value = self.coordinator.data[self.solar_net_id][ + self.entity_description.key + ]["value"] + return round(new_value, 4) if isinstance(new_value, float) else new_value @callback - def register(self, sensor): - """Register child sensor for update subscriptions.""" - self._registered_sensors.add(sensor) - return lambda: self._registered_sensors.remove(sensor) + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + try: + self._attr_native_value = self._get_entity_value() + except KeyError: + return + self.async_write_ha_state() -class FroniusInverterSystem(FroniusAdapter): - """Adapter for the fronius inverter with system scope.""" +class InverterSensor(_FroniusSensorEntity): + """Defines a Fronius inverter device sensor entity.""" - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_system_inverter_data() + entity_descriptions = INVERTER_ENTITY_DESCRIPTIONS + + def __init__( + self, + coordinator: FroniusInverterUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius inverter sensor.""" + self._entity_id_prefix = f"inverter_{solar_net_id}" + super().__init__(coordinator, key, solar_net_id) + # device_info created in __init__ from a `GetInverterInfo` request + self._attr_device_info = coordinator.inverter_info.device_info + self._attr_unique_id = f"{coordinator.inverter_info.unique_id}-{key}" -class FroniusInverterDevice(FroniusAdapter): - """Adapter for the fronius inverter with device scope.""" +class LoggerSensor(_FroniusSensorEntity): + """Defines a Fronius logger device sensor entity.""" - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_inverter_data(self._device) + entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS + _entity_id_prefix = "logger_info_0" + + def __init__( + self, + coordinator: FroniusLoggerUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius meter sensor.""" + super().__init__(coordinator, key, solar_net_id) + logger_data = self._device_data() + # Logger device is already created in FroniusSolarNet._create_solar_net_device + self._attr_device_info = coordinator.solar_net.system_device_info + self._attr_native_unit_of_measurement = logger_data[key].get("unit") + self._attr_unique_id = f'{logger_data["unique_identifier"]["value"]}-{key}' -class FroniusStorage(FroniusAdapter): - """Adapter for the fronius battery storage.""" +class MeterSensor(_FroniusSensorEntity): + """Defines a Fronius meter device sensor entity.""" - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_storage_data(self._device) + entity_descriptions = METER_ENTITY_DESCRIPTIONS + + def __init__( + self, + coordinator: FroniusMeterUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius meter sensor.""" + self._entity_id_prefix = f"meter_{solar_net_id}" + super().__init__(coordinator, key, solar_net_id) + meter_data = self._device_data() + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, meter_data["serial"]["value"])}, + manufacturer=meter_data["manufacturer"]["value"], + model=meter_data["model"]["value"], + name=meter_data["model"]["value"], + via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), + ) + self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' -class FroniusMeterSystem(FroniusAdapter): - """Adapter for the fronius meter with system scope.""" +class PowerFlowSensor(_FroniusSensorEntity): + """Defines a Fronius power flow sensor entity.""" - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_system_meter_data() + entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS + _entity_id_prefix = "power_flow_0" + + def __init__( + self, + coordinator: FroniusPowerFlowUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius power flow sensor.""" + super().__init__(coordinator, key, solar_net_id) + # SolarNet device is already created in FroniusSolarNet._create_solar_net_device + self._attr_device_info = coordinator.solar_net.system_device_info + self._attr_unique_id = ( + f"{coordinator.solar_net.solar_net_device_id}-power_flow-{key}" + ) -class FroniusMeterDevice(FroniusAdapter): - """Adapter for the fronius meter with device scope.""" +class StorageSensor(_FroniusSensorEntity): + """Defines a Fronius storage device sensor entity.""" - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_meter_data(self._device) + entity_descriptions = STORAGE_ENTITY_DESCRIPTIONS + def __init__( + self, + coordinator: FroniusStorageUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius storage sensor.""" + self._entity_id_prefix = f"storage_{solar_net_id}" + super().__init__(coordinator, key, solar_net_id) + storage_data = self._device_data() -class FroniusPowerFlow(FroniusAdapter): - """Adapter for the fronius power flow.""" - - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_power_flow() - - -class FroniusLoggerInfo(FroniusAdapter): - """Adapter for the fronius power flow.""" - - async def _update(self): - """Get the values for the current state.""" - return await self.bridge.current_logger_info() - - -class FroniusTemplateSensor(SensorEntity): - """Sensor for the single values (e.g. pv power, ac power).""" - - 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 - 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): - """Device should not be polled, returns False.""" - return False - - @property - def available(self): - """Whether the fronius device is active.""" - return self._parent.available - - async def async_update(self): - """Update the internal state.""" - state = self._parent.data.get(self._key) - 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.""" - self.async_on_remove(self._parent.register(self)) - - def __hash__(self): - """Hash sensor by hashing its name.""" - return hash(self.name) + self._attr_unique_id = f'{storage_data["serial"]["value"]}-{key}' + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, storage_data["serial"]["value"])}, + manufacturer=storage_data["manufacturer"]["value"], + model=storage_data["model"]["value"], + name=storage_data["model"]["value"], + via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), + ) diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json new file mode 100644 index 00000000000..7e411476559 --- /dev/null +++ b/homeassistant/components/fronius/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Fronius SolarNet", + "description": "Configure the IP address or local hostname of your Fronius device.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/fronius/translations/en.json b/homeassistant/components/fronius/translations/en.json new file mode 100644 index 00000000000..75bbeede6e0 --- /dev/null +++ b/homeassistant/components/fronius/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Configure the IP address or local hostname of your Fronius device.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 35bc6269d6c..d54e515681e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -102,6 +102,7 @@ FLOWS = [ "fritz", "fritzbox", "fritzbox_callmonitor", + "fronius", "garages_amsterdam", "gdacs", "geofency", diff --git a/mypy.ini b/mypy.ini index 6e9cc991f7f..da1fb4f08d4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -550,6 +550,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fronius.*] +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.frontend.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 41413166157..e2ef369987c 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,23 +1,72 @@ """Tests for the Fronius integration.""" +from homeassistant.components.fronius.const import DOMAIN +from homeassistant.const import CONF_HOST -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_RESOURCE -from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker -from .const import DOMAIN, MOCK_HOST +MOCK_HOST = "http://fronius" +MOCK_UID = "123.4567890" # has to match mocked logger unique_id -async def setup_fronius_integration(hass, devices): +async def setup_fronius_integration(hass): """Create the Fronius integration.""" - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_RESOURCE: MOCK_HOST, - CONF_MONITORED_CONDITIONS: devices, - } + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_UID, + data={ + CONF_HOST: MOCK_HOST, + "is_logger": True, }, ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + + +def mock_responses( + aioclient_mock: AiohttpClientMocker, + host: str = MOCK_HOST, + night: bool = False, +) -> None: + """Mock responses for Fronius Symo inverter with meter.""" + aioclient_mock.clear_requests() + _day_or_night = "night" if night else "day" + + aioclient_mock.get( + f"{host}/solar_api/GetAPIVersion.cgi", + text=load_fixture("symo/GetAPIVersion.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + "DeviceId=1&DataCollection=CommonInverterData", + text=load_fixture( + f"symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json", "fronius" + ), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterInfo.cgi", + text=load_fixture("symo/GetInverterInfo.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetLoggerInfo.cgi", + text=load_fixture("symo/GetLoggerInfo.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0", + text=load_fixture("symo/GetMeterRealtimeData_Device_0.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System", + text=load_fixture("symo/GetMeterRealtimeData_System.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetPowerFlowRealtimeData.fcgi", + text=load_fixture( + f"symo/GetPowerFlowRealtimeData_{_day_or_night}.json", "fronius" + ), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", + text=load_fixture("symo/GetStorageRealtimeData_System.json", "fronius"), + ) diff --git a/tests/components/fronius/const.py b/tests/components/fronius/const.py deleted file mode 100644 index 7b7635e4d92..00000000000 --- a/tests/components/fronius/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants for Fronius tests.""" - -DOMAIN = "fronius" -MOCK_HOST = "http://fronius" diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py new file mode 100644 index 00000000000..f0ff1e0ce48 --- /dev/null +++ b/tests/components/fronius/test_config_flow.py @@ -0,0 +1,263 @@ +"""Test the Fronius config flow.""" +from unittest.mock import patch + +from pyfronius import FroniusError + +from homeassistant import config_entries +from homeassistant.components.fronius.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.setup import async_setup_component + +from . import MOCK_HOST, mock_responses + +from tests.common import MockConfigEntry + +INVERTER_INFO_RETURN_VALUE = { + "inverters": [ + { + "device_id": {"value": "1"}, + "unique_id": {"value": "1234567"}, + } + ] +} +LOGGER_INFO_RETURN_VALUE = {"unique_identifier": {"value": "123.4567"}} + + +async def test_form_with_logger(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ), patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.9.8.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" + assert result2["data"] == { + "host": "10.9.8.1", + "is_logger": True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_with_inverter(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), patch( + "pyfronius.Fronius.inverter_info", + return_value=INVERTER_INFO_RETURN_VALUE, + ), patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.9.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "SolarNet Inverter at 10.9.1.1" + assert result2["data"] == { + "host": "10.9.1.1", + "is_logger": False, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_no_device(hass: HomeAssistant) -> None: + """Test we handle no device found error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), patch( + "pyfronius.Fronius.inverter_info", + return_value={"inverters": []}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected(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( + "pyfronius.Fronius.current_logger_info", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_existing(hass): + """Test existing entry.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567", + data={CONF_HOST: "10.9.8.1", "is_logger": True}, + ).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} + ) + with patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.9.8.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_updates_host(hass, aioclient_mock): + """Test existing entry gets updated.""" + old_host = "http://10.1.0.1" + new_host = "http://10.1.0.2" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", # has to match mocked logger unique_id + data={ + CONF_HOST: old_host, + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + mock_responses(aioclient_mock, host=old_host) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_responses(aioclient_mock, host=new_host) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": new_host, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data == { + "host": new_host, + "is_logger": True, + } + + +async def test_import(hass, aioclient_mock): + """Test import step.""" + mock_responses(aioclient_mock) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_RESOURCE: MOCK_HOST, + } + }, + ) + await hass.async_block_till_done() + + fronius_entries = hass.config_entries.async_entries(DOMAIN) + assert len(fronius_entries) == 1 + + test_entry = fronius_entries[0] + assert test_entry.unique_id == "123.4567890" # has to match mocked logger unique_id + assert test_entry.data == { + "host": MOCK_HOST, + "is_logger": True, + } diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py new file mode 100644 index 00000000000..b729c4d97ac --- /dev/null +++ b/tests/components/fronius/test_coordinator.py @@ -0,0 +1,55 @@ +"""Test the Fronius update coordinators.""" +from unittest.mock import patch + +from pyfronius import FroniusError + +from homeassistant.components.fronius.coordinator import ( + FroniusInverterUpdateCoordinator, +) +from homeassistant.util import dt + +from . import mock_responses, setup_fronius_integration + +from tests.common import async_fire_time_changed + + +async def test_adaptive_update_interval(hass, aioclient_mock): + """Test coordinators changing their update interval when inverter not available.""" + with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data: + mock_responses(aioclient_mock) + await setup_fronius_integration(hass) + assert mock_inverter_data.call_count == 1 + + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + assert mock_inverter_data.call_count == 2 + + mock_inverter_data.side_effect = FroniusError + # first 3 requests at default interval - 4th has different interval + for _ in range(4): + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + assert mock_inverter_data.call_count == 5 + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval + ) + await hass.async_block_till_done() + assert mock_inverter_data.call_count == 6 + + mock_inverter_data.side_effect = None + # next successful request resets to default interval + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval + ) + await hass.async_block_till_done() + assert mock_inverter_data.call_count == 7 + + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + assert mock_inverter_data.call_count == 8 diff --git a/tests/components/fronius/test_init.py b/tests/components/fronius/test_init.py new file mode 100644 index 00000000000..6ceeb273cba --- /dev/null +++ b/tests/components/fronius/test_init.py @@ -0,0 +1,23 @@ +"""Test the Fronius integration.""" +from homeassistant.components.fronius.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from . import mock_responses, setup_fronius_integration + + +async def test_unload_config_entry(hass, aioclient_mock): + """Test that configuration entry supports unloading.""" + mock_responses(aioclient_mock) + await setup_fronius_integration(hass) + + fronius_entries = hass.config_entries.async_entries(DOMAIN) + assert len(fronius_entries) == 1 + + test_entry = fronius_entries[0] + assert test_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(test_entry.entry_id) + await hass.async_block_till_done() + + assert test_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 4564d26fa9f..fd64c127496 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,66 +1,15 @@ """Tests for the Fronius sensor platform.""" - -from homeassistant.components.fronius.sensor import ( - CONF_SCOPE, - DEFAULT_SCAN_INTERVAL, - SCOPE_DEVICE, - TYPE_INVERTER, - TYPE_LOGGER_INFO, - TYPE_METER, - TYPE_POWER_FLOW, +from homeassistant.components.fronius.coordinator import ( + FroniusInverterUpdateCoordinator, + FroniusPowerFlowUpdateCoordinator, ) -from homeassistant.const import CONF_DEVICE, CONF_SENSOR_TYPE, STATE_UNKNOWN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNKNOWN from homeassistant.util import dt -from . import setup_fronius_integration -from .const import MOCK_HOST +from . import mock_responses, setup_fronius_integration -from tests.common import async_fire_time_changed, load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker - - -def mock_responses(aioclient_mock: AiohttpClientMocker, night: bool = False) -> None: - """Mock responses for Fronius Symo inverter with meter.""" - aioclient_mock.clear_requests() - _day_or_night = "night" if night else "day" - - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/GetAPIVersion.cgi", - text=load_fixture("symo/GetAPIVersion.json", "fronius"), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" - "DeviceId=1&DataCollection=CommonInverterData", - text=load_fixture( - f"symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json", "fronius" - ), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetInverterInfo.cgi", - text=load_fixture("symo/GetInverterInfo.json", "fronius"), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetLoggerInfo.cgi", - text=load_fixture("symo/GetLoggerInfo.json", "fronius"), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0", - text=load_fixture("symo/GetMeterRealtimeData_Device_0.json", "fronius"), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System", - text=load_fixture("symo/GetMeterRealtimeData_System.json", "fronius"), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetPowerFlowRealtimeData.fcgi", - text=load_fixture( - f"symo/GetPowerFlowRealtimeData_{_day_or_night}.json", "fronius" - ), - ) - aioclient_mock.get( - f"{MOCK_HOST}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", - text=load_fixture("symo/GetStorageRealtimeData_System.json", "fronius"), - ) +from tests.common import async_fire_time_changed async def test_symo_inverter(hass, aioclient_mock): @@ -72,15 +21,9 @@ async def test_symo_inverter(hass, aioclient_mock): # Init at night mock_responses(aioclient_mock, night=True) - config = { - CONF_SENSOR_TYPE: TYPE_INVERTER, - CONF_SCOPE: SCOPE_DEVICE, - CONF_DEVICE: 1, - } - await setup_fronius_integration(hass, [config]) + await setup_fronius_integration(hass) - assert len(hass.states.async_all()) == 10 - # 5 ignored from DeviceStatus + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) @@ -89,10 +32,12 @@ async def test_symo_inverter(hass, aioclient_mock): # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 14 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # 4 additional AC entities assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) @@ -114,12 +59,9 @@ async def test_symo_logger(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock) - config = { - CONF_SENSOR_TYPE: TYPE_LOGGER_INFO, - } - await setup_fronius_integration(hass, [config]) + await setup_fronius_integration(hass) - assert len(hass.states.async_all()) == 12 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # ignored constant entities: # hardware_platform, hardware_version, product_type # software_version, time_zone, time_zone_location @@ -128,7 +70,7 @@ async def test_symo_logger(hass, aioclient_mock): # states are rounded to 2 decimals assert_state( "sensor.cash_factor_fronius_logger_info_0_http_fronius", - 0.08, + 0.078, ) assert_state( "sensor.co2_factor_fronius_logger_info_0_http_fronius", @@ -149,21 +91,16 @@ async def test_symo_meter(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock) - config = { - CONF_SENSOR_TYPE: TYPE_METER, - CONF_SCOPE: SCOPE_DEVICE, - CONF_DEVICE: 0, - } - await setup_fronius_integration(hass, [config]) + await setup_fronius_integration(hass) - assert len(hass.states.async_all()) == 39 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # ignored entities: # manufacturer, model, serial, enable, timestamp, visible, meter_location # # states are rounded to 2 decimals - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.75) + assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.1) + assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.102) assert_state( "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 59960790 ) @@ -175,9 +112,9 @@ async def test_symo_meter(hass, aioclient_mock): assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 15303334) assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 35623065) assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 50) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.79) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.05) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.56) + assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.793) + assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.048) + assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.562) assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 5592.57) assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", -0.99) assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", -0.99) @@ -215,12 +152,9 @@ async def test_symo_power_flow(hass, aioclient_mock): # First test at night mock_responses(aioclient_mock, night=True) - config = { - CONF_SENSOR_TYPE: TYPE_POWER_FLOW, - } - await setup_fronius_integration(hass, [config]) + await setup_fronius_integration(hass) - assert len(hass.states.async_all()) == 12 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 # ignored: location, mode, timestamp # # states are rounded to 2 decimals @@ -263,13 +197,15 @@ async def test_symo_power_flow(hass, aioclient_mock): # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed( + hass, dt.utcnow() + FroniusPowerFlowUpdateCoordinator.default_interval + ) await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 12 + # still 55 because power_flow update interval is shorter than others + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 assert_state( "sensor.energy_day_fronius_power_flow_0_http_fronius", - 1101.70, + 1101.7001, ) assert_state( "sensor.energy_total_fronius_power_flow_0_http_fronius", @@ -297,7 +233,7 @@ async def test_symo_power_flow(hass, aioclient_mock): ) assert_state( "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 39.47, + 39.4708, ) assert_state( "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", From 4c3163196e132d5bba145b7bf1f5571258c3694a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 02:13:38 +0100 Subject: [PATCH 0808/1452] Temporary disable partial runs in CI (#60258) --- .github/workflows/ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6af4eb444fe..d4fab3d1a2d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -654,7 +654,9 @@ jobs: changes: name: Determine what has changed outputs: - core: ${{ steps.core.outputs.any }} + # core: ${{ steps.core.outputs.any }} + # Temporary disable + core: 'true' integrations: ${{ steps.integrations.outputs.changes }} tests: ${{ steps.tests.outputs.integrations }} runs-on: ubuntu-latest From 4555f52e50ea543b4274473d2804e4d4219d142f Mon Sep 17 00:00:00 2001 From: cvroque <65680394+cvroque@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:15:49 -0300 Subject: [PATCH 0809/1452] Add configuration entities to Tuya Vacuum (sd) (#59936) --- homeassistant/components/tuya/const.py | 3 +++ homeassistant/components/tuya/number.py | 10 ++++++++++ homeassistant/components/tuya/switch.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index f0707b1b37a..0173bde9a6b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -271,6 +271,7 @@ class DPCode(str, Enum): SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_CHARGE = "switch_charge" SWITCH_CONTROLLER = "switch_controller" + SWITCH_DISTURB = "switch_disturb" SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_LED = "switch_led" # Switch SWITCH_LED_1 = "switch_led_1" @@ -304,6 +305,8 @@ class DPCode(str, Enum): VA_TEMPERATURE = "va_temperature" VOC_STATE = "voc_state" VOC_VALUE = "voc_value" + VOICE_SWITCH = "voice_switch" + VOLUME_SET = "volume_set" WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time WATER_RESET = "water_reset" # Resetting of water usage days diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 8db2e4debd5..634f4858186 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -103,6 +103,16 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Robot Vacuum + # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo + "sd": ( + NumberEntityDescription( + key=DPCode.VOLUME_SET, + name="Volume", + icon="mdi:volume-high", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 8395c02cf39..5fb5a0f39b0 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -290,6 +290,22 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Robot Vacuum + # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo + "sd": ( + SwitchEntityDescription( + key=DPCode.SWITCH_DISTURB, + name="Do Not Disturb", + icon="mdi:minus-circle", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.VOICE_SWITCH, + name="Voice", + icon="mdi:account-voice", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From e1de6612be7e9ff8d3f05ef3cdf531021edacad1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Nov 2021 03:02:03 +0100 Subject: [PATCH 0810/1452] Fix socket usage in Aprs test (#60253) --- tests/components/aprs/test_device_tracker.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index dc0cf09f28d..3da8cb825e4 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -327,19 +327,18 @@ def test_setup_scanner(): def test_setup_scanner_timeout(): """Test setup_scanner failure from timeout.""" - hass = get_test_home_assistant() - hass.start() + with patch("aprslib.IS.connect", side_effect=TimeoutError): + hass = get_test_home_assistant() + hass.start() - config = { - "username": TEST_CALLSIGN, - "password": TEST_PASSWORD, - "host": "localhost", - "timeout": 0.01, - "callsigns": ["XX0FOO*", "YY0BAR-1"], - } + config = { + "username": TEST_CALLSIGN, + "password": TEST_PASSWORD, + "host": "localhost", + "timeout": 0.01, + "callsigns": ["XX0FOO*", "YY0BAR-1"], + } - see = Mock() - try: + see = Mock() assert not device_tracker.setup_scanner(hass, config, see) - finally: hass.stop() From be94ce42a5447cbc018fe120e01cf865e114c2a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Nov 2021 20:16:09 -0600 Subject: [PATCH 0811/1452] Prevent get_mac_address from blocking event loop in samsungtv config flow (#60246) --- homeassistant/components/samsungtv/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index f476d1dc87d..9c118ca1caa 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Samsung TV.""" from __future__ import annotations +from functools import partial import socket from types import MappingProxyType from typing import Any @@ -167,7 +168,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if mac := mac_from_device_info(info): self._mac = mac - elif mac := getmac.get_mac_address(ip=self._host): + elif mac := await self.hass.async_add_executor_job( + partial(getmac.get_mac_address, ip=self._host) + ): self._mac = mac self._device_info = info return True From 774e1b0022747972bce0dc98768603511bad8e20 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 24 Nov 2021 00:59:18 -0300 Subject: [PATCH 0812/1452] Decouple BroadlinkDevice.available property (#58853) * Decouple BroadlinkDevice.available property * Exclude unreachable line from coverage --- homeassistant/components/broadlink/device.py | 7 +++++++ homeassistant/components/broadlink/entity.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 1d1fe273252..951be9b26bb 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -56,6 +56,13 @@ class BroadlinkDevice: """Return the mac address of the device.""" return self.config.data[CONF_MAC] + @property + def available(self): + """Return True if the device is available.""" + if self.update_manager is None: # pragma: no cover + return False + return self.update_manager.available + @staticmethod async def async_update(hass, entry): """Update the device and related entities. diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index 25bfd87140b..2c7a05a7e70 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -47,8 +47,8 @@ class BroadlinkEntity(Entity): @property def available(self): - """Return True if the remote is available.""" - return self._device.update_manager.available + """Return True if the entity is available.""" + return self._device.available @property def device_info(self) -> DeviceInfo: From c0d2a666099816013db68c64c275aeae8e077c02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 09:01:35 +0100 Subject: [PATCH 0813/1452] Add stable ID to entity registry entries (#60218) * Add UUID to entity registry entries * Fix test --- homeassistant/helpers/entity_registry.py | 14 +++++++++++--- tests/helpers/test_entity_platform.py | 3 ++- tests/helpers/test_entity_registry.py | 6 ++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 6e68d4f5499..f4b733eac56 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -39,7 +39,7 @@ from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, storage from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass -from homeassistant.util import slugify +from homeassistant.util import slugify, uuid as uuid_util from homeassistant.util.yaml import load_yaml from .typing import UNDEFINED, UndefinedType @@ -59,7 +59,7 @@ DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -103,6 +103,7 @@ class RegistryEntry: ) entity_category: str | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) + id: str = attr.ib(factory=uuid_util.random_uuid_hex) name: str | None = attr.ib(default=None) # As set by integration original_device_class: str | None = attr.ib(default=None) @@ -558,6 +559,7 @@ class EntityRegistry: entity_category=entity["entity_category"], entity_id=entity["entity_id"], icon=entity["icon"], + id=entity["id"], name=entity["name"], original_device_class=entity["original_device_class"], original_icon=entity["original_icon"], @@ -592,6 +594,7 @@ class EntityRegistry: "entity_category": entry.entity_category, "entity_id": entry.entity_id, "icon": entry.icon, + "id": entry.id, "name": entry.name, "original_device_class": entry.original_device_class, "original_icon": entry.original_icon, @@ -752,12 +755,17 @@ async def _async_migrate( entity["unit_of_measurement"] = entity.get("unit_of_measurement") if old_major_version < 2 and old_minor_version < 3: - # From version 1.2 + # Version 1.3 adds original_device_class for entity in data["entities"]: # Move device_class to original_device_class entity["original_device_class"] = entity["device_class"] entity["device_class"] = None + if old_major_version < 2 and old_minor_version < 4: + # Version 1.4 adds id + for entity in data["entities"]: + entity["id"] = uuid_util.random_uuid_hex() + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 11cbc78ed71..e367fa248d5 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import logging -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch import pytest @@ -1108,6 +1108,7 @@ async def test_entity_info_added_to_entity_registry(hass): device_class=None, entity_category="config", icon=None, + id=ANY, name=None, original_device_class="mock-device-class", original_icon="nice:icon", diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 00900e013c0..ad88a23f76f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -96,6 +96,7 @@ def test_get_or_create_updates_data(registry): disabled_by=er.DISABLED_HASS, entity_category="config", icon=None, + id=orig_entry.id, name=None, original_device_class="mock-device-class", original_icon="initial-original_icon", @@ -135,6 +136,7 @@ def test_get_or_create_updates_data(registry): disabled_by=er.DISABLED_HASS, # Should not be updated entity_category="config", icon=None, + id=orig_entry.id, name=None, original_device_class="new-mock-device-class", original_icon="updated-original_icon", @@ -418,8 +420,8 @@ async def test_migration_yaml_to_json(hass): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_2(hass, hass_storage): - """Test migration from version 1.1 to 1.2.""" +async def test_migration_1_1(hass, hass_storage): + """Test migration from version 1.1.""" hass_storage[er.STORAGE_KEY] = { "version": 1, "minor_version": 1, From 4ffb0b8380100d008f044fa5340b67f2d4127e0a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Nov 2021 09:40:52 +0100 Subject: [PATCH 0814/1452] Use UsbServiceInfo in modem_callerid (#60268) Co-authored-by: epenet --- .../components/modem_callerid/config_flow.py | 4 ++-- .../modem_callerid/test_config_flow.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index 70fd3969e02..2bc857a16f4 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -32,10 +32,10 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB Discovery.""" - device = discovery_info["device"] + device = discovery_info.device dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device) - unique_id = f"{discovery_info['vid']}:{discovery_info['pid']}_{discovery_info['serial_number']}_{discovery_info['manufacturer']}_{discovery_info['description']}" + unique_id = f"{discovery_info.vid}:{discovery_info.pid}_{discovery_info.serial_number}_{discovery_info.manufacturer}_{discovery_info.description}" if ( await self.validate_device_errors(dev_path=dev_path, unique_id=unique_id) is None diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 4d76ae8a944..0956a8fe1b7 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -17,14 +17,14 @@ from homeassistant.data_entry_flow import ( from . import _patch_config_flow_modem -DISCOVERY_INFO = { - "device": phone_modem.DEFAULT_PORT, - "pid": "1340", - "vid": "0572", - "serial_number": "1234", - "description": "modem", - "manufacturer": "Connexant", -} +DISCOVERY_INFO = usb.UsbServiceInfo( + device=phone_modem.DEFAULT_PORT, + pid="1340", + vid="0572", + serial_number="1234", + description="modem", + manufacturer="Connexant", +) def _patch_setup(): From ed9d40378ef4dcba3ce1652531116082313c0fa4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 24 Nov 2021 09:48:34 +0100 Subject: [PATCH 0815/1452] Update base image 2021.11.0 (#60227) --- .github/workflows/builder.yml | 21 +++++++++------------ build.json | 22 ---------------------- build.yaml | 20 ++++++++++++++++++++ machine/build.json | 16 ---------------- machine/build.yaml | 14 ++++++++++++++ 5 files changed, 43 insertions(+), 50 deletions(-) delete mode 100644 build.json create mode 100644 build.yaml delete mode 100644 machine/build.json create mode 100644 machine/build.yaml diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 20bed0fffcc..7b5a145cd09 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -56,9 +56,7 @@ jobs: uses: home-assistant/actions/helpers/codenotary@master with: source: file://${{ github.workspace }}/OFFICIAL_IMAGE - user: ${{ secrets.VCN_USER }} - password: ${{ secrets.VCN_PASSWORD }} - organisation: home-assistant.io + token: ${{ secrets.CAS_TOKEN }} build_python: name: Build PyPi package @@ -139,9 +137,9 @@ jobs: $BUILD_ARGS \ --${{ matrix.arch }} \ --target /data \ - --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ - --validate-from "${{ secrets.VCN_ORG }}" \ --generic ${{ needs.init.outputs.version }} + env: + CAS_API_KEY: ${{ secrets.CAS_TOKEN }} build_machine: name: Build ${{ matrix.machine }} machine core image @@ -186,14 +184,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.09.0 + uses: home-assistant/builder@2021.11.3 with: args: | $BUILD_ARGS \ --target /data/machine \ - --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ - --validate-from "${{ secrets.VCN_ORG }}" \ --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" + env: + CAS_API_KEY: ${{ secrets.CAS_TOKEN }} publish_ha: name: Publish version files @@ -248,8 +246,8 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Install VCN tools - uses: home-assistant/actions/helpers/vcn@master + - name: Install CAS tools + uses: home-assistant/actions/helpers/cas@master - name: Build Meta Image shell: bash @@ -293,8 +291,7 @@ jobs: function validate_image() { local image=${1} - state="$(vcn authenticate --org home-assistant.io --output json docker://${image} | jq '.verification.status // 2')" - if [[ "${state}" != "0" ]]; then + if ! cas authenticate --signerID notary@home-assistant.io; then echo "Invalid signature!" exit 1 fi diff --git a/build.json b/build.json deleted file mode 100644 index 1b9c72e8675..00000000000 --- a/build.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "image": "homeassistant/{arch}-homeassistant", - "shadow_repository": "ghcr.io/home-assistant", - "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.09.0", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.09.0", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.09.0", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.09.0", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.09.0" - }, - "labels": { - "io.hass.type": "core", - "org.opencontainers.image.title": "Home Assistant", - "org.opencontainers.image.description": "Open-source home automation platform running on Python 3", - "org.opencontainers.image.source": "https://github.com/home-assistant/core", - "org.opencontainers.image.authors": "The Home Assistant Authors", - "org.opencontainers.image.url": "https://www.home-assistant.io/", - "org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/", - "org.opencontainers.image.licenses": "Apache License 2.0" - }, - "version_tag": true -} \ No newline at end of file diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000000..1d0e18c79ea --- /dev/null +++ b/build.yaml @@ -0,0 +1,20 @@ +image: homeassistant/{arch}-homeassistant +shadow_repository: ghcr.io/home-assistant +build_from: + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2021.09.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2021.09.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2021.09.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2021.09.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2021.09.0 +codenotary: + signer: notary@home-assistant.io + base_image: notary@home-assistant.io +labels: + io.hass.type: core + org.opencontainers.image.title: Home Assistant + org.opencontainers.image.description: Open-source home automation platform running on Python 3 + org.opencontainers.image.source: https://github.com/home-assistant/core + org.opencontainers.image.authors: The Home Assistant Authors + org.opencontainers.image.url: https://www.home-assistant.io/ + org.opencontainers.image.documentation: https://www.home-assistant.io/docs/ + org.opencontainers.image.licenses: Apache License 2.0 diff --git a/machine/build.json b/machine/build.json deleted file mode 100644 index 3b4d804dc1c..00000000000 --- a/machine/build.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "image": "homeassistant/{machine}-homeassistant", - "shadow_repository": "ghcr.io/home-assistant", - "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant:", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant:", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant:", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant:", - "i386": "ghcr.io/home-assistant/i386-homeassistant:" - }, - "labels": { - "io.hass.type": "core", - "org.opencontainers.image.source": "https://github.com/home-assistant/core" - }, - "version_tag": true -} diff --git a/machine/build.yaml b/machine/build.yaml new file mode 100644 index 00000000000..340b8079b9f --- /dev/null +++ b/machine/build.yaml @@ -0,0 +1,14 @@ +image: homeassistant/{machine}-homeassistant +shadow_repository: ghcr.io/home-assistant +build_from: + aarch64: "ghcr.io/home-assistant/aarch64-homeassistant:" + armv7: "ghcr.io/home-assistant/armv7-homeassistant:" + armhf: "ghcr.io/home-assistant/armhf-homeassistant:" + amd64: "ghcr.io/home-assistant/amd64-homeassistant:" + i386: "ghcr.io/home-assistant/i386-homeassistant:" +codenotary: + signer: notary@home-assistant.io + base_image: notary@home-assistant.io +labels: + io.hass.type: core + org.opencontainers.image.source: https://github.com/home-assistant/core From fa0d3a6c4802398bfabf293c035bd7e902128643 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Nov 2021 09:49:03 +0100 Subject: [PATCH 0816/1452] Change output template filters `timestamp_local` and `timestamp_utc` to isoformat (#60269) --- homeassistant/helpers/template.py | 6 ++---- tests/helpers/test_template.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4c6888cbf2f..ca2828c4402 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1462,9 +1462,7 @@ def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SE def timestamp_local(value, default=_SENTINEL): """Filter to convert given timestamp to local date/time.""" try: - return dt_util.as_local(dt_util.utc_from_timestamp(value)).strftime( - DATE_STR_FORMAT - ) + return dt_util.as_local(dt_util.utc_from_timestamp(value)).isoformat() except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: @@ -1476,7 +1474,7 @@ def timestamp_local(value, default=_SENTINEL): def timestamp_utc(value, default=_SENTINEL): """Filter to convert given timestamp to UTC date/time.""" try: - return dt_util.utc_from_timestamp(value).strftime(DATE_STR_FORMAT) + return dt_util.utc_from_timestamp(value).isoformat() except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index f1dc740a068..1a8bf8e526b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -692,7 +692,7 @@ def test_timestamp_custom(hass): def test_timestamp_local(hass): """Test the timestamps to local filter.""" - tests = {None: None, 1469119144: "2016-07-21 16:39:04"} + tests = {None: None, 1469119144: "2016-07-21T16:39:04+00:00"} for inp, out in tests.items(): assert ( @@ -859,8 +859,8 @@ def test_timestamp_utc(hass): now = dt_util.utcnow() tests = { None: None, - 1469119144: "2016-07-21 16:39:04", - dt_util.as_timestamp(now): now.strftime("%Y-%m-%d %H:%M:%S"), + 1469119144: "2016-07-21T16:39:04+00:00", + dt_util.as_timestamp(now): now.isoformat(), } for inp, out in tests.items(): From d41d22303326a13ec68ffc962621ba8b0c4f194d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Nov 2021 09:51:56 +0100 Subject: [PATCH 0817/1452] Add UNIX timestamp detection to `as_datetime` template filter (#60126) --- homeassistant/helpers/template.py | 14 ++++++++++++-- tests/helpers/test_template.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ca2828c4402..07219942f98 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1494,6 +1494,16 @@ def forgiving_as_timestamp(value, default=_SENTINEL): return default +def as_datetime(value): + """Filter and to convert a time string or UNIX timestamp to datetime object.""" + try: + # Check for a valid UNIX timestamp string, int or float + timestamp = float(value) + return dt_util.utc_from_timestamp(timestamp) + except ValueError: + return dt_util.parse_datetime(value) + + def strptime(string, fmt, default=_SENTINEL): """Parse a time string to datetime.""" try: @@ -1789,7 +1799,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["atan"] = arc_tangent self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root - self.filters["as_datetime"] = dt_util.parse_datetime + self.filters["as_datetime"] = as_datetime self.filters["as_timestamp"] = forgiving_as_timestamp self.filters["today_at"] = today_at self.filters["as_local"] = dt_util.as_local @@ -1830,7 +1840,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["atan"] = arc_tangent self.globals["atan2"] = arc_tangent2 self.globals["float"] = forgiving_float - self.globals["as_datetime"] = dt_util.parse_datetime + self.globals["as_datetime"] = as_datetime self.globals["as_local"] = dt_util.as_local self.globals["as_timestamp"] = forgiving_as_timestamp self.globals["today_at"] = today_at diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 1a8bf8e526b..d6649b058e2 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -732,6 +732,36 @@ def test_as_datetime(hass, input): ) +def test_as_datetime_from_timestamp(hass): + """Test converting a UNIX timestamp to a date object.""" + tests = [ + (1469119144, "2016-07-21 16:39:04+00:00"), + (1469119144.0, "2016-07-21 16:39:04+00:00"), + (-1, "1969-12-31 23:59:59+00:00"), + ] + for input, output in tests: + # expected = dt_util.parse_datetime(input) + if output is not None: + output = str(output) + + assert ( + template.Template(f"{{{{ as_datetime({input}) }}}}", hass).async_render() + == output + ) + assert ( + template.Template(f"{{{{ {input} | as_datetime }}}}", hass).async_render() + == output + ) + assert ( + template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render() + == output + ) + assert ( + template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render() + == output + ) + + def test_as_local(hass): """Test converting time to local.""" From 9027ee7828c122dd06f1f1ce5a64de5f06321740 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Nov 2021 10:14:48 +0100 Subject: [PATCH 0818/1452] Revert "Add type hints to SSDP (#59840)" (#60270) --- homeassistant/components/ssdp/__init__.py | 78 ++++------------------- 1 file changed, 13 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index a84b2e690da..c937f210368 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta from enum import Enum from ipaddress import IPv4Address, IPv6Address import logging -from typing import Any, Callable, Final, Mapping, TypedDict, cast +from typing import Any, Callable, Mapping from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource @@ -32,7 +32,7 @@ SCAN_INTERVAL = timedelta(seconds=60) IPV4_BROADCAST = IPv4Address("255.255.255.255") # Attributes for accessing info from SSDP response -ATTR_SSDP_LOCATION: Final = "ssdp_location" +ATTR_SSDP_LOCATION = "ssdp_location" ATTR_SSDP_ST = "ssdp_st" ATTR_SSDP_NT = "ssdp_nt" ATTR_SSDP_UDN = "ssdp_udn" @@ -56,7 +56,7 @@ ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" # Attributes for accessing info added by Home Assistant -ATTR_HA_MATCHING_DOMAINS: Final = "x_homeassistant_matching_domains" +ATTR_HA_MATCHING_DOMAINS = "x-homeassistant-matching-domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] @@ -85,58 +85,6 @@ SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { _LOGGER = logging.getLogger(__name__) -class _HaServiceInfoDescription(TypedDict, total=True): - """Keys added by HA.""" - - x_homeassistant_matching_domains: set[str] - - -class _SsdpDescriptionBase(TypedDict, total=True): - """Compulsory keys for SSDP info.""" - - ssdp_usn: str - ssdp_st: str - - -class SsdpDescription(_SsdpDescriptionBase, total=False): - """SSDP info with optional keys.""" - - ssdp_location: str - ssdp_nt: str - ssdp_udn: str - ssdp_ext: str - ssdp_server: str - - -class _UpnpDescriptionBase(TypedDict, total=True): - """Compulsory keys for UPnP info.""" - - deviceType: str - friendlyName: str - manufacturer: str - modelName: str - UDN: str - - -class UpnpDescription(_UpnpDescriptionBase, total=False): - """UPnP info with optional keys.""" - - manufacturerURL: str - modelDescription: str - modelNumber: str - modelURL: str - serialNumber: str - UPC: str - iconList: dict[str, list[dict[str, str]]] - serviceList: dict[str, list[dict[str, str]]] - deviceList: dict[str, Any] - presentationURL: str - - -class SsdpServiceInfo(SsdpDescription, UpnpDescription, _HaServiceInfoDescription): - """Prepared info from ssdp/upnp entries.""" - - @bind_hass async def async_register_callback( hass: HomeAssistant, @@ -154,7 +102,7 @@ async def async_register_callback( @bind_hass async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name hass: HomeAssistant, udn: str, st: str -) -> SsdpServiceInfo | None: +) -> dict[str, str] | None: """Fetch the discovery info cache.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn_st(udn, st) @@ -163,7 +111,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_st( # pylint: disable=invalid-name hass: HomeAssistant, st: str -) -> list[SsdpServiceInfo]: +) -> list[dict[str, str]]: """Fetch all the entries matching the st.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_st(st) @@ -172,7 +120,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_udn( hass: HomeAssistant, udn: str -) -> list[SsdpServiceInfo]: +) -> list[dict[str, str]]: """Fetch all the entries matching the udn.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn(udn) @@ -193,7 +141,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_process_callbacks( callbacks: list[SsdpCallback], - discovery_info: SsdpServiceInfo, + discovery_info: dict[str, str], ssdp_change: SsdpChange, ) -> None: for callback in callbacks: @@ -479,7 +427,7 @@ class Scanner: async def _async_headers_to_discovery_info( self, headers: Mapping[str, Any] - ) -> SsdpServiceInfo: + ) -> dict[str, Any]: """Combine the headers and description into discovery_info. Building this is a bit expensive so we only do it on demand. @@ -495,7 +443,7 @@ class Scanner: async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name self, udn: str, st: str - ) -> SsdpServiceInfo | None: + ) -> dict[str, Any] | None: """Return discovery_info for a udn and st.""" if headers := self._all_headers_from_ssdp_devices.get((udn, st)): return await self._async_headers_to_discovery_info(headers) @@ -503,7 +451,7 @@ class Scanner: async def async_get_discovery_info_by_st( # pylint: disable=invalid-name self, st: str - ) -> list[SsdpServiceInfo]: + ) -> list[dict[str, Any]]: """Return matching discovery_infos for a st.""" return [ await self._async_headers_to_discovery_info(headers) @@ -511,7 +459,7 @@ class Scanner: if udn_st[1] == st ] - async def async_get_discovery_info_by_udn(self, udn: str) -> list[SsdpServiceInfo]: + async def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, Any]]: """Return matching discovery_infos for a udn.""" return [ await self._async_headers_to_discovery_info(headers) @@ -522,7 +470,7 @@ class Scanner: def discovery_info_from_headers_and_description( info_with_desc: CaseInsensitiveDict, -) -> SsdpServiceInfo: +) -> dict[str, Any]: """Convert headers and description to discovery_info.""" info = { DISCOVERY_MAPPING.get(k.lower(), k): v @@ -537,7 +485,7 @@ def discovery_info_from_headers_and_description( if ATTR_SSDP_ST not in info and ATTR_SSDP_NT in info: info[ATTR_SSDP_ST] = info[ATTR_SSDP_NT] - return cast(SsdpServiceInfo, info) + return info def _udn_from_usn(usn: str | None) -> str | None: From dee4ce921df6fc92ef3c3569349ee21cb1ff2329 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 10:46:31 +0100 Subject: [PATCH 0819/1452] Correct entity category on UPS type in NUT (#60277) --- homeassistant/components/nut/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 1022dfe526d..12306b63dc1 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -17,7 +17,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC, - ENTITY_CATEGORY_SYSTEM, FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, @@ -230,7 +229,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.type", name="UPS Type", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_SYSTEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.watchdog.status": SensorEntityDescription( From 07c90575ac44d1b689216c75141ec0b1f813b6bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 10:46:53 +0100 Subject: [PATCH 0820/1452] Add comments to entity category constants (#60276) --- homeassistant/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd8bbc5e555..45a88e2fe43 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -701,8 +701,11 @@ PRECISION_TENTHS: Final = 0.1 # cloud, alexa, or google_home components CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] +# Config: An entity which allows changing the configuration of a device ENTITY_CATEGORY_CONFIG: Final = "config" +# Diagnostic: An entity exposing some configuration parameter or diagnostics of a device ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" +# System: An entity which is not useful for the user to interact with ENTITY_CATEGORY_SYSTEM: Final = "system" # Entity categories which will: From 1a5f2c9c32d7b4705b8c4dd3ea43e9902660214b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Nov 2021 11:17:38 +0100 Subject: [PATCH 0821/1452] Disable less popular Fronius entities by default (#60264) --- homeassistant/components/fronius/sensor.py | 41 ++++++++++++++++++++++ tests/components/fronius/__init__.py | 18 +++++++++- tests/components/fronius/test_sensor.py | 28 +++++++++++---- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 7c2a1bf6c09..fa2a33ca7ea 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, @@ -145,7 +146,9 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="frequency_ac", name="Frequency AC", native_unit_of_measurement=FREQUENCY_HERTZ, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac", @@ -183,6 +186,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_dc", @@ -220,11 +224,13 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="led_state", name="LED state", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="led_color", name="LED color", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, ), ] @@ -256,6 +262,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac_phase_2", @@ -263,6 +270,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac_phase_3", @@ -270,6 +278,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_reactive_ac_consumed", @@ -277,6 +286,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, icon="mdi:lightning-bolt-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_reactive_ac_produced", @@ -284,6 +294,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, icon="mdi:lightning-bolt-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_real_ac_minus", @@ -291,6 +302,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_real_ac_plus", @@ -298,6 +310,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_real_consumed", @@ -317,6 +330,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="frequency_phase_average", name="Frequency phase average", native_unit_of_measurement=FREQUENCY_HERTZ, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( @@ -330,6 +344,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_apparent_phase_2", @@ -337,6 +352,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_apparent_phase_3", @@ -344,6 +360,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_apparent", @@ -351,24 +368,28 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_1", name="Power factor phase 1", device_class=DEVICE_CLASS_POWER_FACTOR, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_2", name="Power factor phase 2", device_class=DEVICE_CLASS_POWER_FACTOR, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_3", name="Power factor phase 3", device_class=DEVICE_CLASS_POWER_FACTOR, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor", @@ -382,6 +403,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_reactive_phase_2", @@ -389,6 +411,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_reactive_phase_3", @@ -396,6 +419,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_reactive", @@ -403,6 +427,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:flash-outline", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real_phase_1", @@ -410,6 +435,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real_phase_2", @@ -417,6 +443,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real_phase_3", @@ -424,6 +451,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real", @@ -438,6 +466,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_2", @@ -445,6 +474,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_3", @@ -452,6 +482,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_12", @@ -459,6 +490,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_23", @@ -466,6 +498,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_31", @@ -473,6 +506,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, ), ] @@ -483,6 +517,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_year", @@ -490,6 +525,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_total", @@ -497,6 +533,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="meter_mode", @@ -552,11 +589,13 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="capacity_maximum", name="Capacity maximum", native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="capacity_designed", name="Capacity designed", native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="current_dc", @@ -581,6 +620,7 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:current-dc", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_dc_minimum_cell", @@ -589,6 +629,7 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, icon="mdi:current-dc", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="state_of_charge", diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index e2ef369987c..37c9481e32e 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,8 +1,10 @@ """Tests for the Fronius integration.""" from homeassistant.components.fronius.const import DOMAIN from homeassistant.const import CONF_HOST +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker MOCK_HOST = "http://fronius" @@ -22,6 +24,7 @@ async def setup_fronius_integration(hass): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + return entry def mock_responses( @@ -70,3 +73,16 @@ def mock_responses( f"{host}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", text=load_fixture("symo/GetStorageRealtimeData_System.json", "fronius"), ) + + +async def enable_all_entities(hass, config_entry_id, time_till_next_update): + """Enable all entities for a config entry and fast forward time to receive data.""" + registry = er.async_get(hass) + entities = er.async_entries_for_config_entry(registry, config_entry_id) + for entry in [ + entry for entry in entities if entry.disabled_by == er.DISABLED_INTEGRATION + ]: + registry.async_update_entity(entry.entity_id, **{"disabled_by": None}) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + time_till_next_update) + await hass.async_block_till_done() diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index fd64c127496..b4e56a06d62 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,13 +1,14 @@ """Tests for the Fronius sensor platform.""" from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, + FroniusMeterUpdateCoordinator, FroniusPowerFlowUpdateCoordinator, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.util import dt -from . import mock_responses, setup_fronius_integration +from . import enable_all_entities, mock_responses, setup_fronius_integration from tests.common import async_fire_time_changed @@ -21,8 +22,12 @@ async def test_symo_inverter(hass, aioclient_mock): # Init at night mock_responses(aioclient_mock, night=True) - await setup_fronius_integration(hass) + config_entry = await setup_fronius_integration(hass) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 + await enable_all_entities( + hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) @@ -36,7 +41,10 @@ async def test_symo_inverter(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 57 + await enable_all_entities( + hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # 4 additional AC entities assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) @@ -60,8 +68,8 @@ async def test_symo_logger(hass, aioclient_mock): mock_responses(aioclient_mock) await setup_fronius_integration(hass) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # ignored constant entities: # hardware_platform, hardware_version, product_type # software_version, time_zone, time_zone_location @@ -91,8 +99,12 @@ async def test_symo_meter(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock) - await setup_fronius_integration(hass) + config_entry = await setup_fronius_integration(hass) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 # ignored entities: # manufacturer, model, serial, enable, timestamp, visible, meter_location @@ -152,8 +164,12 @@ async def test_symo_power_flow(hass, aioclient_mock): # First test at night mock_responses(aioclient_mock, night=True) - await setup_fronius_integration(hass) + config_entry = await setup_fronius_integration(hass) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 + await enable_all_entities( + hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 # ignored: location, mode, timestamp # From 8e6a3b2799100b1734b311239302982f2e554a9c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Nov 2021 11:25:25 +0100 Subject: [PATCH 0822/1452] Fix init slow tests for SamsungTV (#60245) --- tests/components/samsungtv/test_init.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 500c39d677a..bd3e2a51256 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,5 +1,5 @@ """Tests for the Samsung TV Integration.""" -from unittest.mock import Mock, call, patch +from unittest.mock import Mock, patch from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( @@ -53,15 +53,15 @@ REMOTE_CALL = { } -async def test_setup(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): +async def test_setup(hass: HomeAssistant, remotews: Mock, no_mac_address: Mock): """Test Samsung TV integration is setup.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote, patch( + with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", return_value="fake_host", ): - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) # test name and turn_on @@ -76,7 +76,6 @@ async def test_setup(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) - assert remote.call_args == call(REMOTE_CALL) async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): @@ -86,6 +85,9 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=OSError, + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=None, ), patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", return_value="fake_host", @@ -130,7 +132,7 @@ async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog) async def test_setup_duplicate_entries( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock, caplog + hass: HomeAssistant, remote: Mock, remotews: Mock, no_mac_address: Mock, caplog ): """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) From 74cfbf5f429af025ba673c1d13c85bae4b506b59 Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Wed, 24 Nov 2021 02:31:59 -0800 Subject: [PATCH 0823/1452] Use configured speed ranges for HomeSeer FC200+ fan controllers in zwave_js (#59697) * Use configured speed ranges for HomeSeer FC200+ fan controllers in zwave_js * Fix pylint errors * Remove unused param in tests * Fix test values * Address various review notes * Remove now-redundant assertion * Add an additional test case for set_percentage=0 * Use round() instead of int() for percentage computations; this makes the percentage setting match the setpoints in the UI * Add additional tests * Fix pct conversions * Make conversion tests exhaustive * Add tests for discovery data templates * Revert "Add tests for discovery data templates" This reverts commit 85dcbc0903a1dd95f8e4e5f3c5d29cd7547b667b. * Improve typing on ConfigurableFanSpeedDataTemplate#resolve_data * Move config error handling to the discovery data template * Fix checks for config data * Revise fallback logic in percentage_to_zwave_speed and ensure that the speed list is non-empty * Rework error handling * Fix runtime fan speed updates * Use warning instead of warn * Move data validation to get_speed_config; turns out that resolve_data is only called once, at startup. * Temporarily remove the not-yet-used fixed fan speed template. Add an additional assertion to ensure speeds are sorted. * Add a comment about the assertions in discovery_data_template.py * Update homeassistant/components/zwave_js/discovery_data_template.py Co-authored-by: Martin Hjelmare * Fix typo in comment Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 16 + .../zwave_js/discovery_data_template.py | 104 +- homeassistant/components/zwave_js/fan.py | 142 +- tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/fan_hs_fc200_state.json | 10506 ++++++++++++++++ tests/components/zwave_js/test_fan.py | 89 +- 6 files changed, 10843 insertions(+), 28 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/fan_hs_fc200_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 80cd6f023fb..5620afb9714 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -44,6 +44,7 @@ from homeassistant.helpers.device_registry import DeviceEntry from .const import LOGGER from .discovery_data_template import ( BaseDiscoverySchemaDataTemplate, + ConfigurableFanSpeedDataTemplate, CoverTiltDataTemplate, DynamicCurrentTempClimateDataTemplate, NumericSensorDataTemplate, @@ -259,6 +260,21 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), ), + # HomeSeer HS-FC200+ + ZWaveDiscoverySchema( + platform="fan", + hint="configured_fan_speed", + manufacturer_id={0x000C}, + product_id={0x0001}, + product_type={0x0203}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + data_template=ConfigurableFanSpeedDataTemplate( + configuration_option=ZwaveValueID( + 5, CommandClass.CONFIGURATION, endpoint=0 + ), + configuration_value_to_speeds={0: [33, 66, 99], 1: [24, 49, 74, 99]}, + ), + ), # Fibaro Shutter Fibaro FGR222 ZWaveDiscoverySchema( platform="cover", diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 2d8e895d9df..77dd6de6ce3 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass, field +import logging from typing import Any from zwave_js_server.const import CommandClass @@ -76,7 +77,11 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( MultilevelSensorType, ) from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import Value as ZwaveValue, get_value_id +from zwave_js_server.model.value import ( + ConfigurationValue as ZwaveConfigurationValue, + Value as ZwaveValue, + get_value_id, +) from zwave_js_server.util.command_class.meter import get_meter_scale_type from zwave_js_server.util.command_class.multilevel_sensor import ( get_multilevel_sensor_scale_type, @@ -218,6 +223,8 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { IRRADIATION_WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, } +_LOGGER = logging.getLogger(__name__) + @dataclass class ZwaveValueID: @@ -422,3 +429,98 @@ class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): def current_tilt_value(resolved_data: dict[str, Any]) -> ZwaveValue | None: """Get current tilt ZwaveValue from resolved data.""" return resolved_data["tilt_value"] + + +@dataclass +class FanSpeedDataTemplate: + """Mixin to define get_speed_config.""" + + def get_speed_config(self, resolved_data: dict[str, Any]) -> list[int] | None: + """ + Get the fan speed configuration for this device. + + Values should indicate the highest allowed device setting for each + actual speed, and should be sorted in ascending order. + + Empty lists are not permissible. + """ + # pylint: disable=no-self-use + raise NotImplementedError + + +@dataclass +class ConfigurableFanSpeedValueMix: + """Mixin data class for defining configurable fan speeds.""" + + configuration_option: ZwaveValueID + configuration_value_to_speeds: dict[int, list[int]] + + def __post_init__(self) -> None: + """ + Validate inputs. + + These inputs are hardcoded in `discovery.py`, so these checks should + only fail due to developer error. + """ + for speeds in self.configuration_value_to_speeds.values(): + assert len(speeds) > 0 + assert sorted(speeds) == speeds + + +@dataclass +class ConfigurableFanSpeedDataTemplate( + BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, ConfigurableFanSpeedValueMix +): + """ + Gets fan speeds based on a configuration value. + + Example: + ZWaveDiscoverySchema( + platform="fan", + hint="configured_fan_speed", + ... + data_template=ConfigurableFanSpeedDataTemplate( + configuration_option=ZwaveValueID( + 5, CommandClass.CONFIGURATION, endpoint=0 + ), + configuration_value_to_speeds={0: [32, 65, 99], 1: [24, 49, 74, 99]}, + ), + ), + + `configuration_option` is a reference to the setting that determines how + many speeds are supported. + + `configuration_value_to_speeds` maps the values from `configuration_option` + to a list of speeds. The specified speeds indicate the maximum setting on + the underlying switch for each actual speed. + """ + + def resolve_data(self, value: ZwaveValue) -> dict[str, ZwaveConfigurationValue]: + """Resolve helper class data for a discovered value.""" + zwave_value: ZwaveValue = self._get_value_from_id( + value.node, self.configuration_option + ) + return {"configuration_value": zwave_value} + + def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + """Return list of all ZwaveValues that should be watched.""" + return [ + resolved_data["configuration_value"], + ] + + def get_speed_config( + self, resolved_data: dict[str, ZwaveConfigurationValue] + ) -> list[int] | None: + """Get current speed configuration from resolved data.""" + zwave_value: ZwaveValue = resolved_data["configuration_value"] + + if zwave_value.value is None: + _LOGGER.warning("Unable to read fan speed configuration value") + return None + + speed_config = self.configuration_value_to_speeds.get(zwave_value.value) + if speed_config is None: + _LOGGER.warning("Unrecognized speed configuration value") + return None + + return speed_config diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 4b4f23a85d2..df9b1a46683 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -2,7 +2,7 @@ from __future__ import annotations import math -from typing import Any +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY @@ -24,11 +24,12 @@ from homeassistant.util.percentage import ( from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo +from .discovery_data_template import FanSpeedDataTemplate from .entity import ZWaveBaseEntity SUPPORTED_FEATURES = SUPPORT_SET_SPEED -SPEED_RANGE = (1, 99) # off is not included +DEFAULT_SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry( @@ -43,7 +44,11 @@ async def async_setup_entry( def async_add_fan(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave fan.""" entities: list[ZWaveBaseEntity] = [] - entities.append(ZwaveFan(config_entry, client, info)) + if info.platform_hint == "configured_fan_speed": + entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info)) + else: + entities.append(ZwaveFan(config_entry, client, info)) + async_add_entities(entities) config_entry.async_on_unload( @@ -58,19 +63,23 @@ async def async_setup_entry( class ZwaveFan(ZWaveBaseEntity, FanEntity): """Representation of a Z-Wave fan.""" - async def async_set_percentage(self, percentage: int | None) -> None: - """Set the speed percentage of the fan.""" - target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the fan.""" + super().__init__(config_entry, client, info) + self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) - if percentage is None: - # Value 255 tells device to return to previous value - zwave_speed = 255 - elif percentage == 0: + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: zwave_speed = 0 else: - zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + zwave_speed = math.ceil( + percentage_to_ranged_value(DEFAULT_SPEED_RANGE, percentage) + ) - await self.info.node.async_set_value(target_value, zwave_speed) + await self.info.node.async_set_value(self._target_value, zwave_speed) async def async_turn_on( self, @@ -80,12 +89,15 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the device on.""" - await self.async_set_percentage(percentage) + if percentage is None: + # Value 255 tells device to return to previous value + await self.info.node.async_set_value(self._target_value, 255) + else: + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) - await self.info.node.async_set_value(target_value, 0) + await self.info.node.async_set_value(self._target_value, 0) @property def is_on(self) -> bool | None: # type: ignore @@ -101,7 +113,9 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): if self.info.primary_value.value is None: # guard missing value return None - return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) + return ranged_value_to_percentage( + DEFAULT_SPEED_RANGE, self.info.primary_value.value + ) @property def percentage_step(self) -> float: @@ -111,9 +125,103 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) + return int_states_in_range(DEFAULT_SPEED_RANGE) @property def supported_features(self) -> int: """Flag supported features.""" return SUPPORTED_FEATURES + + +class ConfiguredSpeedRangeZwaveFan(ZwaveFan): + """A Zwave fan with a configured speed range (e.g., 1-24 is low).""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the fan.""" + super().__init__(config_entry, client, info) + self.data_template = cast( + FanSpeedDataTemplate, self.info.platform_data_template + ) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + zwave_speed = self.percentage_to_zwave_speed(percentage) + await self.info.node.async_set_value(self._target_value, zwave_speed) + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return super().available and self.has_speed_configuration + + @property + def percentage(self) -> int | None: + """Return the current speed percentage.""" + if self.info.primary_value.value is None: + # guard missing value + return None + + return self.zwave_speed_to_percentage(self.info.primary_value.value) + + @property + def percentage_step(self) -> float: + """Return the step size for percentage.""" + # This is the same implementation as the base fan type, but + # it needs to be overridden here because the ZwaveFan does + # something different for fans with unknown speeds. + return 100 / self.speed_count + + @property + def has_speed_configuration(self) -> bool: + """Check if the speed configuration is valid.""" + return self.data_template.get_speed_config(self.info.platform_data) is not None + + @property + def speed_configuration(self) -> list[int]: + """Return the speed configuration for this fan.""" + speed_configuration = self.data_template.get_speed_config( + self.info.platform_data + ) + + # Entity should be unavailable if this isn't set + assert speed_configuration is not None + + return speed_configuration + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return len(self.speed_configuration) + + def percentage_to_zwave_speed(self, percentage: int) -> int: + """Map a percentage to a ZWave speed.""" + if percentage == 0: + return 0 + + # Since the percentage steps are computed with rounding, we have to + # search to find the appropriate speed. + for speed_limit in self.speed_configuration: + step_percentage = self.zwave_speed_to_percentage(speed_limit) + if percentage <= step_percentage: + return speed_limit + + # This shouldn't actually happen; the last entry in + # `self.speed_configuration` should map to 100%. + return self.speed_configuration[-1] + + def zwave_speed_to_percentage(self, zwave_speed: int) -> int: + """Convert a Zwave speed to a percentage.""" + if zwave_speed == 0: + return 0 + + percentage = 0.0 + for speed_limit in self.speed_configuration: + percentage += self.percentage_step + if zwave_speed <= speed_limit: + break + + # This choice of rounding function is to provide consistency with how + # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, + # 67, and 100. + return round(percentage) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index be9dec3b6bc..6d75f899e4c 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -332,6 +332,12 @@ def in_wall_smart_fan_control_state_fixture(): return json.loads(load_fixture("zwave_js/in_wall_smart_fan_control_state.json")) +@pytest.fixture(name="hs_fc200_state", scope="session") +def hs_fc200_state_fixture(): + """Load the HS FC200+ node state fixture data.""" + return json.loads(load_fixture("zwave_js/fan_hs_fc200_state.json")) + + @pytest.fixture(name="gdc_zw062_state", scope="session") def motorized_barrier_cover_state_fixture(): """Load the motorized barrier cover node state fixture data.""" @@ -697,6 +703,14 @@ def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): return node +@pytest.fixture(name="hs_fc200") +def hs_fc200_fixture(client, hs_fc200_state): + """Mock a fan node.""" + node = Node(client, copy.deepcopy(hs_fc200_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="null_name_check") def null_name_check_fixture(client, null_name_check_state): """Mock a node with no name.""" diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json new file mode 100644 index 00000000000..f83a1193c22 --- /dev/null +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -0,0 +1,10506 @@ +{ + "nodeId": 39, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": "unknown", + "manufacturerId": 12, + "productId": 1, + "productType": 515, + "firmwareVersion": "50.5", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/cache/db/devices/0x000c/hs-fc200.json", + "isEmbedded": true, + "manufacturer": "HomeSeer Technologies", + "manufacturerId": 12, + "label": "HS-FC200+", + "description": "Scene Capable Fan Control Switch", + "devices": [ + { + "productType": 515, + "productId": 1 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Inclusion: Add the device into your network by a Z-Wave certified controller. HS-FC200+ supports the latest S2 security offered by Z-Wave certified controllers. If your controller supports S2, please refer to the user guide of the controller for detailed instructions on adding devices to the network. You should be able to add HS-FC200+ into your network using the unique QR code or the DSK 5 digit pin located on the product or packaging. In addition, the device can be added or removed using the following 2-step procedure:\n\n1. Put your Z-Wave controller into Inclusion mode. Consult your controller manual if you're unsure how to do this.\n2. Tap the paddle of your new HomeSeer switch to begin the inclusion process. This will take a few moments to complete", + "exclusion": "Exclusion: Remove the device from your network by a Z-Wave certified controller. HS-FC200+ supports the latest S2 security offered by Z-Wave certified controllers. If your controller supports S2, please refer to the user guide of the controller for detailed instructions on removing devices from the network. You should be able to remove HS-FC200+ into your network using the unique QR code or the DSK 5 digit pin located on the product or packaging. In addition, the device can be added or removed using the following 2-step procedure:\n\n1. Put your Z-Wave controller into Exclusion mode. Consult your controller manual if you're unsure how to do this.\n2. Tap the paddle of your new HomeSeer switch to begin the Exclusion process. This will take a few moments to complete", + "reset": "To be used only in the event that the network primary controller is lost or otherwise inoperable. \n\n(1) Turn switch on by tapping the top of the paddle once. \n(2) Quickly ap top of the paddle 3 times. \n(3) Quickly tap bottom of paddle 3 times. \n(4) If the LED turns off then on again, switch is reset. If not, repeat manual rest.", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2957/HS%20FC200%20Manual%20Market%20Cert%20v1.pdf" + } + }, + "label": "HS-FC200+", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 39, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 8, + "label": "Fan Switch" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 133, + 89, + 114, + 115, + 134, + 94 + ], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "On", + "propertyName": "On", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (On)", + "ccSpecific": { + "switchType": 1 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Off", + "propertyName": "Off", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Off)", + "ccSpecific": { + "switchType": 1 + } + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 1, + "propertyName": "level", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (1)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 1, + "propertyName": "dimmingDuration", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (1)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 2, + "propertyName": "level", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (2)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 2, + "propertyName": "dimmingDuration", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (2)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 3, + "propertyName": "level", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (3)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 3, + "propertyName": "dimmingDuration", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (3)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 4, + "propertyName": "level", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (4)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 4, + "propertyName": "dimmingDuration", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (4)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 5, + "propertyName": "level", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (5)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 5, + "propertyName": "dimmingDuration", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (5)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 6, + "propertyName": "level", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (6)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 6, + "propertyName": "dimmingDuration", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (6)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 7, + "propertyName": "level", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (7)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 7, + "propertyName": "dimmingDuration", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (7)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 8, + "propertyName": "level", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (8)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 8, + "propertyName": "dimmingDuration", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (8)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 9, + "propertyName": "level", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (9)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 9, + "propertyName": "dimmingDuration", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (9)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 10, + "propertyName": "level", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (10)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 10, + "propertyName": "dimmingDuration", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (10)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 11, + "propertyName": "level", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (11)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 11, + "propertyName": "dimmingDuration", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (11)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 12, + "propertyName": "level", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (12)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 12, + "propertyName": "dimmingDuration", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (12)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 13, + "propertyName": "level", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (13)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 13, + "propertyName": "dimmingDuration", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (13)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 14, + "propertyName": "level", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (14)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 14, + "propertyName": "dimmingDuration", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (14)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 15, + "propertyName": "level", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (15)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 15, + "propertyName": "dimmingDuration", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (15)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 16, + "propertyName": "level", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (16)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 16, + "propertyName": "dimmingDuration", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (16)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 17, + "propertyName": "level", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (17)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 17, + "propertyName": "dimmingDuration", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (17)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 18, + "propertyName": "level", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (18)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 18, + "propertyName": "dimmingDuration", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (18)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 19, + "propertyName": "level", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (19)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 19, + "propertyName": "dimmingDuration", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (19)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 20, + "propertyName": "level", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (20)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 20, + "propertyName": "dimmingDuration", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (20)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 21, + "propertyName": "level", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (21)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 21, + "propertyName": "dimmingDuration", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (21)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 22, + "propertyName": "level", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (22)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 22, + "propertyName": "dimmingDuration", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (22)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 23, + "propertyName": "level", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (23)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 23, + "propertyName": "dimmingDuration", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (23)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 24, + "propertyName": "level", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (24)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 24, + "propertyName": "dimmingDuration", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (24)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 25, + "propertyName": "level", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (25)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 25, + "propertyName": "dimmingDuration", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (25)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 26, + "propertyName": "level", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (26)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 26, + "propertyName": "dimmingDuration", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (26)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 27, + "propertyName": "level", + "propertyKeyName": "27", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (27)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 27, + "propertyName": "dimmingDuration", + "propertyKeyName": "27", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (27)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 28, + "propertyName": "level", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (28)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 28, + "propertyName": "dimmingDuration", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (28)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 29, + "propertyName": "level", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (29)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 29, + "propertyName": "dimmingDuration", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (29)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 30, + "propertyName": "level", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (30)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 30, + "propertyName": "dimmingDuration", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (30)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 31, + "propertyName": "level", + "propertyKeyName": "31", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (31)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 31, + "propertyName": "dimmingDuration", + "propertyKeyName": "31", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (31)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 32, + "propertyName": "level", + "propertyKeyName": "32", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (32)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 32, + "propertyName": "dimmingDuration", + "propertyKeyName": "32", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (32)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 33, + "propertyName": "level", + "propertyKeyName": "33", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (33)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 33, + "propertyName": "dimmingDuration", + "propertyKeyName": "33", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (33)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 34, + "propertyName": "level", + "propertyKeyName": "34", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (34)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 34, + "propertyName": "dimmingDuration", + "propertyKeyName": "34", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (34)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 35, + "propertyName": "level", + "propertyKeyName": "35", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (35)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 35, + "propertyName": "dimmingDuration", + "propertyKeyName": "35", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (35)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 36, + "propertyName": "level", + "propertyKeyName": "36", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (36)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 36, + "propertyName": "dimmingDuration", + "propertyKeyName": "36", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (36)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 37, + "propertyName": "level", + "propertyKeyName": "37", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (37)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 37, + "propertyName": "dimmingDuration", + "propertyKeyName": "37", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (37)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 38, + "propertyName": "level", + "propertyKeyName": "38", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (38)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 38, + "propertyName": "dimmingDuration", + "propertyKeyName": "38", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (38)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 39, + "propertyName": "level", + "propertyKeyName": "39", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (39)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 39, + "propertyName": "dimmingDuration", + "propertyKeyName": "39", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (39)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 40, + "propertyName": "level", + "propertyKeyName": "40", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (40)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 40, + "propertyName": "dimmingDuration", + "propertyKeyName": "40", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (40)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 41, + "propertyName": "level", + "propertyKeyName": "41", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (41)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 41, + "propertyName": "dimmingDuration", + "propertyKeyName": "41", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (41)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 42, + "propertyName": "level", + "propertyKeyName": "42", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (42)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 42, + "propertyName": "dimmingDuration", + "propertyKeyName": "42", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (42)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 43, + "propertyName": "level", + "propertyKeyName": "43", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (43)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 43, + "propertyName": "dimmingDuration", + "propertyKeyName": "43", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (43)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 44, + "propertyName": "level", + "propertyKeyName": "44", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (44)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 44, + "propertyName": "dimmingDuration", + "propertyKeyName": "44", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (44)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 45, + "propertyName": "level", + "propertyKeyName": "45", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (45)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 45, + "propertyName": "dimmingDuration", + "propertyKeyName": "45", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (45)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 46, + "propertyName": "level", + "propertyKeyName": "46", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (46)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 46, + "propertyName": "dimmingDuration", + "propertyKeyName": "46", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (46)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 47, + "propertyName": "level", + "propertyKeyName": "47", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (47)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 47, + "propertyName": "dimmingDuration", + "propertyKeyName": "47", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (47)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 48, + "propertyName": "level", + "propertyKeyName": "48", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (48)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 48, + "propertyName": "dimmingDuration", + "propertyKeyName": "48", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (48)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 49, + "propertyName": "level", + "propertyKeyName": "49", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (49)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 49, + "propertyName": "dimmingDuration", + "propertyKeyName": "49", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (49)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 50, + "propertyName": "level", + "propertyKeyName": "50", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (50)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 50, + "propertyName": "dimmingDuration", + "propertyKeyName": "50", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (50)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 51, + "propertyName": "level", + "propertyKeyName": "51", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (51)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 51, + "propertyName": "dimmingDuration", + "propertyKeyName": "51", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (51)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 52, + "propertyName": "level", + "propertyKeyName": "52", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (52)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 52, + "propertyName": "dimmingDuration", + "propertyKeyName": "52", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (52)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 53, + "propertyName": "level", + "propertyKeyName": "53", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (53)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 53, + "propertyName": "dimmingDuration", + "propertyKeyName": "53", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (53)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 54, + "propertyName": "level", + "propertyKeyName": "54", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (54)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 54, + "propertyName": "dimmingDuration", + "propertyKeyName": "54", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (54)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 55, + "propertyName": "level", + "propertyKeyName": "55", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (55)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 55, + "propertyName": "dimmingDuration", + "propertyKeyName": "55", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (55)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 56, + "propertyName": "level", + "propertyKeyName": "56", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (56)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 56, + "propertyName": "dimmingDuration", + "propertyKeyName": "56", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (56)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 57, + "propertyName": "level", + "propertyKeyName": "57", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (57)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 57, + "propertyName": "dimmingDuration", + "propertyKeyName": "57", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (57)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 58, + "propertyName": "level", + "propertyKeyName": "58", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (58)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 58, + "propertyName": "dimmingDuration", + "propertyKeyName": "58", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (58)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 59, + "propertyName": "level", + "propertyKeyName": "59", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (59)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 59, + "propertyName": "dimmingDuration", + "propertyKeyName": "59", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (59)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 60, + "propertyName": "level", + "propertyKeyName": "60", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (60)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 60, + "propertyName": "dimmingDuration", + "propertyKeyName": "60", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (60)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 61, + "propertyName": "level", + "propertyKeyName": "61", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (61)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 61, + "propertyName": "dimmingDuration", + "propertyKeyName": "61", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (61)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 62, + "propertyName": "level", + "propertyKeyName": "62", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (62)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 62, + "propertyName": "dimmingDuration", + "propertyKeyName": "62", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (62)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 63, + "propertyName": "level", + "propertyKeyName": "63", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (63)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 63, + "propertyName": "dimmingDuration", + "propertyKeyName": "63", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (63)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 64, + "propertyName": "level", + "propertyKeyName": "64", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (64)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 64, + "propertyName": "dimmingDuration", + "propertyKeyName": "64", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (64)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 65, + "propertyName": "level", + "propertyKeyName": "65", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (65)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 65, + "propertyName": "dimmingDuration", + "propertyKeyName": "65", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (65)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 66, + "propertyName": "level", + "propertyKeyName": "66", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (66)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 66, + "propertyName": "dimmingDuration", + "propertyKeyName": "66", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (66)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 67, + "propertyName": "level", + "propertyKeyName": "67", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (67)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 67, + "propertyName": "dimmingDuration", + "propertyKeyName": "67", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (67)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 68, + "propertyName": "level", + "propertyKeyName": "68", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (68)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 68, + "propertyName": "dimmingDuration", + "propertyKeyName": "68", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (68)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 69, + "propertyName": "level", + "propertyKeyName": "69", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (69)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 69, + "propertyName": "dimmingDuration", + "propertyKeyName": "69", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (69)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 70, + "propertyName": "level", + "propertyKeyName": "70", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (70)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 70, + "propertyName": "dimmingDuration", + "propertyKeyName": "70", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (70)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 71, + "propertyName": "level", + "propertyKeyName": "71", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (71)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 71, + "propertyName": "dimmingDuration", + "propertyKeyName": "71", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (71)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 72, + "propertyName": "level", + "propertyKeyName": "72", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (72)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 72, + "propertyName": "dimmingDuration", + "propertyKeyName": "72", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (72)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 73, + "propertyName": "level", + "propertyKeyName": "73", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (73)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 73, + "propertyName": "dimmingDuration", + "propertyKeyName": "73", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (73)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 74, + "propertyName": "level", + "propertyKeyName": "74", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (74)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 74, + "propertyName": "dimmingDuration", + "propertyKeyName": "74", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (74)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 75, + "propertyName": "level", + "propertyKeyName": "75", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (75)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 75, + "propertyName": "dimmingDuration", + "propertyKeyName": "75", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (75)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 76, + "propertyName": "level", + "propertyKeyName": "76", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (76)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 76, + "propertyName": "dimmingDuration", + "propertyKeyName": "76", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (76)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 77, + "propertyName": "level", + "propertyKeyName": "77", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (77)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 77, + "propertyName": "dimmingDuration", + "propertyKeyName": "77", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (77)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 78, + "propertyName": "level", + "propertyKeyName": "78", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (78)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 78, + "propertyName": "dimmingDuration", + "propertyKeyName": "78", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (78)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 79, + "propertyName": "level", + "propertyKeyName": "79", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (79)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 79, + "propertyName": "dimmingDuration", + "propertyKeyName": "79", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (79)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 80, + "propertyName": "level", + "propertyKeyName": "80", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (80)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 80, + "propertyName": "dimmingDuration", + "propertyKeyName": "80", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (80)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 81, + "propertyName": "level", + "propertyKeyName": "81", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (81)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 81, + "propertyName": "dimmingDuration", + "propertyKeyName": "81", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (81)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 82, + "propertyName": "level", + "propertyKeyName": "82", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (82)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 82, + "propertyName": "dimmingDuration", + "propertyKeyName": "82", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (82)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 83, + "propertyName": "level", + "propertyKeyName": "83", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (83)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 83, + "propertyName": "dimmingDuration", + "propertyKeyName": "83", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (83)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 84, + "propertyName": "level", + "propertyKeyName": "84", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (84)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 84, + "propertyName": "dimmingDuration", + "propertyKeyName": "84", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (84)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 85, + "propertyName": "level", + "propertyKeyName": "85", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (85)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 85, + "propertyName": "dimmingDuration", + "propertyKeyName": "85", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (85)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 86, + "propertyName": "level", + "propertyKeyName": "86", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (86)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 86, + "propertyName": "dimmingDuration", + "propertyKeyName": "86", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (86)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 87, + "propertyName": "level", + "propertyKeyName": "87", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (87)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 87, + "propertyName": "dimmingDuration", + "propertyKeyName": "87", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (87)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 88, + "propertyName": "level", + "propertyKeyName": "88", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (88)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 88, + "propertyName": "dimmingDuration", + "propertyKeyName": "88", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (88)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 89, + "propertyName": "level", + "propertyKeyName": "89", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (89)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 89, + "propertyName": "dimmingDuration", + "propertyKeyName": "89", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (89)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 90, + "propertyName": "level", + "propertyKeyName": "90", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (90)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 90, + "propertyName": "dimmingDuration", + "propertyKeyName": "90", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (90)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 91, + "propertyName": "level", + "propertyKeyName": "91", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (91)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 91, + "propertyName": "dimmingDuration", + "propertyKeyName": "91", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (91)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 92, + "propertyName": "level", + "propertyKeyName": "92", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (92)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 92, + "propertyName": "dimmingDuration", + "propertyKeyName": "92", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (92)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 93, + "propertyName": "level", + "propertyKeyName": "93", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (93)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 93, + "propertyName": "dimmingDuration", + "propertyKeyName": "93", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (93)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 94, + "propertyName": "level", + "propertyKeyName": "94", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (94)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 94, + "propertyName": "dimmingDuration", + "propertyKeyName": "94", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (94)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 95, + "propertyName": "level", + "propertyKeyName": "95", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (95)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 95, + "propertyName": "dimmingDuration", + "propertyKeyName": "95", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (95)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 96, + "propertyName": "level", + "propertyKeyName": "96", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (96)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 96, + "propertyName": "dimmingDuration", + "propertyKeyName": "96", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (96)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 97, + "propertyName": "level", + "propertyKeyName": "97", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (97)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 97, + "propertyName": "dimmingDuration", + "propertyKeyName": "97", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (97)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 98, + "propertyName": "level", + "propertyKeyName": "98", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (98)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 98, + "propertyName": "dimmingDuration", + "propertyKeyName": "98", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (98)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 99, + "propertyName": "level", + "propertyKeyName": "99", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (99)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 99, + "propertyName": "dimmingDuration", + "propertyKeyName": "99", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (99)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 100, + "propertyName": "level", + "propertyKeyName": "100", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (100)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 100, + "propertyName": "dimmingDuration", + "propertyKeyName": "100", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (100)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 101, + "propertyName": "level", + "propertyKeyName": "101", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (101)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 101, + "propertyName": "dimmingDuration", + "propertyKeyName": "101", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (101)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 102, + "propertyName": "level", + "propertyKeyName": "102", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (102)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 102, + "propertyName": "dimmingDuration", + "propertyKeyName": "102", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (102)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 103, + "propertyName": "level", + "propertyKeyName": "103", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (103)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 103, + "propertyName": "dimmingDuration", + "propertyKeyName": "103", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (103)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 104, + "propertyName": "level", + "propertyKeyName": "104", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (104)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 104, + "propertyName": "dimmingDuration", + "propertyKeyName": "104", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (104)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 105, + "propertyName": "level", + "propertyKeyName": "105", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (105)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 105, + "propertyName": "dimmingDuration", + "propertyKeyName": "105", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (105)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 106, + "propertyName": "level", + "propertyKeyName": "106", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (106)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 106, + "propertyName": "dimmingDuration", + "propertyKeyName": "106", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (106)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 107, + "propertyName": "level", + "propertyKeyName": "107", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (107)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 107, + "propertyName": "dimmingDuration", + "propertyKeyName": "107", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (107)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 108, + "propertyName": "level", + "propertyKeyName": "108", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (108)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 108, + "propertyName": "dimmingDuration", + "propertyKeyName": "108", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (108)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 109, + "propertyName": "level", + "propertyKeyName": "109", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (109)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 109, + "propertyName": "dimmingDuration", + "propertyKeyName": "109", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (109)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 110, + "propertyName": "level", + "propertyKeyName": "110", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (110)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 110, + "propertyName": "dimmingDuration", + "propertyKeyName": "110", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (110)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 111, + "propertyName": "level", + "propertyKeyName": "111", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (111)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 111, + "propertyName": "dimmingDuration", + "propertyKeyName": "111", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (111)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 112, + "propertyName": "level", + "propertyKeyName": "112", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (112)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 112, + "propertyName": "dimmingDuration", + "propertyKeyName": "112", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (112)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 113, + "propertyName": "level", + "propertyKeyName": "113", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (113)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 113, + "propertyName": "dimmingDuration", + "propertyKeyName": "113", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (113)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 114, + "propertyName": "level", + "propertyKeyName": "114", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (114)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 114, + "propertyName": "dimmingDuration", + "propertyKeyName": "114", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (114)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 115, + "propertyName": "level", + "propertyKeyName": "115", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (115)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 115, + "propertyName": "dimmingDuration", + "propertyKeyName": "115", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (115)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 116, + "propertyName": "level", + "propertyKeyName": "116", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (116)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 116, + "propertyName": "dimmingDuration", + "propertyKeyName": "116", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (116)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 117, + "propertyName": "level", + "propertyKeyName": "117", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (117)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 117, + "propertyName": "dimmingDuration", + "propertyKeyName": "117", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (117)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 118, + "propertyName": "level", + "propertyKeyName": "118", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (118)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 118, + "propertyName": "dimmingDuration", + "propertyKeyName": "118", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (118)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 119, + "propertyName": "level", + "propertyKeyName": "119", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (119)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 119, + "propertyName": "dimmingDuration", + "propertyKeyName": "119", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (119)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 120, + "propertyName": "level", + "propertyKeyName": "120", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (120)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 120, + "propertyName": "dimmingDuration", + "propertyKeyName": "120", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (120)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 121, + "propertyName": "level", + "propertyKeyName": "121", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (121)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 121, + "propertyName": "dimmingDuration", + "propertyKeyName": "121", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (121)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 122, + "propertyName": "level", + "propertyKeyName": "122", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (122)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 122, + "propertyName": "dimmingDuration", + "propertyKeyName": "122", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (122)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 123, + "propertyName": "level", + "propertyKeyName": "123", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (123)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 123, + "propertyName": "dimmingDuration", + "propertyKeyName": "123", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (123)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 124, + "propertyName": "level", + "propertyKeyName": "124", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (124)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 124, + "propertyName": "dimmingDuration", + "propertyKeyName": "124", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (124)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 125, + "propertyName": "level", + "propertyKeyName": "125", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (125)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 125, + "propertyName": "dimmingDuration", + "propertyKeyName": "125", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (125)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 126, + "propertyName": "level", + "propertyKeyName": "126", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (126)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 126, + "propertyName": "dimmingDuration", + "propertyKeyName": "126", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (126)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 127, + "propertyName": "level", + "propertyKeyName": "127", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (127)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 127, + "propertyName": "dimmingDuration", + "propertyKeyName": "127", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (127)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 128, + "propertyName": "level", + "propertyKeyName": "128", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (128)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 128, + "propertyName": "dimmingDuration", + "propertyKeyName": "128", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (128)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 129, + "propertyName": "level", + "propertyKeyName": "129", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (129)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 129, + "propertyName": "dimmingDuration", + "propertyKeyName": "129", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (129)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 130, + "propertyName": "level", + "propertyKeyName": "130", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (130)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 130, + "propertyName": "dimmingDuration", + "propertyKeyName": "130", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (130)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 131, + "propertyName": "level", + "propertyKeyName": "131", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (131)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 131, + "propertyName": "dimmingDuration", + "propertyKeyName": "131", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (131)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 132, + "propertyName": "level", + "propertyKeyName": "132", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (132)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 132, + "propertyName": "dimmingDuration", + "propertyKeyName": "132", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (132)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 133, + "propertyName": "level", + "propertyKeyName": "133", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (133)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 133, + "propertyName": "dimmingDuration", + "propertyKeyName": "133", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (133)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 134, + "propertyName": "level", + "propertyKeyName": "134", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (134)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 134, + "propertyName": "dimmingDuration", + "propertyKeyName": "134", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (134)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 135, + "propertyName": "level", + "propertyKeyName": "135", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (135)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 135, + "propertyName": "dimmingDuration", + "propertyKeyName": "135", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (135)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 136, + "propertyName": "level", + "propertyKeyName": "136", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (136)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 136, + "propertyName": "dimmingDuration", + "propertyKeyName": "136", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (136)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 137, + "propertyName": "level", + "propertyKeyName": "137", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (137)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 137, + "propertyName": "dimmingDuration", + "propertyKeyName": "137", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (137)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 138, + "propertyName": "level", + "propertyKeyName": "138", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (138)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 138, + "propertyName": "dimmingDuration", + "propertyKeyName": "138", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (138)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 139, + "propertyName": "level", + "propertyKeyName": "139", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (139)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 139, + "propertyName": "dimmingDuration", + "propertyKeyName": "139", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (139)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 140, + "propertyName": "level", + "propertyKeyName": "140", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (140)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 140, + "propertyName": "dimmingDuration", + "propertyKeyName": "140", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (140)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 141, + "propertyName": "level", + "propertyKeyName": "141", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (141)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 141, + "propertyName": "dimmingDuration", + "propertyKeyName": "141", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (141)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 142, + "propertyName": "level", + "propertyKeyName": "142", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (142)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 142, + "propertyName": "dimmingDuration", + "propertyKeyName": "142", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (142)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 143, + "propertyName": "level", + "propertyKeyName": "143", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (143)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 143, + "propertyName": "dimmingDuration", + "propertyKeyName": "143", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (143)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 144, + "propertyName": "level", + "propertyKeyName": "144", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (144)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 144, + "propertyName": "dimmingDuration", + "propertyKeyName": "144", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (144)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 145, + "propertyName": "level", + "propertyKeyName": "145", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (145)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 145, + "propertyName": "dimmingDuration", + "propertyKeyName": "145", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (145)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 146, + "propertyName": "level", + "propertyKeyName": "146", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (146)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 146, + "propertyName": "dimmingDuration", + "propertyKeyName": "146", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (146)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 147, + "propertyName": "level", + "propertyKeyName": "147", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (147)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 147, + "propertyName": "dimmingDuration", + "propertyKeyName": "147", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (147)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 148, + "propertyName": "level", + "propertyKeyName": "148", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (148)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 148, + "propertyName": "dimmingDuration", + "propertyKeyName": "148", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (148)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 149, + "propertyName": "level", + "propertyKeyName": "149", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (149)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 149, + "propertyName": "dimmingDuration", + "propertyKeyName": "149", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (149)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 150, + "propertyName": "level", + "propertyKeyName": "150", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (150)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 150, + "propertyName": "dimmingDuration", + "propertyKeyName": "150", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (150)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 151, + "propertyName": "level", + "propertyKeyName": "151", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (151)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 151, + "propertyName": "dimmingDuration", + "propertyKeyName": "151", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (151)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 152, + "propertyName": "level", + "propertyKeyName": "152", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (152)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 152, + "propertyName": "dimmingDuration", + "propertyKeyName": "152", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (152)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 153, + "propertyName": "level", + "propertyKeyName": "153", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (153)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 153, + "propertyName": "dimmingDuration", + "propertyKeyName": "153", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (153)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 154, + "propertyName": "level", + "propertyKeyName": "154", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (154)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 154, + "propertyName": "dimmingDuration", + "propertyKeyName": "154", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (154)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 155, + "propertyName": "level", + "propertyKeyName": "155", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (155)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 155, + "propertyName": "dimmingDuration", + "propertyKeyName": "155", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (155)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 156, + "propertyName": "level", + "propertyKeyName": "156", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (156)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 156, + "propertyName": "dimmingDuration", + "propertyKeyName": "156", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (156)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 157, + "propertyName": "level", + "propertyKeyName": "157", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (157)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 157, + "propertyName": "dimmingDuration", + "propertyKeyName": "157", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (157)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 158, + "propertyName": "level", + "propertyKeyName": "158", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (158)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 158, + "propertyName": "dimmingDuration", + "propertyKeyName": "158", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (158)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 159, + "propertyName": "level", + "propertyKeyName": "159", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (159)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 159, + "propertyName": "dimmingDuration", + "propertyKeyName": "159", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (159)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 160, + "propertyName": "level", + "propertyKeyName": "160", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (160)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 160, + "propertyName": "dimmingDuration", + "propertyKeyName": "160", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (160)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 161, + "propertyName": "level", + "propertyKeyName": "161", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (161)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 161, + "propertyName": "dimmingDuration", + "propertyKeyName": "161", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (161)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 162, + "propertyName": "level", + "propertyKeyName": "162", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (162)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 162, + "propertyName": "dimmingDuration", + "propertyKeyName": "162", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (162)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 163, + "propertyName": "level", + "propertyKeyName": "163", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (163)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 163, + "propertyName": "dimmingDuration", + "propertyKeyName": "163", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (163)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 164, + "propertyName": "level", + "propertyKeyName": "164", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (164)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 164, + "propertyName": "dimmingDuration", + "propertyKeyName": "164", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (164)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 165, + "propertyName": "level", + "propertyKeyName": "165", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (165)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 165, + "propertyName": "dimmingDuration", + "propertyKeyName": "165", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (165)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 166, + "propertyName": "level", + "propertyKeyName": "166", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (166)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 166, + "propertyName": "dimmingDuration", + "propertyKeyName": "166", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (166)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 167, + "propertyName": "level", + "propertyKeyName": "167", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (167)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 167, + "propertyName": "dimmingDuration", + "propertyKeyName": "167", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (167)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 168, + "propertyName": "level", + "propertyKeyName": "168", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (168)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 168, + "propertyName": "dimmingDuration", + "propertyKeyName": "168", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (168)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 169, + "propertyName": "level", + "propertyKeyName": "169", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (169)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 169, + "propertyName": "dimmingDuration", + "propertyKeyName": "169", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (169)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 170, + "propertyName": "level", + "propertyKeyName": "170", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (170)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 170, + "propertyName": "dimmingDuration", + "propertyKeyName": "170", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (170)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 171, + "propertyName": "level", + "propertyKeyName": "171", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (171)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 171, + "propertyName": "dimmingDuration", + "propertyKeyName": "171", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (171)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 172, + "propertyName": "level", + "propertyKeyName": "172", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (172)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 172, + "propertyName": "dimmingDuration", + "propertyKeyName": "172", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (172)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 173, + "propertyName": "level", + "propertyKeyName": "173", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (173)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 173, + "propertyName": "dimmingDuration", + "propertyKeyName": "173", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (173)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 174, + "propertyName": "level", + "propertyKeyName": "174", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (174)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 174, + "propertyName": "dimmingDuration", + "propertyKeyName": "174", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (174)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 175, + "propertyName": "level", + "propertyKeyName": "175", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (175)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 175, + "propertyName": "dimmingDuration", + "propertyKeyName": "175", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (175)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 176, + "propertyName": "level", + "propertyKeyName": "176", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (176)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 176, + "propertyName": "dimmingDuration", + "propertyKeyName": "176", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (176)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 177, + "propertyName": "level", + "propertyKeyName": "177", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (177)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 177, + "propertyName": "dimmingDuration", + "propertyKeyName": "177", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (177)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 178, + "propertyName": "level", + "propertyKeyName": "178", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (178)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 178, + "propertyName": "dimmingDuration", + "propertyKeyName": "178", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (178)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 179, + "propertyName": "level", + "propertyKeyName": "179", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (179)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 179, + "propertyName": "dimmingDuration", + "propertyKeyName": "179", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (179)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 180, + "propertyName": "level", + "propertyKeyName": "180", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (180)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 180, + "propertyName": "dimmingDuration", + "propertyKeyName": "180", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (180)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 181, + "propertyName": "level", + "propertyKeyName": "181", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (181)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 181, + "propertyName": "dimmingDuration", + "propertyKeyName": "181", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (181)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 182, + "propertyName": "level", + "propertyKeyName": "182", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (182)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 182, + "propertyName": "dimmingDuration", + "propertyKeyName": "182", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (182)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 183, + "propertyName": "level", + "propertyKeyName": "183", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (183)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 183, + "propertyName": "dimmingDuration", + "propertyKeyName": "183", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (183)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 184, + "propertyName": "level", + "propertyKeyName": "184", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (184)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 184, + "propertyName": "dimmingDuration", + "propertyKeyName": "184", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (184)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 185, + "propertyName": "level", + "propertyKeyName": "185", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (185)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 185, + "propertyName": "dimmingDuration", + "propertyKeyName": "185", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (185)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 186, + "propertyName": "level", + "propertyKeyName": "186", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (186)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 186, + "propertyName": "dimmingDuration", + "propertyKeyName": "186", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (186)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 187, + "propertyName": "level", + "propertyKeyName": "187", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (187)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 187, + "propertyName": "dimmingDuration", + "propertyKeyName": "187", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (187)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 188, + "propertyName": "level", + "propertyKeyName": "188", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (188)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 188, + "propertyName": "dimmingDuration", + "propertyKeyName": "188", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (188)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 189, + "propertyName": "level", + "propertyKeyName": "189", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (189)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 189, + "propertyName": "dimmingDuration", + "propertyKeyName": "189", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (189)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 190, + "propertyName": "level", + "propertyKeyName": "190", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (190)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 190, + "propertyName": "dimmingDuration", + "propertyKeyName": "190", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (190)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 191, + "propertyName": "level", + "propertyKeyName": "191", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (191)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 191, + "propertyName": "dimmingDuration", + "propertyKeyName": "191", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (191)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 192, + "propertyName": "level", + "propertyKeyName": "192", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (192)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 192, + "propertyName": "dimmingDuration", + "propertyKeyName": "192", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (192)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 193, + "propertyName": "level", + "propertyKeyName": "193", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (193)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 193, + "propertyName": "dimmingDuration", + "propertyKeyName": "193", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (193)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 194, + "propertyName": "level", + "propertyKeyName": "194", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (194)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 194, + "propertyName": "dimmingDuration", + "propertyKeyName": "194", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (194)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 195, + "propertyName": "level", + "propertyKeyName": "195", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (195)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 195, + "propertyName": "dimmingDuration", + "propertyKeyName": "195", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (195)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 196, + "propertyName": "level", + "propertyKeyName": "196", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (196)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 196, + "propertyName": "dimmingDuration", + "propertyKeyName": "196", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (196)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 197, + "propertyName": "level", + "propertyKeyName": "197", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (197)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 197, + "propertyName": "dimmingDuration", + "propertyKeyName": "197", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (197)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 198, + "propertyName": "level", + "propertyKeyName": "198", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (198)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 198, + "propertyName": "dimmingDuration", + "propertyKeyName": "198", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (198)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 199, + "propertyName": "level", + "propertyKeyName": "199", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (199)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 199, + "propertyName": "dimmingDuration", + "propertyKeyName": "199", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (199)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 200, + "propertyName": "level", + "propertyKeyName": "200", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (200)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 200, + "propertyName": "dimmingDuration", + "propertyKeyName": "200", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (200)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 201, + "propertyName": "level", + "propertyKeyName": "201", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (201)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 201, + "propertyName": "dimmingDuration", + "propertyKeyName": "201", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (201)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 202, + "propertyName": "level", + "propertyKeyName": "202", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (202)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 202, + "propertyName": "dimmingDuration", + "propertyKeyName": "202", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (202)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 203, + "propertyName": "level", + "propertyKeyName": "203", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (203)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 203, + "propertyName": "dimmingDuration", + "propertyKeyName": "203", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (203)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 204, + "propertyName": "level", + "propertyKeyName": "204", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (204)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 204, + "propertyName": "dimmingDuration", + "propertyKeyName": "204", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (204)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 205, + "propertyName": "level", + "propertyKeyName": "205", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (205)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 205, + "propertyName": "dimmingDuration", + "propertyKeyName": "205", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (205)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 206, + "propertyName": "level", + "propertyKeyName": "206", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (206)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 206, + "propertyName": "dimmingDuration", + "propertyKeyName": "206", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (206)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 207, + "propertyName": "level", + "propertyKeyName": "207", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (207)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 207, + "propertyName": "dimmingDuration", + "propertyKeyName": "207", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (207)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 208, + "propertyName": "level", + "propertyKeyName": "208", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (208)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 208, + "propertyName": "dimmingDuration", + "propertyKeyName": "208", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (208)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 209, + "propertyName": "level", + "propertyKeyName": "209", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (209)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 209, + "propertyName": "dimmingDuration", + "propertyKeyName": "209", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (209)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 210, + "propertyName": "level", + "propertyKeyName": "210", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (210)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 210, + "propertyName": "dimmingDuration", + "propertyKeyName": "210", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (210)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 211, + "propertyName": "level", + "propertyKeyName": "211", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (211)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 211, + "propertyName": "dimmingDuration", + "propertyKeyName": "211", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (211)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 212, + "propertyName": "level", + "propertyKeyName": "212", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (212)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 212, + "propertyName": "dimmingDuration", + "propertyKeyName": "212", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (212)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 213, + "propertyName": "level", + "propertyKeyName": "213", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (213)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 213, + "propertyName": "dimmingDuration", + "propertyKeyName": "213", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (213)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 214, + "propertyName": "level", + "propertyKeyName": "214", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (214)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 214, + "propertyName": "dimmingDuration", + "propertyKeyName": "214", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (214)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 215, + "propertyName": "level", + "propertyKeyName": "215", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (215)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 215, + "propertyName": "dimmingDuration", + "propertyKeyName": "215", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (215)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 216, + "propertyName": "level", + "propertyKeyName": "216", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (216)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 216, + "propertyName": "dimmingDuration", + "propertyKeyName": "216", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (216)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 217, + "propertyName": "level", + "propertyKeyName": "217", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (217)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 217, + "propertyName": "dimmingDuration", + "propertyKeyName": "217", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (217)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 218, + "propertyName": "level", + "propertyKeyName": "218", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (218)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 218, + "propertyName": "dimmingDuration", + "propertyKeyName": "218", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (218)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 219, + "propertyName": "level", + "propertyKeyName": "219", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (219)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 219, + "propertyName": "dimmingDuration", + "propertyKeyName": "219", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (219)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 220, + "propertyName": "level", + "propertyKeyName": "220", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (220)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 220, + "propertyName": "dimmingDuration", + "propertyKeyName": "220", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (220)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 221, + "propertyName": "level", + "propertyKeyName": "221", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (221)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 221, + "propertyName": "dimmingDuration", + "propertyKeyName": "221", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (221)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 222, + "propertyName": "level", + "propertyKeyName": "222", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (222)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 222, + "propertyName": "dimmingDuration", + "propertyKeyName": "222", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (222)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 223, + "propertyName": "level", + "propertyKeyName": "223", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (223)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 223, + "propertyName": "dimmingDuration", + "propertyKeyName": "223", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (223)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 224, + "propertyName": "level", + "propertyKeyName": "224", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (224)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 224, + "propertyName": "dimmingDuration", + "propertyKeyName": "224", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (224)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 225, + "propertyName": "level", + "propertyKeyName": "225", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (225)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 225, + "propertyName": "dimmingDuration", + "propertyKeyName": "225", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (225)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 226, + "propertyName": "level", + "propertyKeyName": "226", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (226)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 226, + "propertyName": "dimmingDuration", + "propertyKeyName": "226", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (226)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 227, + "propertyName": "level", + "propertyKeyName": "227", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (227)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 227, + "propertyName": "dimmingDuration", + "propertyKeyName": "227", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (227)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 228, + "propertyName": "level", + "propertyKeyName": "228", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (228)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 228, + "propertyName": "dimmingDuration", + "propertyKeyName": "228", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (228)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 229, + "propertyName": "level", + "propertyKeyName": "229", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (229)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 229, + "propertyName": "dimmingDuration", + "propertyKeyName": "229", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (229)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 230, + "propertyName": "level", + "propertyKeyName": "230", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (230)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 230, + "propertyName": "dimmingDuration", + "propertyKeyName": "230", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (230)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 231, + "propertyName": "level", + "propertyKeyName": "231", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (231)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 231, + "propertyName": "dimmingDuration", + "propertyKeyName": "231", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (231)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 232, + "propertyName": "level", + "propertyKeyName": "232", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (232)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 232, + "propertyName": "dimmingDuration", + "propertyKeyName": "232", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (232)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 233, + "propertyName": "level", + "propertyKeyName": "233", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (233)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 233, + "propertyName": "dimmingDuration", + "propertyKeyName": "233", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (233)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 234, + "propertyName": "level", + "propertyKeyName": "234", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (234)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 234, + "propertyName": "dimmingDuration", + "propertyKeyName": "234", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (234)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 235, + "propertyName": "level", + "propertyKeyName": "235", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (235)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 235, + "propertyName": "dimmingDuration", + "propertyKeyName": "235", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (235)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 236, + "propertyName": "level", + "propertyKeyName": "236", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (236)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 236, + "propertyName": "dimmingDuration", + "propertyKeyName": "236", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (236)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 237, + "propertyName": "level", + "propertyKeyName": "237", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (237)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 237, + "propertyName": "dimmingDuration", + "propertyKeyName": "237", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (237)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 238, + "propertyName": "level", + "propertyKeyName": "238", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (238)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 238, + "propertyName": "dimmingDuration", + "propertyKeyName": "238", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (238)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 239, + "propertyName": "level", + "propertyKeyName": "239", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (239)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 239, + "propertyName": "dimmingDuration", + "propertyKeyName": "239", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (239)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 240, + "propertyName": "level", + "propertyKeyName": "240", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (240)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 240, + "propertyName": "dimmingDuration", + "propertyKeyName": "240", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (240)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 241, + "propertyName": "level", + "propertyKeyName": "241", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (241)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 241, + "propertyName": "dimmingDuration", + "propertyKeyName": "241", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (241)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 242, + "propertyName": "level", + "propertyKeyName": "242", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (242)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 242, + "propertyName": "dimmingDuration", + "propertyKeyName": "242", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (242)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 243, + "propertyName": "level", + "propertyKeyName": "243", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (243)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 243, + "propertyName": "dimmingDuration", + "propertyKeyName": "243", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (243)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 244, + "propertyName": "level", + "propertyKeyName": "244", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (244)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 244, + "propertyName": "dimmingDuration", + "propertyKeyName": "244", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (244)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 245, + "propertyName": "level", + "propertyKeyName": "245", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (245)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 245, + "propertyName": "dimmingDuration", + "propertyKeyName": "245", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (245)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 246, + "propertyName": "level", + "propertyKeyName": "246", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (246)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 246, + "propertyName": "dimmingDuration", + "propertyKeyName": "246", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (246)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 247, + "propertyName": "level", + "propertyKeyName": "247", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (247)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 247, + "propertyName": "dimmingDuration", + "propertyKeyName": "247", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (247)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 248, + "propertyName": "level", + "propertyKeyName": "248", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (248)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 248, + "propertyName": "dimmingDuration", + "propertyKeyName": "248", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (248)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 249, + "propertyName": "level", + "propertyKeyName": "249", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (249)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 249, + "propertyName": "dimmingDuration", + "propertyKeyName": "249", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (249)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 250, + "propertyName": "level", + "propertyKeyName": "250", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (250)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 250, + "propertyName": "dimmingDuration", + "propertyKeyName": "250", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (250)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 251, + "propertyName": "level", + "propertyKeyName": "251", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (251)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 251, + "propertyName": "dimmingDuration", + "propertyKeyName": "251", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (251)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 252, + "propertyName": "level", + "propertyKeyName": "252", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (252)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 252, + "propertyName": "dimmingDuration", + "propertyKeyName": "252", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (252)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 253, + "propertyName": "level", + "propertyKeyName": "253", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (253)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 253, + "propertyName": "dimmingDuration", + "propertyKeyName": "253", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (253)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 254, + "propertyName": "level", + "propertyKeyName": "254", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (254)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 254, + "propertyName": "dimmingDuration", + "propertyKeyName": "254", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (254)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 255, + "propertyName": "level", + "propertyKeyName": "255", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (255)", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 255, + "propertyName": "dimmingDuration", + "propertyKeyName": "255", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (255)" + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.", + "label": "Send held down notifications at a slow rate" + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 001", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x", + "5": "KeyPressed4x", + "6": "KeyPressed5x" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "002", + "propertyName": "scene", + "propertyKeyName": "002", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 002", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x", + "5": "KeyPressed4x", + "6": "KeyPressed5x" + } + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "LED Indicator", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Indicator", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "On when load is off", + "1": "On when load is on" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Inverted Orientation", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Inverted Orientation", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Fan Type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Fan Type", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "3-speed", + "1": "4-speed" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Enable / Disable Custom LED Status Mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Enable / Disable Custom LED Status Mode", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Default LED Color", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default LED Color", + "default": 0, + "min": 0, + "max": 6, + "states": { + "0": "White", + "1": "Red", + "2": "Green", + "3": "Blue", + "4": "Magenta", + "5": "Yellow", + "6": "Cyan" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Status Mode LED 1 Color", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Status Mode LED 1 Color", + "default": 0, + "min": 0, + "max": 7, + "states": { + "0": "Off", + "1": "Red", + "2": "Green", + "3": "Blue", + "4": "Magenta", + "5": "Yellow", + "6": "Cyan", + "7": "White" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "Status Mode LED 2 Color", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Status Mode LED 2 Color", + "default": 0, + "min": 0, + "max": 7, + "states": { + "0": "Off", + "1": "Red", + "2": "Green", + "3": "Blue", + "4": "Magenta", + "5": "Yellow", + "6": "Cyan", + "7": "White" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyName": "Status Mode LED 3 Color", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Status Mode LED 3 Color", + "default": 0, + "min": 0, + "max": 7, + "states": { + "0": "Off", + "1": "Red", + "2": "Green", + "3": "Blue", + "4": "Magenta", + "5": "Yellow", + "6": "Cyan", + "7": "White" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 24, + "propertyName": "Status Mode LED 4 Color", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Status Mode LED 4 Color", + "default": 0, + "min": 0, + "max": 7, + "states": { + "0": "Off", + "1": "Red", + "2": "Green", + "3": "Blue", + "4": "Magenta", + "5": "Yellow", + "6": "Cyan", + "7": "White" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyName": "Blink Frequency", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the blink frequency for LEDs; 0 for off", + "label": "Blink Frequency", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 1, + "propertyName": "Enable / Disable Blinking - LED 1", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Enable / Disable Blinking - LED 1", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 2, + "propertyName": "Enable / Disable Blinking - LED 2", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Enable / Disable Blinking - LED 2", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 4, + "propertyName": "Enable / Disable Blinking - LED 3", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Enable / Disable Blinking - LED 3", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 8, + "propertyName": "Enable / Disable Blinking - LED 4", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Enable / Disable Blinking - LED 4", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "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": 12 + }, + { + "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": 515 + }, + { + "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": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "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": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "50.5" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version" + }, + "value": "6.81.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version" + }, + "value": "4.1.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number" + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version" + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.1.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number" + }, + "value": 58 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version" + }, + "value": "50.5.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number" + }, + "value": 52445 + } + ], + "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": 8, + "label": "Fan Switch" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 133, + 89, + 114, + 115, + 134, + 94 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x000c:0x0203:0x0001:50.5", + "statistics": { + "commandsTX": 400, + "commandsRX": 402, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 2 + } +} diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 5bd856c664a..f5d8a0b89e3 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -1,16 +1,24 @@ """Test the Z-Wave JS fan platform.""" +import math + import pytest from zwave_js_server.event import Event -from homeassistant.components.fan import ATTR_SPEED, SPEED_MEDIUM +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + ATTR_SPEED, + SPEED_MEDIUM, +) -FAN_ENTITY = "fan.in_wall_smart_fan_control" +STANDARD_FAN_ENTITY = "fan.in_wall_smart_fan_control" +HS_FAN_ENTITY = "fan.scene_capable_fan_control_switch" -async def test_fan(hass, client, in_wall_smart_fan_control, integration): +async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration): """Test the fan entity.""" node = in_wall_smart_fan_control - state = hass.states.get(FAN_ENTITY) + state = hass.states.get(STANDARD_FAN_ENTITY) assert state assert state.state == "off" @@ -19,7 +27,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): await hass.services.async_call( "fan", "turn_on", - {"entity_id": FAN_ENTITY, "speed": SPEED_MEDIUM}, + {"entity_id": STANDARD_FAN_ENTITY, "speed": SPEED_MEDIUM}, blocking=True, ) @@ -52,7 +60,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): await hass.services.async_call( "fan", "set_speed", - {"entity_id": FAN_ENTITY, "speed": 99}, + {"entity_id": STANDARD_FAN_ENTITY, "speed": 99}, blocking=True, ) @@ -62,7 +70,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): await hass.services.async_call( "fan", "turn_on", - {"entity_id": FAN_ENTITY}, + {"entity_id": STANDARD_FAN_ENTITY}, blocking=True, ) @@ -94,7 +102,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): await hass.services.async_call( "fan", "turn_off", - {"entity_id": FAN_ENTITY}, + {"entity_id": STANDARD_FAN_ENTITY}, blocking=True, ) @@ -142,7 +150,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): ) node.receive_event(event) - state = hass.states.get(FAN_ENTITY) + state = hass.states.get(STANDARD_FAN_ENTITY) assert state.state == "on" assert state.attributes[ATTR_SPEED] == "high" @@ -167,6 +175,67 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): ) node.receive_event(event) - state = hass.states.get(FAN_ENTITY) + state = hass.states.get(STANDARD_FAN_ENTITY) assert state.state == "off" assert state.attributes[ATTR_SPEED] == "off" + + +async def test_hs_fan(hass, client, hs_fc200, integration): + """Test a fan entity with configurable speeds.""" + + async def get_zwave_speed_from_percentage(percentage): + """Set the fan to a particular percentage and get the resulting Zwave speed.""" + client.async_send_command.reset_mock() + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": HS_FAN_ENTITY, "percentage": percentage}, + 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"] == 39 + return args["value"] + + async def get_percentage_from_zwave_speed(zwave_speed): + """Set the underlying device speed and get the resulting percentage.""" + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": zwave_speed, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + hs_fc200.receive_event(event) + state = hass.states.get(HS_FAN_ENTITY) + return state.attributes[ATTR_PERCENTAGE] + + percentages_to_zwave_speeds = [ + [[0], [0]], + [range(1, 34), range(1, 34)], + [range(34, 68), range(34, 67)], + [range(68, 101), range(67, 100)], + ] + + for percentages, zwave_speeds in percentages_to_zwave_speeds: + for percentage in percentages: + actual_zwave_speed = await get_zwave_speed_from_percentage(percentage) + assert actual_zwave_speed in zwave_speeds + for zwave_speed in zwave_speeds: + actual_percentage = await get_percentage_from_zwave_speed(zwave_speed) + assert actual_percentage in percentages + + state = hass.states.get(HS_FAN_ENTITY) + assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) From f4f945e65e4ddb0c5985856f605ce1cbf15ef6b9 Mon Sep 17 00:00:00 2001 From: h2zero <32826625+h2zero@users.noreply.github.com> Date: Wed, 24 Nov 2021 03:35:00 -0700 Subject: [PATCH 0824/1452] Fix Konnected multiple discovery of panels (#59953) * Konnected - Fix multiple discovery of panels. This resolves an issue which creates multiple discoveries of a Konnected panel if it is restarted and fails to connect to home assistant. See #57467. * Revert changes to user step, add handling to ssdp step. * Add abort reason string to strings.json * Abort ssdp discovery if device is already known. * Add test for multiple discovery fix. * Remove unrelated file change. * Add ssdp discovery abort tests. * Add missing abort reason check. * Add "already_configured" to strings. * Use "cannot_connect" abort reason. --- .../components/konnected/config_flow.py | 22 +++++- .../components/konnected/strings.json | 3 +- .../components/konnected/test_config_flow.py | 77 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 4cc53c9069a..82a720bfff3 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -267,10 +267,28 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: # extract host/port from ssdp_location netloc = urlparse(discovery_info["ssdp_location"]).netloc.split(":") - return await self.async_step_user( - user_input={CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} + self._async_abort_entries_match( + {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} ) + try: + status = await get_status(self.hass, netloc[0], int(netloc[1])) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + else: + self.data[CONF_HOST] = netloc[0] + self.data[CONF_PORT] = int(netloc[1]) + self.data[CONF_ID] = status.get( + "chipId", status["mac"].replace(":", "") + ) + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + + KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { + CONF_HOST: self.data[CONF_HOST], + CONF_PORT: self.data[CONF_PORT], + } + return await self.async_step_confirm() + return self.async_abort(reason="unknown") async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index e53838ad0d7..905597035d5 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -24,7 +24,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "not_konn_panel": "Not a recognized Konnected.io device" + "not_konn_panel": "Not a recognized Konnected.io device", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "options": { diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 36f582fcb57..466359c3dc3 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -109,6 +109,7 @@ async def test_ssdp(hass, mock_panel): "model": "Konnected", } + # Test success result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -128,6 +129,82 @@ async def test_ssdp(hass, mock_panel): "port": 1234, } + # Test abort if connection failed + mock_panel.get_status.side_effect = config_flow.CannotConnect + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + # Test abort if invalid data + mock_panel.get_status.side_effect = KeyError + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" + + # Test abort if invalid manufacturer + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + "manufacturer": "SHOULD_FAIL", + "modelName": config_flow.KONN_MODEL, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_konn_panel" + + # Test abort if invalid model + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": "SHOULD_FAIL", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_konn_panel" + + # Test abort if already configured + config_entry = MockConfigEntry( + domain=config_flow.DOMAIN, + data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 1234}, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + async def test_import_no_host_user_finish(hass, mock_panel): """Test importing a panel with no host info.""" From 3aac757e7cb2022ac6cc6ef83728b8366867ba65 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 11:56:50 +0100 Subject: [PATCH 0825/1452] CI: Combine smaller linters into a single job (#60279) * CI: Combine smaller linters into a single job * Remove unrelated changhe * Remove stale exit --- .github/workflows/ci.yaml | 237 ++++++-------------------------------- 1 file changed, 38 insertions(+), 199 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4fab3d1a2d..c9a9ac4eff8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -162,111 +162,6 @@ jobs: . venv/bin/activate pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - lint-codespell: - name: Check codespell - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v2.1.7 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Register codespell problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/codespell.json" - - name: Run codespell - run: | - . venv/bin/activate - pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files - - lint-dockerfile: - name: Check Dockerfile - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Register hadolint problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/hadolint.json" - - name: Check Dockerfile - uses: docker://hadolint/hadolint:v1.18.2 - with: - args: hadolint Dockerfile - - name: Check Dockerfile.dev - uses: docker://hadolint/hadolint:v1.18.2 - with: - args: hadolint Dockerfile.dev - - lint-executable-shebangs: - name: Check executables - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v2.1.7 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Register check executables problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" - - name: Run executables check - run: | - . venv/bin/activate - pre-commit run --hook-stage manual check-executables-have-shebangs --all-files - lint-flake8: name: Check flake8 runs-on: ubuntu-latest @@ -350,8 +245,8 @@ jobs: . venv/bin/activate pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure - lint-json: - name: Check JSON + lint-other: + name: Check other linters runs-on: ubuntu-latest needs: prepare-base steps: @@ -385,6 +280,20 @@ jobs: run: | echo "Failed to restore pre-commit environment from cache" exit 1 + + - name: Run pyupgrade + run: | + . venv/bin/activate + pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure + + - name: Register yamllint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/yamllint.json" + - name: Run yamllint + run: | + . venv/bin/activate + pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure + - name: Register check-json problem matcher run: | echo "::add-matcher::.github/workflows/matchers/check-json.json" @@ -393,99 +302,34 @@ jobs: . venv/bin/activate pre-commit run --hook-stage manual check-json --all-files - lint-pyupgrade: - name: Check pyupgrade - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + - name: Register check executables problem matcher run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v2.1.7 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Run pyupgrade + echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" + - name: Run executables check run: | . venv/bin/activate - pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure + pre-commit run --hook-stage manual check-executables-have-shebangs --all-files - # Disabled until we have the existing issues fixed - # lint-shellcheck: - # name: Check ShellCheck - # runs-on: ubuntu-latest - # needs: prepare-base - # steps: - # - name: Check out code from GitHub - # uses: actions/checkout@v2.4.0 - # - name: Run ShellCheck - # uses: ludeeus/action-shellcheck@0.3.0 - - lint-yaml: - name: Check YAML - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + - name: Register codespell problem matcher run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v2.1.7 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Register yamllint problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/yamllint.json" - - name: Run yamllint + echo "::add-matcher::.github/workflows/matchers/codespell.json" + - name: Run codespell run: | . venv/bin/activate - pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure + pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files + + - name: Register hadolint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/hadolint.json" + + - name: Check Dockerfile + uses: docker://hadolint/hadolint:v1.18.2 + with: + args: hadolint Dockerfile + - name: Check Dockerfile.dev + uses: docker://hadolint/hadolint:v1.18.2 + with: + args: hadolint Dockerfile.dev hassfest: name: Check hassfest @@ -708,13 +552,8 @@ jobs: - hassfest - lint-bandit - lint-black - - lint-codespell - - lint-dockerfile - - lint-executable-shebangs + - lint-other - lint-isort - - lint-json - - lint-pyupgrade - - lint-yaml - mypy - prepare-tests strategy: From b8f4b761949aa3150e4601b00414ab82a354d033 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 24 Nov 2021 13:42:44 +0100 Subject: [PATCH 0826/1452] Add additional statistics characteristics, remove attributes (#59867) * Add additional statistics characterics, improve rounding * Improve name of age_usage_ratio * Replace difference by three relevant distances * Refactor attributes, remove stats, add metadata * Fix binary sensor testcase * Fix sensor defaults testcase * Fix and enhance all testcases * Remove age coverage from attr when not configured * Refactor so only the relevant characteristic value is calculated * Rename unclear characteristics, add timebound average * Fix coverage warning * Remove explicit functions dict --- homeassistant/components/statistics/sensor.py | 387 ++++++---- tests/components/statistics/test_sensor.py | 717 +++++++++++------- 2 files changed, 680 insertions(+), 424 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 92fc682196b..6905170f470 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -34,21 +34,38 @@ from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -STAT_AVERAGE_CHANGE = "average_change" +STAT_AGE_COVERAGE_RATIO = "age_coverage_ratio" +STAT_BUFFER_USAGE_RATIO = "buffer_usage_ratio" +STAT_SOURCE_VALUE_VALID = "source_value_valid" + +STAT_AVERAGE_LINEAR = "average_linear" +STAT_AVERAGE_STEP = "average_step" +STAT_AVERAGE_TIMELESS = "average_timeless" STAT_CHANGE = "change" -STAT_CHANGE_RATE = "change_rate" +STAT_CHANGE_SAMPLE = "change_sample" +STAT_CHANGE_SECOND = "change_second" STAT_COUNT = "count" -STAT_MAX_AGE = "max_age" -STAT_MAX_VALUE = "max_value" +STAT_DATETIME_NEWEST = "datetime_newest" +STAT_DATETIME_OLDEST = "datetime_oldest" +STAT_DISTANCE_95P = "distance_95_percent_of_values" +STAT_DISTANCE_99P = "distance_99_percent_of_values" +STAT_DISTANCE_ABSOLUTE = "distance_absolute" STAT_MEAN = "mean" STAT_MEDIAN = "median" -STAT_MIN_AGE = "min_age" -STAT_MIN_VALUE = "min_value" +STAT_NOISINESS = "noisiness" STAT_QUANTILES = "quantiles" STAT_STANDARD_DEVIATION = "standard_deviation" STAT_TOTAL = "total" +STAT_VALUE_MAX = "value_max" +STAT_VALUE_MIN = "value_min" STAT_VARIANCE = "variance" +STATS_NOT_A_NUMBER = ( + STAT_DATETIME_OLDEST, + STAT_DATETIME_NEWEST, + STAT_QUANTILES, +) + CONF_STATE_CHARACTERISTIC = "state_characteristic" CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" @@ -69,19 +86,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_MEAN): vol.In( [ - STAT_AVERAGE_CHANGE, + STAT_AVERAGE_LINEAR, + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_CHANGE_SAMPLE, + STAT_CHANGE_SECOND, STAT_CHANGE, - STAT_CHANGE_RATE, STAT_COUNT, - STAT_MAX_AGE, - STAT_MAX_VALUE, + STAT_DATETIME_NEWEST, + STAT_DATETIME_OLDEST, + STAT_DISTANCE_95P, + STAT_DISTANCE_99P, + STAT_DISTANCE_ABSOLUTE, STAT_MEAN, STAT_MEDIAN, - STAT_MIN_AGE, - STAT_MIN_VALUE, + STAT_NOISINESS, STAT_QUANTILES, STAT_STANDARD_DEVIATION, STAT_TOTAL, + STAT_VALUE_MAX, + STAT_VALUE_MIN, STAT_VARIANCE, ] ), @@ -141,32 +165,26 @@ class StatisticsSensor(SensorEntity): self._source_entity_id = source_entity_id self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name - self._available = False self._state_characteristic = state_characteristic self._samples_max_buffer_size = samples_max_buffer_size self._samples_max_age = samples_max_age self._precision = precision self._quantile_intervals = quantile_intervals self._quantile_method = quantile_method + self._value = None self._unit_of_measurement = None + self._available = False self.states = deque(maxlen=self._samples_max_buffer_size) self.ages = deque(maxlen=self._samples_max_buffer_size) - self.attr = { - STAT_COUNT: 0, - STAT_TOTAL: None, - STAT_MEAN: None, - STAT_MEDIAN: None, - STAT_STANDARD_DEVIATION: None, - STAT_VARIANCE: None, - STAT_MIN_VALUE: None, - STAT_MAX_VALUE: None, - STAT_MIN_AGE: None, - STAT_MAX_AGE: None, - STAT_CHANGE: None, - STAT_AVERAGE_CHANGE: None, - STAT_CHANGE_RATE: None, - STAT_QUANTILES: None, + self.attributes = { + STAT_AGE_COVERAGE_RATIO: STATE_UNKNOWN, + STAT_BUFFER_USAGE_RATIO: STATE_UNKNOWN, + STAT_SOURCE_VALUE_VALID: STATE_UNKNOWN, } + self._state_characteristic_fn = getattr( + self, f"_stat_{self._state_characteristic}" + ) + self._update_listener = None async def async_added_to_hass(self): @@ -201,7 +219,11 @@ class StatisticsSensor(SensorEntity): def _add_state_to_queue(self, new_state): """Add the state to the queue.""" self._available = new_state.state != STATE_UNAVAILABLE - if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE, None): + if new_state.state == STATE_UNAVAILABLE: + self.attributes[STAT_SOURCE_VALUE_VALID] = None + return + if new_state.state in (STATE_UNKNOWN, None): + self.attributes[STAT_SOURCE_VALUE_VALID] = False return try: @@ -210,7 +232,9 @@ class StatisticsSensor(SensorEntity): else: self.states.append(float(new_state.state)) self.ages.append(new_state.last_updated) + self.attributes[STAT_SOURCE_VALUE_VALID] = True except ValueError: + self.attributes[STAT_SOURCE_VALUE_VALID] = False _LOGGER.error( "%s: parsing error, expected number and received %s", self.entity_id, @@ -226,28 +250,35 @@ class StatisticsSensor(SensorEntity): unit = None elif self.is_binary: unit = None + elif self._state_characteristic in ( + STAT_AVERAGE_LINEAR, + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_CHANGE, + STAT_DISTANCE_95P, + STAT_DISTANCE_99P, + STAT_DISTANCE_ABSOLUTE, + STAT_MEAN, + STAT_MEDIAN, + STAT_NOISINESS, + STAT_STANDARD_DEVIATION, + STAT_TOTAL, + STAT_VALUE_MAX, + STAT_VALUE_MIN, + ): + unit = base_unit elif self._state_characteristic in ( STAT_COUNT, - STAT_MIN_AGE, - STAT_MAX_AGE, + STAT_DATETIME_NEWEST, + STAT_DATETIME_OLDEST, STAT_QUANTILES, ): unit = None - elif self._state_characteristic in ( - STAT_TOTAL, - STAT_MEAN, - STAT_MEDIAN, - STAT_STANDARD_DEVIATION, - STAT_MIN_VALUE, - STAT_MAX_VALUE, - STAT_CHANGE, - ): - unit = base_unit elif self._state_characteristic == STAT_VARIANCE: unit = base_unit + "²" - elif self._state_characteristic == STAT_AVERAGE_CHANGE: + elif self._state_characteristic == STAT_CHANGE_SAMPLE: unit = base_unit + "/sample" - elif self._state_characteristic == STAT_CHANGE_RATE: + elif self._state_characteristic == STAT_CHANGE_SECOND: unit = base_unit + "/s" return unit @@ -259,29 +290,16 @@ class StatisticsSensor(SensorEntity): @property def state_class(self): """Return the state class of this entity.""" - if self._state_characteristic in ( - STAT_MIN_AGE, - STAT_MAX_AGE, - STAT_QUANTILES, - ): + if self.is_binary: + return STATE_CLASS_MEASUREMENT + if self._state_characteristic in STATS_NOT_A_NUMBER: return None return STATE_CLASS_MEASUREMENT @property def native_value(self): """Return the state of the sensor.""" - if self.is_binary: - return self.attr[STAT_COUNT] - if self._state_characteristic in ( - STAT_MIN_AGE, - STAT_MAX_AGE, - STAT_QUANTILES, - ): - return self.attr[self._state_characteristic] - if self._precision == 0: - with contextlib.suppress(TypeError, ValueError): - return int(self.attr[self._state_characteristic]) - return self.attr[self._state_characteristic] + return self._value @property def native_unit_of_measurement(self): @@ -301,9 +319,16 @@ class StatisticsSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" - if self.is_binary: - return None - return self.attr + extra_attr = {} + if self._samples_max_age is not None: + extra_attr = { + STAT_AGE_COVERAGE_RATIO: self.attributes[STAT_AGE_COVERAGE_RATIO] + } + return { + **extra_attr, + STAT_BUFFER_USAGE_RATIO: self.attributes[STAT_BUFFER_USAGE_RATIO], + STAT_SOURCE_VALUE_VALID: self.attributes[STAT_SOURCE_VALUE_VALID], + } @property def icon(self): @@ -340,84 +365,14 @@ class StatisticsSensor(SensorEntity): return self.ages[0] + self._samples_max_age return None - def _update_characteristics(self): - """Calculate and update the various statistical characteristics.""" - states_count = len(self.states) - self.attr[STAT_COUNT] = states_count - - if self.is_binary: - return - - if states_count >= 2: - self.attr[STAT_STANDARD_DEVIATION] = round( - statistics.stdev(self.states), self._precision - ) - self.attr[STAT_VARIANCE] = round( - statistics.variance(self.states), self._precision - ) - else: - self.attr[STAT_STANDARD_DEVIATION] = STATE_UNKNOWN - self.attr[STAT_VARIANCE] = STATE_UNKNOWN - - if states_count > self._quantile_intervals: - self.attr[STAT_QUANTILES] = [ - round(quantile, self._precision) - for quantile in statistics.quantiles( - self.states, - n=self._quantile_intervals, - method=self._quantile_method, - ) - ] - else: - self.attr[STAT_QUANTILES] = STATE_UNKNOWN - - if states_count == 0: - self.attr[STAT_MEAN] = STATE_UNKNOWN - self.attr[STAT_MEDIAN] = STATE_UNKNOWN - self.attr[STAT_TOTAL] = STATE_UNKNOWN - self.attr[STAT_MIN_VALUE] = self.attr[STAT_MAX_VALUE] = STATE_UNKNOWN - self.attr[STAT_MIN_AGE] = self.attr[STAT_MAX_AGE] = STATE_UNKNOWN - self.attr[STAT_CHANGE] = self.attr[STAT_AVERAGE_CHANGE] = STATE_UNKNOWN - self.attr[STAT_CHANGE_RATE] = STATE_UNKNOWN - return - - self.attr[STAT_MEAN] = round(statistics.mean(self.states), self._precision) - self.attr[STAT_MEDIAN] = round(statistics.median(self.states), self._precision) - - self.attr[STAT_TOTAL] = round(sum(self.states), self._precision) - self.attr[STAT_MIN_VALUE] = round(min(self.states), self._precision) - self.attr[STAT_MAX_VALUE] = round(max(self.states), self._precision) - - self.attr[STAT_MIN_AGE] = self.ages[0] - self.attr[STAT_MAX_AGE] = self.ages[-1] - - self.attr[STAT_CHANGE] = self.states[-1] - self.states[0] - - self.attr[STAT_AVERAGE_CHANGE] = self.attr[STAT_CHANGE] - self.attr[STAT_CHANGE_RATE] = 0 - if states_count > 1: - self.attr[STAT_AVERAGE_CHANGE] /= len(self.states) - 1 - - time_diff = ( - self.attr[STAT_MAX_AGE] - self.attr[STAT_MIN_AGE] - ).total_seconds() - if time_diff > 0: - self.attr[STAT_CHANGE_RATE] = self.attr[STAT_CHANGE] / time_diff - self.attr[STAT_CHANGE] = round(self.attr[STAT_CHANGE], self._precision) - self.attr[STAT_AVERAGE_CHANGE] = round( - self.attr[STAT_AVERAGE_CHANGE], self._precision - ) - self.attr[STAT_CHANGE_RATE] = round( - self.attr[STAT_CHANGE_RATE], self._precision - ) - async def async_update(self): """Get the latest data and updates the states.""" _LOGGER.debug("%s: updating statistics", self.entity_id) if self._samples_max_age is not None: self._purge_old() - self._update_characteristics() + self._update_attributes() + self._update_value() # If max_age is set, ensure to update again after the defined interval. next_to_purge_timestamp = self._next_to_purge_timestamp() @@ -480,3 +435,165 @@ class StatisticsSensor(SensorEntity): self.async_schedule_update_ha_state(True) _LOGGER.debug("%s: initializing from database completed", self.entity_id) + + def _update_attributes(self): + """Calculate and update the various attributes.""" + self.attributes[STAT_BUFFER_USAGE_RATIO] = round( + len(self.states) / self._samples_max_buffer_size, 2 + ) + + if len(self.states) >= 1 and self._samples_max_age is not None: + self.attributes[STAT_AGE_COVERAGE_RATIO] = round( + (self.ages[-1] - self.ages[0]).total_seconds() + / self._samples_max_age.total_seconds(), + 2, + ) + else: + self.attributes[STAT_AGE_COVERAGE_RATIO] = STATE_UNKNOWN + + def _update_value(self): + """Front to call the right statistical characteristics functions. + + One of the _stat_*() functions is represented by self._state_characteristic_fn(). + """ + + if self.is_binary: + self._value = len(self.states) + return + + value = self._state_characteristic_fn() + + if self._state_characteristic not in STATS_NOT_A_NUMBER: + with contextlib.suppress(TypeError): + value = round(value, self._precision) + if self._precision == 0: + value = int(value) + self._value = value + + def _stat_average_linear(self): + if len(self.states) >= 2: + area = 0 + for i in range(1, len(self.states)): + area += ( + 0.5 + * (self.states[i] + self.states[i - 1]) + * (self.ages[i] - self.ages[i - 1]).total_seconds() + ) + age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() + return area / age_range_seconds + return STATE_UNKNOWN + + def _stat_average_step(self): + if len(self.states) >= 2: + area = 0 + for i in range(1, len(self.states)): + area += ( + self.states[i - 1] + * (self.ages[i] - self.ages[i - 1]).total_seconds() + ) + age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() + return area / age_range_seconds + return STATE_UNKNOWN + + def _stat_average_timeless(self): + return self._stat_mean() + + def _stat_change(self): + if len(self.states) > 0: + return self.states[-1] - self.states[0] + return STATE_UNKNOWN + + def _stat_change_sample(self): + if len(self.states) > 1: + return (self.states[-1] - self.states[0]) / (len(self.states) - 1) + return STATE_UNKNOWN + + def _stat_change_second(self): + if len(self.states) > 1: + age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() + if age_range_seconds > 0: + return (self.states[-1] - self.states[0]) / age_range_seconds + return STATE_UNKNOWN + + def _stat_count(self): + return len(self.states) + + def _stat_datetime_newest(self): + if len(self.states) > 0: + return self.ages[-1] + return STATE_UNKNOWN + + def _stat_datetime_oldest(self): + if len(self.states) > 0: + return self.ages[0] + return STATE_UNKNOWN + + def _stat_distance_95_percent_of_values(self): + if len(self.states) >= 2: + return 2 * 1.96 * self._stat_standard_deviation() + return STATE_UNKNOWN + + def _stat_distance_99_percent_of_values(self): + if len(self.states) >= 2: + return 2 * 2.58 * self._stat_standard_deviation() + return STATE_UNKNOWN + + def _stat_distance_absolute(self): + if len(self.states) > 0: + return max(self.states) - min(self.states) + return STATE_UNKNOWN + + def _stat_mean(self): + if len(self.states) > 0: + return statistics.mean(self.states) + return STATE_UNKNOWN + + def _stat_median(self): + if len(self.states) > 0: + return statistics.median(self.states) + return STATE_UNKNOWN + + def _stat_noisiness(self): + if len(self.states) >= 2: + diff_sum = sum( + abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) + ) + return diff_sum / (len(self.states) - 1) + return STATE_UNKNOWN + + def _stat_quantiles(self): + if len(self.states) > self._quantile_intervals: + return [ + round(quantile, self._precision) + for quantile in statistics.quantiles( + self.states, + n=self._quantile_intervals, + method=self._quantile_method, + ) + ] + return STATE_UNKNOWN + + def _stat_standard_deviation(self): + if len(self.states) >= 2: + return statistics.stdev(self.states) + return STATE_UNKNOWN + + def _stat_total(self): + if len(self.states) > 0: + return sum(self.states) + return STATE_UNKNOWN + + def _stat_value_max(self): + if len(self.states) > 0: + return max(self.states) + return STATE_UNKNOWN + + def _stat_value_min(self): + if len(self.states) > 0: + return min(self.states) + return STATE_UNKNOWN + + def _stat_variance(self): + if len(self.states) >= 2: + return statistics.variance(self.states) + return STATE_UNKNOWN diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 4412dad843a..91e5f07497b 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,26 +41,13 @@ class TestStatisticsSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.values_binary = ["on", "off", "on", "off", "on", "off", "on"] self.values = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] - self.count = len(self.values) - self.min = min(self.values) - self.max = max(self.values) - self.total = sum(self.values) self.mean = round(sum(self.values) / len(self.values), 2) - self.median = round(statistics.median(self.values), 2) - self.deviation = round(statistics.stdev(self.values), 2) - self.variance = round(statistics.variance(self.values), 2) - self.quantiles = [ - round(quantile, 2) for quantile in statistics.quantiles(self.values) - ] - self.change = round(self.values[-1] - self.values[0], 2) - self.average_change = round(self.change / (len(self.values) - 1), 2) - self.change_rate = round(self.change / (60 * (self.count - 1)), 2) self.addCleanup(self.hass.stop) - def test_binary_sensor_source(self): - """Test if source is a sensor.""" - values = ["on", "off", "on", "off", "on", "off", "on"] + def test_sensor_defaults_binary(self): + """Test the general behavior of the sensor, with binary source sensor.""" assert setup_component( self.hass, "sensor", @@ -84,7 +71,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.start() self.hass.block_till_done() - for value in values: + for value in self.values_binary: self.hass.states.set( "binary_sensor.test_monitored", value, @@ -94,24 +81,29 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert state.state == str(len(values)) + assert state.state == str(len(self.values_binary)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(7 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes state = self.hass.states.get("sensor.test_unitless") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - def test_sensor_source(self): - """Test if source is a sensor.""" + def test_sensor_defaults_numeric(self): + """Test the general behavior of the sensor, with numeric source sensor.""" assert setup_component( self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + }, + ] }, ) @@ -128,21 +120,12 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert str(self.mean) == state.state - assert self.min == state.attributes.get("min_value") - assert self.max == state.attributes.get("max_value") - assert self.variance == state.attributes.get("variance") - assert self.median == state.attributes.get("median") - assert self.deviation == state.attributes.get("standard_deviation") - assert self.quantiles == state.attributes.get("quantiles") - assert self.mean == state.attributes.get("mean") - assert self.count == state.attributes.get("count") - assert self.total == state.attributes.get("total") - assert self.change == state.attributes.get("change") - assert self.average_change == state.attributes.get("average_change") - + assert state.state == str(self.mean) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes # Source sensor turns unavailable, then available with valid value, # statistics sensor should follow @@ -154,6 +137,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") assert new_state.state == STATE_UNAVAILABLE + assert new_state.attributes.get("source_value_valid") is None self.hass.states.set( "sensor.test_monitored", 0, @@ -161,35 +145,53 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") - assert new_state.state != STATE_UNAVAILABLE - assert new_state.attributes.get("count") == state.attributes.get("count") + 1 + new_mean = round(sum(self.values) / (len(self.values) + 1), 2) + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) + assert new_state.attributes.get("source_value_valid") is True - # Source sensor has a non-float state, unit and state should not change + # Source sensor has a nonnumerical state, unit and state should not change state = self.hass.states.get("sensor.test") self.hass.states.set("sensor.test_monitored", "beer", {}) self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") - assert state == new_state + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False + + # Source sensor has the STATE_UNKNOWN state, unit and state should not change + state = self.hass.states.get("sensor.test") + self.hass.states.set("sensor.test_monitored", STATE_UNKNOWN, {}) + self.hass.block_till_done() + new_state = self.hass.states.get("sensor.test") + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False # Source sensor is removed, unit and state should not change # This is equal to a None value being published self.hass.states.remove("sensor.test_monitored") self.hass.block_till_done() new_state = self.hass.states.get("sensor.test") - assert state == new_state + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False - def test_sampling_size(self): + def test_sampling_size_non_default(self): """Test rotation.""" assert setup_component( self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 5, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 5, + }, + ] }, ) @@ -206,9 +208,9 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - - assert state.attributes.get("min_value") == 3.8 - assert state.attributes.get("max_value") == 14 + new_mean = round(sum(self.values[-5:]) / len(self.values[-5:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(5 / 5, 2) def test_sampling_size_1(self): """Test validity of stats requiring only one sample.""" @@ -216,12 +218,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 1, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 1, + }, + ] }, ) @@ -238,23 +242,12 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") + new_mean = float(self.values[-1]) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(1 / 1, 2) - # require only one data point - assert self.values[-1] == state.attributes.get("min_value") - assert self.values[-1] == state.attributes.get("max_value") - assert self.values[-1] == state.attributes.get("mean") - assert self.values[-1] == state.attributes.get("median") - assert self.values[-1] == state.attributes.get("total") - assert state.attributes.get("change") == 0 - assert state.attributes.get("average_change") == 0 - - # require at least two data points - assert state.attributes.get("variance") == STATE_UNKNOWN - assert state.attributes.get("standard_deviation") == STATE_UNKNOWN - assert state.attributes.get("quantiles") == STATE_UNKNOWN - - def test_max_age(self): - """Test value deprecation.""" + def test_age_limit_expiry(self): + """Test that values are removed after certain age.""" now = dt_util.utcnow() mock_data = { "return_time": datetime(now.year + 1, 8, 2, 12, 23, tzinfo=dt_util.UTC) @@ -270,12 +263,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "max_age": {"minutes": 3}, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "max_age": {"minutes": 4}, + }, + ] }, ) @@ -290,118 +285,50 @@ class TestStatisticsSensor(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() - # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test") - - assert state.attributes.get("min_value") == 6 - assert state.attributes.get("max_value") == 14 - - def test_max_age_without_sensor_change(self): - """Test value deprecation.""" - now = dt_util.utcnow() - mock_data = { - "return_time": datetime(now.year + 1, 8, 2, 12, 23, tzinfo=dt_util.UTC) - } - - def mock_now(): - return mock_data["return_time"] - - with patch( - "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ): - assert setup_component( - self.hass, - "sensor", - { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "max_age": {"minutes": 3}, - } - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - # insert the next value 30 seconds later - mock_data["return_time"] += timedelta(seconds=30) + # After adding all values, we should only see 5 values in memory state = self.hass.states.get("sensor.test") + new_mean = round(sum(self.values[-5:]) / len(self.values[-5:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(5 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 1.0 - assert state.attributes.get("min_value") == 3.8 - assert state.attributes.get("max_value") == 15.2 + # Values expire over time. Only two are left - # wait for 3 minutes (max_age). - mock_data["return_time"] += timedelta(minutes=3) + mock_data["return_time"] += timedelta(minutes=2) fire_time_changed(self.hass, mock_data["return_time"]) self.hass.block_till_done() state = self.hass.states.get("sensor.test") + new_mean = round(sum(self.values[-2:]) / len(self.values[-2:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(2 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 1 / 4 - assert state.attributes.get("min_value") == STATE_UNKNOWN - assert state.attributes.get("max_value") == STATE_UNKNOWN - assert state.attributes.get("count") == 0 - - def test_change_rate(self): - """Test min_age/max_age and change_rate.""" - now = dt_util.utcnow() - mock_data = { - "return_time": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) - } - - def mock_now(): - return mock_data["return_time"] - - with patch( - "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ): - assert setup_component( - self.hass, - "sensor", - { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - } - }, - ) + # Values expire over time. Only one is left + mock_data["return_time"] += timedelta(minutes=1) + fire_time_changed(self.hass, mock_data["return_time"]) self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - # insert the next value one minute later - mock_data["return_time"] += timedelta(minutes=1) state = self.hass.states.get("sensor.test") + new_mean = float(self.values[-1]) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(1 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 0 - assert datetime( - now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC - ) == state.attributes.get("min_age") - assert datetime( - now.year + 1, 8, 2, 12, 23 + self.count - 1, 42, tzinfo=dt_util.UTC - ) == state.attributes.get("max_age") - assert self.change_rate == state.attributes.get("change_rate") + # Values expire over time. Memory is empty + + mock_data["return_time"] += timedelta(minutes=1) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == STATE_UNKNOWN def test_precision_0(self): """Test correct result with precision=0 as integer.""" @@ -409,12 +336,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "precision": 0, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "precision": 0, + }, + ] }, ) @@ -431,7 +360,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert state.state == str(int(state.attributes.get("mean"))) + assert state.state == str(int(round(self.mean))) def test_precision_1(self): """Test correct result with precision=1 rounded to one decimal.""" @@ -439,12 +368,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "precision": 1, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "precision": 1, + }, + ] }, ) @@ -463,72 +394,6 @@ class TestStatisticsSensor(unittest.TestCase): state = self.hass.states.get("sensor.test") assert state.state == str(round(sum(self.values) / len(self.values), 1)) - def test_state_characteristic_unit(self): - """Test statistics characteristic selection (via config).""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test_min_age", - "entity_id": "sensor.test_monitored", - "state_characteristic": "min_age", - }, - { - "platform": "statistics", - "name": "test_variance", - "entity_id": "sensor.test_monitored", - "state_characteristic": "variance", - }, - { - "platform": "statistics", - "name": "test_average_change", - "entity_id": "sensor.test_monitored", - "state_characteristic": "average_change", - }, - { - "platform": "statistics", - "name": "test_change_rate", - "entity_id": "sensor.test_monitored", - "state_characteristic": "change_rate", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set( - "sensor.test_monitored_unitless", - value, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test_min_age") - assert state.state == str(state.attributes.get("min_age")) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = self.hass.states.get("sensor.test_variance") - assert state.state == str(state.attributes.get("variance")) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "²" - state = self.hass.states.get("sensor.test_average_change") - assert state.state == str(state.attributes.get("average_change")) - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "/sample" - ) - state = self.hass.states.get("sensor.test_change_rate") - assert state.state == str(state.attributes.get("change_rate")) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + "/s" - def test_state_class(self): """Test state class, which depends on the characteristic configured.""" assert setup_component( @@ -546,7 +411,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test_nan", "entity_id": "sensor.test_monitored", - "state_characteristic": "min_age", + "state_characteristic": "datetime_oldest", }, ] }, @@ -592,7 +457,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test_unitless_3", "entity_id": "sensor.test_monitored_unitless", - "state_characteristic": "change_rate", + "state_characteristic": "change_second", }, ] }, @@ -616,7 +481,281 @@ class TestStatisticsSensor(unittest.TestCase): state = self.hass.states.get("sensor.test_unitless_3") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + def test_state_characteristics(self): + """Test configured state characteristic for value and unit.""" + now = dt_util.utcnow() + mock_data = { + "return_time": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) + } + + def mock_now(): + return mock_data["return_time"] + + value_spacing_minutes = 1 + + characteristics = ( + { + "name": "average_linear", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 10.68, + "unit": "°C", + }, + { + "name": "average_step", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 11.36, + "unit": "°C", + }, + { + "name": "average_timeless", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(self.mean), + "unit": "°C", + }, + { + "name": "change", + "value_0": STATE_UNKNOWN, + "value_1": float(0), + "value_9": float(round(self.values[-1] - self.values[0], 2)), + "unit": "°C", + }, + { + "name": "change_sample", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float( + round( + (self.values[-1] - self.values[0]) / (len(self.values) - 1), 2 + ) + ), + "unit": "°C/sample", + }, + { + "name": "change_second", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float( + round( + (self.values[-1] - self.values[0]) + / (60 * (len(self.values) - 1)), + 2, + ) + ), + "unit": "°C/s", + }, + { + "name": "count", + "value_0": 0, + "value_1": 1, + "value_9": len(self.values), + "unit": None, + }, + { + "name": "datetime_newest", + "value_0": STATE_UNKNOWN, + "value_1": datetime( + now.year + 1, + 8, + 2, + 12, + 23 + len(self.values) + 10, + 42, + tzinfo=dt_util.UTC, + ), + "value_9": datetime( + now.year + 1, + 8, + 2, + 12, + 23 + len(self.values) - 1, + 42, + tzinfo=dt_util.UTC, + ), + "unit": None, + }, + { + "name": "datetime_oldest", + "value_0": STATE_UNKNOWN, + "value_1": datetime( + now.year + 1, + 8, + 2, + 12, + 23 + len(self.values) + 10, + 42, + tzinfo=dt_util.UTC, + ), + "value_9": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC), + "unit": None, + }, + { + "name": "distance_95_percent_of_values", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(2 * 1.96 * statistics.stdev(self.values), 2)), + "unit": "°C", + }, + { + "name": "distance_99_percent_of_values", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(2 * 2.58 * statistics.stdev(self.values), 2)), + "unit": "°C", + }, + { + "name": "distance_absolute", + "value_0": STATE_UNKNOWN, + "value_1": float(0), + "value_9": float(max(self.values) - min(self.values)), + "unit": "°C", + }, + { + "name": "mean", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(self.mean), + "unit": "°C", + }, + { + "name": "median", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(round(statistics.median(self.values), 2)), + "unit": "°C", + }, + { + "name": "noisiness", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float( + round(sum([3, 4.8, 10.2, 1.2, 5.4, 2.5, 7.3, 8]) / 8, 2) + ), + "unit": "°C", + }, + { + "name": "quantiles", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": [ + round(quantile, 2) for quantile in statistics.quantiles(self.values) + ], + "unit": None, + }, + { + "name": "standard_deviation", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(statistics.stdev(self.values), 2)), + "unit": "°C", + }, + { + "name": "total", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(sum(self.values)), + "unit": "°C", + }, + { + "name": "value_max", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(max(self.values)), + "unit": "°C", + }, + { + "name": "value_min", + "value_0": STATE_UNKNOWN, + "value_1": float(self.values[0]), + "value_9": float(min(self.values)), + "unit": "°C", + }, + { + "name": "variance", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(statistics.variance(self.values), 2)), + "unit": "°C²", + }, + ) + sensors_config = [] + for characteristic in characteristics: + sensors_config.append( + { + "platform": "statistics", + "name": "test_" + characteristic["name"], + "entity_id": "sensor.test_monitored", + "state_characteristic": characteristic["name"], + "max_age": {"minutes": 10}, + } + ) + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ): + assert setup_component( + self.hass, + "sensor", + {"sensor": sensors_config}, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + # With all values in buffer + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + mock_data["return_time"] += timedelta(minutes=value_spacing_minutes) + + for characteristic in characteristics: + state = self.hass.states.get("sensor.test_" + characteristic["name"]) + assert state.state == str(characteristic["value_9"]), ( + f"value mismatch for characteristic '{characteristic['name']}' (buffer filled) " + f"- assert {state.state} == {str(characteristic['value_9'])}" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == characteristic["unit"] + ), f"unit mismatch for characteristic '{characteristic['name']}'" + + # With empty buffer + + mock_data["return_time"] += timedelta(minutes=10) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() + + for characteristic in characteristics: + state = self.hass.states.get("sensor.test_" + characteristic["name"]) + assert state.state == str(characteristic["value_0"]), ( + f"value mismatch for characteristic '{characteristic['name']}' (buffer empty) " + f"- assert {state.state} == {str(characteristic['value_0'])}" + ) + + # With single value in buffer + + self.hass.states.set( + "sensor.test_monitored", + self.values[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + mock_data["return_time"] += timedelta(minutes=1) + + for characteristic in characteristics: + state = self.hass.states.get("sensor.test_" + characteristic["name"]) + assert state.state == str(characteristic["value_1"]), ( + f"value mismatch for characteristic '{characteristic['name']}' (one stored value) " + f"- assert {state.state} == {str(characteristic['value_1'])}" + ) def test_initialize_from_database(self): """Test initializing the statistics from the database.""" @@ -640,12 +779,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 100, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 100, + }, + ] }, ) @@ -673,13 +814,6 @@ class TestStatisticsSensor(unittest.TestCase): def mock_purge(self): return - # Set maximum age to 3 hours. - max_age = 3 - # Determine what our minimum age should be based on test values. - expected_min_age = mock_data["return_time"] + timedelta( - hours=len(self.values) - max_age - ) - # enable the recorder init_recorder_component(self.hass) self.hass.block_till_done() @@ -707,13 +841,16 @@ class TestStatisticsSensor(unittest.TestCase): self.hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 100, - "max_age": {"hours": max_age}, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 100, + "state_characteristic": "datetime_newest", + "max_age": {"hours": 3}, + }, + ] }, ) self.hass.block_till_done() @@ -725,12 +862,12 @@ class TestStatisticsSensor(unittest.TestCase): # check if the result is as in test_sensor_source() state = self.hass.states.get("sensor.test") - assert expected_min_age == state.attributes.get("min_age") + assert state.attributes.get("age_coverage_ratio") == round(2 / 3, 2) # The max_age timestamp should be 1 hour before what we have right # now in mock_data['return_time']. - assert mock_data["return_time"] == state.attributes.get("max_age") + timedelta( - hours=1 - ) + assert mock_data["return_time"] == datetime.strptime( + state.state, "%Y-%m-%d %H:%M:%S%z" + ) + timedelta(hours=1) async def test_reload(hass): @@ -744,12 +881,14 @@ async def test_reload(hass): hass, "sensor", { - "sensor": { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 100, - } + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 100, + }, + ] }, ) await hass.async_block_till_done() From d990fe1957fdb8cbe68f676aa2595861e7a8999b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Nov 2021 14:49:44 +0100 Subject: [PATCH 0827/1452] Update ssdp matching_domain constant (#60283) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c937f210368..a4dbbbf4576 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -56,7 +56,7 @@ ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" # Attributes for accessing info added by Home Assistant -ATTR_HA_MATCHING_DOMAINS = "x-homeassistant-matching-domains" +ATTR_HA_MATCHING_DOMAINS = "x_homeassistant_matching_domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] From 3bf12fcd29c42205a14082a4e7cbd7916d0c27e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Nov 2021 14:50:45 +0100 Subject: [PATCH 0828/1452] Use UsbServiceInfo and ZeroconfServiceInfo in zha (#60266) Co-authored-by: epenet --- homeassistant/components/zha/config_flow.py | 28 +-- tests/components/zha/test_config_flow.py | 185 ++++++++++---------- 2 files changed, 112 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index a3eaffdf1ba..4d6b22a82e5 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -8,9 +8,9 @@ import voluptuous as vol from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries -from homeassistant.components import usb -from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.components import usb, zeroconf +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult from .core.const import ( CONF_BAUDRATE, @@ -94,14 +94,14 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) - async def async_step_usb(self, discovery_info: DiscoveryInfoType): + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" - vid = discovery_info["vid"] - pid = discovery_info["pid"] - serial_number = discovery_info["serial_number"] - device = discovery_info["device"] - manufacturer = discovery_info["manufacturer"] - description = discovery_info["description"] + vid = discovery_info.vid + pid = discovery_info.pid + serial_number = discovery_info.serial_number + device = discovery_info.device + manufacturer = discovery_info.manufacturer + description = discovery_info.description dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device) unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" if current_entry := await self.async_set_unique_id(unique_id): @@ -159,12 +159,14 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({}), ) - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = discovery_info["hostname"][:-1] + local_name = discovery_info.hostname[:-1] node_name = local_name[: -len(".local")] - host = discovery_info[CONF_HOST] + host = discovery_info.host device_path = f"socket://{host}:6638" if current_entry := await self.async_set_unique_id(node_name): diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index bb91ccbeda3..12d9f6e1690 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -8,6 +8,7 @@ import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries +from homeassistant.components import usb, zeroconf from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER_URL, @@ -52,12 +53,14 @@ def com_port(): @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) async def test_discovery(detect_mock, hass): """Test zeroconf flow -- radio detected.""" - service_info = { - "host": "192.168.1.200", - "port": 6053, - "hostname": "_tube_zb_gw._tcp.local.", - "properties": {"name": "tube_123456"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.200", + hostname="_tube_zb_gw._tcp.local.", + name="mock_name", + port=6053, + properties={"name": "tube_123456"}, + type="mock_type", + ) flow = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) @@ -94,12 +97,14 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): ) entry.add_to_hass(hass) - service_info = { - "host": "192.168.1.22", - "port": 6053, - "hostname": "tube_zb_gw_cc2652p2_poe.local.", - "properties": {"address": "tube_zb_gw_cc2652p2_poe.local"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.22", + hostname="tube_zb_gw_cc2652p2_poe.local.", + name="mock_name", + port=6053, + properties={"address": "tube_zb_gw_cc2652p2_poe.local"}, + type="mock_type", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) @@ -124,12 +129,14 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): ) entry.add_to_hass(hass) - service_info = { - "host": "192.168.1.22", - "port": 6053, - "hostname": "tube_zb_gw_cc2652p2_poe.local.", - "properties": {"address": "tube_zb_gw_cc2652p2_poe.local"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.22", + hostname="tube_zb_gw_cc2652p2_poe.local.", + name="mock_name", + port=6053, + properties={"address": "tube_zb_gw_cc2652p2_poe.local"}, + type="mock_type", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) @@ -144,14 +151,14 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) async def test_discovery_via_usb(detect_mock, hass): """Test usb flow -- radio detected.""" - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -180,14 +187,14 @@ async def test_discovery_via_usb(detect_mock, hass): @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=False) async def test_discovery_via_usb_no_radio(detect_mock, hass): """Test usb flow -- no radio detected.""" - discovery_info = { - "device": "/dev/null", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/null", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -213,14 +220,14 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} ).add_to_hass(hass) - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -247,14 +254,14 @@ async def test_discovery_via_usb_path_changes(hass): ) entry.add_to_hass(hass) - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -282,14 +289,14 @@ async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass): context={"source": SOURCE_SSDP}, ) await hass.async_block_till_done() - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -304,14 +311,14 @@ async def test_discovery_via_usb_deconz_already_setup(detect_mock, hass): """Test usb flow -- deconz setup.""" MockConfigEntry(domain="deconz", data={}).add_to_hass(hass) await hass.async_block_till_done() - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -328,14 +335,14 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): domain="deconz", source=config_entries.SOURCE_IGNORE, data={} ).add_to_hass(hass) await hass.async_block_till_done() - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -356,14 +363,14 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): ) entry.add_to_hass(hass) await hass.async_block_till_done() - discovery_info = { - "device": "/dev/ttyZIGBEE", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zigbee radio", - "manufacturer": "test", - } + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) result = await hass.config_entries.flow.async_init( "zha", context={"source": SOURCE_USB}, data=discovery_info ) @@ -380,12 +387,14 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) async def test_discovery_already_setup(detect_mock, hass): """Test zeroconf flow -- radio detected.""" - service_info = { - "host": "192.168.1.200", - "port": 6053, - "hostname": "_tube_zb_gw._tcp.local.", - "properties": {"name": "tube_123456"}, - } + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.200", + hostname="_tube_zb_gw._tcp.local.", + name="mock_name", + port=6053, + properties={"name": "tube_123456"}, + type="mock_type", + ) MockConfigEntry( domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} From 5a8cbb8cab901b69bddafb18f718feff5b16c199 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Nov 2021 14:51:28 +0100 Subject: [PATCH 0829/1452] Use UsbServiceInfo in zwave-js (#60267) Co-authored-by: epenet --- .../components/zwave_js/config_flow.py | 12 ++--- tests/components/zwave_js/test_config_flow.py | 49 ++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 86107cd52f8..023baf10bd4 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -345,12 +345,12 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): if self._async_in_progress(): return self.async_abort(reason="already_in_progress") - vid = discovery_info["vid"] - pid = discovery_info["pid"] - serial_number = discovery_info["serial_number"] - device = discovery_info["device"] - manufacturer = discovery_info["manufacturer"] - description = discovery_info["description"] + vid = discovery_info.vid + pid = discovery_info.pid + serial_number = discovery_info.serial_number + device = discovery_info.device + manufacturer = discovery_info.manufacturer + description = discovery_info.description # Zooz uses this vid/pid, but so do 2652 sticks if vid == "10C4" and pid == "EA60" and description and "2652" in description: return self.async_abort(reason="not_zwave_device") diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index a61d13be8eb..d5829283ff2 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from zwave_js_server.version import VersionInfo from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN @@ -20,32 +21,32 @@ ADDON_DISCOVERY_INFO = { } -USB_DISCOVERY_INFO = { - "device": "/dev/zwave", - "pid": "AAAA", - "vid": "AAAA", - "serial_number": "1234", - "description": "zwave radio", - "manufacturer": "test", -} +USB_DISCOVERY_INFO = usb.UsbServiceInfo( + device="/dev/zwave", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zwave radio", + manufacturer="test", +) -NORTEK_ZIGBEE_DISCOVERY_INFO = { - "device": "/dev/zigbee", - "pid": "8A2A", - "vid": "10C4", - "serial_number": "1234", - "description": "nortek zigbee radio", - "manufacturer": "nortek", -} +NORTEK_ZIGBEE_DISCOVERY_INFO = usb.UsbServiceInfo( + device="/dev/zigbee", + pid="8A2A", + vid="10C4", + serial_number="1234", + description="nortek zigbee radio", + manufacturer="nortek", +) -CP2652_ZIGBEE_DISCOVERY_INFO = { - "device": "/dev/zigbee", - "pid": "EA60", - "vid": "10C4", - "serial_number": "", - "description": "cp2652", - "manufacturer": "generic", -} +CP2652_ZIGBEE_DISCOVERY_INFO = usb.UsbServiceInfo( + device="/dev/zigbee", + pid="EA60", + vid="10C4", + serial_number="", + description="cp2652", + manufacturer="generic", +) @pytest.fixture(name="setup_entry") From fa34153b20b2c40a5690951bbd242e3b13ec0d34 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 15:00:54 +0100 Subject: [PATCH 0830/1452] Use UTC timestamp in uptime sensor (#60240) --- homeassistant/components/uptime/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 9a48b09e452..4afddc0f834 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_native_value = dt_util.now() + self._attr_native_value = dt_util.utcnow() From d33457b7bc094aabeeaf4f1d4fe51e7adcf3b92c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Nov 2021 15:15:27 +0100 Subject: [PATCH 0831/1452] Add bytes support for bitwise template operations (#60211) * Add bytes support for bitwise template operations * spelling * Update bitwise tests * remove try block for bytes conversion * do not accept empty `bytes` object --- homeassistant/helpers/template.py | 19 ++++++--- homeassistant/util/__init__.py | 25 ++++++++++++ tests/helpers/test_template.py | 66 +++++++++++++++++++++++++++++++ tests/util/test_init.py | 16 ++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 07219942f98..3e2e3a55c3e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -52,7 +52,12 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass -from homeassistant.util import convert, dt as dt_util, location as loc_util +from homeassistant.util import ( + convert, + convert_to_int, + dt as dt_util, + location as loc_util, +) from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException @@ -1629,14 +1634,18 @@ def regex_findall(value, find="", ignorecase=False): return re.findall(find, value, flags) -def bitwise_and(first_value, second_value): +def bitwise_and(first_value, second_value, little_endian=False): """Perform a bitwise and operation.""" - return first_value & second_value + return convert_to_int(first_value, little_endian=little_endian) & convert_to_int( + second_value, little_endian=little_endian + ) -def bitwise_or(first_value, second_value): +def bitwise_or(first_value, second_value, little_endian=False): """Perform a bitwise or operation.""" - return first_value | second_value + return convert_to_int(first_value, little_endian=little_endian) | convert_to_int( + second_value, little_endian=little_endian + ) def base64_encode(value): diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 3840958d580..81f1389b47c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -109,6 +109,31 @@ def convert( return default +def convert_to_int( + value: Any, default: int | None = None, little_endian: bool = False +) -> int | None: + """Convert value or bytes to int, returns default if fails. + + This supports bitwise integer operations on `bytes` objects. + By default the conversion is in Big-endian style (The last byte contains the least significant bit). + In Little-endian style the first byte contains the least significant bit. + """ + if isinstance(value, int): + return value + if isinstance(value, bytes) and value: + bytes_value = bytearray(value) + return_value = 0 + while len(bytes_value): + return_value <<= 8 + if little_endian: + return_value |= bytes_value.pop(len(bytes_value) - 1) + else: + return_value |= bytes_value.pop(0) + + return return_value + return convert(value, int, default=default) + + def ensure_unique_string( preferred_string: str, current_strings: Iterable[str] | KeysView[str] ) -> str: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d6649b058e2..a50884b71c8 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1452,6 +1452,24 @@ def test_bitwise_and(hass): ) assert tpl.async_render() == 8 & 2 + tpl = template.Template( + """ +{{ ( value_a ) | bitwise_and(value_b) }} + """, + hass, + ) + variables = {"value_a": b"\x9b\xc2", "value_b": 0xFF00} + assert tpl.async_render(variables=variables) == 0x9B00 + + tpl = template.Template( + """ +{{ ( value_a ) | bitwise_and(value_b, little_endian=True) }} + """, + hass, + ) + variables = {"value_a": b"\xc2\x9b", "value_b": 0xFFFF} + assert tpl.async_render(variables=variables) == 0x9BC2 + def test_bitwise_or(hass): """Test bitwise_or method.""" @@ -1477,6 +1495,54 @@ def test_bitwise_or(hass): ) assert tpl.async_render() == 8 | 2 + tpl = template.Template( + """ +{{ value_a | bitwise_or(value_b) }} + """, + hass, + ) + variables = { + "value_a": b"\xc2\x9b", + "value_b": 0xFFFF, + } + assert tpl.async_render(variables=variables) == 65535 # 39874 + + tpl = template.Template( + """ +{{ ( value_a ) | bitwise_or(value_b) }} + """, + hass, + ) + variables = { + "value_a": 0xFF00, + "value_b": b"\xc2\x9b", + } + assert tpl.async_render(variables=variables) == 0xFF9B + + tpl = template.Template( + """ +{{ ( value_a ) | bitwise_or(value_b) }} + """, + hass, + ) + variables = { + "value_a": b"\xc2\x9b", + "value_b": 0x0000, + } + assert tpl.async_render(variables=variables) == 0xC29B + + tpl = template.Template( + """ +{{ ( value_a ) | bitwise_or(value_b, little_endian=True) }} + """, + hass, + ) + variables = { + "value_a": b"\xc2\x9b", + "value_b": 0, + } + assert tpl.async_render(variables=variables) == 0x9BC2 + def test_distance_function_with_1_state(hass): """Test distance function with 1 state.""" diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 7a4f13cb767..0634f3cb6cf 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -98,6 +98,22 @@ def test_convert(): assert util.convert(object, int, 1) == 1 +def test_convert_to_int(): + """Test convert of bytes and numbers to int.""" + assert util.convert_to_int(b"\x9b\xc2") == 39874 + assert util.convert_to_int(b"") is None + assert util.convert_to_int(b"\x9b\xc2", 10) == 39874 + assert util.convert_to_int(b"\xc2\x9b", little_endian=True) == 39874 + assert util.convert_to_int(b"\xc2\x9b", 10, little_endian=True) == 39874 + assert util.convert_to_int("abc", 10) == 10 + assert util.convert_to_int("11.0", 10) == 10 + assert util.convert_to_int("12", 10) == 12 + assert util.convert_to_int("\xc2\x9b", 10) == 10 + assert util.convert_to_int(None, 10) == 10 + assert util.convert_to_int(None) is None + assert util.convert_to_int("NOT A NUMBER", 1) == 1 + + def test_ensure_unique_string(): """Test ensure_unique_string.""" assert util.ensure_unique_string("Beer", ["Beer", "Beer_2"]) == "Beer_3" From 7b57033265edc5c4f53b10148d962b292ab58a71 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 15:51:43 +0100 Subject: [PATCH 0832/1452] Correct today_at template function / filter (#60291) Co-authored-by: Franck Nijhof --- homeassistant/helpers/template.py | 16 +++--- tests/helpers/test_template.py | 81 ++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3e2e3a55c3e..62e9f3fab6a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1689,16 +1689,16 @@ def random_every_time(context, values): def today_at(time_str: str = "") -> datetime: """Record fetching now where the time has been replaced with value.""" - start = dt_util.start_of_local_day(datetime.now()) + today = dt_util.start_of_local_day() + if not time_str: + return today - dttime = start.time() if time_str == "" else dt_util.parse_time(time_str) + if (time_today := dt_util.parse_time(time_str)) is None: + raise ValueError( + f"could not convert {type(time_str).__name__} to datetime: '{time_str}'" + ) - if dttime: - return datetime.combine(start.date(), dttime, tzinfo=dt_util.DEFAULT_TIME_ZONE) - - raise ValueError( - f"could not convert {type(time_str).__name__} to datetime: '{time_str}'" - ) + return datetime.combine(today, time_today, today.tzinfo) def relative_time(value): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index a50884b71c8..bd405acf597 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5,6 +5,7 @@ import math import random from unittest.mock import patch +from freezegun import freeze_time import pytest import voluptuous as vol @@ -1149,42 +1150,68 @@ def test_utcnow(mock_is_safe, hass): assert info.has_time is True +@pytest.mark.parametrize( + "now, expected, expected_midnight, timezone_str", + [ + # Host clock in UTC + ( + "2021-11-24 03:00:00+00:00", + "2021-11-23T10:00:00-08:00", + "2021-11-23T00:00:00-08:00", + "America/Los_Angeles", + ), + # Host clock in local time + ( + "2021-11-23 19:00:00-08:00", + "2021-11-23T10:00:00-08:00", + "2021-11-23T00:00:00-08:00", + "America/Los_Angeles", + ), + ], +) @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) -def test_today_at(mock_is_safe, hass): +def test_today_at(mock_is_safe, hass, now, expected, expected_midnight, timezone_str): """Test today_at method.""" - now = dt_util.now() - with patch("homeassistant.util.dt.now", return_value=now): - now = now.replace(hour=10, minute=0, second=0, microsecond=0) - result = template.Template( - "{{ today_at('10:00').isoformat() }}", - hass, - ).async_render() - assert result == now.isoformat() + freezer = freeze_time(now) + freezer.start() - result = template.Template( - "{{ today_at('10:00:00').isoformat() }}", - hass, - ).async_render() - assert result == now.isoformat() + original_tz = dt_util.DEFAULT_TIME_ZONE - result = template.Template( - "{{ ('10:00:00' | today_at).isoformat() }}", - hass, - ).async_render() - assert result == now.isoformat() + timezone = dt_util.get_time_zone(timezone_str) + dt_util.set_default_time_zone(timezone) - now = now.replace(hour=0) - result = template.Template( - "{{ today_at().isoformat() }}", - hass, - ).async_render() - assert result == now.isoformat() + result = template.Template( + "{{ today_at('10:00').isoformat() }}", + hass, + ).async_render() + assert result == expected - with pytest.raises(TemplateError): - template.Template("{{ today_at('bad') }}", hass).async_render() + result = template.Template( + "{{ today_at('10:00:00').isoformat() }}", + hass, + ).async_render() + assert result == expected + + result = template.Template( + "{{ ('10:00:00' | today_at).isoformat() }}", + hass, + ).async_render() + assert result == expected + + result = template.Template( + "{{ today_at().isoformat() }}", + hass, + ).async_render() + assert result == expected_midnight + + with pytest.raises(TemplateError): + template.Template("{{ today_at('bad') }}", hass).async_render() + + freezer.stop() + dt_util.set_default_time_zone(original_tz) @patch( From fb40a5c0d1578d96903704953832d41618a4bf34 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 18:14:41 +0100 Subject: [PATCH 0833/1452] Partial CI workflows: take 2 (#60294) --- .github/workflows/ci.yaml | 353 ++++++++++++++++++-------------------- 1 file changed, 167 insertions(+), 186 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c9a9ac4eff8..4a35f7b8e35 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,106 @@ concurrency: cancel-in-progress: true jobs: + changes: + name: Determine what has changed + outputs: + # In case of issues with the partial run, use the following line instead: + # test_full_suite: 'true' + test_full_suite: ${{ steps.info.outputs.test_full_suite }} + core: ${{ steps.core.outputs.changes }} + integrations: ${{ steps.integrations.outputs.changes }} + integrations_glob: ${{ steps.info.outputs.integrations_glob }} + tests: ${{ steps.info.outputs.tests }} + tests_glob: ${{ steps.info.outputs.tests_glob }} + test_groups: ${{ steps.info.outputs.test_groups }} + test_group_count: ${{ steps.info.outputs.test_group_count }} + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Filter for core changes + uses: dorny/paths-filter@v2.10.2 + id: core + with: + filters: .core_files.yaml + - name: Create a list of integrations to filter for changes + run: | + integrations=$(ls -Ad ./homeassistant/components/[!_]* | xargs -n 1 basename) + touch .integration_paths.yaml + for integration in $integrations; do + echo "${integration}: [homeassistant/components/${integration}/*, tests/components/${integration}/*]" \ + >> .integration_paths.yaml; + done + echo "Result:" + cat .integration_paths.yaml + - name: Filter for integration changes + uses: dorny/paths-filter@v2.10.2 + id: integrations + with: + filters: .integration_paths.yaml + - name: Collect additional information + id: info + run: | + # Defaults + integrations_glob="" + test_full_suite="true" + test_groups="[1, 2, 3, 4, 5, 6]" + test_group_count=6 + tests="[]" + tests_glob="" + + if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]]; + then + # Create a file glob for the integrations + integrations_glob=$(echo '${{ steps.integrations.outputs.changes }}' | jq -cSr '. | join(",")') + [[ "${integrations_glob}" == *","* ]] && integrations_glob="{${integrations_glob}}" + + # Create list of testable integrations + possible_integrations=$(echo '${{ steps.integrations.outputs.changes }}' | jq -cSr '.[]') + tests=$( + for integration in ${possible_integrations}; + do + if [[ -d "tests/components/${integration}" ]]; then + echo -n "\"${integration}\","; + fi; + done + ) + + [[ ! -z "${tests}" ]] && tests="${tests::-1}" + tests="[${tests}]" + test_groups="${tests}" + # Test group count should be 1, we don't split partial tests + test_group_count=1 + + # Create a file glob for the integrations tests + tests_glob=$(echo "${tests}" | jq -cSr '. | join(",")') + [[ "${tests_glob}" == *","* ]] && tests_glob="{${tests_glob}}" + + test_full_suite="false" + fi + + # We need to run the full suite on certain branches + if [[ "${{ github.ref }}" == "refs/heads/dev" ]] \ + || [[ "${{ github.ref }}" == "refs/heads/master" ]] \ + || [[ "${{ github.ref }}" == "refs/heads/rc" ]]; + then + test_full_suite="true" + fi + + # Output & sent to GitHub Actions + echo "test_full_suite: ${test_full_suite}" + echo "::set-output name=test_full_suite::${test_full_suite}" + echo "integrations_glob: ${integrations_glob}" + echo "::set-output name=integrations_glob::${integrations_glob}" + echo "test_group_count: ${test_group_count}" + echo "::set-output name=test_group_count::${test_group_count}" + echo "test_groups: ${test_groups}" + echo "::set-output name=test_groups::${test_groups}" + echo "tests: ${tests}" + echo "::set-output name=tests::${tests}" + echo "tests_glob: ${tests_glob}" + echo "::set-output name=tests_glob::${tests_glob}" + # Separate job to pre-populate the base dependency cache # This prevent upcoming jobs to do the same individually prepare-base: @@ -85,7 +185,9 @@ jobs: lint-bandit: name: Check bandit runs-on: ubuntu-latest - needs: prepare-base + needs: + - changes + - prepare-base steps: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 @@ -117,15 +219,23 @@ jobs: run: | echo "Failed to restore pre-commit environment from cache" exit 1 - - name: Run bandit + - name: Run bandit (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure + - name: Run bandit (partially) + if: needs.changes.outputs.test_full_suite == 'false' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure lint-black: name: Check black runs-on: ubuntu-latest - needs: prepare-base + needs: + - changes + - prepare-base steps: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 @@ -157,15 +267,23 @@ jobs: run: | echo "Failed to restore pre-commit environment from cache" exit 1 - - name: Run black + - name: Run black (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual black --all-files --show-diff-on-failure + - name: Run black (partially) + if: needs.changes.outputs.test_full_suite == 'false' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure lint-flake8: name: Check flake8 runs-on: ubuntu-latest - needs: prepare-base + needs: + - changes + - prepare-base steps: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 @@ -200,10 +318,16 @@ jobs: - name: Register flake8 problem matcher run: | echo "::add-matcher::.github/workflows/matchers/flake8.json" - - name: Run flake8 + - name: Run flake8 (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --all-files + - name: Run flake8 (partially) + if: needs.changes.outputs.test_full_suite == 'false' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* lint-isort: name: Check isort @@ -436,7 +560,9 @@ jobs: pylint: name: Check pylint runs-on: ubuntu-latest - needs: prepare-tests + needs: + - changes + - prepare-tests strategy: matrix: python-version: [3.8] @@ -459,15 +585,23 @@ jobs: - name: Register pylint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pylint.json" - - name: Run pylint + - name: Run pylint (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate pylint homeassistant + - name: Run pylint (partially) + if: needs.changes.outputs.test_full_suite == 'false' + run: | + . venv/bin/activate + pylint homeassistant/components/${{ needs.changes.outputs.integrations_glob }} mypy: name: Check mypy runs-on: ubuntu-latest - needs: prepare-tests + needs: + - changes + - prepare-tests strategy: matrix: python-version: [3.8] @@ -490,62 +624,20 @@ jobs: - name: Register mypy problem matcher run: | echo "::add-matcher::.github/workflows/matchers/mypy.json" - - name: Run mypy + - name: Run mypy (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate mypy homeassistant - - changes: - name: Determine what has changed - outputs: - # core: ${{ steps.core.outputs.any }} - # Temporary disable - core: 'true' - integrations: ${{ steps.integrations.outputs.changes }} - tests: ${{ steps.tests.outputs.integrations }} - runs-on: ubuntu-latest - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Filter for core changes - uses: dorny/paths-filter@v2.10.2 - id: core - with: - filters: .core_files.yaml - - name: Create a list of integrations to filter for changes - id: integration-filters + - name: Run mypy (partially) + if: needs.changes.outputs.test_full_suite == 'false' run: | - integrations=$(ls -Ad ./homeassistant/components/[!_]* | xargs -n 1 basename) - touch .integration_paths.yaml - for integration in $integrations; do - echo "${integration}: [homeassistant/components/${integration}/*, tests/components/${integration}/*]" \ - >> .integration_paths.yaml; - done - echo "Result:" - cat .integration_paths.yaml - - name: Filter for integration changes - uses: dorny/paths-filter@v2.10.2 - id: integrations - with: - filters: .integration_paths.yaml - - name: Determine integration tests to run - if: ${{ steps.integrations.outputs.changes }} - id: tests - run: | - possible_integrations=$(echo '${{ steps.integrations.outputs.changes }}' | jq -cSr '. | join(" ")') - integrations=$(for integration in $possible_integrations; do [[ -d "tests/components/${integration}" ]] && echo -n "${integration},"; done) - integrations="${integrations::-1}" + . venv/bin/activate + mypy homeassistant/components/${{ needs.changes.outputs.integrations_glob }} - # If more than one, add brackets to it - if [[ "${integrations}" == *","* ]]; then - integrations="{${integrations}}" - fi - - echo "::set-output name=integrations::${integrations}" - - pytest-full: - if: ${{ needs.changes.outputs.core == 'true' }} + pytest: runs-on: ubuntu-latest + if: needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob needs: - changes - gen-requirements-all @@ -559,10 +651,10 @@ jobs: strategy: fail-fast: false matrix: - group: [1, 2, 3, 4, 5, 6] + group: ${{ fromJson(needs.changes.outputs.test_groups) }} python-version: [3.8, 3.9] name: >- - Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) + Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub @@ -589,7 +681,8 @@ jobs: # However this plugin is fairly new and doesn't run correctly # on a non-GitHub environment. pip install pytest-github-actions-annotate-failures==0.1.3 - - name: Run pytest + - name: Run pytest (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate python3 -X dev -m pytest \ @@ -598,61 +691,15 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count 6 \ + --test-group-count ${{ needs.changes.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ --cov homeassistant \ --cov-report= \ -o console_output_style=count \ -p no:sugar \ tests - - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.4 - with: - name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} - path: .coverage - - name: Check dirty - run: | - ./script/check_dirty - - pytest-partial: - if: ${{ needs.changes.outputs.core == 'false' }} - runs-on: ubuntu-latest - needs: - - changes - - prepare-tests - strategy: - fail-fast: false - matrix: - python-version: [3.8, 3.9] - name: >- - Run partial tests Python ${{ matrix.python-version }} - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures==0.1.3 - - name: Run pytest + - name: Run pytest (partially) + if: needs.changes.outputs.test_full_suite == 'false' run: | . venv/bin/activate python3 -X dev -m pytest \ @@ -661,85 +708,19 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --cov homeassistant \ + --cov homeassistant.components.${{ matrix.group }} \ --cov-report= \ -o console_output_style=count \ -p no:sugar \ - tests/components/${{ needs.changes.outputs.tests }} - - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.4 + tests/components/${{ matrix.group }} + - name: Upload coverage to Codecov (full coverage) + if: needs.changes.outputs.test_full_suite == 'true' + uses: codecov/codecov-action@v2.1.0 + - name: Upload coverage to Codecov (partial coverage) + if: needs.changes.outputs.test_full_suite == 'false' + uses: codecov/codecov-action@v2.1.0 with: - name: coverage-${{ matrix.python-version }} - path: .coverage + flags: ${{ matrix.group }} - name: Check dirty run: | ./script/check_dirty - - coverage-full: - name: Process test coverage - runs-on: ubuntu-latest - needs: ["prepare-tests", "pytest-full"] - strategy: - matrix: - python-version: [3.8] - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Download all coverage artifacts - uses: actions/download-artifact@v2 - - name: Combine coverage results - run: | - . venv/bin/activate - coverage combine coverage*/.coverage* - coverage report --fail-under=94 - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2.1.0 - - coverage-partial: - name: Process partial test coverage - runs-on: ubuntu-latest - needs: ["prepare-tests", "pytest-partial"] - strategy: - matrix: - python-version: [3.8] - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Download all coverage artifacts - uses: actions/download-artifact@v2 - - name: Combine coverage results - run: | - . venv/bin/activate - coverage combine coverage*/.coverage* - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2.1.0 - with: - flags: partial From 29f1abd2d5d1c73ddf32540af835b7d87ed3ccb2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 18:43:58 +0100 Subject: [PATCH 0834/1452] CI: Fix full suite on core file changes (#60299) --- .github/workflows/ci.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a35f7b8e35..02fa6fabd11 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -98,10 +98,12 @@ jobs: test_full_suite="false" fi - # We need to run the full suite on certain branches + # We need to run the full suite on certain branches. + # Or, in case core files are touched, for the full suite as well. if [[ "${{ github.ref }}" == "refs/heads/dev" ]] \ || [[ "${{ github.ref }}" == "refs/heads/master" ]] \ - || [[ "${{ github.ref }}" == "refs/heads/rc" ]]; + || [[ "${{ github.ref }}" == "refs/heads/rc" ]] \ + || [[ "${{ steps.core.outputs.any }}" == "true" ]]; then test_full_suite="true" fi From c5d480f0c9fdf8659863dfc62acb301f9b2bc293 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 19:03:33 +0100 Subject: [PATCH 0835/1452] CI: Fix test groups when running full CI suite (#60300) --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 02fa6fabd11..aa25f426037 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -105,6 +105,8 @@ jobs: || [[ "${{ github.ref }}" == "refs/heads/rc" ]] \ || [[ "${{ steps.core.outputs.any }}" == "true" ]]; then + test_groups="[1, 2, 3, 4, 5, 6]" + test_group_count=6 test_full_suite="true" fi From 39f5eba97d139e292136f08d488d243c01087d2b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 19:51:25 +0100 Subject: [PATCH 0836/1452] Use native datetime value in OpenWeatherMap sensors (#60303) --- .../components/openweathermap/sensor.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 07d122d6b00..fd18ef32725 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,7 +1,13 @@ """Support for the OpenWeatherMap (OWM) service.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from datetime import datetime + +from homeassistant.components.sensor import ( + DEVICE_CLASS_TIMESTAMP, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant @@ -10,6 +16,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import ( ATTR_API_FORECAST, @@ -143,9 +150,14 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): self._weather_coordinator = weather_coordinator @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) - if forecasts is not None and len(forecasts) > 0: - return forecasts[0].get(self.entity_description.key, None) - return None + if not forecasts: + return None + + value = forecasts[0].get(self.entity_description.key, None) + if value and self.entity_description.device_class == DEVICE_CLASS_TIMESTAMP: + return dt_util.parse_datetime(value) + + return value From 86cd46a0dd2096da3f2a80dd89669ad28dd0f05f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Nov 2021 12:55:00 -0600 Subject: [PATCH 0837/1452] Add support for adjusting effect brightness in flux_led (#60247) --- homeassistant/components/flux_led/light.py | 46 +++++++++++++++---- .../components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/number.py | 6 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_light.py | 25 +++++++--- tests/components/flux_led/test_number.py | 10 ++-- 7 files changed, 67 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0a0bccd4e6f..aecf1646726 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -46,7 +46,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PROTOCOL, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -88,6 +88,16 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLUX_LED: Final = SUPPORT_TRANSITION + +MODE_ATTRS = { + ATTR_EFFECT, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_WHITE, +} + # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 @@ -303,12 +313,13 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): await self._device.async_turn_on() if not kwargs: return - if effect := kwargs.get(ATTR_EFFECT): - await self._async_set_effect(effect) - return - await self._async_set_colors(**kwargs) - async def _async_set_effect(self, effect: str) -> None: + if MODE_ATTRS.intersection(kwargs): + await self._async_set_mode(**kwargs) + return + await self._async_adjust_brightness(self._async_brightness(**kwargs)) + + async def _async_set_effect(self, effect: str, brightness: int) -> None: """Set an effect.""" # Random color effect if effect == EFFECT_RANDOM: @@ -327,12 +338,14 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): self._custom_effect_transition, ) return + effect_brightness = round(brightness / 255 * 100) await self._device.async_set_effect( - effect, self._device.speed or DEFAULT_EFFECT_SPEED + effect, self._device.speed or DEFAULT_EFFECT_SPEED, effect_brightness ) - async def _async_set_colors(self, **kwargs: Any) -> None: - """Set color (can be done before turning on).""" + @callback + def _async_brightness(self, **kwargs: Any) -> int: + """Determine brightness from kwargs or current value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: brightness = self.brightness if not brightness: @@ -341,7 +354,15 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): # If the device was on and brightness was not # set, it means it was masked by an effect brightness = 255 if self.is_on else 1 + return brightness + async def _async_set_mode(self, **kwargs: Any) -> None: + """Set an effect or color mode.""" + brightness = self._async_brightness(**kwargs) + # Handle switch to Effect Mode + if effect := kwargs.get(ATTR_EFFECT): + await self._async_set_effect(effect, brightness) + return # Handle switch to CCT Color Mode if ATTR_COLOR_TEMP in kwargs: color_temp_mired = kwargs[ATTR_COLOR_TEMP] @@ -385,7 +406,12 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) return - # Handle brightness adjustment in CCT Color Mode + async def _async_adjust_brightness(self, brightness: int) -> None: + """Adjust brightness.""" + # Handle brightness adjustment in effect mode + if effect := self.effect: + await self._async_set_effect(effect, brightness) + return if self.color_mode == COLOR_MODE_COLOR_TEMP: await self._device.async_set_white_temp(self._device.color_temp, brightness) return diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 202a6bc1584..e42ad3b1afd 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.28"], + "requirements": ["flux_led==0.24.33"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 9ae581d8c88..3942f61e8fa 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -68,6 +68,7 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set the flux speed value.""" current_effect = self._device.effect + current_brightness = self._device.brightness new_speed = int(value) if not current_effect: raise HomeAssistantError( @@ -75,5 +76,8 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): ) if self._device.original_addressable and not self._device.is_on: raise HomeAssistantError("Speed can only be adjusted when the light is on") - await self._device.async_set_effect(current_effect, new_speed) + effect_brightness = round(current_brightness / 255 * 100) + await self._device.async_set_effect( + current_effect, new_speed, effect_brightness + ) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 154c62074c1..aa91e362c7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.28 +flux_led==0.24.33 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2277a6cdf49..68451703626 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.28 +flux_led==0.24.33 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 9a89632513c..b56e655af08 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -281,7 +281,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) bulb.async_set_effect.reset_mock() @@ -359,7 +359,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) bulb.async_set_effect.reset_mock() bulb.color_mode = FLUX_COLOR_MODE_CCT bulb.getWhiteTemperature = Mock(return_value=(5000, 128)) @@ -500,10 +500,10 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade", ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 100) bulb.async_set_effect.reset_mock() @@ -584,10 +584,10 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade", ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 100) bulb.async_set_effect.reset_mock() await hass.services.async_call( @@ -741,7 +741,18 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + bulb.effect = "purple_fade" + bulb.brightness = 128 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + bulb.async_set_effect.assert_called_with("purple_fade", 50, 100) bulb.async_set_effect.reset_mock() diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 115414b8201..88cecd48cd7 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -95,7 +95,7 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, blocking=True, ) - bulb.async_set_effect.assert_called_with("colorloop", 100) + bulb.async_set_effect.assert_called_with("colorloop", 100, 50) bulb.async_set_effect.reset_mock() await async_mock_effect_speed(hass, bulb, "red_fade", 50) @@ -105,7 +105,7 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 50}, blocking=True, ) - bulb.async_set_effect.assert_called_with("red_fade", 50) + bulb.async_set_effect.assert_called_with("red_fade", 50, 50) bulb.async_set_effect.reset_mock() state = hass.states.get(number_entity_id) @@ -161,7 +161,7 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, blocking=True, ) - bulb.async_set_effect.assert_called_with("7 colors change gradually", 100) + bulb.async_set_effect.assert_called_with("7 colors change gradually", 100, 50) bulb.async_set_effect.reset_mock() await async_mock_effect_speed(hass, bulb, "7 colors run in olivary", 100) @@ -209,7 +209,7 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, blocking=True, ) - bulb.async_set_effect.assert_called_with("RBM 1", 100) + bulb.async_set_effect.assert_called_with("RBM 1", 100, 50) bulb.async_set_effect.reset_mock() await async_mock_device_turn_on(hass, bulb) @@ -219,7 +219,7 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: number_entity_id, ATTR_VALUE: 100}, blocking=True, ) - bulb.async_set_effect.assert_called_with("RBM 1", 100) + bulb.async_set_effect.assert_called_with("RBM 1", 100, 50) bulb.async_set_effect.reset_mock() await async_mock_effect_speed(hass, bulb, "RBM 2", 100) From fd116fc40803f0787fc1a4d4d2aea86814dee5d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Nov 2021 12:57:13 -0600 Subject: [PATCH 0838/1452] Refactor zeroconf matching to be more DRY (#60293) --- homeassistant/components/zeroconf/__init__.py | 80 +++++++++---------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index dd42b2146d7..932c2dee377 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -48,6 +48,18 @@ HOMEKIT_TYPES = [ "_hap._udp.local.", ] +# Keys we support matching against in properties that are always matched in +# upper case. ex: ZeroconfServiceInfo.properties["macaddress"] +UPPER_MATCH_PROPS = {"macaddress"} +# Keys we support matching against in properties that are always matched in +# lower case. ex: ZeroconfServiceInfo.properties["model"] +LOWER_MATCH_PROPS = {"manufacturer", "model"} +# Top level keys we support matching against in properties that are always matched in +# lower case. ex: ZeroconfServiceInfo.name +LOWER_MATCH_ATTRS = {"name"} +# Everything we support matching +ALL_MATCHERS = UPPER_MATCH_PROPS | LOWER_MATCH_PROPS | LOWER_MATCH_ATTRS + CONF_DEFAULT_INTERFACE = "default_interface" CONF_IPV6 = "ipv6" DEFAULT_DEFAULT_INTERFACE = True @@ -306,6 +318,18 @@ async def _async_register_hass_zc_service( await aio_zc.async_register_service(info, allow_name_change=True) +def _match_against_data(matcher: dict[str, str], match_data: dict[str, str]) -> bool: + """Check a matcher to ensure all values in match_data match.""" + return not any( + key + for key in ALL_MATCHERS + if key in matcher + and ( + key not in match_data or not fnmatch.fnmatch(match_data[key], matcher[key]) + ) + ) + + class ZeroconfDiscovery: """Discovery via zeroconf.""" @@ -380,10 +404,10 @@ class ZeroconfDiscovery: return _LOGGER.debug("Discovered new device %s %s", name, info) + props: dict[str, str] = info.properties # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - props = info.properties if domain := async_get_homekit_discovery_domain(self.homekit_models, props): discovery_flow.async_create_flow( self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT}, info @@ -405,52 +429,22 @@ class ZeroconfDiscovery: # likely bad homekit data return - if info.name: - lowercase_name: str | None = info.name.lower() - else: - lowercase_name = None - - if "macaddress" in info.properties: - uppercase_mac: str | None = info.properties["macaddress"].upper() - else: - uppercase_mac = None - - if "manufacturer" in info.properties: - lowercase_manufacturer: str | None = info.properties["manufacturer"].lower() - else: - lowercase_manufacturer = None - - if "model" in info.properties: - lowercase_model: str | None = info.properties["model"].lower() - else: - lowercase_model = None + match_data: dict[str, str] = {} + for key in LOWER_MATCH_ATTRS: + attr_value: str = getattr(info, key) + match_data[key] = attr_value.lower() + for key in UPPER_MATCH_PROPS: + if key in props: + match_data[key] = props[key].upper() + for key in LOWER_MATCH_PROPS: + if key in props: + match_data[key] = props[key].lower() # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for matcher in self.zeroconf_types.get(service_type, []): - if len(matcher) > 1: - if "macaddress" in matcher and ( - uppercase_mac is None - or not fnmatch.fnmatch(uppercase_mac, matcher["macaddress"]) - ): - continue - if "name" in matcher and ( - lowercase_name is None - or not fnmatch.fnmatch(lowercase_name, matcher["name"]) - ): - continue - if "manufacturer" in matcher and ( - lowercase_manufacturer is None - or not fnmatch.fnmatch( - lowercase_manufacturer, matcher["manufacturer"] - ) - ): - continue - if "model" in matcher and ( - lowercase_model is None - or not fnmatch.fnmatch(lowercase_model, matcher["model"]) - ): - continue + if len(matcher) > 1 and not _match_against_data(matcher, match_data): + continue discovery_flow.async_create_flow( self.hass, From 7c3edf24f2235d0deceffd633666ea0d1285a4a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 20:06:12 +0100 Subject: [PATCH 0839/1452] Allow MQTT selects to have a single or no options (#60281) --- homeassistant/components/mqtt/select.py | 18 ++------------ tests/components/mqtt/test_select.py | 33 ++++--------------------- 2 files changed, 7 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 45304d68079..7b374ba8955 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -35,14 +35,6 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( ) -def validate_config(config): - """Validate that the configuration is valid, throws if it isn't.""" - if len(config[CONF_OPTIONS]) < 2: - raise vol.Invalid(f"'{CONF_OPTIONS}' must include at least 2 options") - - return config - - _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, @@ -53,15 +45,9 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All( - _PLATFORM_SCHEMA_BASE, - validate_config, -) +PLATFORM_SCHEMA = vol.All(_PLATFORM_SCHEMA_BASE) -DISCOVERY_SCHEMA = vol.All( - _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), - validate_config, -) +DISCOVERY_SCHEMA = vol.All(_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA)) async def async_setup_platform( diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 731aca2e178..2f31ce788fd 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -5,10 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components import select -from homeassistant.components.mqtt.select import ( - CONF_OPTIONS, - MQTT_SELECT_ATTRIBUTES_BLOCKED, -) +from homeassistant.components.mqtt.select import MQTT_SELECT_ATTRIBUTES_BLOCKED from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, @@ -478,7 +475,8 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_options_attributes(hass, mqtt_mock): +@pytest.mark.parametrize("options", [["milk", "beer"], ["milk"], []]) +async def test_options_attributes(hass, mqtt_mock, options): """Test options attribute.""" topic = "test/select" await async_setup_component( @@ -490,35 +488,14 @@ async def test_options_attributes(hass, mqtt_mock): "state_topic": topic, "command_topic": topic, "name": "Test select", - "options": ["milk", "beer"], + "options": options, } }, ) await hass.async_block_till_done() state = hass.states.get("select.test_select") - assert state.attributes.get(ATTR_OPTIONS) == ["milk", "beer"] - - -async def test_invalid_options(hass, caplog, mqtt_mock): - """Test invalid options.""" - topic = "test/select" - await async_setup_component( - hass, - "select", - { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": "beer", - } - }, - ) - await hass.async_block_till_done() - - assert f"'{CONF_OPTIONS}' must include at least 2 options" in caplog.text + assert state.attributes.get(ATTR_OPTIONS) == options async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): From 2439f6b5625a7d52aeddecaa8d7fa9d40bae2775 Mon Sep 17 00:00:00 2001 From: Fabian Seitz Date: Wed, 24 Nov 2021 20:12:39 +0100 Subject: [PATCH 0840/1452] Bump openwrt-luci-rpc from 1.1.8 to 1.1.11 (#60124) --- homeassistant/components/luci/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 6feac638637..705bb7ecb4b 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -2,7 +2,7 @@ "domain": "luci", "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", - "requirements": ["openwrt-luci-rpc==1.1.8"], + "requirements": ["openwrt-luci-rpc==1.1.11"], "codeowners": ["@mzdrale"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index aa91e362c7a..e5a962b12cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1150,7 +1150,7 @@ opensensemap-api==0.1.5 openwebifpy==3.2.7 # homeassistant.components.luci -openwrt-luci-rpc==1.1.8 +openwrt-luci-rpc==1.1.11 # homeassistant.components.ubus openwrt-ubus-rpc==0.0.2 From a399037a46921d749358bf6d6ad7b9549e20f1e1 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Wed, 24 Nov 2021 20:45:13 +0100 Subject: [PATCH 0841/1452] Add TOLO Sauna (tolo) integration (#55619) --- .coveragerc | 2 + .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/tolo/__init__.py | 101 ++++++++++++ homeassistant/components/tolo/climate.py | 156 ++++++++++++++++++ homeassistant/components/tolo/config_flow.py | 96 +++++++++++ homeassistant/components/tolo/const.py | 13 ++ homeassistant/components/tolo/manifest.json | 14 ++ homeassistant/components/tolo/strings.json | 23 +++ .../components/tolo/translations/en.json | 23 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 4 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/tolo/__init__.py | 1 + tests/components/tolo/test_config_flow.py | 107 ++++++++++++ 17 files changed, 560 insertions(+) create mode 100644 homeassistant/components/tolo/__init__.py create mode 100644 homeassistant/components/tolo/climate.py create mode 100644 homeassistant/components/tolo/config_flow.py create mode 100644 homeassistant/components/tolo/const.py create mode 100644 homeassistant/components/tolo/manifest.json create mode 100644 homeassistant/components/tolo/strings.json create mode 100644 homeassistant/components/tolo/translations/en.json create mode 100644 tests/components/tolo/__init__.py create mode 100644 tests/components/tolo/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f03ad280be6..ab8e540636b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1091,6 +1091,8 @@ omit = homeassistant/components/todoist/calendar.py homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py + homeassistant/components/tolo/__init__.py + homeassistant/components/tolo/climate.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py diff --git a/.strict-typing b/.strict-typing index e8941c307e6..ce04c74702b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -131,6 +131,7 @@ homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* homeassistant.components.tplink.* +homeassistant.components.tolo.* homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.tts.* diff --git a/CODEOWNERS b/CODEOWNERS index 242ffa1ea03..48a3b5ed02b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -541,6 +541,7 @@ homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl +homeassistant/components/tolo/* @MatthiasLohr homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py new file mode 100644 index 00000000000..d4b182061c3 --- /dev/null +++ b/homeassistant/components/tolo/__init__.py @@ -0,0 +1,101 @@ +"""Component to control TOLO Sauna/Steam Bath.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import NamedTuple + +from tololib import ToloClient +from tololib.errors import ResponseTimedOutError +from tololib.message_info import SettingsInfo, StatusInfo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN + +PLATFORMS = ["climate"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up tolo from a config entry.""" + coordinator = ToloSaunaUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(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 ToloSaunaData(NamedTuple): + """Compound class for reflecting full state (status and info) of a TOLO Sauna.""" + + status: StatusInfo + settings: SettingsInfo + + +class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): + """DataUpdateCoordinator for TOLO Sauna.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize ToloSaunaUpdateCoordinator.""" + self.client = ToloClient(entry.data[CONF_HOST]) + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{entry.title} ({entry.data[CONF_HOST]}) Data Update Coordinator", + update_interval=timedelta(seconds=3), + ) + + async def _async_update_data(self) -> ToloSaunaData: + return await self.hass.async_add_executor_job(self._get_tolo_sauna_data) + + def _get_tolo_sauna_data(self) -> ToloSaunaData: + try: + status = self.client.get_status_info( + resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT + ) + settings = self.client.get_settings_info( + resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT + ) + return ToloSaunaData(status, settings) + except ResponseTimedOutError as error: + raise UpdateFailed("communication timeout") from error + + +class ToloSaunaCoordinatorEntity(CoordinatorEntity): + """CoordinatorEntity for TOLO Sauna.""" + + coordinator: ToloSaunaUpdateCoordinator + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize ToloSaunaCoordinatorEntity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + name="TOLO Sauna", + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="SteamTec", + model=self.coordinator.data.status.model.name.capitalize(), + ) diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py new file mode 100644 index 00000000000..659dfcbda16 --- /dev/null +++ b/homeassistant/components/tolo/climate.py @@ -0,0 +1,156 @@ +"""TOLO Sauna climate controls (main sauna control).""" + +from __future__ import annotations + +from typing import Any + +from tololib.const import Calefaction + +from homeassistant.components.climate import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ClimateEntity, +) +from homeassistant.components.climate.const import ( + CURRENT_HVAC_DRY, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_OFF, + FAN_ON, + HVAC_MODE_DRY, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, + DEFAULT_MIN_TEMP, + DOMAIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate controls for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SaunaClimate(coordinator, entry)]) + + +class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): + """Sauna climate control.""" + + _attr_fan_modes = [FAN_ON, FAN_OFF] + _attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_DRY] + _attr_max_humidity = DEFAULT_MAX_HUMIDITY + _attr_max_temp = DEFAULT_MAX_TEMP + _attr_min_humidity = DEFAULT_MIN_HUMIDITY + _attr_min_temp = DEFAULT_MIN_TEMP + _attr_name = "Sauna Climate" + _attr_precision = PRECISION_WHOLE + _attr_supported_features = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | SUPPORT_FAN_MODE + ) + _attr_target_temperature_step = 1 + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Sauna Climate entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_climate" + + @property + def current_temperature(self) -> int: + """Return current temperature.""" + return self.coordinator.data.status.current_temperature + + @property + def current_humidity(self) -> int: + """Return current humidity.""" + return self.coordinator.data.status.current_humidity + + @property + def target_temperature(self) -> int: + """Return target temperature.""" + return self.coordinator.data.settings.target_temperature + + @property + def target_humidity(self) -> int: + """Return target humidity.""" + return self.coordinator.data.settings.target_humidity + + @property + def hvac_mode(self) -> str: + """Get current HVAC mode.""" + if self.coordinator.data.status.power_on: + return HVAC_MODE_HEAT + if ( + not self.coordinator.data.status.power_on + and self.coordinator.data.status.fan_on + ): + return HVAC_MODE_DRY + return HVAC_MODE_OFF + + @property + def hvac_action(self) -> str | None: + """Execute HVAC action.""" + if self.coordinator.data.status.calefaction == Calefaction.HEAT: + return CURRENT_HVAC_HEAT + if self.coordinator.data.status.calefaction == Calefaction.KEEP: + return CURRENT_HVAC_IDLE + if self.coordinator.data.status.calefaction == Calefaction.INACTIVE: + if self.coordinator.data.status.fan_on: + return CURRENT_HVAC_DRY + return CURRENT_HVAC_OFF + return None + + @property + def fan_mode(self) -> str: + """Return current fan mode.""" + if self.coordinator.data.status.fan_on: + return FAN_ON + return FAN_OFF + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set HVAC mode.""" + if hvac_mode == HVAC_MODE_OFF: + self._set_power_and_fan(False, False) + if hvac_mode == HVAC_MODE_HEAT: + self._set_power_and_fan(True, False) + if hvac_mode == HVAC_MODE_DRY: + self._set_power_and_fan(False, True) + + def set_fan_mode(self, fan_mode: str) -> None: + """Set fan mode.""" + self.coordinator.client.set_fan_on(fan_mode == FAN_ON) + + def set_humidity(self, humidity: float) -> None: + """Set desired target humidity.""" + self.coordinator.client.set_target_humidity(round(humidity)) + + def set_temperature(self, **kwargs: Any) -> None: + """Set desired target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + self.coordinator.client.set_target_temperature(round(temperature)) + + def _set_power_and_fan(self, power_on: bool, fan_on: bool) -> None: + """Shortcut for setting power and fan of TOLO device on one method.""" + self.coordinator.client.set_power_on(power_on) + self.coordinator.client.set_fan_on(fan_on) diff --git a/homeassistant/components/tolo/config_flow.py b/homeassistant/components/tolo/config_flow.py new file mode 100644 index 00000000000..4503fd511ba --- /dev/null +++ b/homeassistant/components/tolo/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for tolo.""" + +from __future__ import annotations + +import logging +from typing import Any + +from tololib import ToloClient +from tololib.errors import ResponseTimedOutError +import voluptuous as vol + +from homeassistant.components import dhcp +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac + +from .const import DEFAULT_NAME, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): + """ConfigFlow for TOLO Sauna.""" + + VERSION = 1 + + _discovered_host: str | None = None + + @staticmethod + def _check_device_availability(host: str) -> bool: + client = ToloClient(host) + try: + result = client.get_status_info( + resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT + ) + return result is not None + except ResponseTimedOutError: + return False + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + device_available = await self.hass.async_add_executor_job( + self._check_device_availability, user_input[CONF_HOST] + ) + + if not device_available: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=DEFAULT_NAME, data={CONF_HOST: user_input[CONF_HOST]} + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle a flow initialized by discovery.""" + await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info[IP_ADDRESS]}) + self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) + + device_available = await self.hass.async_add_executor_job( + self._check_device_availability, discovery_info[IP_ADDRESS] + ) + + if device_available: + self._discovered_host = discovery_info[IP_ADDRESS] + return await self.async_step_confirm() + return self.async_abort(reason="not_tolo_device") + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user-confirmation of discovered node.""" + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: self._discovered_host}) + return self.async_create_entry( + title=DEFAULT_NAME, data={CONF_HOST: self._discovered_host} + ) + + return self.async_show_form( + step_id="confirm", + description_placeholders={CONF_HOST: self._discovered_host}, + ) diff --git a/homeassistant/components/tolo/const.py b/homeassistant/components/tolo/const.py new file mode 100644 index 00000000000..bfd700bb955 --- /dev/null +++ b/homeassistant/components/tolo/const.py @@ -0,0 +1,13 @@ +"""Constants for the tolo integration.""" + +DOMAIN = "tolo" +DEFAULT_NAME = "TOLO Sauna" + +DEFAULT_RETRY_TIMEOUT = 1 +DEFAULT_RETRY_COUNT = 3 + +DEFAULT_MAX_TEMP = 60 +DEFAULT_MIN_TEMP = 20 + +DEFAULT_MAX_HUMIDITY = 99 +DEFAULT_MIN_HUMIDITY = 60 diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json new file mode 100644 index 00000000000..4aa84f3f2d2 --- /dev/null +++ b/homeassistant/components/tolo/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "tolo", + "name": "TOLO Sauna", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tolo", + "requirements": [ + "tololib==0.1.0b2" + ], + "codeowners": [ + "@MatthiasLohr" + ], + "iot_class": "local_polling", + "dhcp": [{"hostname": "usr-tcp232-ed2"}] +} \ No newline at end of file diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json new file mode 100644 index 00000000000..f9316f4d72b --- /dev/null +++ b/homeassistant/components/tolo/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Enter the hostname or IP address of your TOLO Sauna device.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/en.json b/homeassistant/components/tolo/translations/en.json new file mode 100644 index 00000000000..c304f583b61 --- /dev/null +++ b/homeassistant/components/tolo/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Enter the hostname or IP address of your TOLO Sauna device." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d54e515681e..6c199b47e32 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -298,6 +298,7 @@ FLOWS = [ "tellduslive", "tibber", "tile", + "tolo", "toon", "totalconnect", "tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 31481df3495..59f346e0be0 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -361,6 +361,10 @@ DHCP = [ "domain": "tado", "hostname": "tado*" }, + { + "domain": "tolo", + "hostname": "usr-tcp232-ed2" + }, { "domain": "toon", "hostname": "eneco-*", diff --git a/mypy.ini b/mypy.ini index da1fb4f08d4..cc1337bfc3c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1452,6 +1452,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tolo.*] +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.tractive.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index e5a962b12cd..e113992ce2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2316,6 +2316,9 @@ tmb==0.0.4 # homeassistant.components.todoist todoist-python==8.0.0 +# homeassistant.components.tolo +tololib==0.1.0b2 + # homeassistant.components.toon toonapi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68451703626..1ae165ba050 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1350,6 +1350,9 @@ tellduslive==0.10.11 # homeassistant.components.powerwall tesla-powerwall==0.3.12 +# homeassistant.components.tolo +tololib==0.1.0b2 + # homeassistant.components.toon toonapi==0.2.1 diff --git a/tests/components/tolo/__init__.py b/tests/components/tolo/__init__.py new file mode 100644 index 00000000000..d8874d9ceec --- /dev/null +++ b/tests/components/tolo/__init__.py @@ -0,0 +1 @@ +"""Tests for the TOLO Sauna integration.""" diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py new file mode 100644 index 00000000000..6634d444bce --- /dev/null +++ b/tests/components/tolo/test_config_flow.py @@ -0,0 +1,107 @@ +"""Tests for the TOLO Sauna config flow.""" +from unittest.mock import Mock, patch + +import pytest +from tololib.errors import ResponseTimedOutError + +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.tolo.const import DOMAIN +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +MOCK_DHCP_DATA = {IP_ADDRESS: "127.0.0.2", MAC_ADDRESS: "00:11:22:33:44:55"} + + +@pytest.fixture(name="toloclient") +def toloclient_fixture() -> Mock: + """Patch libraries.""" + with patch("homeassistant.components.tolo.config_flow.ToloClient") as toloclient: + yield toloclient + + +async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock): + """Test a user initiated config flow with provided host which times out.""" + toloclient().get_status_info.side_effect = lambda *args, **kwargs: ( + _ for _ in () + ).throw(ResponseTimedOutError()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "127.0.0.1"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): + """Test complete user flow with first wrong and then correct host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert "flow_id" in result + + toloclient().get_status_info.side_effect = lambda *args, **kwargs: None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.2"}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == SOURCE_USER + assert result2["errors"] == {"base": "cannot_connect"} + assert "flow_id" in result2 + + toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.1"}, + ) + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "TOLO Sauna" + assert result3["data"][CONF_HOST] == "127.0.0.1" + + +async def test_dhcp(hass: HomeAssistant, toloclient: Mock): + """Test starting a flow from discovery.""" + toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "TOLO Sauna" + assert result["data"][CONF_HOST] == "127.0.0.2" + assert result["result"].unique_id == "00:11:22:33:44:55" + + +async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock): + """Test starting a flow from discovery.""" + toloclient().get_status_info.side_effect = lambda *args, **kwargs: None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT From 37219e8d04485666a98c74e9a6b7efe29a612662 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Nov 2021 21:10:26 +0100 Subject: [PATCH 0842/1452] Add button platform to Tuya (#60304) --- .coveragerc | 1 + homeassistant/components/tuya/button.py | 105 ++++++++++++++++++++++++ homeassistant/components/tuya/const.py | 8 +- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/button.py diff --git a/.coveragerc b/.coveragerc index ab8e540636b..6dfca6f7acc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1136,6 +1136,7 @@ omit = homeassistant/components/tuya/__init__.py homeassistant/components/tuya/base.py homeassistant/components/tuya/binary_sensor.py + homeassistant/components/tuya/button.py homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py new file mode 100644 index 00000000000..d07d6947272 --- /dev/null +++ b/homeassistant/components/tuya/button.py @@ -0,0 +1,105 @@ +"""Support for Tuya buttons.""" +from __future__ import annotations + +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantTuyaData +from .base import TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { + # Robot Vacuum + # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo + "sd": ( + ButtonEntityDescription( + key=DPCode.RESET_DUSTER_CLOTH, + name="Reset Duster Cloth", + icon="mdi:restart", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ButtonEntityDescription( + key=DPCode.RESET_EDGE_BRUSH, + name="Reset Edge Brush", + icon="mdi:restart", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ButtonEntityDescription( + key=DPCode.RESET_FILTER, + name="Reset Filter", + icon="mdi:air-filter", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ButtonEntityDescription( + key=DPCode.RESET_MAP, + name="Reset Map", + icon="mdi:map-marker-remove", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ButtonEntityDescription( + key=DPCode.RESET_ROLL_BRUSH, + name="Reset Roll Brush", + icon="mdi:restart", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya buttons dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya buttons.""" + entities: list[TuyaButtonEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := BUTTONS.get(device.category): + for description in descriptions: + if description.key in device.function: + entities.append( + TuyaButtonEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaButtonEntity(TuyaEntity, ButtonEntity): + """Tuya Button Device.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: ButtonEntityDescription, + ) -> None: + """Init Tuya button.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + def press(self, **kwargs: Any) -> None: + """Press the button.""" + self._send_command([{"code": self.entity_description.key, "value": True}]) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0173bde9a6b..672001dd3fa 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -100,6 +100,7 @@ SMARTLIFE_APP = "smartlife" PLATFORMS = [ "binary_sensor", + "button", "camera", "climate", "cover", @@ -173,8 +174,8 @@ class DPCode(str, Enum): CO2_STATE = "co2_state" CO2_VALUE = "co2_value" # CO2 concentration COLOR_DATA_V2 = "color_data_v2" - COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode COLOUR_DATA = "colour_data" # Colored light mode + COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting CONTROL = "control" @@ -243,6 +244,11 @@ class DPCode(str, Enum): RECORD_MODE = "record_mode" RECORD_SWITCH = "record_switch" # Recording switch RELAY_STATUS = "relay_status" + RESET_DUSTER_CLOTH = "reset_duster_cloth" + RESET_EDGE_BRUSH = "reset_edge_brush" + RESET_FILTER = "reset_filter" + RESET_MAP = "reset_map" + RESET_ROLL_BRUSH = "reset_roll_brush" SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity SENSOR_HUMIDITY = "sensor_humidity" From 5853d819440f62dda96947240e577fb5a33fcb1e Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Wed, 24 Nov 2021 22:26:08 +0100 Subject: [PATCH 0843/1452] Add tolo light platform (#60305) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/tolo/light.py | 51 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tolo/light.py diff --git a/.coveragerc b/.coveragerc index 6dfca6f7acc..619b8468a07 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1093,6 +1093,7 @@ omit = homeassistant/components/tof/sensor.py homeassistant/components/tolo/__init__.py homeassistant/components/tolo/climate.py + homeassistant/components/tolo/light.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index d4b182061c3..5151449f1fd 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["climate"] +PLATFORMS = ["climate", "light"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/light.py b/homeassistant/components/tolo/light.py new file mode 100644 index 00000000000..f58f7e7b8c9 --- /dev/null +++ b/homeassistant/components/tolo/light.py @@ -0,0 +1,51 @@ +"""TOLO Sauna light controls.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.light import COLOR_MODE_ONOFF, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up light controls for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([ToloLight(coordinator, entry)]) + + +class ToloLight(ToloSaunaCoordinatorEntity, LightEntity): + """Sauna light control.""" + + _attr_name = "Sauna Light" + _attr_supported_color_modes = {COLOR_MODE_ONOFF} + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Sauna Light entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_light" + + @property + def is_on(self) -> bool: + """Return current lamp status.""" + return self.coordinator.data.status.lamp_on + + def turn_on(self, **kwargs: Any) -> None: + """Turn on TOLO Sauna lamp.""" + self.coordinator.client.set_lamp_on(True) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off TOLO Sauna lamp.""" + self.coordinator.client.set_lamp_on(False) From 42389fc81bc90ec947a513c98c92cfe2e20c816d Mon Sep 17 00:00:00 2001 From: alim4r <7687869+alim4r@users.noreply.github.com> Date: Wed, 24 Nov 2021 22:30:08 +0100 Subject: [PATCH 0844/1452] Support numeric sensors with no unit_of_measurement in prometheus (#60157) --- .../components/prometheus/__init__.py | 16 ++++++--- tests/components/prometheus/test_init.py | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e0c82061c2c..2eb4193377d 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -418,9 +418,11 @@ class PrometheusMetrics: break if metric is not None: - _metric = self._metric( - metric, self.prometheus_cli.Gauge, f"Sensor data measured in {unit}" - ) + documentation = "State of the sensor" + if unit: + documentation = f"Sensor data measured in {unit}" + + _metric = self._metric(metric, self.prometheus_cli.Gauge, documentation) try: value = self.state_as_number(state) @@ -458,8 +460,12 @@ class PrometheusMetrics: def _sensor_fallback_metric(state, unit): """Get metric from fallback logic for compatibility.""" if unit in (None, ""): - _LOGGER.debug("Unsupported sensor: %s", state.entity_id) - return None + try: + state_helper.state_as_number(state) + except ValueError: + _LOGGER.debug("Unsupported sensor: %s", state.entity_id) + return None + return "sensor_state" return f"sensor_unit_{unit}" @staticmethod diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 0fdbc23bb07..f9f6ebbf0f5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -100,6 +100,21 @@ async def prometheus_client(hass, hass_client, namespace): sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" await sensor5.async_update_ha_state() + sensor6 = DemoSensor(None, "Trend Gradient", 0.002, None, None, None, None) + sensor6.hass = hass + sensor6.entity_id = "sensor.trend_gradient" + await sensor6.async_update_ha_state() + + sensor7 = DemoSensor(None, "Text", "should_not_work", None, None, None, None) + sensor7.hass = hass + sensor7.entity_id = "sensor.text" + await sensor7.async_update_ha_state() + + sensor8 = DemoSensor(None, "Text Unit", "should_not_work", None, None, "Text", None) + sensor8.hass = hass + sensor8.entity_id = "sensor.text_unit" + await sensor8.async_update_ha_state() + number1 = DemoNumber(None, "Threshold", 5.2, None, False, 0, 10, 0.1) number1.hass = hass number1.entity_id = "input_number.threshold" @@ -241,6 +256,24 @@ async def test_view_empty_namespace(hass, hass_client): 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body ) + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.trend_gradient",' + 'friendly_name="Trend Gradient"} 0.002' in body + ) + + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.text",' + 'friendly_name="Text"} 0' not in body + ) + + assert ( + 'sensor_unit_text{domain="sensor",' + 'entity="sensor.text_unit",' + 'friendly_name="Text Unit"} 0' not in body + ) + assert ( 'input_number_state{domain="input_number",' 'entity="input_number.threshold",' From 9f4de8df18013e91fab1ff9cc8ed536dc046884d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 24 Nov 2021 23:32:16 +0200 Subject: [PATCH 0845/1452] Make device entry disabled by an enum (#60239) --- .../components/config/device_registry.py | 8 +++- homeassistant/helpers/device_registry.py | 48 +++++++++++-------- homeassistant/helpers/entity_registry.py | 2 +- .../components/config/test_device_registry.py | 4 +- .../components/config/test_entity_registry.py | 3 +- tests/helpers/test_device_registry.py | 22 ++++----- tests/helpers/test_entity_registry.py | 4 +- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 1cc63297352..4d4b8333d70 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -7,7 +7,10 @@ from homeassistant.components.websocket_api.decorators import ( require_admin, ) from homeassistant.core import callback -from homeassistant.helpers.device_registry import DISABLED_USER, async_get_registry +from homeassistant.helpers.device_registry import ( + DeviceEntryDisabler, + async_get_registry, +) WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -22,7 +25,8 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( vol.Optional("area_id"): vol.Any(str, None), vol.Optional("name_by_user"): vol.Any(str, None), # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), + # No Enum support like this in voluptuous, use .value + vol.Optional("disabled_by"): vol.Any(DeviceEntryDisabler.USER.value, None), } ) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index b99e80e197a..45a3d2bc795 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -41,10 +41,6 @@ CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" -DISABLED_CONFIG_ENTRY = "config_entry" -DISABLED_INTEGRATION = "integration" -DISABLED_USER = "user" - ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 @@ -53,6 +49,14 @@ class _DeviceIndex(NamedTuple): connections: dict[tuple[str, str], str] +class DeviceEntryDisabler(StrEnum): + """What disabled a device entry.""" + + CONFIG_ENTRY = "config_entry" + INTEGRATION = "integration" + USER = "user" + + class DeviceEntryType(StrEnum): """Device entry type.""" @@ -67,17 +71,7 @@ class DeviceEntry: config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) - disabled_by: str | None = attr.ib( - default=None, - validator=attr.validators.in_( - ( - DISABLED_CONFIG_ENTRY, - DISABLED_INTEGRATION, - DISABLED_USER, - None, - ) - ), - ) + disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) @@ -302,7 +296,7 @@ class DeviceRegistry: default_model: str | None | UndefinedType = UNDEFINED, default_name: str | None | UndefinedType = UNDEFINED, # To disable a device if it gets created - disabled_by: str | None | UndefinedType = UNDEFINED, + disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None = None, manufacturer: str | None | UndefinedType = UNDEFINED, @@ -389,7 +383,7 @@ class DeviceRegistry: add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name_by_user: str | None | UndefinedType = UNDEFINED, @@ -426,7 +420,7 @@ class DeviceRegistry: add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, @@ -447,6 +441,16 @@ class DeviceRegistry: config_entries = old.config_entries + if isinstance(disabled_by, str) and not isinstance( + disabled_by, DeviceEntryDisabler + ): + report( # type: ignore[unreachable] + "uses str for device registry disabled_by. This is deprecated, " + "it should be updated to use DeviceEntryDisabler instead", + error_if_core=False, + ) + disabled_by = DeviceEntryDisabler(disabled_by) + if ( suggested_area not in (UNDEFINED, None, "") and area_id is UNDEFINED @@ -737,7 +741,7 @@ def async_config_entry_disabled_by_changed( Disable devices in the registry that are associated with a config entry when the config entry is disabled, enable devices in the registry that are associated with a config entry when the config entry is enabled and the devices are marked - DISABLED_CONFIG_ENTRY. + DeviceEntryDisabler.CONFIG_ENTRY. Only disable a device if all associated config entries are disabled. """ @@ -745,7 +749,7 @@ def async_config_entry_disabled_by_changed( if not config_entry.disabled_by: for device in devices: - if device.disabled_by != DISABLED_CONFIG_ENTRY: + if device.disabled_by is not DeviceEntryDisabler.CONFIG_ENTRY: continue registry.async_update_device(device.id, disabled_by=None) return @@ -764,7 +768,9 @@ def async_config_entry_disabled_by_changed( enabled_config_entries ): continue - registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) + registry.async_update_device( + device.id, disabled_by=DeviceEntryDisabler.CONFIG_ENTRY + ) @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index f4b733eac56..036f235e132 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -390,7 +390,7 @@ class EntityRegistry: self.async_update_entity(entity.entity_id, disabled_by=None) return - if device.disabled_by == dr.DISABLED_CONFIG_ENTRY: + if device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY: # Handled by async_config_entry_disabled return diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index edc167405ac..ee8c933f761 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -97,7 +97,7 @@ async def test_update_device(hass, client, registry): "device_id": device.id, "area_id": "12345A", "name_by_user": "Test Friendly Name", - "disabled_by": helpers_dr.DISABLED_USER, + "disabled_by": helpers_dr.DeviceEntryDisabler.USER, "type": "config/device_registry/update", } ) @@ -107,5 +107,5 @@ async def test_update_device(hass, client, registry): assert msg["result"]["id"] == device.id assert msg["result"]["area_id"] == "12345A" assert msg["result"]["name_by_user"] == "Test Friendly Name" - assert msg["result"]["disabled_by"] == helpers_dr.DISABLED_USER + assert msg["result"]["disabled_by"] == helpers_dr.DeviceEntryDisabler.USER assert len(registry.devices) == 1 diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 074d8e223da..17762f20df3 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON +from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import DISABLED_USER, RegistryEntry from tests.common import ( @@ -325,7 +326,7 @@ async def test_enable_entity_disabled_device(hass, client, device_registry): identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", - disabled_by=DISABLED_USER, + disabled_by=DeviceEntryDisabler.USER, ) mock_registry( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 2955c02345f..a689cc9ac3d 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -176,7 +176,7 @@ async def test_loading_from_storage(hass, hass_storage): "config_entries": ["1234"], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], - "disabled_by": device_registry.DISABLED_USER, + "disabled_by": device_registry.DeviceEntryDisabler.USER, "entry_type": device_registry.DeviceEntryType.SERVICE, "id": "abcdefghijklm", "identifiers": [["serial", "12:34:56:AB:CD:EF"]], @@ -216,7 +216,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" assert entry.entry_type is device_registry.DeviceEntryType.SERVICE - assert entry.disabled_by == device_registry.DISABLED_USER + assert entry.disabled_by is device_registry.DeviceEntryDisabler.USER assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -574,7 +574,7 @@ async def test_loading_saving_data(hass, registry, area_registry): manufacturer="manufacturer", model="light", via_device=("hue", "0123"), - disabled_by=device_registry.DISABLED_USER, + disabled_by=device_registry.DeviceEntryDisabler.USER, ) orig_light2 = registry.async_get_or_create( @@ -623,7 +623,7 @@ async def test_loading_saving_data(hass, registry, area_registry): manufacturer="manufacturer", model="light", via_device=("hue", "0123"), - disabled_by=device_registry.DISABLED_USER, + disabled_by=device_registry.DeviceEntryDisabler.USER, suggested_area="Kitchen", ) @@ -732,7 +732,7 @@ async def test_update(registry): name_by_user="Test Friendly Name", new_identifiers=new_identifiers, via_device_id="98765B", - disabled_by=device_registry.DISABLED_USER, + disabled_by=device_registry.DeviceEntryDisabler.USER, ) assert mock_save.call_count == 1 @@ -743,7 +743,7 @@ async def test_update(registry): assert updated_entry.name_by_user == "Test Friendly Name" assert updated_entry.identifiers == new_identifiers assert updated_entry.via_device_id == "98765B" - assert updated_entry.disabled_by == device_registry.DISABLED_USER + assert updated_entry.disabled_by is device_registry.DeviceEntryDisabler.USER assert registry.async_get_device({("hue", "456")}) is None assert registry.async_get_device({("bla", "123")}) is None @@ -1307,7 +1307,7 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry2 = registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, - disabled_by=device_registry.DISABLED_USER, + disabled_by=device_registry.DeviceEntryDisabler.USER, ) assert not entry1.disabled @@ -1320,10 +1320,10 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry1 = registry.async_get(entry1.id) assert entry1.disabled - assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY + assert entry1.disabled_by is device_registry.DeviceEntryDisabler.CONFIG_ENTRY entry2 = registry.async_get(entry2.id) assert entry2.disabled - assert entry2.disabled_by == device_registry.DISABLED_USER + assert entry2.disabled_by is device_registry.DeviceEntryDisabler.USER await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) await hass.async_block_till_done() @@ -1332,7 +1332,7 @@ async def test_disable_config_entry_disables_devices(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.id) assert entry2.disabled - assert entry2.disabled_by == device_registry.DISABLED_USER + assert entry2.disabled_by is device_registry.DeviceEntryDisabler.USER async def test_only_disable_device_if_all_config_entries_are_disabled(hass, registry): @@ -1368,7 +1368,7 @@ async def test_only_disable_device_if_all_config_entries_are_disabled(hass, regi entry1 = registry.async_get(entry1.id) assert entry1.disabled - assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY + assert entry1.disabled_by is device_registry.DeviceEntryDisabler.CONFIG_ENTRY await hass.config_entries.async_set_disabled_by(config_entry1.entry_id, None) await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index ad88a23f76f..3dc9cf775c4 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -850,7 +850,9 @@ async def test_disable_device_disables_entities(hass, registry): assert entry2.disabled assert entry3.disabled - device_registry.async_update_device(device_entry.id, disabled_by=er.DISABLED_USER) + device_registry.async_update_device( + device_entry.id, disabled_by=dr.DeviceEntryDisabler.USER + ) await hass.async_block_till_done() entry1 = registry.async_get(entry1.entity_id) From 970df6d0678354659547967fe8556ebd45b1040a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 24 Nov 2021 22:50:35 +0100 Subject: [PATCH 0846/1452] Move sensor stuff to the sensor platform in Brother integration (#60306) --- homeassistant/components/brother/const.py | 262 -------------------- homeassistant/components/brother/sensor.py | 274 ++++++++++++++++++++- tests/components/brother/test_sensor.py | 3 +- 3 files changed, 265 insertions(+), 274 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index a91d84103e1..21f535ec1e4 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -3,272 +3,10 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) -from homeassistant.const import ( - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) - -ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" -ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" -ATTR_BLACK_DRUM_REMAINING_LIFE: Final = "black_drum_remaining_life" -ATTR_BLACK_DRUM_REMAINING_PAGES: Final = "black_drum_remaining_pages" -ATTR_BLACK_INK_REMAINING: Final = "black_ink_remaining" -ATTR_BLACK_TONER_REMAINING: Final = "black_toner_remaining" -ATTR_BW_COUNTER: Final = "b/w_counter" -ATTR_COLOR_COUNTER: Final = "color_counter" -ATTR_COUNTER: Final = "counter" -ATTR_CYAN_DRUM_COUNTER: Final = "cyan_drum_counter" -ATTR_CYAN_DRUM_REMAINING_LIFE: Final = "cyan_drum_remaining_life" -ATTR_CYAN_DRUM_REMAINING_PAGES: Final = "cyan_drum_remaining_pages" -ATTR_CYAN_INK_REMAINING: Final = "cyan_ink_remaining" -ATTR_CYAN_TONER_REMAINING: Final = "cyan_toner_remaining" -ATTR_DRUM_COUNTER: Final = "drum_counter" -ATTR_DRUM_REMAINING_LIFE: Final = "drum_remaining_life" -ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages" -ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter" -ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life" -ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life" -ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter" -ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life" -ATTR_MAGENTA_DRUM_REMAINING_PAGES: Final = "magenta_drum_remaining_pages" -ATTR_MAGENTA_INK_REMAINING: Final = "magenta_ink_remaining" -ATTR_MAGENTA_TONER_REMAINING: Final = "magenta_toner_remaining" -ATTR_MANUFACTURER: Final = "Brother" -ATTR_PAGE_COUNTER: Final = "page_counter" -ATTR_PF_KIT_1_REMAINING_LIFE: Final = "pf_kit_1_remaining_life" -ATTR_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life" -ATTR_REMAINING_PAGES: Final = "remaining_pages" -ATTR_STATUS: Final = "status" -ATTR_UPTIME: Final = "uptime" -ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter" -ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life" -ATTR_YELLOW_DRUM_REMAINING_PAGES: Final = "yellow_drum_remaining_pages" -ATTR_YELLOW_INK_REMAINING: Final = "yellow_ink_remaining" -ATTR_YELLOW_TONER_REMAINING: Final = "yellow_toner_remaining" - DATA_CONFIG_ENTRY: Final = "config_entry" DOMAIN: Final = "brother" -UNIT_PAGES: Final = "p" - PRINTER_TYPES: Final = ["laser", "ink"] SNMP: Final = "snmp" - -ATTRS_MAP: Final[dict[str, tuple[str, str]]] = { - ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), - ATTR_BLACK_DRUM_REMAINING_LIFE: ( - ATTR_BLACK_DRUM_REMAINING_PAGES, - ATTR_BLACK_DRUM_COUNTER, - ), - ATTR_CYAN_DRUM_REMAINING_LIFE: ( - ATTR_CYAN_DRUM_REMAINING_PAGES, - ATTR_CYAN_DRUM_COUNTER, - ), - ATTR_MAGENTA_DRUM_REMAINING_LIFE: ( - ATTR_MAGENTA_DRUM_REMAINING_PAGES, - ATTR_MAGENTA_DRUM_COUNTER, - ), - ATTR_YELLOW_DRUM_REMAINING_LIFE: ( - ATTR_YELLOW_DRUM_REMAINING_PAGES, - ATTR_YELLOW_DRUM_COUNTER, - ), -} - -SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( - key=ATTR_STATUS, - icon="mdi:printer", - name=ATTR_STATUS.title(), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PAGE_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_PAGE_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BW_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_BW_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_COLOR_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_COLOR_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_DUPLEX_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BELT_UNIT_REMAINING_LIFE, - icon="mdi:current-ac", - name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_FUSER_REMAINING_LIFE, - icon="mdi:water-outline", - name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_LASER_REMAINING_LIFE, - icon="mdi:spotlight-beam", - name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PF_KIT_1_REMAINING_LIFE, - icon="mdi:printer-3d", - name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PF_KIT_MP_REMAINING_LIFE, - icon="mdi:printer-3d", - name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_UPTIME, - name=ATTR_UPTIME.title(), - entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), -) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 00cb91b2860..bdec3049f8f 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,9 +3,18 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + CONF_HOST, + DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -13,15 +22,258 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator -from .const import ( - ATTR_COUNTER, - ATTR_MANUFACTURER, - ATTR_REMAINING_PAGES, - ATTR_UPTIME, - ATTRS_MAP, - DATA_CONFIG_ENTRY, - DOMAIN, - SENSOR_TYPES, +from .const import DATA_CONFIG_ENTRY, DOMAIN + +ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" +ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" +ATTR_BLACK_DRUM_REMAINING_LIFE = "black_drum_remaining_life" +ATTR_BLACK_DRUM_REMAINING_PAGES = "black_drum_remaining_pages" +ATTR_BLACK_INK_REMAINING = "black_ink_remaining" +ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" +ATTR_BW_COUNTER = "b/w_counter" +ATTR_COLOR_COUNTER = "color_counter" +ATTR_COUNTER = "counter" +ATTR_CYAN_DRUM_COUNTER = "cyan_drum_counter" +ATTR_CYAN_DRUM_REMAINING_LIFE = "cyan_drum_remaining_life" +ATTR_CYAN_DRUM_REMAINING_PAGES = "cyan_drum_remaining_pages" +ATTR_CYAN_INK_REMAINING = "cyan_ink_remaining" +ATTR_CYAN_TONER_REMAINING = "cyan_toner_remaining" +ATTR_DRUM_COUNTER = "drum_counter" +ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life" +ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" +ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter" +ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" +ATTR_LASER_REMAINING_LIFE = "laser_remaining_life" +ATTR_MAGENTA_DRUM_COUNTER = "magenta_drum_counter" +ATTR_MAGENTA_DRUM_REMAINING_LIFE = "magenta_drum_remaining_life" +ATTR_MAGENTA_DRUM_REMAINING_PAGES = "magenta_drum_remaining_pages" +ATTR_MAGENTA_INK_REMAINING = "magenta_ink_remaining" +ATTR_MAGENTA_TONER_REMAINING = "magenta_toner_remaining" +ATTR_MANUFACTURER = "Brother" +ATTR_PAGE_COUNTER = "page_counter" +ATTR_PF_KIT_1_REMAINING_LIFE = "pf_kit_1_remaining_life" +ATTR_PF_KIT_MP_REMAINING_LIFE = "pf_kit_mp_remaining_life" +ATTR_REMAINING_PAGES = "remaining_pages" +ATTR_STATUS = "status" +ATTR_UPTIME = "uptime" +ATTR_YELLOW_DRUM_COUNTER = "yellow_drum_counter" +ATTR_YELLOW_DRUM_REMAINING_LIFE = "yellow_drum_remaining_life" +ATTR_YELLOW_DRUM_REMAINING_PAGES = "yellow_drum_remaining_pages" +ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining" +ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining" + +UNIT_PAGES = "p" + +ATTRS_MAP: dict[str, tuple[str, str]] = { + ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), + ATTR_BLACK_DRUM_REMAINING_LIFE: ( + ATTR_BLACK_DRUM_REMAINING_PAGES, + ATTR_BLACK_DRUM_COUNTER, + ), + ATTR_CYAN_DRUM_REMAINING_LIFE: ( + ATTR_CYAN_DRUM_REMAINING_PAGES, + ATTR_CYAN_DRUM_COUNTER, + ), + ATTR_MAGENTA_DRUM_REMAINING_LIFE: ( + ATTR_MAGENTA_DRUM_REMAINING_PAGES, + ATTR_MAGENTA_DRUM_COUNTER, + ), + ATTR_YELLOW_DRUM_REMAINING_LIFE: ( + ATTR_YELLOW_DRUM_REMAINING_PAGES, + ATTR_YELLOW_DRUM_COUNTER, + ), +} + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_STATUS, + icon="mdi:printer", + name=ATTR_STATUS.title(), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_PAGE_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_PAGE_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_BW_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_BW_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_COLOR_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_COLOR_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_DUPLEX_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_BLACK_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_CYAN_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_YELLOW_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_BELT_UNIT_REMAINING_LIFE, + icon="mdi:current-ac", + name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_FUSER_REMAINING_LIFE, + icon="mdi:water-outline", + name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_LASER_REMAINING_LIFE, + icon="mdi:spotlight-beam", + name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_PF_KIT_1_REMAINING_LIFE, + icon="mdi:printer-3d", + name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_PF_KIT_MP_REMAINING_LIFE, + icon="mdi:printer-3d", + name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_BLACK_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_CYAN_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_MAGENTA_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_YELLOW_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_BLACK_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_CYAN_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_MAGENTA_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_YELLOW_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_UPTIME, + name=ATTR_UPTIME.title(), + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), ) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 1f103390b0a..a2763c3ed91 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -3,7 +3,8 @@ from datetime import datetime, timedelta import json from unittest.mock import Mock, patch -from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES +from homeassistant.components.brother.const import DOMAIN +from homeassistant.components.brother.sensor import UNIT_PAGES from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, From 15f62a7237f5616d2985fba7ec7b5a916df256b3 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Nov 2021 22:54:49 +0100 Subject: [PATCH 0847/1452] More tests optimizations for SamsungTV (#60297) * More tests optimizations * Apply review comments --- .../components/samsungtv/test_config_flow.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4c5d77fcc1f..2443955ac14 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -300,13 +300,14 @@ async def test_user_not_successful_2(hass: HomeAssistant, remotews: Mock): assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp(hass: HomeAssistant, remote: Mock): +async def test_ssdp(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): """Test starting a flow from discovery.""" + no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", return_value=MOCK_DEVICE_INFO, - ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -327,13 +328,14 @@ async def test_ssdp(hass: HomeAssistant, remote: Mock): assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): +async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): """Test starting a flow from discovery without prefixes.""" + no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", return_value=MOCK_DEVICE_INFO_2, - ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, @@ -529,13 +531,16 @@ async def test_ssdp_not_successful_2( assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_already_in_progress(hass: HomeAssistant, remote: Mock): +async def test_ssdp_already_in_progress( + hass: HomeAssistant, remote: Mock, no_mac_address: Mock +): """Test starting a flow from discovery twice.""" + no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", return_value=MOCK_DEVICE_INFO, - ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -552,13 +557,16 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): +async def test_ssdp_already_configured( + hass: HomeAssistant, remote: Mock, no_mac_address: Mock +): """Test starting a flow from discovery when already configured.""" + no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", return_value=MOCK_DEVICE_INFO, - ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + ): # entry was added result = await hass.config_entries.flow.async_init( @@ -581,12 +589,14 @@ async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_import_legacy(hass: HomeAssistant, remote: Mock): +async def test_import_legacy(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): """Test importing from yaml with hostname.""" + + no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", return_value="fake_host", - ), patch("getmac.get_mac_address", return_value="aa:bb:cc:dd:ee:ff"): + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, From de1527d0e9b3b4e98925eb37ba83b5473d456796 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 25 Nov 2021 00:02:18 +0200 Subject: [PATCH 0848/1452] Add wirelesstag ambient temperature sensor for Outdoor Probe (#60243) * Added support for ambient temperature sensor for Outdoor Probe. * Switched to use of SensorEntityDescription for device_class. * Removed unused config param, restored use of _sensor_type ivar. * Use entity descriptions as dict for shorter iteration. * Clean up Co-authored-by: Martin Hjelmare --- .../components/wirelesstag/sensor.py | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 7ad0a7f52c2..8038b42bffe 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -1,10 +1,22 @@ """Sensor support for Wireless Sensor Tags platform.""" +from __future__ import annotations + import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,16 +26,45 @@ from . import DOMAIN as WIRELESSTAG_DOMAIN, SIGNAL_TAG_UPDATE, WirelessTagBaseSe _LOGGER = logging.getLogger(__name__) SENSOR_TEMPERATURE = "temperature" +SENSOR_AMBIENT_TEMPERATURE = "ambient_temperature" SENSOR_HUMIDITY = "humidity" SENSOR_MOISTURE = "moisture" SENSOR_LIGHT = "light" -SENSOR_TYPES = [SENSOR_TEMPERATURE, SENSOR_HUMIDITY, SENSOR_MOISTURE, SENSOR_LIGHT] +SENSOR_TYPES: dict[str, SensorEntityDescription] = { + SENSOR_TEMPERATURE: SensorEntityDescription( + key=SENSOR_TEMPERATURE, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SENSOR_AMBIENT_TEMPERATURE: SensorEntityDescription( + key=SENSOR_AMBIENT_TEMPERATURE, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SENSOR_HUMIDITY: SensorEntityDescription( + key=SENSOR_HUMIDITY, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SENSOR_MOISTURE: SensorEntityDescription( + key=SENSOR_MOISTURE, + device_class=SENSOR_MOISTURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SENSOR_LIGHT: SensorEntityDescription( + key=SENSOR_LIGHT, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), +} + +SENSOR_KEYS: list[str] = list(SENSOR_TYPES) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(SENSOR_KEYS)] ) } ) @@ -35,11 +76,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] tags = platform.tags for tag in tags.values(): - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - if sensor_type in tag.allowed_sensor_types: - sensors.append( - WirelessTagSensor(platform, tag, sensor_type, hass.config) - ) + for key in config[CONF_MONITORED_CONDITIONS]: + if key not in tag.allowed_sensor_types: + continue + description = SENSOR_TYPES[key] + sensors.append(WirelessTagSensor(platform, tag, description)) add_entities(sensors, True) @@ -47,11 +88,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): """Representation of a Sensor.""" - def __init__(self, api, tag, sensor_type, config): + entity_description: SensorEntityDescription + + def __init__(self, api, tag, description): """Initialize a WirelessTag sensor.""" super().__init__(api, tag) - self._sensor_type = sensor_type + self._sensor_type = description.key + self.entity_description = description self._name = self._tag.name # I want to see entity_id as: @@ -87,11 +131,6 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): """Return the state of the sensor.""" return self._state - @property - def device_class(self): - """Return the class of the sensor.""" - return self._sensor_type - @property def native_unit_of_measurement(self): """Return the unit of measurement.""" From 301ef0f636c735e0b89afcbc1e47b0748c84b0b0 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 24 Nov 2021 23:18:45 +0100 Subject: [PATCH 0849/1452] Fix BMW ConnectedDrive, update to My BMW API (#59881) * Fix BMW ConnectedDrive, update to My BMW API * Use const device classes * Implement native_value via EntityDescription * Use device class const, reomve device_class from charging_status * Cleanup * Remove max_range_electric * Revert removing sensor name & unique_id * Add region china again, update bimmer_connected * Update to bimmer_connected==0.8.2 Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 4 +- .../bmw_connected_drive/binary_sensor.py | 48 +- .../components/bmw_connected_drive/const.py | 14 + .../bmw_connected_drive/device_tracker.py | 9 +- .../components/bmw_connected_drive/lock.py | 8 +- .../bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/notify.py | 7 +- .../components/bmw_connected_drive/sensor.py | 499 ++---------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 103 insertions(+), 492 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c7592c5db34..78a41ec8548 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -347,9 +347,9 @@ class BMWConnectedDriveBaseEntity(Entity): } self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, vehicle.vin)}, - manufacturer=vehicle.attributes.get("brand"), + manufacturer=vehicle.brand.name, model=vehicle.name, - name=f'{vehicle.attributes.get("brand")} {vehicle.name}', + name=f"{vehicle.brand.name} {vehicle.name}", ) def update_callback(self) -> None: diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 7ed23f72389..37c0271a034 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -6,11 +6,18 @@ from dataclasses import dataclass import logging from typing import Any, cast -from bimmer_connected.state import ChargingState, LockState, VehicleState from bimmer_connected.vehicle import ConnectedDriveVehicle -from bimmer_connected.vehicle_status import ConditionBasedServiceReport +from bimmer_connected.vehicle_status import ( + ChargingState, + ConditionBasedServiceReport, + LockState, + VehicleStatus, +) from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, DEVICE_CLASS_OPENING, DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, @@ -18,7 +25,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import LENGTH_KILOMETERS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import UnitSystem @@ -28,13 +34,13 @@ from . import ( BMWConnectedDriveAccount, BMWConnectedDriveBaseEntity, ) -from .const import CONF_ACCOUNT, DATA_ENTRIES +from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP _LOGGER = logging.getLogger(__name__) def _are_doors_closed( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class opening: On means open, Off means closed _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) @@ -44,7 +50,7 @@ def _are_doors_closed( def _are_windows_closed( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class opening: On means open, Off means closed for window in vehicle_state.windows: @@ -53,7 +59,7 @@ def _are_windows_closed( def _are_doors_locked( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class lock: On means unlocked, Off means locked # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED @@ -63,7 +69,7 @@ def _are_doors_locked( def _are_parking_lights_on( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class light: On means light detected, Off means no light extra_attributes["lights_parking"] = vehicle_state.parking_lights.value @@ -71,7 +77,7 @@ def _are_parking_lights_on( def _are_problems_detected( - vehicle_state: VehicleState, + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], unit_system: UnitSystem, ) -> bool: @@ -82,7 +88,7 @@ def _are_problems_detected( def _check_control_messages( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class problem: On means problem detected, Off means no problem check_control_messages = vehicle_state.check_control_messages @@ -96,7 +102,7 @@ def _check_control_messages( def _is_vehicle_charging( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class power: On means power detected, Off means no power extra_attributes["charging_status"] = vehicle_state.charging_status.value @@ -107,7 +113,7 @@ def _is_vehicle_charging( def _is_vehicle_plugged_in( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class plug: On means device is plugged in, # Off means device is unplugged @@ -124,7 +130,12 @@ def _format_cbs_report( if report.due_date is not None: result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance is not None: - distance = round(unit_system.length(report.due_distance, LENGTH_KILOMETERS)) + distance = round( + unit_system.length( + report.due_distance[0], + UNIT_MAP.get(report.due_distance[1], report.due_distance[1]), + ) + ) result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" return result @@ -133,7 +144,7 @@ def _format_cbs_report( class BMWRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[VehicleState, dict[str, Any], UnitSystem], bool] + value_fn: Callable[[VehicleStatus, dict[str, Any], UnitSystem], bool] @dataclass @@ -161,14 +172,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="door_lock_state", name="Door lock state", - device_class="lock", + device_class=DEVICE_CLASS_LOCK, icon="mdi:car-key", value_fn=_are_doors_locked, ), BMWBinarySensorEntityDescription( key="lights_parking", name="Parking lights", - device_class="light", + device_class=DEVICE_CLASS_LIGHT, icon="mdi:car-parking-lights", value_fn=_are_parking_lights_on, ), @@ -190,7 +201,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="charging_status", name="Charging status", - device_class="power", + device_class=DEVICE_CLASS_BATTERY_CHARGING, icon="mdi:ev-station", value_fn=_is_vehicle_charging, ), @@ -245,7 +256,8 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): def update(self) -> None: """Read new state data from the library.""" - vehicle_state = self._vehicle.state + _LOGGER.debug("Updating binary sensors of %s", self._vehicle.name) + vehicle_state = self._vehicle.status result = self._attrs.copy() self._attr_is_on = self.entity_description.value_fn( diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 7af24496838..83609d239c1 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,4 +1,11 @@ """Const file for the BMW Connected Drive integration.""" +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_MILES, + VOLUME_GALLONS, + VOLUME_LITERS, +) + ATTRIBUTION = "Data provided by BMW Connected Drive" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] @@ -9,3 +16,10 @@ CONF_ACCOUNT = "account" DATA_HASS_CONFIG = "hass_config" DATA_ENTRIES = "entries" + +UNIT_MAP = { + "KILOMETERS": LENGTH_KILOMETERS, + "MILES": LENGTH_MILES, + "LITERS": VOLUME_LITERS, + "GALLONS": VOLUME_GALLONS, +} diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 4d7b6094968..d17920fef0c 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) - if not vehicle.state.is_vehicle_tracking_enabled: + if not vehicle.status.is_vehicle_tracking_enabled: _LOGGER.info( "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", vehicle.name, @@ -59,7 +59,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): super().__init__(account, vehicle) self._attr_unique_id = vehicle.vin - self._location = pos if (pos := vehicle.state.gps_position) else None + self._location = pos if (pos := vehicle.status.gps_position) else None self._attr_name = vehicle.name @property @@ -79,9 +79,10 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def update(self) -> None: """Update state of the decvice tracker.""" + _LOGGER.debug("Updating device tracker of %s", self._vehicle.name) self._attr_extra_state_attributes = self._attrs self._location = ( - self._vehicle.state.gps_position - if self._vehicle.state.is_vehicle_tracking_enabled + self._vehicle.status.gps_position + if self._vehicle.status.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 62e42476812..71539019f82 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -2,8 +2,8 @@ import logging from typing import Any -from bimmer_connected.state import LockState from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle_status import LockState from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -78,8 +78,10 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): def update(self) -> None: """Update state of the lock.""" - _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) - vehicle_state = self._vehicle.state + _LOGGER.debug( + "Updating lock data for '%s' of %s", self._attribute, self._vehicle.name + ) + vehicle_state = self._vehicle.status if not self.door_lock_state_available: self._attr_is_locked = None else: diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 110f8295c8a..95ea2061fb4 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.21"], + "requirements": ["bimmer_connected==0.8.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 4f4645bf78a..2db25aaa592 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -9,8 +9,6 @@ from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, BaseNotificationService, ) from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME @@ -63,7 +61,6 @@ class BMWNotificationService(BaseNotificationService): _LOGGER.debug("Sending message to %s", vehicle.name) # Extract params from data dict - title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) # Check if message is a POI @@ -84,6 +81,4 @@ class BMWNotificationService(BaseNotificationService): vehicle.remote_services.trigger_send_poi(location_dict) else: - vehicle.remote_services.trigger_send_message( - {ATTR_TEXT: message, ATTR_SUBJECT: title} - ) + raise ValueError(f"'data.{ATTR_LOCATION}' is required.") diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4f0dc7904fc..3eda7ccd5b0 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,34 +1,28 @@ """Support for reading vehicle status from BMW connected drive portal.""" from __future__ import annotations -from copy import copy +from collections.abc import Callable from dataclasses import dataclass import logging +from typing import cast -from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS -from bimmer_connected.state import ChargingState from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, - DEVICE_CLASS_TIMESTAMP, - ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, + DEVICE_CLASS_BATTERY, LENGTH_KILOMETERS, LENGTH_MILES, - MASS_KILOGRAMS, PERCENTAGE, TIME_HOURS, - TIME_MINUTES, VOLUME_GALLONS, VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import StateType from homeassistant.util.unit_system import UnitSystem from . import ( @@ -36,7 +30,7 @@ from . import ( BMWConnectedDriveAccount, BMWConnectedDriveBaseEntity, ) -from .const import CONF_ACCOUNT, DATA_ENTRIES +from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP _LOGGER = logging.getLogger(__name__) @@ -47,6 +41,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric: str | None = None unit_imperial: str | None = None + value: Callable = lambda x, y: x SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { @@ -59,58 +54,14 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "charging_status": BMWSensorEntityDescription( key="charging_status", - icon="mdi:battery-charging", + icon="mdi:ev-station", + value=lambda x, y: x.value, ), - # No icon as this is dealt with directly as a special case in icon() "charging_level_hv": BMWSensorEntityDescription( key="charging_level_hv", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, - ), - # LastTrip attributes - "date_utc": BMWSensorEntityDescription( - key="date_utc", - device_class=DEVICE_CLASS_TIMESTAMP, - ), - "duration": BMWSensorEntityDescription( - key="duration", - icon="mdi:timer-outline", - unit_metric=TIME_MINUTES, - unit_imperial=TIME_MINUTES, - ), - "electric_distance_ratio": BMWSensorEntityDescription( - key="electric_distance_ratio", - icon="mdi:percent-outline", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - # AllTrips attributes - "battery_size_max": BMWSensorEntityDescription( - key="battery_size_max", - icon="mdi:battery-charging-high", - unit_metric=ENERGY_WATT_HOUR, - unit_imperial=ENERGY_WATT_HOUR, - entity_registry_enabled_default=False, - ), - "reset_date_utc": BMWSensorEntityDescription( - key="reset_date_utc", - device_class=DEVICE_CLASS_TIMESTAMP, - entity_registry_enabled_default=False, - ), - "saved_co2": BMWSensorEntityDescription( - key="saved_co2", - icon="mdi:tree-outline", - unit_metric=MASS_KILOGRAMS, - unit_imperial=MASS_KILOGRAMS, - entity_registry_enabled_default=False, - ), - "saved_co2_green_energy": BMWSensorEntityDescription( - key="saved_co2_green_energy", - icon="mdi:tree-outline", - unit_metric=MASS_KILOGRAMS, - unit_imperial=MASS_KILOGRAMS, - entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( @@ -118,254 +69,61 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { icon="mdi:speedometer", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - ), - "max_range_electric": BMWSensorEntityDescription( - key="max_range_electric", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", icon="mdi:gas-station", unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, + value=lambda x, hass: round( + hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), - # LastTrip attributes - "average_combined_consumption": BMWSensorEntityDescription( - key="average_combined_consumption", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_electric_consumption": BMWSensorEntityDescription( - key="average_electric_consumption", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_recuperation": BMWSensorEntityDescription( - key="average_recuperation", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "electric_distance": BMWSensorEntityDescription( - key="electric_distance", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "saved_fuel": BMWSensorEntityDescription( - key="saved_fuel", - icon="mdi:fuel", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, - entity_registry_enabled_default=False, - ), - "total_distance": BMWSensorEntityDescription( - key="total_distance", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - # AllTrips attributes - "average_combined_consumption_community_average": BMWSensorEntityDescription( - key="average_combined_consumption_community_average", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_community_high": BMWSensorEntityDescription( - key="average_combined_consumption_community_high", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_community_low": BMWSensorEntityDescription( - key="average_combined_consumption_community_low", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_user_average": BMWSensorEntityDescription( - key="average_combined_consumption_user_average", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_electric_consumption_community_average": BMWSensorEntityDescription( - key="average_electric_consumption_community_average", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_community_high": BMWSensorEntityDescription( - key="average_electric_consumption_community_high", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_community_low": BMWSensorEntityDescription( - key="average_electric_consumption_community_low", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_user_average": BMWSensorEntityDescription( - key="average_electric_consumption_user_average", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_recuperation_community_average": BMWSensorEntityDescription( - key="average_recuperation_community_average", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_community_high": BMWSensorEntityDescription( - key="average_recuperation_community_high", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_community_low": BMWSensorEntityDescription( - key="average_recuperation_community_low", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_user_average": BMWSensorEntityDescription( - key="average_recuperation_user_average", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "chargecycle_range_community_average": BMWSensorEntityDescription( - key="chargecycle_range_community_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_community_high": BMWSensorEntityDescription( - key="chargecycle_range_community_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_community_low": BMWSensorEntityDescription( - key="chargecycle_range_community_low", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_user_average": BMWSensorEntityDescription( - key="chargecycle_range_user_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "chargecycle_range_user_current_charge_cycle": BMWSensorEntityDescription( - key="chargecycle_range_user_current_charge_cycle", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "chargecycle_range_user_high": BMWSensorEntityDescription( - key="chargecycle_range_user_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "total_electric_distance_community_average": BMWSensorEntityDescription( - key="total_electric_distance_community_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_community_high": BMWSensorEntityDescription( - key="total_electric_distance_community_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_community_low": BMWSensorEntityDescription( - key="total_electric_distance_community_low", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_user_average": BMWSensorEntityDescription( - key="total_electric_distance_user_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_user_total": BMWSensorEntityDescription( - key="total_electric_distance_user_total", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_saved_fuel": BMWSensorEntityDescription( - key="total_saved_fuel", - icon="mdi:fuel", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, - entity_registry_enabled_default=False, + "fuel_percent": BMWSensorEntityDescription( + key="fuel_percent", + icon="mdi:gas-station", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, ), } -DEFAULT_BMW_DESCRIPTION = BMWSensorEntityDescription( - key="", - entity_registry_enabled_default=True, -) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BMW ConnectedDrive sensors from config entry.""" - # pylint: disable=too-many-nested-blocks unit_system = hass.config.units account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ config_entry.entry_id @@ -373,118 +131,13 @@ async def async_setup_entry( entities: list[BMWConnectedDriveSensor] = [] for vehicle in account.account.vehicles: - for service in vehicle.available_state_services: - if service == SERVICE_STATUS: - entities.extend( - [ - BMWConnectedDriveSensor( - account, vehicle, description, unit_system - ) - for attribute_name in vehicle.drive_train_attributes - if attribute_name in vehicle.available_attributes - and (description := SENSOR_TYPES.get(attribute_name)) - ] - ) - if service == SERVICE_LAST_TRIP: - entities.extend( - [ - # mypy issues will be fixed in next release - # https://github.com/python/mypy/issues/9096 - BMWConnectedDriveSensor( - account, - vehicle, - description, # type: ignore[arg-type] - unit_system, - service, - ) - for attribute_name in vehicle.state.last_trip.available_attributes - if attribute_name != "date" - and (description := SENSOR_TYPES.get(attribute_name)) # type: ignore[no-redef] - ] - ) - if "date" in vehicle.state.last_trip.available_attributes: - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES["date_utc"], - unit_system, - service, - ) - ) - if service == SERVICE_ALL_TRIPS: - for attribute_name in vehicle.state.all_trips.available_attributes: - if attribute_name == "reset_date": - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES["reset_date_utc"], - unit_system, - service, - ) - ) - elif attribute_name in ( - "average_combined_consumption", - "average_electric_consumption", - "average_recuperation", - "chargecycle_range", - "total_electric_distance", - ): - entities.extend( - [ - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ( - "community_average", - "community_high", - "community_low", - "user_average", - ) - ] - ) - if attribute_name == "chargecycle_range": - entities.extend( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ("user_current_charge_cycle", "user_high") - ) - elif attribute_name == "total_electric_distance": - entities.extend( - [ - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ("user_total",) - ] - ) - else: - if (description := SENSOR_TYPES.get(attribute_name)) is None: - description = copy(DEFAULT_BMW_DESCRIPTION) - description.key = attribute_name - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - description, - unit_system, - service, - ) - ) + entities.extend( + [ + BMWConnectedDriveSensor(account, vehicle, description, unit_system) + for attribute_name in vehicle.available_attributes + if (description := SENSOR_TYPES.get(attribute_name)) + ] + ) async_add_entities(entities, True) @@ -500,87 +153,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): vehicle: ConnectedDriveVehicle, description: BMWSensorEntityDescription, unit_system: UnitSystem, - service: str | None = None, ) -> None: """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) self.entity_description = description - self._service = service - if service: - self._attr_name = f"{vehicle.name} {service.lower()}_{description.key}" - self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{description.key}" - else: - self._attr_name = f"{vehicle.name} {description.key}" - self._attr_unique_id = f"{vehicle.vin}-{description.key}" + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: self._attr_native_unit_of_measurement = description.unit_imperial else: self._attr_native_unit_of_measurement = description.unit_metric - def update(self) -> None: - """Read new state data from the library.""" - _LOGGER.debug("Updating %s", self._vehicle.name) - vehicle_state = self._vehicle.state - sensor_key = self.entity_description.key - if sensor_key == "charging_status": - self._attr_native_value = getattr(vehicle_state, sensor_key).value - elif self.unit_of_measurement == VOLUME_GALLONS: - value = getattr(vehicle_state, sensor_key) - value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) - self._attr_native_value = round(value_converted) - elif self.unit_of_measurement == LENGTH_MILES: - value = getattr(vehicle_state, sensor_key) - value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) - self._attr_native_value = round(value_converted) - elif self._service is None: - self._attr_native_value = getattr(vehicle_state, sensor_key) - elif self._service == SERVICE_LAST_TRIP: - vehicle_last_trip = self._vehicle.state.last_trip - if sensor_key == "date_utc": - date_str = getattr(vehicle_last_trip, "date") - if parsed_date := dt_util.parse_datetime(date_str): - self._attr_native_value = parsed_date.isoformat() - else: - _LOGGER.debug( - "Could not parse date string for 'date_utc' sensor: %s", - date_str, - ) - self._attr_native_value = None - else: - self._attr_native_value = getattr(vehicle_last_trip, sensor_key) - elif self._service == SERVICE_ALL_TRIPS: - vehicle_all_trips = self._vehicle.state.all_trips - for attribute in ( - "average_combined_consumption", - "average_electric_consumption", - "average_recuperation", - "chargecycle_range", - "total_electric_distance", - ): - if sensor_key.startswith(f"{attribute}_"): - attr = getattr(vehicle_all_trips, attribute) - sub_attr = sensor_key.replace(f"{attribute}_", "") - self._attr_native_value = getattr(attr, sub_attr) - return - if sensor_key == "reset_date_utc": - date_str = getattr(vehicle_all_trips, "reset_date") - if parsed_date := dt_util.parse_datetime(date_str): - self._attr_native_value = parsed_date.isoformat() - else: - _LOGGER.debug( - "Could not parse date string for 'reset_date_utc' sensor: %s", - date_str, - ) - self._attr_native_value = None - else: - self._attr_native_value = getattr(vehicle_all_trips, sensor_key) - - vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in {ChargingState.CHARGING} - - if sensor_key == "charging_level_hv": - self._attr_icon = icon_for_battery_level( - battery_level=vehicle_state.charging_level_hv, charging=charging_state - ) + @property + def native_value(self) -> StateType: + """Return the state.""" + state = getattr(self._vehicle.status, self.entity_description.key) + return cast(StateType, self.entity_description.value(state, self.hass)) diff --git a/requirements_all.txt b/requirements_all.txt index e113992ce2d..e0c9a429cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.21 +bimmer_connected==0.8.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae165ba050..c7a5a0145f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.21 +bimmer_connected==0.8.2 # homeassistant.components.blebox blebox_uniapi==1.3.3 From c808fa8d3d63629e5ae31fff027a49cee2c026fa Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Nov 2021 23:27:13 +0100 Subject: [PATCH 0850/1452] fix configuration url (#60311) --- homeassistant/components/fronius/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 9afd34ddc4a..d114d732f1f 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -141,7 +141,7 @@ class FroniusSolarNet: async def _create_solar_net_device(self) -> DeviceInfo: """Create a device for the Fronius SolarNet system.""" solar_net_device: DeviceInfo = DeviceInfo( - configuration_url=self.host, + configuration_url=self.fronius.url, identifiers={(DOMAIN, self.solar_net_device_id)}, manufacturer="Fronius", name="SolarNet", From f3d5c07615f04135bc958bba7a1c91200a8ede18 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 23:35:54 +0100 Subject: [PATCH 0851/1452] Use async_validate_actions_config helper in scripts (#60287) --- homeassistant/components/script/config.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 44b739e84c7..24c5f86078b 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -1,5 +1,4 @@ """Config validation helper for the script integration.""" -import asyncio from contextlib import suppress import voluptuous as vol @@ -24,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv from homeassistant.helpers.script import ( SCRIPT_MODE_SINGLE, - async_validate_action_config, + async_validate_actions_config, make_script_schema, ) from homeassistant.helpers.selector import validate_selector @@ -72,11 +71,8 @@ async def async_validate_config_item(hass, config, full_config=None): return await blueprints.async_inputs_from_config(config) config = SCRIPT_ENTITY_SCHEMA(config) - config[CONF_SEQUENCE] = await asyncio.gather( - *( - async_validate_action_config(hass, action) - for action in config[CONF_SEQUENCE] - ) + config[CONF_SEQUENCE] = await async_validate_actions_config( + hass, config[CONF_SEQUENCE] ) return config From d3c020325b392c7a270361f4476608454e3ceb31 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Nov 2021 00:13:33 +0000 Subject: [PATCH 0852/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 5 +- .../accuweather/translations/ja.json | 5 ++ .../components/acmeda/translations/ja.json | 3 + .../components/adguard/translations/ja.json | 7 +- .../advantage_air/translations/ja.json | 6 ++ .../components/agent_dvr/translations/ja.json | 7 ++ .../components/airly/translations/ja.json | 1 + .../components/airvisual/translations/ja.json | 7 ++ .../alarmdecoder/translations/ja.json | 6 ++ .../components/almond/translations/ja.json | 4 +- .../ambiclimate/translations/ja.json | 4 +- .../components/arcam_fmj/translations/ja.json | 10 +++ .../components/asuswrt/translations/ja.json | 1 + .../components/atag/translations/ja.json | 4 ++ .../components/august/translations/ja.json | 3 +- .../components/awair/translations/ja.json | 11 ++++ .../components/axis/translations/ja.json | 8 ++- .../azure_devops/translations/ja.json | 6 ++ .../binary_sensor/translations/ja.json | 39 ++++++++--- .../components/blebox/translations/ja.json | 7 ++ .../components/blink/translations/ja.json | 9 +++ .../bmw_connected_drive/translations/ja.json | 3 +- .../components/bond/translations/ja.json | 14 ++++ .../components/braviatv/translations/ja.json | 6 ++ .../components/broadlink/translations/ja.json | 12 +++- .../components/brother/translations/ja.json | 1 + .../components/brunt/translations/he.json | 27 ++++++++ .../components/bsblan/translations/ja.json | 6 ++ .../buienradar/translations/ja.json | 4 +- .../components/button/translations/ja.json | 2 +- .../components/canary/translations/ja.json | 17 +++++ .../components/cast/translations/ja.json | 3 +- .../cert_expiry/translations/ja.json | 5 +- .../components/climacell/translations/ja.json | 7 +- .../components/climate/translations/ja.json | 6 ++ .../components/cloud/translations/ja.json | 2 + .../cloudflare/translations/ja.json | 19 +++++- .../configurator/translations/ja.json | 3 +- .../components/control4/translations/ja.json | 8 +++ .../conversation/translations/ja.json | 3 + .../coolmaster/translations/ja.json | 1 + .../components/cover/translations/ja.json | 6 ++ .../components/daikin/translations/ja.json | 10 ++- .../components/deconz/translations/ja.json | 37 ++++++++++- .../components/demo/translations/ja.json | 17 +++++ .../components/denonavr/translations/ja.json | 5 ++ .../device_tracker/translations/ja.json | 6 ++ .../devolo_home_control/translations/ja.json | 3 + .../components/dexcom/translations/ja.json | 13 +++- .../dialogflow/translations/ja.json | 4 ++ .../components/directv/translations/ja.json | 3 + .../components/doorbird/translations/ja.json | 15 +++++ .../components/dsmr/translations/ja.json | 11 ++++ .../components/dunehd/translations/ja.json | 11 +++- .../components/eafm/translations/ja.json | 4 ++ .../components/ecobee/translations/ja.json | 3 + .../components/elgato/translations/ja.json | 10 ++- .../components/elkm1/translations/ja.json | 5 ++ .../emulated_roku/translations/ja.json | 3 + .../components/enocean/translations/ja.json | 4 ++ .../components/epson/translations/ja.json | 1 + .../components/esphome/translations/ja.json | 2 + .../components/ezviz/translations/ja.json | 4 +- .../components/fan/translations/ja.json | 8 +++ .../fireservicerota/translations/ja.json | 11 ++++ .../components/firmata/translations/ja.json | 7 ++ .../flick_electric/translations/ja.json | 9 +++ .../components/flo/translations/ja.json | 8 +++ .../components/flume/translations/ja.json | 9 ++- .../flunearyou/translations/ja.json | 9 ++- .../forecast_solar/translations/zh-Hans.json | 31 +++++++++ .../forked_daapd/translations/ja.json | 8 ++- .../components/fritz/translations/ja.json | 2 + .../components/fritzbox/translations/ja.json | 13 +++- .../fritzbox_callmonitor/translations/ja.json | 13 ++++ .../components/fronius/translations/ca.json | 20 ++++++ .../components/fronius/translations/de.json | 20 ++++++ .../components/fronius/translations/et.json | 20 ++++++ .../components/fronius/translations/id.json | 20 ++++++ .../components/fronius/translations/ja.json | 20 ++++++ .../components/fronius/translations/no.json | 20 ++++++ .../components/fronius/translations/ru.json | 20 ++++++ .../fronius/translations/zh-Hant.json | 20 ++++++ .../components/geofency/translations/ja.json | 4 ++ .../geonetnz_quakes/translations/ja.json | 3 +- .../geonetnz_volcano/translations/ja.json | 3 + .../components/goalzero/translations/ja.json | 9 ++- .../components/gogogate2/translations/ja.json | 10 ++- .../google_travel_time/translations/ja.json | 6 +- .../components/gpslogger/translations/ja.json | 4 ++ .../components/gree/translations/ja.json | 13 ++++ .../components/group/translations/ja.json | 5 +- .../components/guardian/translations/ja.json | 6 ++ .../components/harmony/translations/ja.json | 14 ++++ .../components/hassio/translations/ja.json | 4 ++ .../components/heos/translations/ja.json | 6 ++ .../components/hive/translations/ja.json | 10 ++- .../components/hlk_sw16/translations/ja.json | 8 +++ .../home_connect/translations/ja.json | 16 +++++ .../home_plus_control/translations/ja.json | 5 +- .../homeassistant/translations/ja.json | 7 +- .../components/homekit/translations/ja.json | 21 +++++- .../homekit_controller/translations/ja.json | 18 ++++- .../homematicip_cloud/translations/ja.json | 7 +- .../huawei_lte/translations/ja.json | 4 +- .../components/hue/translations/he.json | 1 + .../components/hue/translations/ja.json | 8 ++- .../humidifier/translations/ja.json | 27 ++++++++ .../translations/ja.json | 7 ++ .../hvv_departures/translations/ja.json | 21 ++++++ .../components/hyperion/translations/ja.json | 25 ++++++- .../components/iaqualink/translations/ja.json | 6 ++ .../components/icloud/translations/ja.json | 7 +- .../components/ifttt/translations/ja.json | 4 ++ .../components/insteon/translations/ja.json | 43 +++++++++++- .../components/ipma/translations/ja.json | 6 ++ .../components/ipp/translations/ja.json | 10 ++- .../components/iqvia/translations/ja.json | 3 + .../islamic_prayer_times/translations/ja.json | 16 ++++- .../components/isy994/translations/ja.json | 17 ++++- .../components/juicenet/translations/ja.json | 19 ++++++ .../components/kmtronic/translations/id.json | 9 +++ .../components/kmtronic/translations/ja.json | 9 +++ .../components/knx/translations/he.json | 29 +++++++++ .../components/knx/translations/id.json | 40 +++++++++++- .../components/knx/translations/zh-Hans.json | 14 ++++ .../components/kodi/translations/ja.json | 15 ++++- .../components/konnected/translations/ca.json | 1 + .../components/konnected/translations/de.json | 1 + .../components/konnected/translations/en.json | 1 + .../components/konnected/translations/et.json | 1 + .../components/konnected/translations/id.json | 1 + .../components/konnected/translations/ja.json | 5 +- .../components/konnected/translations/no.json | 1 + .../konnected/translations/zh-Hant.json | 1 + .../components/kraken/translations/id.json | 10 +++ .../components/life360/translations/ja.json | 9 ++- .../components/litejet/translations/id.json | 10 +++ .../components/local_ip/translations/ja.json | 3 + .../components/locative/translations/ja.json | 4 ++ .../components/lock/translations/ja.json | 19 +++++- .../logi_circle/translations/ja.json | 9 ++- .../components/lookin/translations/id.json | 3 + .../components/lovelace/translations/ja.json | 2 + .../lutron_caseta/translations/ja.json | 29 ++++++++- .../components/lyric/translations/id.json | 1 + .../components/lyric/translations/ja.json | 2 +- .../components/mailgun/translations/ja.json | 4 ++ .../media_player/translations/ja.json | 6 +- .../components/met/translations/ja.json | 7 +- .../met_eireann/translations/ja.json | 1 + .../meteo_france/translations/ja.json | 25 ++++++- .../meteoclimatic/translations/id.json | 9 +++ .../components/metoffice/translations/ja.json | 10 ++- .../components/mikrotik/translations/ja.json | 9 ++- .../components/mill/translations/he.json | 11 ++++ .../components/mill/translations/id.json | 3 +- .../components/mill/translations/ja.json | 6 ++ .../minecraft_server/translations/ja.json | 3 +- .../modem_callerid/translations/id.json | 8 ++- .../modern_forms/translations/id.json | 10 ++- .../components/monoprice/translations/ja.json | 13 +++- .../motion_blinds/translations/id.json | 12 +++- .../motion_blinds/translations/ja.json | 4 ++ .../components/motioneye/translations/id.json | 1 + .../components/mqtt/translations/id.json | 13 +++- .../components/mqtt/translations/ja.json | 10 +++ .../components/mutesync/translations/id.json | 1 + .../components/myq/translations/ja.json | 9 ++- .../components/mysensors/translations/id.json | 3 + .../components/mysensors/translations/ja.json | 6 ++ .../components/nam/translations/he.json | 16 ++++- .../components/nanoleaf/translations/id.json | 1 + .../components/neato/translations/ja.json | 5 +- .../components/nest/translations/ja.json | 18 ++++- .../components/netatmo/translations/id.json | 1 + .../components/netatmo/translations/ja.json | 9 ++- .../components/netgear/translations/id.json | 11 +++- .../components/netgear/translations/ja.json | 4 +- .../components/nexia/translations/ja.json | 11 +++- .../nfandroidtv/translations/id.json | 3 +- .../nightscout/translations/ja.json | 14 +++- .../nmap_tracker/translations/id.json | 13 +++- .../nmap_tracker/translations/ja.json | 2 +- .../components/notion/translations/id.json | 1 + .../components/notion/translations/ja.json | 1 + .../components/nuheat/translations/ja.json | 11 +++- .../components/nuki/translations/ja.json | 1 + .../components/number/translations/ja.json | 5 ++ .../components/nut/translations/ja.json | 14 +++- .../components/nws/translations/ja.json | 7 ++ .../components/nzbget/translations/ja.json | 14 +++- .../components/octoprint/translations/id.json | 7 ++ .../components/omnilogic/translations/ja.json | 11 +++- .../onboarding/translations/ja.json | 2 +- .../ondilo_ico/translations/ja.json | 2 +- .../components/onewire/translations/ja.json | 6 ++ .../components/onvif/translations/ja.json | 26 +++++++- .../opentherm_gw/translations/ja.json | 4 +- .../components/openuv/translations/ja.json | 5 +- .../openweathermap/translations/ja.json | 11 +++- .../ovo_energy/translations/ja.json | 5 ++ .../components/owntracks/translations/ja.json | 3 + .../components/ozw/translations/ja.json | 17 ++++- .../panasonic_viera/translations/ja.json | 21 +++++- .../philips_js/translations/ja.json | 1 + .../components/pi_hole/translations/ja.json | 11 +++- .../components/plaato/translations/ja.json | 5 +- .../components/plant/translations/ja.json | 3 +- .../components/plex/translations/ja.json | 18 ++++- .../components/plugwise/translations/ja.json | 26 +++++++- .../plum_lightpad/translations/ja.json | 6 ++ .../components/point/translations/ja.json | 3 +- .../components/poolsense/translations/ja.json | 10 ++- .../components/powerwall/translations/ja.json | 6 +- .../components/profiler/translations/ja.json | 12 ++++ .../progettihwsw/translations/ja.json | 25 +++++++ .../components/ps4/translations/ja.json | 6 +- .../pvpc_hourly_pricing/translations/ja.json | 10 ++- .../components/rachio/translations/ja.json | 10 +++ .../rainmachine/translations/ja.json | 13 ++++ .../recollect_waste/translations/ja.json | 10 +++ .../components/rfxtrx/translations/ja.json | 65 ++++++++++++++++++- .../components/risco/translations/ja.json | 23 +++++++ .../components/roku/translations/ja.json | 12 +++- .../components/roomba/translations/ja.json | 19 +++++- .../components/roon/translations/ja.json | 7 ++ .../components/rpi_power/translations/ja.json | 14 ++++ .../ruckus_unleashed/translations/ja.json | 8 +++ .../components/samsungtv/translations/ja.json | 1 + .../screenlogic/translations/ja.json | 5 ++ .../season/translations/sensor.ja.json | 6 ++ .../components/sense/translations/ja.json | 3 +- .../components/sentry/translations/ja.json | 6 +- .../components/sharkiq/translations/ja.json | 11 ++++ .../components/shelly/translations/ja.json | 12 +++- .../shopping_list/translations/ja.json | 1 + .../components/sia/translations/ja.json | 3 +- .../simplisafe/translations/ja.json | 18 ++++- .../components/smappee/translations/ja.json | 13 ++++ .../smart_meter_texas/translations/ja.json | 8 +++ .../components/smarthab/translations/ja.json | 7 +- .../smartthings/translations/ja.json | 7 ++ .../components/sms/translations/ja.json | 20 ++++++ .../components/solaredge/translations/ja.json | 9 +++ .../solaredge/translations/zh-Hans.json | 6 +- .../components/soma/translations/ja.json | 5 +- .../components/somfy/translations/ja.json | 9 +++ .../components/sonarr/translations/ja.json | 20 +++++- .../components/songpal/translations/ja.json | 9 +++ .../speedtestdotnet/translations/ja.json | 12 ++++ .../components/spider/translations/ja.json | 7 ++ .../components/spotify/translations/ja.json | 11 +++- .../squeezebox/translations/ja.json | 9 +++ .../srp_energy/translations/ja.json | 1 + .../components/syncthru/translations/ja.json | 14 +++- .../synology_dsm/translations/ja.json | 35 ++++++++-- .../components/tado/translations/ja.json | 22 ++++++- .../components/tasmota/translations/ja.json | 10 ++- .../tellduslive/translations/ja.json | 7 +- .../components/tibber/translations/ja.json | 19 ++++++ .../components/tile/translations/ja.json | 16 +++++ .../components/timer/translations/ja.json | 9 +++ .../components/tolo/translations/en.json | 4 +- .../components/toon/translations/ja.json | 11 ++++ .../totalconnect/translations/ja.json | 3 + .../components/traccar/translations/ja.json | 5 ++ .../components/tradfri/translations/ja.json | 3 +- .../transmission/translations/ja.json | 10 +++ .../components/tuya/translations/ja.json | 7 ++ .../twentemilieu/translations/ja.json | 11 +++- .../components/twilio/translations/ja.json | 4 ++ .../components/unifi/translations/ja.json | 21 +++++- .../components/upb/translations/ja.json | 7 +- .../components/upcloud/translations/ja.json | 13 ++++ .../components/vacuum/translations/ja.json | 7 ++ .../components/velbus/translations/ja.json | 7 ++ .../components/vera/translations/ja.json | 7 ++ .../components/verisure/translations/ja.json | 4 +- .../components/vesync/translations/ja.json | 6 ++ .../components/vilfo/translations/ja.json | 9 +++ .../components/vizio/translations/ja.json | 5 +- .../components/volumio/translations/ja.json | 5 ++ .../components/wallbox/translations/he.json | 9 ++- .../waze_travel_time/translations/ja.json | 6 +- .../components/wemo/translations/ja.json | 6 +- .../components/wilight/translations/ja.json | 13 ++++ .../components/withings/translations/ja.json | 9 ++- .../components/wled/translations/ja.json | 19 ++++++ .../wled/translations/select.he.json | 8 +++ .../wled/translations/select.hu.json | 9 +++ .../components/wolflink/translations/ja.json | 8 +++ .../wolflink/translations/sensor.ja.json | 7 ++ .../components/xbox/translations/ja.json | 17 +++++ .../xiaomi_aqara/translations/ja.json | 3 + .../xiaomi_miio/translations/ja.json | 3 + .../components/yeelight/translations/ja.json | 7 ++ .../components/zerproc/translations/ja.json | 13 ++++ .../components/zha/translations/ja.json | 11 +++- .../zoneminder/translations/ja.json | 12 +++- .../components/zwave/translations/ja.json | 7 +- .../components/zwave_js/translations/ja.json | 1 + 302 files changed, 2672 insertions(+), 184 deletions(-) create mode 100644 homeassistant/components/brunt/translations/he.json create mode 100644 homeassistant/components/conversation/translations/ja.json create mode 100644 homeassistant/components/firmata/translations/ja.json create mode 100644 homeassistant/components/forecast_solar/translations/zh-Hans.json create mode 100644 homeassistant/components/fronius/translations/ca.json create mode 100644 homeassistant/components/fronius/translations/de.json create mode 100644 homeassistant/components/fronius/translations/et.json create mode 100644 homeassistant/components/fronius/translations/id.json create mode 100644 homeassistant/components/fronius/translations/ja.json create mode 100644 homeassistant/components/fronius/translations/no.json create mode 100644 homeassistant/components/fronius/translations/ru.json create mode 100644 homeassistant/components/fronius/translations/zh-Hant.json create mode 100644 homeassistant/components/gree/translations/ja.json create mode 100644 homeassistant/components/home_connect/translations/ja.json create mode 100644 homeassistant/components/humidifier/translations/ja.json create mode 100644 homeassistant/components/juicenet/translations/ja.json create mode 100644 homeassistant/components/knx/translations/he.json create mode 100644 homeassistant/components/knx/translations/zh-Hans.json create mode 100644 homeassistant/components/profiler/translations/ja.json create mode 100644 homeassistant/components/rpi_power/translations/ja.json create mode 100644 homeassistant/components/sms/translations/ja.json create mode 100644 homeassistant/components/speedtestdotnet/translations/ja.json create mode 100644 homeassistant/components/tibber/translations/ja.json create mode 100644 homeassistant/components/timer/translations/ja.json create mode 100644 homeassistant/components/toon/translations/ja.json create mode 100644 homeassistant/components/wilight/translations/ja.json create mode 100644 homeassistant/components/wled/translations/select.he.json create mode 100644 homeassistant/components/wled/translations/select.hu.json create mode 100644 homeassistant/components/xbox/translations/ja.json create mode 100644 homeassistant/components/zerproc/translations/ja.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index 77e6382f84c..964ee5dee7e 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -1,9 +1,12 @@ { "config": { "abort": { - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_mfa_code": "\u7121\u52b9\u306aMFA\u30b3\u30fc\u30c9" }, "step": { diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 8e4d5ad858c..63c8561abd2 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "requests_exceeded": "Accuweather API\u3078\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u6570\u304c\u8a31\u53ef\u3055\u308c\u305f\u6570\u3092\u8d85\u3048\u307e\u3057\u305f\u3002\u6642\u9593\u3092\u7f6e\u304f\u304b\u3001API\u30ad\u30fc\u3092\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { diff --git a/homeassistant/components/acmeda/translations/ja.json b/homeassistant/components/acmeda/translations/ja.json index fd270e982e2..83eb75daebf 100644 --- a/homeassistant/components/acmeda/translations/ja.json +++ b/homeassistant/components/acmeda/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index a4bd092db59..0ea8436d990 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -4,6 +4,9 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "hassio_confirm": { "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAdGuard Home" @@ -13,7 +16,9 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "description": "\u76e3\u8996\u3068\u5236\u5fa1\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001AdGuardHome\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/advantage_air/translations/ja.json b/homeassistant/components/advantage_air/translations/ja.json index 5c8751e4ced..b47eaa705a6 100644 --- a/homeassistant/components/advantage_air/translations/ja.json +++ b/homeassistant/components/advantage_air/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/agent_dvr/translations/ja.json b/homeassistant/components/agent_dvr/translations/ja.json index 0ef445eda34..5da28535c9d 100644 --- a/homeassistant/components/agent_dvr/translations/ja.json +++ b/homeassistant/components/agent_dvr/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index cc0b861718a..347868bc35e 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "wrong_location": "\u3053\u306e\u30a8\u30ea\u30a2\u306b\u3001Airly\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index cd1b1403827..09565aabc62 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -1,6 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u307e\u305f\u306f\u3001Node/Pro ID\u306f\u65e2\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "general_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "location_not_found": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "step": { diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index 544d097afd5..b710d58d10d 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -1,8 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "create_entry": { "default": "AlarmDecoder\u3068\u306e\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index ce80c17ea52..807df670b92 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -2,7 +2,9 @@ "config": { "abort": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambiclimate/translations/ja.json b/homeassistant/components/ambiclimate/translations/ja.json index 64c9bcaf9b7..a6fbfc256ea 100644 --- a/homeassistant/components/ambiclimate/translations/ja.json +++ b/homeassistant/components/ambiclimate/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/arcam_fmj/translations/ja.json b/homeassistant/components/arcam_fmj/translations/ja.json index 045f155378d..7c14e270f34 100644 --- a/homeassistant/components/arcam_fmj/translations/ja.json +++ b/homeassistant/components/arcam_fmj/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{host}", "step": { "user": { @@ -10,5 +15,10 @@ "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} \u3092\u30aa\u30f3\u306b\u3059\u308b\u3088\u3046\u306b\u30ea\u30af\u30a8\u30b9\u30c8\u3055\u308c\u307e\u3057\u305f" + } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 53ed33fd9ee..7517a342d4b 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -32,6 +32,7 @@ "step": { "init": { "data": { + "consider_home": "\u30c7\u30d0\u30a4\u30b9\u306e\u96e2\u8131\u3092\u691c\u8a0e\u3059\u308b\u307e\u3067\u306e\u5f85\u3061\u6642\u9593(\u79d2)", "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)", "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)", diff --git a/homeassistant/components/atag/translations/ja.json b/homeassistant/components/atag/translations/ja.json index ed7241cafc5..f90666e5f39 100644 --- a/homeassistant/components/atag/translations/ja.json +++ b/homeassistant/components/atag/translations/ja.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "unauthorized": "\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u3067\u8a8d\u8a3c\u8981\u6c42\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, "step": { diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index f97a29b576e..4b602e71948 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 73ba8e2b341..ee87ceaba8c 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -1,13 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth": { "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" } }, "user": { "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" } } diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index d1867fc2ae5..66761cc0dc6 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -6,7 +6,10 @@ "not_axis_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fAxis\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{name} ({host})", "step": { @@ -26,7 +29,8 @@ "configure_stream": { "data": { "stream_profile": "\u4f7f\u7528\u3059\u308b\u30b9\u30c8\u30ea\u30fc\u30e0\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u9078\u629e" - } + }, + "title": "Axis\u30c7\u30d0\u30a4\u30b9\u306e\u30d3\u30c7\u30aa\u30b9\u30c8\u30ea\u30fc\u30e0\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/azure_devops/translations/ja.json b/homeassistant/components/azure_devops/translations/ja.json index dac3aa396ce..a358aadbe06 100644 --- a/homeassistant/components/azure_devops/translations/ja.json +++ b/homeassistant/components/azure_devops/translations/ja.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "project_error": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "flow_title": "{project_url}", diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 15afd63be23..0218f242979 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -9,6 +9,14 @@ "is_light": "{entity_name} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "is_moist": "{entity_name} \u306f\u6e7f\u3063\u3066\u3044\u307e\u3059", + "is_motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_moving": "{entity_name} \u304c\u3001\u79fb\u52d5\u4e2d\u3067\u3059", + "is_no_gas": "{entity_name} \u306f\u3001\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", + "is_no_light": "{entity_name} \u306f\u3001\u5149\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", + "is_no_motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", + "is_no_problem": "{entity_name} \u306f\u3001\u554f\u984c\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", + "is_no_smoke": "{entity_name} \u306f\u3001\u7159\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", + "is_no_sound": "{entity_name} \u306f\u3001\u97f3\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", "is_no_update": "{entity_name} \u306f\u6700\u65b0\u3067\u3059", "is_no_vibration": "{entity_name} \u306f\u632f\u52d5\u3092\u611f\u77e5\u3057\u3066\u3044\u307e\u305b\u3093", "is_not_bat_low": "{entity_name} \u30d0\u30c3\u30c6\u30ea\u30fc\u306f\u6b63\u5e38\u3067\u3059", @@ -50,8 +58,19 @@ "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", "is_not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "light": "{entity_name} \u306f\u3001\u5149\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", + "motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u59cb\u3081\u307e\u3057\u305f", + "moving": "{entity_name} \u306f\u3001\u79fb\u52d5\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "no_gas": "{entity_name} \u306f\u3001\u30ac\u30b9\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "no_light": "{entity_name} \u306f\u3001\u5149\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "no_motion": "{entity_name} \u306f\u3001\u52d5\u304d\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "no_problem": "{entity_name} \u306f\u3001\u554f\u984c\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "no_smoke": "{entity_name} \u306f\u3001\u7159\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", + "no_sound": "{entity_name} \u306f\u3001\u97f3\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "no_update": "{entity_name} \u304c\u6700\u65b0\u306b\u306a\u308a\u307e\u3057\u305f", "no_vibration": "{entity_name} \u304c\u632f\u52d5\u3092\u611f\u77e5\u3057\u306a\u304f\u306a\u3063\u305f", + "not_bat_low": "{entity_name} \u30d0\u30c3\u30c6\u30ea\u30fc\u6b63\u5e38", "not_cold": "{entity_name} \u306f\u51b7\u3048\u3066\u3044\u307e\u305b\u3093", "not_connected": "{entity_name} \u304c\u5207\u65ad\u3055\u308c\u307e\u3057\u305f", "not_hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u305b\u3093", @@ -115,15 +134,15 @@ "on": "\u63a5\u7d9a\u6e08" }, "door": { - "off": "\u9589", - "on": "\u958b" + "off": "\u9589\u9396", + "on": "\u958b\u653e" }, "garage_door": { - "off": "\u9589", - "on": "\u958b" + "off": "\u9589\u9396", + "on": "\u958b\u653e" }, "gas": { - "off": "\u672a\u691c\u51fa", + "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa" }, "heat": { @@ -135,8 +154,8 @@ "on": "\u30e9\u30a4\u30c8\u3092\u691c\u51fa" }, "lock": { - "off": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", - "on": "\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "off": "\u65bd\u9320\u4e2d", + "on": "\u30ed\u30c3\u30af\u89e3\u9664" }, "moisture": { "off": "\u30c9\u30e9\u30a4", @@ -146,6 +165,10 @@ "off": "\u672a\u691c\u51fa", "on": "\u691c\u51fa" }, + "moving": { + "off": "\u52d5\u3044\u3066\u3044\u306a\u3044", + "on": "\u52d5\u3044\u3066\u3044\u308b" + }, "occupancy": { "off": "\u672a\u691c\u51fa", "on": "\u691c\u51fa" @@ -175,7 +198,7 @@ "on": "\u5371\u967a" }, "smoke": { - "off": "\u672a\u691c\u51fa", + "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa" }, "sound": { diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index 8d1401bde6d..0c77aff5b87 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name} ({host})", "step": { "user": { diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index 55e13ee6ec1..cdc7f16105b 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "2fa": { "data": { diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 47723fe6951..7e0b57d32f2 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -21,7 +21,8 @@ "step": { "account_options": { "data": { - "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)" + "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)", + "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b((2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" } } } diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json index cf7f15de753..47c72db38a5 100644 --- a/homeassistant/components/bond/translations/ja.json +++ b/homeassistant/components/bond/translations/ja.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name} ({host})", "step": { + "confirm": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + } + }, "user": { "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "host": "\u30db\u30b9\u30c8" } } diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 324db726109..dbaaef07641 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -1,13 +1,19 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_ip_control": "\u30c6\u30ec\u30d3\u3067IP\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30c6\u30ec\u30d3\u304c\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unsupported_model": "\u304a\u4f7f\u3044\u306e\u30c6\u30ec\u30d3\u306e\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "step": { "authorize": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + }, "description": "\u30bd\u30cb\u30fc\u88fd\u306e\u30c6\u30ec\u30d3 \u30d6\u30e9\u30d3\u30a2\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Sony Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" }, diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index 8002e7bc119..47985c1d2ee 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({model} at {host})", "step": { diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index 5d6c140419d..f9673cf5d9c 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -5,6 +5,7 @@ "unsupported_model": "\u3053\u306e\u30d7\u30ea\u30f3\u30bf\u30fc\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "snmp_error": "SNMP\u30b5\u30fc\u30d0\u30fc\u304c\u30aa\u30d5\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, diff --git a/homeassistant/components/brunt/translations/he.json b/homeassistant/components/brunt/translations/he.json new file mode 100644 index 00000000000..d6636c6f865 --- /dev/null +++ b/homeassistant/components/brunt/translations/he.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index fcdcef76149..ff3dcf38b74 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/buienradar/translations/ja.json b/homeassistant/components/buienradar/translations/ja.json index e7ef59b81b4..f21946e9d8c 100644 --- a/homeassistant/components/buienradar/translations/ja.json +++ b/homeassistant/components/buienradar/translations/ja.json @@ -19,7 +19,9 @@ "step": { "init": { "data": { - "delta": "\u30ab\u30e1\u30e9\u753b\u50cf\u306e\u66f4\u65b0\u9593\u9694(\u79d2)" + "country_code": "\u30ab\u30e1\u30e9\u753b\u50cf\u3092\u8868\u793a\u3059\u308b\u56fd\u306e\u56fd\u30b3\u30fc\u30c9\u3002", + "delta": "\u30ab\u30e1\u30e9\u753b\u50cf\u306e\u66f4\u65b0\u9593\u9694(\u79d2)", + "timeframe": "\u5206\u9593\u306e\u964d\u6c34\u91cf\u4e88\u5831\u3092\u5148\u8aad\u307f\u3059\u308b" } } } diff --git a/homeassistant/components/button/translations/ja.json b/homeassistant/components/button/translations/ja.json index 9d2f1615dc1..2c54ca49380 100644 --- a/homeassistant/components/button/translations/ja.json +++ b/homeassistant/components/button/translations/ja.json @@ -4,7 +4,7 @@ "press": "{entity_name} \u30dc\u30bf\u30f3\u3092\u62bc\u3059" }, "trigger_type": { - "pressed": "{entity_name} \u304c\u62bc\u3055\u308c\u307e\u3057\u305f" + "pressed": "{entity_name} \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "title": "\u30dc\u30bf\u30f3" diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 8d0400a38ce..6b5b2546122 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "user": { @@ -10,5 +17,15 @@ "title": "Canary\u306b\u63a5\u7d9a" } } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "ffmpeg\u306b\u6e21\u3055\u308c\u308b\u30ab\u30e1\u30e9\u7528\u306e\u5f15\u6570", + "timeout": "\u30ea\u30af\u30a8\u30b9\u30c8\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u79d2)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 577b6d07bc5..d16d72dbcbf 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -15,7 +15,7 @@ "title": "Google Cast\u306e\u8a2d\u5b9a" }, "confirm": { - "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } }, @@ -29,6 +29,7 @@ "ignore_cec": "CEC\u3092\u7121\u8996\u3059\u308b", "uuid": "\u8a31\u53ef\u3055\u308c\u305fUUID" }, + "description": "Allowed(\u8a31\u53ef) UUIDs - Home Assistant\u306b\u8ffd\u52a0\u3059\u308b\u30ad\u30e3\u30b9\u30c8\u30c7\u30d0\u30a4\u30b9\u306eUUID\u3092\u30b3\u30f3\u30de\u533a\u5207\u306e\u30ea\u30b9\u30c8\u3002\u4f7f\u7528\u53ef\u80fd\u306a\u3059\u3079\u3066\u306e\u30ad\u30e3\u30b9\u30c8\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u305f\u304f\u306a\u3044\u5834\u5408\u306b\u306e\u307f\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002Ignore(\u7121\u8996) CEC - \u30a2\u30af\u30c6\u30a3\u30d6\u306a\u5165\u529b\u3092\u6c7a\u5b9a\u3059\u308b\u305f\u3081\u306b\u3001CEC\u30c7\u30fc\u30bf\u3092\u7121\u8996\u3057\u305f\u3044\u30af\u30ed\u30fc\u30e0\u30ad\u30e3\u30b9\u30c8\u306e\u30b3\u30f3\u30de\u533a\u5207\u306e\u30ea\u30b9\u30c8\u3002\u3053\u308c\u306f\u3001pychromecast.IGNORE_CEC \u306b\u6e21\u3055\u308c\u307e\u3059\u3002", "title": "Google Cast\u306e\u9ad8\u5ea6\u306a\u8a2d\u5b9a" }, "basic_options": { diff --git a/homeassistant/components/cert_expiry/translations/ja.json b/homeassistant/components/cert_expiry/translations/ja.json index 864b632d81f..5b3aa8dbe61 100644 --- a/homeassistant/components/cert_expiry/translations/ja.json +++ b/homeassistant/components/cert_expiry/translations/ja.json @@ -1,16 +1,19 @@ { "config": { "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "import_failed": "\u30b3\u30f3\u30d5\u30a3\u30b0\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "connection_refused": "\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u63a5\u7d9a\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f", - "connection_timeout": "\u3053\u306e\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + "connection_timeout": "\u3053\u306e\u30db\u30b9\u30c8\u306b\u63a5\u7d9a\u3059\u308b\u3068\u304d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "resolve_failed": "\u3053\u306e\u30db\u30b9\u30c8\u306f\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093" }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u8a3c\u660e\u66f8\u306e\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, "title": "\u30c6\u30b9\u30c8\u3059\u308b\u8a3c\u660e\u66f8\u3092\u5b9a\u7fa9\u3059\u308b" diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index f5843d0478e..6f8f3592891 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -14,13 +14,18 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" - } + }, + "description": "\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001Home Assistant\u8a2d\u5b9a\u306e\u65e2\u5b9a\u5024\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306f\u4e88\u6e2c\u30bf\u30a4\u30d7\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u307e\u3059\u304c\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u9078\u629e\u3057\u305f\u3082\u306e\u3060\u3051\u304c\u6709\u52b9\u306b\u306a\u308a\u307e\u3059\u3002" } } }, "options": { "step": { "init": { + "data": { + "timestep": "\u6700\u5c0f: NowCast Forecasts\u306e\u9593" + }, + "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", "title": "ClimaCell\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" } } diff --git a/homeassistant/components/climate/translations/ja.json b/homeassistant/components/climate/translations/ja.json index 853bc73ae81..53a68714c30 100644 --- a/homeassistant/components/climate/translations/ja.json +++ b/homeassistant/components/climate/translations/ja.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \u306eHVAC\u30e2\u30fc\u30c9\u3092\u5909\u66f4", + "set_preset_mode": "{entity_name} \u306e\u30d7\u30ea\u30bb\u30c3\u30c8\u3092\u5909\u66f4" + } + }, "state": { "_": { "auto": "\u30aa\u30fc\u30c8", diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json index 26b21da35bf..d9c6674fa6b 100644 --- a/homeassistant/components/cloud/translations/ja.json +++ b/homeassistant/components/cloud/translations/ja.json @@ -2,6 +2,8 @@ "system_health": { "info": { "alexa_enabled": "Alexa\u6709\u52b9", + "can_reach_cert_server": "\u8a3c\u660e\u66f8\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054\u30a2\u30af\u30bb\u30b9\u3059\u308b", + "can_reach_cloud": "Home AssistantCloud\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b", "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", "google_enabled": "Google\u6709\u52b9", "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08", diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index f0a63b8b492..85e39392a9c 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_zone": "\u7121\u52b9\u306a\u30be\u30fc\u30f3" }, "flow_title": "{name}", "step": { @@ -11,6 +18,16 @@ "description": "Cloudflare\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" } }, + "records": { + "data": { + "records": "\u30ec\u30b3\u30fc\u30c9" + } + }, + "user": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + } + }, "zone": { "data": { "zone": "\u30be\u30fc\u30f3" diff --git a/homeassistant/components/configurator/translations/ja.json b/homeassistant/components/configurator/translations/ja.json index 44c6ef349c0..aedc05d5f1c 100644 --- a/homeassistant/components/configurator/translations/ja.json +++ b/homeassistant/components/configurator/translations/ja.json @@ -4,5 +4,6 @@ "configure": "\u8a2d\u5b9a", "configured": "\u8a2d\u5b9a\u6e08\u307f" } - } + }, + "title": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30bf\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json index a9dfd62f83c..1c9668a0085 100644 --- a/homeassistant/components/control4/translations/ja.json +++ b/homeassistant/components/control4/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/conversation/translations/ja.json b/homeassistant/components/conversation/translations/ja.json new file mode 100644 index 00000000000..c8dbdcf8f1f --- /dev/null +++ b/homeassistant/components/conversation/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u4f1a\u8a71" +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json index c66c45c785a..2cbcf95a7de 100644 --- a/homeassistant/components/coolmaster/translations/ja.json +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "no_units": "CoolMasterNet\u306e\u30db\u30b9\u30c8\u306bHVAC\u30e6\u30cb\u30c3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index 7d05d1616ea..95343f36356 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -1,7 +1,13 @@ { + "device_automation": { + "action_type": { + "stop": "\u505c\u6b62 {entity_name}" + } + }, "state": { "_": { "closed": "\u9589\u9396", + "open": "\u958b\u653e", "opening": "\u6249" } }, diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index b68357e82d9..476fd0840c0 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "error": { - "api_password": "\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002API\u30ad\u30fc\u307e\u305f\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u3044\u305a\u308c\u304b\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "api_password": "\u7121\u52b9\u306a\u8a8d\u8a3c\u3002API\u30ad\u30fc\u307e\u305f\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u3044\u305a\u308c\u304b\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { @@ -13,7 +17,7 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API Key\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "description": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u306a\u304a\u3001API\u30ad\u30fc\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u305d\u308c\u305e\u308cBRP072Cxx\u3068SKYFi\u30c7\u30d0\u30a4\u30b9\u3067\u306e\u307f\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30c0\u30a4\u30ad\u30f3\u88fd\u30a8\u30a2\u30b3\u30f3\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index d503a3658e4..57fa9849784 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -2,14 +2,21 @@ "config": { "abort": { "already_configured": "\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059", - "no_bridges": "deCONZ\u30d6\u30ea\u30c3\u30b8\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f" + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_bridges": "deCONZ\u30d6\u30ea\u30c3\u30b8\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "no_hardware_available": "deCONZ\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u7121\u7dda\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u3042\u308a\u307e\u305b\u3093", + "not_deconz_bridge": "deCONZ bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306e\u3001deCONZ Zigbee gateway" + }, "link": { + "description": "deCONZ\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3057\u3066\u3001Home Assistant\u306b\u767b\u9332\u3057\u307e\u3059\u3002 \n\n1. deCONZ\u8a2d\u5b9a -> \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 -> \u8a73\u7d30\u306b\u79fb\u52d5\n2. \"\u30a2\u30d7\u30ea\u306e\u8a8d\u8a3c(Authenticate app)\" \u30dc\u30bf\u30f3\u3092\u62bc\u3059", "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" }, "manual_input": { @@ -23,6 +30,7 @@ "device_automation": { "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", + "bottom_buttons": "\u4e0b\u90e8\u306e\u30dc\u30bf\u30f3", "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", @@ -35,13 +43,38 @@ "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", + "open": "\u30aa\u30fc\u30d7\u30f3", "right": "\u53f3", "side_1": "\u30b5\u30a4\u30c91", + "side_2": "\u30b5\u30a4\u30c92", + "side_3": "\u30b5\u30a4\u30c93", + "side_4": "\u30b5\u30a4\u30c94", + "side_5": "\u30b5\u30a4\u30c95", + "side_6": "\u30b5\u30a4\u30c96", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { - "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b" + "remote_awakened": "\u30c7\u30d0\u30a4\u30b9\u304c\u76ee\u899a\u3081\u305f", + "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", + "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3059\u308b \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", + "remote_turned_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u6642\u8a08\u56de\u308a\u306b", + "remote_turned_counter_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u6642\u8a08\u56de\u308a\u306b" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP\u30bb\u30f3\u30b5\u30fc\u3092\u8a31\u53ef\u3059\u308b", + "allow_deconz_groups": "deCONZ light\u30b0\u30eb\u30fc\u30d7\u3092\u8a31\u53ef\u3059\u308b", + "allow_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u81ea\u52d5\u8ffd\u52a0\u3092\u8a31\u53ef\u3059\u308b" + }, + "description": "deCONZ \u30c7\u30d0\u30a4\u30b9\u30bf\u30a4\u30d7\u306e\u53ef\u8996\u6027\u3092\u8a2d\u5b9a\u3057\u307e\u3059", + "title": "deCONZ\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 713cdd6ae35..3f95f9134ca 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,3 +1,20 @@ { + "options": { + "step": { + "options_1": { + "data": { + "constant": "\u5b9a\u6570", + "int": "\u6570\u5024\u5165\u529b" + } + }, + "options_2": { + "data": { + "multi": "\u30de\u30eb\u30c1\u30bb\u30ec\u30af\u30c8", + "select": "\u9078\u629e\u80a2\u4e00\u3064\u3092\u9078\u629e", + "string": "\u6587\u5b57\u5217\u5024" + } + } + } + }, "title": "\u30c7\u30e2" } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 2e97efde076..f0f8831234b 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e3b\u96fb\u6e90\u30b1\u30fc\u30d6\u30eb\u3068\u30a4\u30fc\u30b5\u30cd\u30c3\u30c8\u30b1\u30fc\u30d6\u30eb\u3092\u53d6\u308a\u5916\u3057\u3066\u3001\u518d\u63a5\u7d9a\u3059\u308b\u3068\u554f\u984c\u304c\u89e3\u6c7a\u3059\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002", "not_denonavr_manufacturer": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u3055\u308c\u305f\u30e1\u30fc\u30ab\u30fc\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", "not_denonavr_missing": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u60c5\u5831\u304c\u5b8c\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093" @@ -8,8 +10,10 @@ "error": { "discovery_error": "Denon AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, + "flow_title": "{name}", "step": { "confirm": { + "description": "\u53d7\u4fe1\u6a5f\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" }, "select": { @@ -35,6 +39,7 @@ "zone2": "\u30be\u30fc\u30f32\u306e\u8a2d\u5b9a", "zone3": "\u30be\u30fc\u30f33\u306e\u8a2d\u5b9a" }, + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" } } diff --git a/homeassistant/components/device_tracker/translations/ja.json b/homeassistant/components/device_tracker/translations/ja.json index 40e8295480a..f53b61ed316 100644 --- a/homeassistant/components/device_tracker/translations/ja.json +++ b/homeassistant/components/device_tracker/translations/ja.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "enters": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u306b\u5165\u308b", + "leaves": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u304b\u3089\u96e2\u308c\u308b" + } + }, "state": { "_": { "home": "\u5728\u5b85", diff --git a/homeassistant/components/devolo_home_control/translations/ja.json b/homeassistant/components/devolo_home_control/translations/ja.json index 0a916bf48dc..c52a21772b2 100644 --- a/homeassistant/components/devolo_home_control/translations/ja.json +++ b/homeassistant/components/devolo_home_control/translations/ja.json @@ -1,14 +1,17 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "reauth_failed": "\u4ee5\u524d\u306e\u3068\u540c\u3058mydevolo\u30e6\u30fc\u30b6\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { "data": { + "mydevolo_url": "mydevolo URL", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb / devolo ID" } diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index c64d4d1b192..c07b80928c6 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "server": "\u30b5\u30fc\u30d0\u30fc", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "Dexcom Share\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3059\u308b", + "title": "Dexcom\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 6b8e68a96c9..0cb6f57eae2 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/directv/translations/ja.json b/homeassistant/components/directv/translations/ja.json index 65534b7f338..2c0ae17f39b 100644 --- a/homeassistant/components/directv/translations/ja.json +++ b/homeassistant/components/directv/translations/ja.json @@ -9,6 +9,9 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 37144b4dca0..9d6b6246d68 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -1,16 +1,31 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u30c7\u30d0\u30a4\u30b9\u540d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } + }, + "options": { + "step": { + "init": { + "description": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u3001\u30b3\u30f3\u30de\u533a\u5207\u308a\u3067\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u3057\u305f\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u307e\u3059\u3002 https://www.home-assistant.io/integrations/doorbird/#events. \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4f8b: somebody_pressed_the_button, motion" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index 44592de0237..f7acb5d9961 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, @@ -38,5 +39,15 @@ "title": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e" } } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u66f4\u65b0\u306e\u6700\u5c0f\u6642\u9593[\u79d2]" + }, + "title": "DSMR\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index 28351c5215a..461af46c93e 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Dune HD" } } } diff --git a/homeassistant/components/eafm/translations/ja.json b/homeassistant/components/eafm/translations/ja.json index 24b479a0fe7..fb891d2d872 100644 --- a/homeassistant/components/eafm/translations/ja.json +++ b/homeassistant/components/eafm/translations/ja.json @@ -1,10 +1,14 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_stations": "\u6d2a\u6c34\u76e3\u8996(Track a flood monitoring)\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { "user": { + "data": { + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" + }, "title": "\u6d2a\u6c34\u76e3\u8996(Track a flood monitoring)\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8ffd\u8de1" } } diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index bde13c55394..7aa67559664 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "authorize": { "description": "\u3053\u306e\u30a2\u30d7\u30ea\u3092 https://www.ecobee.com/consumerportal/index.html \u3067PIN\u30b3\u30fc\u30c9\u3067\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n {pin}\n\n\u6b21\u306b\u3001\u9001\u4fe1(submit) \u3092\u62bc\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index 167f57fb8b8..88196db6d69 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -1,8 +1,13 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "flow_title": "{serial_number}", "step": { "user": { "data": { @@ -10,6 +15,9 @@ "port": "\u30dd\u30fc\u30c8" }, "description": "Elgato Key Light\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + }, + "zeroconf_confirm": { + "title": "Elgato Light device\u3092\u767a\u898b" } } } diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 62ca50f0af0..7475aada4a9 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emulated_roku/translations/ja.json b/homeassistant/components/emulated_roku/translations/ja.json index 4e1b189552f..2ea21df53a1 100644 --- a/homeassistant/components/emulated_roku/translations/ja.json +++ b/homeassistant/components/emulated_roku/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json index 2989d7396d6..ea71065763b 100644 --- a/homeassistant/components/enocean/translations/ja.json +++ b/homeassistant/components/enocean/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_dongle_path": "\u30c9\u30f3\u30b0\u30eb\u30d1\u30b9\u304c\u7121\u52b9", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "detect": { "data": { diff --git a/homeassistant/components/epson/translations/ja.json b/homeassistant/components/epson/translations/ja.json index d4d0d85fd04..435b1eb8355 100644 --- a/homeassistant/components/epson/translations/ja.json +++ b/homeassistant/components/epson/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "powered_off": "\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u306f\u5165\u3063\u3066\u3044\u307e\u3059\u304b\uff1f\u521d\u671f\u8a2d\u5b9a\u3092\u884c\u3046\u305f\u3081\u306b\u306f\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u304a\u304f\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 98ab9160f30..12c452a0139 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -2,10 +2,12 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, diff --git a/homeassistant/components/ezviz/translations/ja.json b/homeassistant/components/ezviz/translations/ja.json index 0b533525258..70a66a09299 100644 --- a/homeassistant/components/ezviz/translations/ja.json +++ b/homeassistant/components/ezviz/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "ezviz_cloud_account_missing": "Ezviz cloud account\u304c\u3042\u308a\u307e\u305b\u3093\u3002Ezviz cloud account\u3092\u518d\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -33,7 +34,8 @@ "url": "URL", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u30ea\u30fc\u30b8\u30e7\u30f3\u306eURL\u3092\u624b\u52d5\u3067\u6307\u5b9a\u3059\u308b" + "description": "\u30ea\u30fc\u30b8\u30e7\u30f3\u306eURL\u3092\u624b\u52d5\u3067\u6307\u5b9a\u3059\u308b", + "title": "\u30ab\u30b9\u30bf\u30e0 Ezviz Cloud\u306b\u63a5\u7d9a" } } }, diff --git a/homeassistant/components/fan/translations/ja.json b/homeassistant/components/fan/translations/ja.json index c7311885159..46e5a16a4d3 100644 --- a/homeassistant/components/fan/translations/ja.json +++ b/homeassistant/components/fan/translations/ja.json @@ -3,6 +3,14 @@ "action_type": { "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" + }, + "trigger_type": { + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } }, "state": { diff --git a/homeassistant/components/fireservicerota/translations/ja.json b/homeassistant/components/fireservicerota/translations/ja.json index 11baaff1a52..00358bb1f36 100644 --- a/homeassistant/components/fireservicerota/translations/ja.json +++ b/homeassistant/components/fireservicerota/translations/ja.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "reauth": { "data": { @@ -10,6 +20,7 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "Web\u30b5\u30a4\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } diff --git a/homeassistant/components/firmata/translations/ja.json b/homeassistant/components/firmata/translations/ja.json new file mode 100644 index 00000000000..34802a0807b --- /dev/null +++ b/homeassistant/components/firmata/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ja.json b/homeassistant/components/flick_electric/translations/ja.json index 6574e5275b5..8373068fb2a 100644 --- a/homeassistant/components/flick_electric/translations/ja.json +++ b/homeassistant/components/flick_electric/translations/ja.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID(\u30aa\u30d7\u30b7\u30e7\u30f3)", "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } diff --git a/homeassistant/components/flo/translations/ja.json b/homeassistant/components/flo/translations/ja.json index 2981d97e3c6..5bd6c037226 100644 --- a/homeassistant/components/flo/translations/ja.json +++ b/homeassistant/components/flo/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json index 19cc78a4a2b..8c51c78880c 100644 --- a/homeassistant/components/flume/translations/ja.json +++ b/homeassistant/components/flume/translations/ja.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "data": { @@ -17,7 +23,8 @@ "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Flume\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 2b4009f9d08..2217ea645b4 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6" - } + }, + "description": "\u30e6\u30fc\u30b6\u30fc\u30d9\u30fc\u30b9\u306e\u30ec\u30dd\u30fc\u30c8\u3068CDC\u306e\u30ec\u30dd\u30fc\u30c8\u3092\u30da\u30a2\u306b\u3057\u3066\u5ea7\u6a19\u3067\u30e2\u30cb\u30bf\u30fc\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/forecast_solar/translations/zh-Hans.json b/homeassistant/components/forecast_solar/translations/zh-Hans.json new file mode 100644 index 00000000000..8a667cf9260 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/zh-Hans.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\uff0c\u4ee5 0 \u4e3a\u5317\uff0c90 \u4e3a\u4e1c\uff0c180 \u4e3a\u5357\uff0c270 \u4e3a\u897f\uff09", + "declination": "\u503e\u89d2\uff08\u4ee5 0 \u4e3a\u6c34\u5e73\uff0c90 \u4e3a\u5782\u76f4\uff09", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "modules power": "\u5149\u4f0f\u53d1\u7535\u6a21\u7ec4\u7684\u603b\u5cf0\u503c\u529f\u7387(W)", + "name": "\u540d\u79f0" + }, + "description": "\u8bf7\u586b\u5199\u60a8\u7684\u592a\u9633\u80fd\u677f\u7684\u53c2\u6570\u3002\u5bf9\u4e8e\u4e0d\u6e05\u695a\u7684\u5b57\u6bb5\uff0c\u8bf7\u53c2\u9605\u6709\u5173\u6587\u6863\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API \u5bc6\u94a5\uff08\u53ef\u9009\uff09", + "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\uff0c\u4ee5 0 \u4e3a\u5317\uff0c90 \u4e3a\u4e1c\uff0c180 \u4e3a\u5357\uff0c270 \u4e3a\u897f\uff09", + "damping": "\u963b\u5c3c\u7cfb\u6570\uff1a\u8c03\u8282\u65e9\u95f4\u548c\u665a\u95f4\u7684\u7ed3\u679c", + "declination": "\u503e\u89d2\uff08\u4ee5 0 \u4e3a\u6c34\u5e73\uff0c90 \u4e3a\u5782\u76f4\uff09", + "modules power": "\u5149\u4f0f\u53d1\u7535\u6a21\u7ec4\u7684\u603b\u5cf0\u503c\u529f\u7387(W)" + }, + "description": "\u8fd9\u4e9b\u503c\u7528\u4e8e\u8c03\u8282 Solar.Forecast \u7ed3\u679c\u3002\u5bf9\u4e8e\u4e0d\u6e05\u695a\u7684\u5b57\u6bb5\uff0c\u8bf7\u53c2\u9605\u6587\u6863\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index bd2c92850ed..3490e61f98c 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" @@ -10,6 +14,7 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", + "name": "\u5206\u304b\u308a\u3084\u3059\u3044\u540d\u524d(Friendly name)", "password": "API\u30d1\u30b9\u30ef\u30fc\u30c9(\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "port": "API\u30dd\u30fc\u30c8" }, @@ -22,7 +27,8 @@ "init": { "data": { "librespot_java_port": "librespot-java\u30d1\u30a4\u30d7\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7528\u30dd\u30fc\u30c8(\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u5834\u5408)", - "max_playlists": "\u30bd\u30fc\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u6700\u5927\u6570" + "max_playlists": "\u30bd\u30fc\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u6700\u5927\u6570", + "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570" } } } diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index af6fc0b8e29..1994cfe3401 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -19,6 +19,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "FRITZ!Box Tools\u3092\u767a\u898b\u3057\u307e\u3057\u305f: {name}\n\nFRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001 {name} \u3092\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u3059\u308b\u3002", "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "reauth_confirm": { @@ -26,6 +27,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "FRITZ!Box Tools\u306e\u8a8d\u8a3c\u3092\u66f4\u65b0\u3057\u307e\u3059: {host}\n\nFRITZ!Box Tools\u304c\u3001FRITZ!Box\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3002", "title": "FRITZ!Box Tools\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 - \u8cc7\u683c\u60c5\u5831" }, "start_config": { diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index 24d7b8b1e2e..6b93e39252a 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -1,25 +1,34 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{name}", "step": { "confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "{name} \u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u66f4\u65b0\u3057\u307e\u3059\u3002" }, "user": { "data": { "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index 3523f811349..2a11bc7a198 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -23,5 +23,18 @@ } } } + }, + "options": { + "error": { + "malformed_prefixes": "\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u5f62\u5f0f\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "init": { + "data": { + "prefixes": "\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8)" + }, + "title": "\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/ca.json b/homeassistant/components/fronius/translations/ca.json new file mode 100644 index 00000000000..3bc7a2cea27 --- /dev/null +++ b/homeassistant/components/fronius/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura l'adre\u00e7a IP o el nom d'amfitri\u00f3 local del teu dispositiu Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/de.json b/homeassistant/components/fronius/translations/de.json new file mode 100644 index 00000000000..b67ed566326 --- /dev/null +++ b/homeassistant/components/fronius/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Konfiguriere die IP-Adresse oder den lokalen Hostnamen deines Fronius-Ger\u00e4ts.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/et.json b/homeassistant/components/fronius/translations/et.json new file mode 100644 index 00000000000..6ccf725ac6f --- /dev/null +++ b/homeassistant/components/fronius/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Seadista Froniuse seadme IP-aadress v\u00f5i kohalik hostinimi.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/id.json b/homeassistant/components/fronius/translations/id.json new file mode 100644 index 00000000000..538dcc33ee8 --- /dev/null +++ b/homeassistant/components/fronius/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Konfigurasikan alamat IP atau nama host lokal perangkat Fronius Anda", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/ja.json b/homeassistant/components/fronius/translations/ja.json new file mode 100644 index 00000000000..dc120a17f6a --- /dev/null +++ b/homeassistant/components/fronius/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "Fronius device\u306eIP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30ed\u30fc\u30ab\u30eb\u30db\u30b9\u30c8\u540d\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/no.json b/homeassistant/components/fronius/translations/no.json new file mode 100644 index 00000000000..8a44a9ff8af --- /dev/null +++ b/homeassistant/components/fronius/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + }, + "description": "Konfigurer IP-adressen eller det lokale vertsnavnet til Fronius-enheten.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/ru.json b/homeassistant/components/fronius/translations/ru.json new file mode 100644 index 00000000000..02f67288518 --- /dev/null +++ b/homeassistant/components/fronius/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/zh-Hant.json b/homeassistant/components/fronius/translations/zh-Hant.json new file mode 100644 index 00000000000..18134514d38 --- /dev/null +++ b/homeassistant/components/fronius/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a Fronius \u88dd\u7f6e IP \u4f4d\u5740\u6216\u672c\u5730\u4e3b\u6a5f\u540d\u3002", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index 69592711efe..ba80a197994 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Geofency\u306ewebhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/geonetnz_quakes/translations/ja.json b/homeassistant/components/geonetnz_quakes/translations/ja.json index deca31ec45e..8948e9c4e4a 100644 --- a/homeassistant/components/geonetnz_quakes/translations/ja.json +++ b/homeassistant/components/geonetnz_quakes/translations/ja.json @@ -8,7 +8,8 @@ "data": { "mmi": "MMI", "radius": "\u534a\u5f84" - } + }, + "title": "\u30d5\u30a3\u30eb\u30bf\u30fc\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/geonetnz_volcano/translations/ja.json b/homeassistant/components/geonetnz_volcano/translations/ja.json index 7f698056b09..c674e4e43dd 100644 --- a/homeassistant/components/geonetnz_volcano/translations/ja.json +++ b/homeassistant/components/geonetnz_volcano/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index be92bad7ba3..b93b6b4a6fc 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -1,6 +1,12 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -14,7 +20,8 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/gogogate2/translations/ja.json b/homeassistant/components/gogogate2/translations/ja.json index f37d3f9edb5..f963a861282 100644 --- a/homeassistant/components/gogogate2/translations/ja.json +++ b/homeassistant/components/gogogate2/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{device} ({ip_address})", "step": { "user": { @@ -7,7 +14,8 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Gogogate2\u307e\u305f\u306fismartgate\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index d0047d97fe5..38dac479d9d 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -13,7 +13,8 @@ "destination": "\u76ee\u7684\u5730", "name": "\u540d\u524d", "origin": "\u30aa\u30ea\u30b8\u30f3" - } + }, + "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u3001\u4f4f\u6240\u3001\u7def\u5ea6/\u7d4c\u5ea6\u306e\u5ea7\u6a19\u3001\u307e\u305f\u306fGoogle place ID\u306e\u5f62\u5f0f\u3067\u3001\u30d1\u30a4\u30d7\u6587\u5b57\u3067\u533a\u5207\u3089\u308c\u305f1\u3064\u4ee5\u4e0a\u306e\u5834\u6240\u3092\u6307\u5b9a\u3067\u304d\u307e\u3059\u3002Google place ID\u3092\u4f7f\u7528\u3057\u3066\u5834\u6240\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u3001ID\u306e\u524d\u306b\u3001`place_id:` \u3092\u4ed8\u3051\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } }, @@ -29,7 +30,8 @@ "transit_mode": "\u30c8\u30e9\u30f3\u30b8\u30c3\u30c8\u30e2\u30fc\u30c9", "transit_routing_preference": "\u30c8\u30e9\u30f3\u30b8\u30c3\u30c8\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306e\u8a2d\u5b9a", "units": "\u5358\u4f4d" - } + }, + "description": "\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u3001\u51fa\u767a\u6642\u523b\u307e\u305f\u306f\u5230\u7740\u6642\u523b\u306e\u3044\u305a\u308c\u304b\u3092\u6307\u5b9a\u3067\u304d\u307e\u3059\u3002\u51fa\u767a\u6642\u523b\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u306f\u3001Unix \u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7\u306e'now' \u3001\u307e\u305f\u306f '08:00:00' \u306e\u3088\u3046\u306a24\u6642\u9593\u306e\u6642\u523b\u6587\u5b57\u5217\u3092\u5165\u529b\u3067\u304d\u307e\u3059\u3002\u5230\u7740\u6642\u523b\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u306f\u3001Unix\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7\u307e\u305f\u306f\u3001'08:00:00' \u306e\u3088\u3046\u306a24\u6642\u9593\u306e\u6642\u523b\u6587\u5b57\u5217\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index 1be2d7c79ed..c04d58020d6 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001GPSLogger\u3067webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/gree/translations/ja.json b/homeassistant/components/gree/translations/ja.json new file mode 100644 index 00000000000..d1234b69652 --- /dev/null +++ b/homeassistant/components/gree/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index d6f283d5ef6..ec4f774b2e1 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -7,7 +7,10 @@ "not_home": "\u5916\u51fa", "off": "\u30aa\u30d5", "ok": "OK", - "on": "\u30aa\u30f3" + "on": "\u30aa\u30f3", + "open": "\u958b\u653e", + "problem": "\u554f\u984c", + "unlocked": "\u30ed\u30c3\u30af\u89e3\u9664" } }, "title": "\u30b0\u30eb\u30fc\u30d7" diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index a8716ef19d3..f49267f73f3 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "discovery_confirm": { "description": "\u3053\u306eGuardian\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" } } diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index 04577fab9f6..7f040e4f684 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name}", "step": { "link": { @@ -13,5 +20,12 @@ "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "init": { + "description": "Harmony Hub\u306e\u8abf\u6574" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index 63eebb77c54..0f704ceba19 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "board": "\u30dc\u30fc\u30c9", + "disk_total": "\u30c7\u30a3\u30b9\u30af\u5408\u8a08", + "disk_used": "\u4f7f\u7528\u6e08\u307f\u30c7\u30a3\u30b9\u30af", "docker_version": "Docker\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", "healthy": "\u5143\u6c17", "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0", @@ -8,6 +11,7 @@ "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "update_channel": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30c1\u30e3\u30f3\u30cd\u30eb", "version_api": "\u30d0\u30fc\u30b8\u30e7\u30f3API" } } diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 94a8c761f93..5502f2e8183 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 74e2a1cfa66..277b941fc0f 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -6,7 +6,10 @@ "unknown_entry": "\u65e2\u5b58\u306e\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "error": { + "invalid_code": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u4e8c\u8981\u7d20\u8a8d\u8a3c\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", "invalid_password": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_username": "Hive\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3042\u306a\u305f\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u304c\u8a8d\u8b58\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "no_internet_available": "Hive\u306b\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u63a5\u7d9a\u304c\u5fc5\u8981\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { @@ -14,6 +17,7 @@ "data": { "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, + "description": "Hive\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n\u5225\u306e\u30b3\u30fc\u30c9\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001\u30b3\u30fc\u30c9 0000 \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30cf\u30a4\u30d6(Hive)2\u8981\u7d20\u8a8d\u8a3c\u3002" }, "reauth": { @@ -21,6 +25,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u518d\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Hive\u30ed\u30b0\u30a4\u30f3" }, "user": { @@ -29,6 +34,7 @@ "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3068\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } @@ -38,7 +44,9 @@ "user": { "data": { "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" - } + }, + "description": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694\u3092\u66f4\u65b0\u3057\u3066\u3001\u30c7\u30fc\u30bf\u3092\u3088\u308a\u983b\u7e41\u306b\u30dd\u30fc\u30ea\u30f3\u30b0\u3057\u307e\u3059\u3002", + "title": "Hive\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/hlk_sw16/translations/ja.json b/homeassistant/components/hlk_sw16/translations/ja.json index 2981d97e3c6..5bd6c037226 100644 --- a/homeassistant/components/hlk_sw16/translations/ja.json +++ b/homeassistant/components/hlk_sw16/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/home_connect/translations/ja.json b/homeassistant/components/home_connect/translations/ja.json new file mode 100644 index 00000000000..66b53ce718b --- /dev/null +++ b/homeassistant/components/home_connect/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json index 00e35f402ba..df5165fc3fa 100644 --- a/homeassistant/components/home_plus_control/translations/ja.json +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, @@ -16,5 +16,6 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } - } + }, + "title": "Legrand Home+ Control" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index fdd0ec93caa..453478aee37 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -7,7 +7,12 @@ "hassio": "Supervisor", "installation_type": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7a2e\u985e", "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30d5\u30a1\u30df\u30ea\u30fc", - "user": "\u30e6\u30fc\u30b6\u30fc" + "os_version": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0 \u30b7\u30b9\u30c6\u30e0\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", + "python_version": "Python \u30d0\u30fc\u30b8\u30e7\u30f3", + "timezone": "\u30bf\u30a4\u30e0\u30be\u30fc\u30f3", + "user": "\u30e6\u30fc\u30b6\u30fc", + "version": "\u30d0\u30fc\u30b8\u30e7\u30f3", + "virtualenv": "\u4eee\u60f3\u74b0\u5883" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 09cf3797153..6a8c3d81d7e 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -2,7 +2,14 @@ "config": { "step": { "pairing": { - "description": "\u201cHomeKit Pairing\u201d\u306e\"\u901a\u77e5\"\u306e\u6307\u793a\u306b\u5f93\u3063\u3066\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002" + "description": "\u201cHomeKit Pairing\u201d\u306e\"\u901a\u77e5\"\u306e\u6307\u793a\u306b\u5f93\u3063\u3066\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002", + "title": "\u30da\u30a2 HomeKit" + }, + "user": { + "data": { + "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3" + }, + "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u306e\u9078\u629e" } } }, @@ -21,7 +28,19 @@ }, "title": "\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a" }, + "include_exclude": { + "data": { + "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "mode": "\u30e2\u30fc\u30c9" + }, + "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001TV\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" + }, "init": { + "data": { + "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3", + "mode": "\u30e2\u30fc\u30c9" + }, "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index cbedf19c794..960b0eff659 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -2,14 +2,18 @@ "config": { "abort": { "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "invalid_properties": "\u7121\u52b9\u306a\u30d7\u30ed\u30d1\u30c6\u30a3\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u77e5\u3089\u3055\u308c\u307e\u3057\u305f\u3002", + "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u6027\u8cea\u306a\u305f\u3081\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", - "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown_error": "\u30c7\u30d0\u30a4\u30b9\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3092\u5831\u544a\u3057\u307e\u3057\u305f\u3002\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, "flow_title": "{name}", "step": { @@ -17,7 +21,8 @@ "data": { "allow_insecure_setup_codes": "\u5b89\u5168\u3067\u306a\u3044\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8a31\u53ef\u3059\u308b\u3002", "pairing_code": "\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9" - } + }, + "title": "HomeKit Accessory Protocol\u3092\u4ecb\u3057\u3066\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0" }, "user": { "data": { @@ -27,5 +32,12 @@ } } }, + "device_automation": { + "trigger_type": { + "double_press": "\"{subtype}\" \u30922\u56de\u62bc\u3059", + "long_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u305f\u307e\u307e", + "single_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" + } + }, "title": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index dfd4a413f79..90f56fcf7b6 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306f\u65e2\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "connection_aborted": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "invalid_sgtin_or_pin": "SGTIN\u3001\u307e\u305f\u306fPIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_sgtin_or_pin": "SGTIN\u3001\u307e\u305f\u306fPIN\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "timeout_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" @@ -15,6 +15,7 @@ "init": { "data": { "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3\u3002\u5168\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u3057\u3066\u4f7f\u7528)", "pin": "PIN\u30b3\u30fc\u30c9" }, "title": "HomematicIP Access point\u3092\u9078\u629e" diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 113f2f6ba4b..35a8de978c0 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -9,8 +9,10 @@ "connection_timeout": "\u63a5\u7d9a\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "incorrect_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", "incorrect_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_url": "\u7121\u52b9\u306aURL", - "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc" + "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/hue/translations/he.json b/homeassistant/components/hue/translations/he.json index ece439b376b..f0a4c2fd484 100644 --- a/homeassistant/components/hue/translations/he.json +++ b/homeassistant/components/hue/translations/he.json @@ -33,6 +33,7 @@ }, "device_automation": { "trigger_subtype": { + "2": "\u05dc\u05d7\u05e6\u05df \u05e9\u05e0\u05d9", "turn_off": "\u05db\u05d1\u05d4", "turn_on": "\u05d4\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 275ef6f0f34..cd9db3eba6a 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -3,11 +3,12 @@ "abort": { "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue bridge\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", "no_bridges": "Hue bridge\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "not_hue_bridge": "Hue bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "linking": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", @@ -37,12 +38,15 @@ "1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + "4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "double_buttons_1_3": "1\u756a\u76ee\u30683\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "double_buttons_2_4": "2\u756a\u76ee\u30684\u756a\u76ee\u306e\u30dc\u30bf\u30f3" }, "trigger_type": { "double_short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3059", "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", + "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" } diff --git a/homeassistant/components/humidifier/translations/ja.json b/homeassistant/components/humidifier/translations/ja.json new file mode 100644 index 00000000000..b03f4389d2c --- /dev/null +++ b/homeassistant/components/humidifier/translations/ja.json @@ -0,0 +1,27 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "{entity_name} \u6e7f\u5ea6\u3092\u8a2d\u5b9a", + "set_mode": "{entity_name} \u30e2\u30fc\u30c9\u3092\u5909\u66f4", + "toggle": "\u30c8\u30b0\u30eb {entity_name}", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} \u306f\u7279\u5b9a\u306e\u30e2\u30fc\u30c9\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" + }, + "trigger_type": { + "target_humidity_changed": "{entity_name} \u30bf\u30fc\u30b2\u30c3\u30c8\u6e7f\u5ea6\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, + "state": { + "_": { + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json index 800082b9e70..e5266579ed1 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ja.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name} ({host})", "step": { "link": { diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json index 048870eee7d..6d28f551904 100644 --- a/homeassistant/components/hvv_departures/translations/ja.json +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -1,12 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "\u884c\u3092\u9078\u629e", + "offset": "\u30aa\u30d5\u30bb\u30c3\u30c8(\u5206)", + "real_time": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30c7\u30fc\u30bf\u3092\u4f7f\u7528\u3059\u308b" + }, + "description": "\u3053\u306e\u51fa\u767a\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u5909\u66f4\u3059\u308b", + "title": "\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index cc6f7799716..0289e0a8a3f 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "auth_new_token_not_granted_error": "\u65b0\u3057\u304f\u4f5c\u6210\u3057\u305f\u30c8\u30fc\u30af\u30f3\u304cHyperion UI\u3067\u627f\u8a8d\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "auth_new_token_not_work_error": "\u65b0\u3057\u304f\u4f5c\u6210\u3055\u308c\u305f\u30c8\u30fc\u30af\u30f3\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "auth_required_error": "\u627f\u8a8d\u304c\u5fc5\u8981\u304b\u3069\u3046\u304b\u3092\u5224\u65ad\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "no_id": "Hyperion Ambilight\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u305d\u306eID\u3092\u30ec\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" @@ -14,16 +17,22 @@ "step": { "auth": { "data": { - "create_token": "\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3059\u308b" - } + "create_token": "\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3059\u308b", + "token": "\u307e\u305f\u306f\u65e2\u5b58\u306e\u30c8\u30fc\u30af\u30f3\u3092\u63d0\u4f9b\u3057\u307e\u3059" + }, + "description": "Hyperion Ambilight\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u8a8d\u8a3c\u8a2d\u5b9a" }, "confirm": { - "description": "\u3053\u306eHyperion Ambilight\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f \n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}" + "description": "\u3053\u306eHyperion Ambilight\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f \n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Hyperion Ambilight\u30b5\u30fc\u30d3\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d" }, "create_token": { "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3057\u307e\u3059" }, + "create_token_external": { + "title": "Hyperion UI\u3067\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u306e\u53d7\u3051\u5165\u308c\u308b" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", @@ -31,5 +40,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Hyperion effects\u3092\u8868\u793a", + "priority": "\u8272\u3068\u30a8\u30d5\u30a7\u30af\u30c8\u306b\u4f7f\u7528\u3059\u308bHyperion\u306e\u512a\u5148\u9806\u4f4d" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 5d2fea9c8e8..8d2a44e9136 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 3f1df84ff34..a749d2fb926 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "validate_verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u78ba\u8a8d\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u518d\u5ea6\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "step": { @@ -11,7 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index c0820401f21..81616a31dd7 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[IFTTT Webhook applet]({applet_url})\u306e\"Make a web request\"\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/json\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 6fb2f55cc39..8d36bf959f8 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "hubv1": { "data": { @@ -14,16 +22,37 @@ "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } + }, + "plm": { + "data": { + "device": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "\u30e2\u30c7\u30e0\u306e\u7a2e\u985e\u3002" + }, + "title": "Insteon" } } }, "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { + "add_override": { + "title": "Insteon" + }, "add_x10": { "data": { + "platform": "\u30d7\u30e9\u30c3\u30c8\u30db\u30fc\u30e0", "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" }, - "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002" + "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002", + "title": "Insteon" }, "change_hub_config": { "data": { @@ -33,6 +62,18 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "title": "Insteon" + }, + "init": { + "data": { + "add_x10": "X10 \u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + }, + "title": "Insteon" + }, + "remove_override": { + "title": "Insteon" + }, + "remove_x10": { + "title": "Insteon" } } } diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json index 3e5e2a4d462..078026d8333 100644 --- a/homeassistant/components/ipma/translations/ja.json +++ b/homeassistant/components/ipma/translations/ja.json @@ -8,11 +8,17 @@ "data": { "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", + "mode": "\u30e2\u30fc\u30c9", "name": "\u540d\u524d" }, "description": "\u30dd\u30eb\u30c8\u30ac\u30eb\u6d77\u6d0b\u5927\u6c17\u7814\u7a76\u6240(Instituto Portugu\u00eas do Mar e Atmosfera)", "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u5230\u9054\u53ef\u80fd" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 7bdbfbf94a0..8b1d1ae0e2d 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -1,12 +1,15 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "connection_upgrade": "\u63a5\u7d9a\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9(connection upgrade)\u304c\u5fc5\u8981\u306a\u305f\u3081\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "ipp_version_error": "IPP\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30d7\u30ea\u30f3\u30bf\u30fc\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "unique_id_required": "\u691c\u51fa\u306b\u5fc5\u8981\u306a\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", @@ -14,10 +17,15 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "description": "\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u5370\u5237\u30d7\u30ed\u30c8\u30b3\u30eb(IPP)\u3092\u4ecb\u3057\u3066\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", "title": "\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, + "zeroconf_confirm": { + "title": "\u691c\u51fa\u3055\u308c\u305f\u30d7\u30ea\u30f3\u30bf\u30fc" } } } diff --git a/homeassistant/components/iqvia/translations/ja.json b/homeassistant/components/iqvia/translations/ja.json index 159fad10e03..7a9e136ddc9 100644 --- a/homeassistant/components/iqvia/translations/ja.json +++ b/homeassistant/components/iqvia/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "invalid_zip_code": "\u90f5\u4fbf\u756a\u53f7\u304c\u7121\u52b9\u3067\u3059" }, diff --git a/homeassistant/components/islamic_prayer_times/translations/ja.json b/homeassistant/components/islamic_prayer_times/translations/ja.json index f75f5acc402..ea6ad0c6522 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ja.json +++ b/homeassistant/components/islamic_prayer_times/translations/ja.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "user": { + "description": "\u30a4\u30b9\u30e9\u30e0\u6559\u306e\u7948\u308a\u306e\u6642\u9593\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "Islamic Prayer Times\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } - } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "\u7948\u308a\u306e\u8a08\u7b97\u65b9\u6cd5" + } + } + } + }, + "title": "\u30a4\u30b9\u30e9\u30e0\u306e\u7948\u308a\u306e\u6642\u9593" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index d2c5559da87..e915abeffe9 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", "step": { @@ -11,13 +17,19 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80" + "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80", + "title": "\u3042\u306a\u305f\u306eISY994\u306b\u63a5\u7d9a" } } }, "options": { "step": { "init": { + "data": { + "ignore_string": "\u7121\u8996\u3059\u308b\u6587\u5b57\u5217", + "restore_light_state": "\u30e9\u30a4\u30c8\u306e\u660e\u308b\u3055\u3092\u5fa9\u5143\u3059\u308b", + "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217" + }, "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" } } @@ -26,6 +38,7 @@ "info": { "device_connected": "ISY\u63a5\u7d9a\u6e08", "host_reachable": "\u30db\u30b9\u30c8\u5230\u9054\u53ef\u80fd", + "last_heartbeat": "\u6700\u5f8c\u306e\u30cf\u30fc\u30c8\u30d3\u30fc\u30c8\u30bf\u30a4\u30e0", "websocket_status": "\u30a4\u30d9\u30f3\u30c8\u30bd\u30b1\u30c3\u30c8 \u30b9\u30c6\u30fc\u30bf\u30b9" } } diff --git a/homeassistant/components/juicenet/translations/ja.json b/homeassistant/components/juicenet/translations/ja.json new file mode 100644 index 00000000000..b140d6731e5 --- /dev/null +++ b/homeassistant/components/juicenet/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/id.json b/homeassistant/components/kmtronic/translations/id.json index ed8fde32106..d436321da7a 100644 --- a/homeassistant/components/kmtronic/translations/id.json +++ b/homeassistant/components/kmtronic/translations/id.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Logika sakelar terbalik (gunakan NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json index 5bd6c037226..84d24f2db6d 100644 --- a/homeassistant/components/kmtronic/translations/ja.json +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "\u30b9\u30a4\u30c3\u30c1\u306e\u8ad6\u7406\u3092\u9006\u306b\u3059\u308b(NC\u3092\u4f7f\u7528)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json new file mode 100644 index 00000000000..3c338886e22 --- /dev/null +++ b/homeassistant/components/knx/translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 95e1045e749..cfab50507ae 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -11,17 +11,51 @@ "manual_tunnel": { "data": { "host": "Host", - "port": "Port" - } + "individual_address": "Alamat individu untuk koneksi", + "port": "Port", + "route_back": "Dirutekan Kembali/Mode NAT" + }, + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + }, + "routing": { + "data": { + "individual_address": "Alamat individu untuk koneksi routing", + "multicast_group": "Grup multicast yang digunakan untuk routing", + "multicast_port": "Port multicast yang digunakan untuk routing" + }, + "description": "Konfigurasikan opsi routing." + }, + "tunnel": { + "data": { + "gateway": "Koneksi Tunnel KNX" + }, + "description": "Pilih gateway dari daftar." + }, + "type": { + "data": { + "connection_type": "Jenis Koneksi KNX" + }, + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." } } }, "options": { "step": { + "init": { + "data": { + "connection_type": "Jenis Koneksi KNX", + "individual_address": "Alamat individu default", + "multicast_group": "Grup multicast yang digunakan untuk routing dan penemuan", + "multicast_port": "Port multicast yang digunakan untuk routing dan penemuan", + "rate_limit": "Jumlah maksimal telegram keluar per detik", + "state_updater": "Aktifkan status membaca secara global dari KNX Bus" + } + }, "tunnel": { "data": { "host": "Host", - "port": "Port" + "port": "Port", + "route_back": "Dirutekan Kembali/Mode NAT" } } } diff --git a/homeassistant/components/knx/translations/zh-Hans.json b/homeassistant/components/knx/translations/zh-Hans.json new file mode 100644 index 00000000000..4ceb76c5ea3 --- /dev/null +++ b/homeassistant/components/knx/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 7e1005545cf..f3d32306703 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -1,8 +1,18 @@ { "config": { "abort": { - "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u4e00\u610f(unique)\u306aID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u4e00\u610f(unique)\u306aID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { @@ -18,7 +28,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b" }, "description": "Kodi\u306e\u63a5\u7d9a\u60c5\u5831\u3067\u3059\u3002\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u3067 \"HTTP\u306b\u3088\u308bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u5fc5\u305a\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index 878ecf32d09..c9bf5414d86 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 11cfee75ef4..937425be1dc 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index 32cf120e8af..b5e6340b562 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", "not_konn_panel": "Not a recognized Konnected.io device", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/konnected/translations/et.json b/homeassistant/components/konnected/translations/et.json index 34c361fd9c6..ddddea5c800 100644 --- a/homeassistant/components/konnected/translations/et.json +++ b/homeassistant/components/konnected/translations/et.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", "not_konn_panel": "Tuvastamata Konnected.io seade", "unknown": "Tundmatu viga" }, diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json index b80b86c25c9..f2e2035ca06 100644 --- a/homeassistant/components/konnected/translations/id.json +++ b/homeassistant/components/konnected/translations/id.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", "not_konn_panel": "Bukan perangkat Konnected.io yang dikenali", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index cb0acbf326b..fcb7275820b 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "not_konn_panel": "\u8a8d\u8b58\u3055\u308c\u305f\u3001Konnected.io\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -85,10 +86,12 @@ "data": { "blink": "\u72b6\u614b\u5909\u66f4\u3092\u9001\u4fe1\u3059\u308b\u3068\u304d\u306b\u3001\u30d1\u30cd\u30eb\u306eLED\u3092\u70b9\u6ec5\u3055\u305b\u308b", "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" - } + }, + "title": "\u305d\u306e\u4ed6\u306e\u8a2d\u5b9a" }, "options_switch": { "data": { + "activation": "\u30aa\u30f3\u306e\u3068\u304d\u306b\u51fa\u529b", "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, "description": "{zone}\u30aa\u30d7\u30b7\u30e7\u30f3 : \u72b6\u614b{state}" diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index 47be4c20bf0..92d3efc4633 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index ad85d9c3060..523bb10b969 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/kraken/translations/id.json b/homeassistant/components/kraken/translations/id.json index a436ac4aee5..438a69f54fe 100644 --- a/homeassistant/components/kraken/translations/id.json +++ b/homeassistant/components/kraken/translations/id.json @@ -8,5 +8,15 @@ "description": "Ingin memulai penyiapan?" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pembaruan", + "tracked_asset_pairs": "Pasangan Aset Terlacak" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index ae0b4576715..725e1cfaae3 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "create_entry": { "default": "\u8a73\u7d30\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001[Life360\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/litejet/translations/id.json b/homeassistant/components/litejet/translations/id.json index 690692ca4cc..332355f8fa9 100644 --- a/homeassistant/components/litejet/translations/id.json +++ b/homeassistant/components/litejet/translations/id.json @@ -15,5 +15,15 @@ "title": "Hubungkan ke LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Waktu Transisi Default (detik)" + }, + "title": "Konfigurasikan LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json index 5da03e5b724..f5d2efd6613 100644 --- a/homeassistant/components/local_ip/translations/ja.json +++ b/homeassistant/components/local_ip/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "user": { "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index 86507260cf0..89003e78a9d 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u5834\u6240\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Locative app\u3067webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/lock/translations/ja.json b/homeassistant/components/lock/translations/ja.json index 23d24b05066..53138d5d8d4 100644 --- a/homeassistant/components/lock/translations/ja.json +++ b/homeassistant/components/lock/translations/ja.json @@ -1,7 +1,24 @@ { "device_automation": { + "action_type": { + "lock": "\u30ed\u30c3\u30af {entity_name}", + "open": "\u30aa\u30fc\u30d7\u30f3 {entity_name}", + "unlock": "\u30a2\u30f3\u30ed\u30c3\u30af {entity_name}" + }, "condition_type": { + "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "is_unlocked": "{entity_name} \u306e\u30ed\u30c3\u30af\u306f\u89e3\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "trigger_type": { + "locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", + "unlocked": "{entity_name} \u306e\u30ed\u30c3\u30af\u304c\u89e3\u9664\u3055\u308c\u307e\u3057\u305f" } - } + }, + "state": { + "_": { + "locked": "\u65bd\u9320\u4e2d", + "unlocked": "\u30ed\u30c3\u30af\u89e3\u9664" + } + }, + "title": "\u30ed\u30c3\u30af" } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ja.json b/homeassistant/components/logi_circle/translations/ja.json index 9b1708ba528..e972b95daee 100644 --- a/homeassistant/components/logi_circle/translations/ja.json +++ b/homeassistant/components/logi_circle/translations/ja.json @@ -1,10 +1,15 @@ { "config": { "abort": { - "external_setup": "Logi Circle\u306f\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "external_error": "\u5225\u306e\u30d5\u30ed\u30fc\u3067\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", + "external_setup": "Logi Circle\u306f\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "follow_link": "\u9001\u4fe1(submit) \u3092\u62bc\u3059\u524d\u306b\u3001\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "follow_link": "\u9001\u4fe1(submit) \u3092\u62bc\u3059\u524d\u306b\u3001\u30ea\u30f3\u30af\u3092\u305f\u3069\u3063\u3066\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "auth": { diff --git a/homeassistant/components/lookin/translations/id.json b/homeassistant/components/lookin/translations/id.json index 99873637964..bb3c4bd7c07 100644 --- a/homeassistant/components/lookin/translations/id.json +++ b/homeassistant/components/lookin/translations/id.json @@ -18,6 +18,9 @@ "name": "Nama" } }, + "discovery_confirm": { + "description": "Ingin menyiapkan {name} ({host})?" + }, "user": { "data": { "ip_address": "Alamat IP" diff --git a/homeassistant/components/lovelace/translations/ja.json b/homeassistant/components/lovelace/translations/ja.json index 88d75d3fcbc..85b5e71cebc 100644 --- a/homeassistant/components/lovelace/translations/ja.json +++ b/homeassistant/components/lovelace/translations/ja.json @@ -1,6 +1,8 @@ { "system_health": { "info": { + "dashboards": "\u30c0\u30c3\u30b7\u30e5 \u30dc\u30fc\u30c9", + "mode": "\u30e2\u30fc\u30c9", "resources": "\u30ea\u30bd\u30fc\u30b9", "views": "\u30d3\u30e5\u30fc" } diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index ece2e8e75f6..1b42430e02c 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -1,27 +1,50 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "not_lutron_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Lutron\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name} ({host})", "step": { "link": { - "description": "{name} ({host}) \u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3059\u308b\u306b\u306f\u3001\u3053\u306e\u30d5\u30a9\u30fc\u30e0\u3092\u9001\u4fe1(submit)\u3057\u305f\u5f8c\u3001\u30d6\u30ea\u30c3\u30b8\u306e\u80cc\u9762\u306b\u3042\u308b\u9ed2\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u307e\u3059\u3002" + "description": "{name} ({host}) \u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3059\u308b\u306b\u306f\u3001\u3053\u306e\u30d5\u30a9\u30fc\u30e0\u3092\u9001\u4fe1(submit)\u3057\u305f\u5f8c\u3001\u30d6\u30ea\u30c3\u30b8\u306e\u80cc\u9762\u306b\u3042\u308b\u9ed2\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u307e\u3059\u3002", + "title": "\u30d6\u30ea\u30c3\u30b8\u3068\u30da\u30a2" }, "user": { "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u81ea\u52d5\u7684\u306b\u30d6\u30ea\u30c3\u30b8\u306b\u63a5\u7d9a" } } }, "device_automation": { "trigger_subtype": { "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "close_1": "\u9589\u3058\u308b1", + "close_2": "\u9589\u3058\u308b2", + "close_3": "\u9589\u3058\u308b3", + "close_4": "\u9589\u3058\u308b4", + "close_all": "\u3059\u3079\u3066\u9589\u3058\u308b", "group_1_button_1": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", "group_1_button_2": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "group_2_button_1": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", "group_2_button_2": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "off": "\u30aa\u30d5", "on": "\u30aa\u30f3", + "open_1": "\u30aa\u30fc\u30d7\u30f31", + "open_2": "\u30aa\u30fc\u30d7\u30f32", + "open_3": "\u30aa\u30fc\u30d7\u30f33", + "open_4": "\u30aa\u30fc\u30d7\u30f34", + "open_all": "\u3059\u3079\u3066\u958b\u304f", "stop": "\u505c\u6b62(\u304a\u6c17\u306b\u5165\u308a)", "stop_1": "\u505c\u6b62 1", "stop_2": "\u505c\u6b62 2", @@ -30,7 +53,7 @@ "stop_all": "\u3059\u3079\u3066\u505c\u6b62" }, "trigger_type": { - "press": "\"{subtype}\" \u62bc\u3059", + "press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f", "release": "\"{subtype}\" \u96e2\u3059" } } diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json index f1057fc7cb2..a519e2e5f9e 100644 --- a/homeassistant/components/lyric/translations/id.json +++ b/homeassistant/components/lyric/translations/id.json @@ -13,6 +13,7 @@ "title": "Pilih Metode Autentikasi" }, "reauth_confirm": { + "description": "Integrasi Lyric perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" } } diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 1ffd6fdacf0..98b2a11c5ff 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "create_entry": { diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 033747d6263..cacb7e92502 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Mailgun]({mailgun_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-fjsorm-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/media_player/translations/ja.json b/homeassistant/components/media_player/translations/ja.json index cea87342685..e27e8dfbf20 100644 --- a/homeassistant/components/media_player/translations/ja.json +++ b/homeassistant/components/media_player/translations/ja.json @@ -1,6 +1,9 @@ { "device_automation": { "trigger_type": { + "idle": "{entity_name} \u304c\u3001\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u306b\u306a\u308a\u307e\u3059", + "paused": "{entity_name} \u306f\u3001\u4e00\u6642\u505c\u6b62\u3057\u3066\u3044\u307e\u3059", + "playing": "{entity_name} \u304c\u3001\u518d\u751f\u3092\u958b\u59cb\u3057\u307e\u3059", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } @@ -11,7 +14,8 @@ "off": "\u30aa\u30d5", "on": "\u30aa\u30f3", "paused": "\u4e00\u6642\u505c\u6b62", - "playing": "\u518d\u751f\u4e2d" + "playing": "\u518d\u751f\u4e2d", + "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4" } }, "title": "\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc" diff --git a/homeassistant/components/met/translations/ja.json b/homeassistant/components/met/translations/ja.json index ae6a3a0f221..aea2493206c 100644 --- a/homeassistant/components/met/translations/ja.json +++ b/homeassistant/components/met/translations/ja.json @@ -3,14 +3,19 @@ "abort": { "no_home": "Home Assistant\u306e\u8a2d\u5b9a\u3067\u3001\u5bb6\u306e\u5ea7\u6a19\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, + "error": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { + "elevation": "\u6a19\u9ad8(Elevation)", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Meteorologisk institutt" + "description": "Meteorologisk institutt", + "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/met_eireann/translations/ja.json b/homeassistant/components/met_eireann/translations/ja.json index 58f6c2b5f36..db065c8c15b 100644 --- a/homeassistant/components/met_eireann/translations/ja.json +++ b/homeassistant/components/met_eireann/translations/ja.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "elevation": "\u6a19\u9ad8(Elevation)", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" diff --git a/homeassistant/components/meteo_france/translations/ja.json b/homeassistant/components/meteo_france/translations/ja.json index 97e650554c7..ca736d1c31d 100644 --- a/homeassistant/components/meteo_france/translations/ja.json +++ b/homeassistant/components/meteo_france/translations/ja.json @@ -1,8 +1,31 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { - "user": { + "cities": { + "data": { + "city": "\u90fd\u5e02" + }, "title": "M\u00e9t\u00e9o-France" + }, + "user": { + "data": { + "city": "\u90fd\u5e02" + }, + "description": "\u90f5\u4fbf\u756a\u53f7(\u30d5\u30e9\u30f3\u30b9\u306e\u307f\u3001\u63a8\u5968) \u307e\u305f\u306f\u3001\u5e02\u533a\u753a\u6751\u540d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "M\u00e9t\u00e9o-France" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "\u4e88\u6e2c\u30e2\u30fc\u30c9" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/id.json b/homeassistant/components/meteoclimatic/translations/id.json index 81dddee653f..4e23dad8dc4 100644 --- a/homeassistant/components/meteoclimatic/translations/id.json +++ b/homeassistant/components/meteoclimatic/translations/id.json @@ -6,6 +6,15 @@ }, "error": { "not_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "step": { + "user": { + "data": { + "code": "Kode stasiun" + }, + "description": "Masukkan kode stasiun Meteoclimatic (misalnya, ESCAT4300000043206B)", + "title": "Meteoclimatic" + } } } } \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/ja.json b/homeassistant/components/metoffice/translations/ja.json index ca01be12afa..0fb2b51574a 100644 --- a/homeassistant/components/metoffice/translations/ja.json +++ b/homeassistant/components/metoffice/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -7,7 +14,8 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6" }, - "description": "\u7def\u5ea6\u3068\u7d4c\u5ea6\u306f\u3001\u6700\u3082\u8fd1\u3044\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u898b\u3064\u3051\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002" + "description": "\u7def\u5ea6\u3068\u7d4c\u5ea6\u306f\u3001\u6700\u3082\u8fd1\u3044\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u898b\u3064\u3051\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "title": "\u82f1\u56fd\u6c17\u8c61\u5e81(UK Met Office)\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index acc93cd2971..0949c1110d2 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "name_exists": "\u540d\u524d\u304c\u5b58\u5728\u3057\u307e\u3059" + }, "step": { "user": { "data": { @@ -10,7 +15,8 @@ "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u3092\u4f7f\u7528" }, "title": "Mikrotik\u30eb\u30fc\u30bf\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } @@ -21,6 +27,7 @@ "device_tracker": { "data": { "arp_ping": "ARP ping\u3092\u6709\u52b9\u306b\u3059\u308b", + "detection_time": "\u30db\u30fc\u30e0\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb\u3092\u691c\u8a0e", "force_dhcp": "DHCP\u3092\u4f7f\u7528\u3057\u305f\u5f37\u5236\u30b9\u30ad\u30e3\u30f3" } } diff --git a/homeassistant/components/mill/translations/he.json b/homeassistant/components/mill/translations/he.json index 40170220ad7..b551a319b2b 100644 --- a/homeassistant/components/mill/translations/he.json +++ b/homeassistant/components/mill/translations/he.json @@ -7,6 +7,17 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { + "cloud": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "local": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/mill/translations/id.json b/homeassistant/components/mill/translations/id.json index d6de96051d3..92670be251a 100644 --- a/homeassistant/components/mill/translations/id.json +++ b/homeassistant/components/mill/translations/id.json @@ -24,7 +24,8 @@ "connection_type": "Pilih jenis koneksi", "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Pilih jenis koneksi. Lokal membutuhkan pemanas generasi 3" } } } diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json index 0705c93038d..fe6edde5862 100644 --- a/homeassistant/components/mill/translations/ja.json +++ b/homeassistant/components/mill/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "cloud": { "data": { diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json index 4bfe44c4fec..82f75825aca 100644 --- a/homeassistant/components/minecraft_server/translations/ja.json +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -14,7 +14,8 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001Minecraft Server\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002" + "description": "\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u3001Minecraft Server\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002", + "title": "\u3042\u306a\u305f\u306eMinecraft\u30b5\u30fc\u30d0\u30fc\u3092\u30ea\u30f3\u30af" } } } diff --git a/homeassistant/components/modem_callerid/translations/id.json b/homeassistant/components/modem_callerid/translations/id.json index 9e8fc6738b9..3aa455d5f4c 100644 --- a/homeassistant/components/modem_callerid/translations/id.json +++ b/homeassistant/components/modem_callerid/translations/id.json @@ -2,17 +2,23 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang tersisa yang ditemukan" }, "error": { "cannot_connect": "Gagal terhubung" }, "step": { + "usb_confirm": { + "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk.", + "title": "Modem Telepon" + }, "user": { "data": { "name": "Nama", "port": "Port" }, + "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk.", "title": "Modem Telepon" } } diff --git a/homeassistant/components/modern_forms/translations/id.json b/homeassistant/components/modern_forms/translations/id.json index 8b2f9fcfa1d..c85667c4449 100644 --- a/homeassistant/components/modern_forms/translations/id.json +++ b/homeassistant/components/modern_forms/translations/id.json @@ -15,8 +15,14 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Siapkan kipas Modern Forms untuk diintegrasikan dengan Home Assistant." + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan kipas Modern Forms `{name}` ke Home Assistant?", + "title": "Perangkat kipas Modern Forms yang Ditemukan" } } - } + }, + "title": "Modern Forms" } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/ja.json b/homeassistant/components/monoprice/translations/ja.json index f1b4a2e3617..24bfd2a4ba7 100644 --- a/homeassistant/components/monoprice/translations/ja.json +++ b/homeassistant/components/monoprice/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -10,7 +17,8 @@ "source_4": "\u30bd\u30fc\u30b9#4\u306e\u540d\u524d", "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d", "source_6": "\u30bd\u30fc\u30b9#6\u306e\u540d\u524d" - } + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a" } } }, @@ -22,7 +30,8 @@ "source_2": "\u30bd\u30fc\u30b9#2\u306e\u540d\u524d", "source_3": "\u30bd\u30fc\u30b9#3\u306e\u540d\u524d", "source_4": "\u30bd\u30fc\u30b9#4\u306e\u540d\u524d", - "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d" + "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d", + "source_6": "\u30bd\u30fc\u30b9#6\u306e\u540d\u524d" }, "title": "\u30bd\u30fc\u30b9\u306e\u8a2d\u5b9a" } diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json index 35e254ef136..269470110ec 100644 --- a/homeassistant/components/motion_blinds/translations/id.json +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -6,13 +6,15 @@ "connection_error": "Gagal terhubung" }, "error": { - "discovery_error": "Gagal menemukan Motion Gateway" + "discovery_error": "Gagal menemukan Motion Gateway", + "invalid_interface": "Antarmuka jaringan tidak valid" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "Kunci API" + "api_key": "Kunci API", + "interface": "Antarmuka jaringan yang akan digunakan" }, "description": "Anda akan memerlukan Kunci API 16 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Motion Blinds" @@ -37,7 +39,11 @@ "options": { "step": { "init": { - "description": "Tentukan pengaturan opsional" + "data": { + "wait_for_push": "Tunggu push multicast pada pembaruan" + }, + "description": "Tentukan pengaturan opsional", + "title": "Motion Blinds" } } } diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index 12749d3aabf..1e84edbfeea 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -9,22 +9,26 @@ "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" }, + "flow_title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9", "step": { "connect": { "data": { "api_key": "API\u30ad\u30fc", "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" }, + "description": "16\u6587\u5b57\u306eAPI\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" }, "select": { "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" }, + "description": "Motion Gateway\u3092\u8ffd\u52a0\u3057\u3066\u63a5\u7d9a\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u63a5\u7d9a\u3057\u305f\u3044Motion Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "user": { "data": { + "api_key": "API\u30ad\u30fc", "host": "IP\u30a2\u30c9\u30ec\u30b9" }, "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", diff --git a/homeassistant/components/motioneye/translations/id.json b/homeassistant/components/motioneye/translations/id.json index 7e3964dd4db..efce25eb1d7 100644 --- a/homeassistant/components/motioneye/translations/id.json +++ b/homeassistant/components/motioneye/translations/id.json @@ -12,6 +12,7 @@ }, "step": { "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke layanan motionEye yang disediakan oleh add-on: {addon}?", "title": "motionEye melalui add-on Home Assistant" }, "user": { diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index d223dfb74df..308c9f31e8c 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -51,6 +51,8 @@ }, "options": { "error": { + "bad_birth": "Topik birth tidak valid", + "bad_will": "Topik will tidak valid", "cannot_connect": "Gagal terhubung" }, "step": { @@ -66,8 +68,17 @@ }, "options": { "data": { + "birth_enable": "Aktifkan pesan 'birth'", + "birth_payload": "Payload pesan birth", + "birth_qos": "QoS pesan birth", + "birth_retain": "Simpan pesan birth", + "birth_topic": "Topik pesan birth", "discovery": "Aktifkan penemuan", - "will_enable": "Aktifkan pesan 'will'" + "will_enable": "Aktifkan pesan 'will'", + "will_payload": "Payload pesan will", + "will_qos": "QoS pesan will", + "will_retain": "Simpan pesan will", + "will_topic": "Topik pesan will" }, "description": "Penemuan - Jika penemuan diaktifkan (disarankan), Home Assistant akan secara otomatis menemukan perangkat dan entitas yang mempublikasikan konfigurasinya di broker MQTT. Jika penemuan dinonaktifkan, semua konfigurasi harus dilakukan secara manual.\nPesan birth - Pesan birth akan dikirim setiap kali Home Assistant terhubung (kembali) ke broker MQTT.\nPesan will akan dikirim setiap kali Home Assistant kehilangan koneksi ke broker, baik dalam kasus bersih (misalnya Home Assistant dimatikan) dan dalam kasus (misalnya Home Assistant mogok atau kehilangan koneksi jaringan) terputus yang tidak bersih.", "title": "Opsi MQTT" diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 7a459d6bd17..7d1c1cac97f 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -29,18 +29,28 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" + }, + "trigger_type": { + "button_long_press": "\"{subtype}\" \u304c\u3001\u7d99\u7d9a\u7684\u306b\u62bc\u3055\u308c\u305f", + "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "broker": { "data": { + "broker": "\u30d6\u30ed\u30fc\u30ab\u30fc", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" diff --git a/homeassistant/components/mutesync/translations/id.json b/homeassistant/components/mutesync/translations/id.json index 66c930e348b..31da1a72f64 100644 --- a/homeassistant/components/mutesync/translations/id.json +++ b/homeassistant/components/mutesync/translations/id.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", + "invalid_auth": "Aktifkan autentikasi di m\u00fctesync: Preferensi > Autentikasi", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/myq/translations/ja.json b/homeassistant/components/myq/translations/ja.json index f95af61734c..bc7fff471d9 100644 --- a/homeassistant/components/myq/translations/ja.json +++ b/homeassistant/components/myq/translations/ja.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "data": { @@ -15,7 +21,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "MyQ Gateway\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index 4db3c491be4..eae16a87784 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -10,11 +10,13 @@ "invalid_ip": "Alamat IP tidak valid", "invalid_persistence_file": "File persistensi tidak valid", "invalid_port": "Nomor port tidak valid", + "invalid_publish_topic": "Topik publish tidak valid", "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", + "same_topic": "Topik subscribe dan publish sama", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { @@ -27,6 +29,7 @@ "invalid_ip": "Alamat IP tidak valid", "invalid_persistence_file": "File persistensi tidak valid", "invalid_port": "Nomor port tidak valid", + "invalid_publish_topic": "Topik publish tidak valid", "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index d46cd61e0ae..1cf50abc3a9 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -3,6 +3,8 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "duplicate_persistence_file": "\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb\u304c\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", + "duplicate_topic": "\u30c8\u30d4\u30c3\u30af\u306f\u65e2\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", @@ -16,12 +18,15 @@ "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "duplicate_persistence_file": "\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb\u304c\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "duplicate_topic": "\u30c8\u30d4\u30c3\u30af\u306f\u65e2\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_persistence_file": "\u7121\u52b9\u306a\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", @@ -30,6 +35,7 @@ "step": { "gw_mqtt": { "data": { + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "retain": "mqtt retain(\u4fdd\u6301)", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, diff --git a/homeassistant/components/nam/translations/he.json b/homeassistant/components/nam/translations/he.json index 39f3a7e4306..dd6dcc60585 100644 --- a/homeassistant/components/nam/translations/he.json +++ b/homeassistant/components/nam/translations/he.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "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" }, "flow_title": "{name}", @@ -12,6 +14,18 @@ "confirm_discovery": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Nettigo Air Monitor \u05d1-{host}?" }, + "credentials": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json index 298e681cadf..f17c0de7209 100644 --- a/homeassistant/components/nanoleaf/translations/id.json +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -15,6 +15,7 @@ "flow_title": "{name}", "step": { "link": { + "description": "Tekan dan tahan tombol daya pada Nanoleaf Anda selama 5 detik hingga LED tombol mulai berkedip, lalu klik **SUBMIT** dalam waktu 30 detik.", "title": "Tautkan Nanoleaf" }, "user": { diff --git a/homeassistant/components/neato/translations/ja.json b/homeassistant/components/neato/translations/ja.json index 0b41f69b65e..c5f4605159c 100644 --- a/homeassistant/components/neato/translations/ja.json +++ b/homeassistant/components/neato/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, @@ -18,5 +18,6 @@ "title": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 9b399384d39..39608c80465 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -2,8 +2,14 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" }, "error": { "internal_error": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u4e2d\u306b\u5185\u90e8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", @@ -30,16 +36,24 @@ "data": { "code": "PIN\u30b3\u30fc\u30c9" }, + "description": "Nest\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001[Authorize your account]({url})\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u8a8d\u8a3c\u5f8c\u3001\u63d0\u4f9b\u3055\u308c\u305fPIN\u30b3\u30fc\u30c9\u3092\u4ee5\u4e0b\u306b\u30b3\u30d4\u30fc\u30da\u30fc\u30b9\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Nest\u30a2\u30ab\u30a6\u30f3\u30c8\u3078\u30ea\u30f3\u30af" }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, "reauth_confirm": { + "description": "Nest \u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } }, "device_automation": { "trigger_type": { - "camera_person": "\u691c\u51fa\u3055\u308c\u305f\u4eba" + "camera_motion": "\u52d5\u4f53\u691c\u77e5", + "camera_person": "\u691c\u51fa\u3055\u308c\u305f\u4eba", + "camera_sound": "\u97f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", + "doorbell_chime": "\u30c9\u30a2\u30d9\u30eb\u304c\u62bc\u3055\u308c\u305f" } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/id.json b/homeassistant/components/netatmo/translations/id.json index dedcb5532e1..bcdd35b0152 100644 --- a/homeassistant/components/netatmo/translations/id.json +++ b/homeassistant/components/netatmo/translations/id.json @@ -15,6 +15,7 @@ "title": "Pilih Metode Autentikasi" }, "reauth_confirm": { + "description": "Integrasi Netatmo perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" } } diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 8aa60160212..97b706fa99d 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" }, "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, "reauth_confirm": { "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" diff --git a/homeassistant/components/netgear/translations/id.json b/homeassistant/components/netgear/translations/id.json index 6ca86af8bbe..6292d3c18dc 100644 --- a/homeassistant/components/netgear/translations/id.json +++ b/homeassistant/components/netgear/translations/id.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi" }, + "error": { + "config": "Koneksi atau kesalahan masuk: periksa konfigurasi Anda" + }, "step": { "user": { "data": { @@ -11,13 +14,19 @@ "port": "Port (Opsional)", "ssl": "Menggunakan sertifikat SSL", "username": "Nama Pengguna (Opsional)" - } + }, + "description": "Host default: {host}\nPort default: {port}\nNama pengguna default: {username}", + "title": "Netgear" } } }, "options": { "step": { "init": { + "data": { + "consider_home": "Pertimbangan waktu sebagai di rumah (detik)" + }, + "description": "Tentukan pengaturan opsional", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index bd1eb2d6d15..a1d795bca9f 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -11,9 +11,9 @@ "data": { "host": "\u30db\u30b9\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8 (\u30aa\u30d7\u30b7\u30e7\u30f3)", + "port": "\u30dd\u30fc\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d (\u30aa\u30d7\u30b7\u30e7\u30f3)" + "username": "\u30e6\u30fc\u30b6\u30fc\u540d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}", "title": "Netgear" diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json index d7413b7de7c..6c2441f2cbd 100644 --- a/homeassistant/components/nexia/translations/ja.json +++ b/homeassistant/components/nexia/translations/ja.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "brand": "\u30d6\u30e9\u30f3\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "mynexia.com\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json index 131bfcb21ce..c7d0ba20f4e 100644 --- a/homeassistant/components/nfandroidtv/translations/id.json +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -12,7 +12,8 @@ "data": { "host": "Host", "name": "Nama" - } + }, + "description": "Integrasi ini memerlukan aplikasi Notifikasi untuk Android TV.\n\nUntuk Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nUntuk Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nAnda harus mengatur reservasi DHCP di router Anda (lihat manual pengguna router Anda) atau alamat IP statis pada perangkat. Jika tidak, perangkat akhirnya akan menjadi tidak tersedia." } } } diff --git a/homeassistant/components/nightscout/translations/ja.json b/homeassistant/components/nightscout/translations/ja.json index 02c83a6e916..50a92184f36 100644 --- a/homeassistant/components/nightscout/translations/ja.json +++ b/homeassistant/components/nightscout/translations/ja.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API\u30ad\u30fc", "url": "URL" - } + }, + "description": "- URL: \u3042\u306a\u305f\u306enightscout\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002\u4f8b: https://myhomeassistant.duckdns.org:5423\n- API\u30ad\u30fc(\u30aa\u30d7\u30b7\u30e7\u30f3): \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304c\u4fdd\u8b77\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u306b\u306e\u307f\u4f7f\u7528(auth_default_roles != readable)", + "title": "Nightscout\u306e\u30b5\u30fc\u30d0\u30fc\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/nmap_tracker/translations/id.json b/homeassistant/components/nmap_tracker/translations/id.json index 6c06e815565..b9cd85326ae 100644 --- a/homeassistant/components/nmap_tracker/translations/id.json +++ b/homeassistant/components/nmap_tracker/translations/id.json @@ -2,15 +2,26 @@ "config": { "abort": { "already_configured": "Lokasi sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap" + }, + "description": "Konfigurasikan host untuk dipindai oleh Nmap. Alamat jaringan dan pengecualian dapat berupa alamat IP (192.168.1.1), jaringan IP (192.168.0.0/24), atau rentang IP (192.168.1.0-32)." + } } }, "options": { "step": { "init": { "data": { + "consider_home": "Waktu tunggu dalam detik untuk pelacak perangkat agar mempertimbangkan perangkat sebagai 'tidak di rumah' jika tidak ditemukan", "interval_seconds": "Interval pindai", + "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap", "track_new_devices": "Lacak perangkat baru" - } + }, + "description": "Konfigurasikan host untuk dipindai oleh Nmap. Alamat jaringan dan pengecualian dapat berupa alamat IP (192.168.1.1), jaringan IP (192.168.0.0/24), atau rentang IP (192.168.1.0-32)." } } }, diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index 721cfa4e5f6..b8e45689a20 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "consider_home": "\u898b\u3089\u308c\u306a\u304b\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u3064\u79d2\u6570\u3002", + "consider_home": "\u898b\u3048\u306a\u304f\u306a\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u898b\u306a\u3057\u3066\u3001\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u6a5f\u3059\u308b\u307e\u3067\u306e\u79d2\u6570\u3002", "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", diff --git a/homeassistant/components/notion/translations/id.json b/homeassistant/components/notion/translations/id.json index 23a1a6eddeb..bf8cb8e1186 100644 --- a/homeassistant/components/notion/translations/id.json +++ b/homeassistant/components/notion/translations/id.json @@ -14,6 +14,7 @@ "data": { "password": "Kata Sandi" }, + "description": "Masukkan kembali kata sandi untuk {username}.", "title": "Autentikasi Ulang Integrasi" }, "user": { diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index cb0a4e7d593..fb764116ecf 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -5,6 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nuheat/translations/ja.json b/homeassistant/components/nuheat/translations/ja.json index 38abb3ce5b6..ab7b02e57dd 100644 --- a/homeassistant/components/nuheat/translations/ja.json +++ b/homeassistant/components/nuheat/translations/ja.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "NuHeat\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index 71c68fa11af..d6ae15ab371 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -13,6 +13,7 @@ "data": { "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, + "description": "Nuki\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/number/translations/ja.json b/homeassistant/components/number/translations/ja.json index 1672cb3bf4f..81c728ea6b8 100644 --- a/homeassistant/components/number/translations/ja.json +++ b/homeassistant/components/number/translations/ja.json @@ -1,3 +1,8 @@ { + "device_automation": { + "action_type": { + "set_value": "{entity_name} \u306e\u5024\u3092\u8a2d\u5b9a" + } + }, "title": "\u6570" } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index c2d014e8614..7441f4c8169 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "resources": { "data": { @@ -9,8 +16,10 @@ }, "ups": { "data": { + "alias": "\u30a8\u30a4\u30ea\u30a2\u30b9", "resources": "\u30ea\u30bd\u30fc\u30b9" - } + }, + "title": "\u76e3\u8996\u3059\u308bUPS\u3092\u9078\u629e" }, "user": { "data": { @@ -30,7 +39,8 @@ "step": { "init": { "data": { - "resources": "\u30ea\u30bd\u30fc\u30b9" + "resources": "\u30ea\u30bd\u30fc\u30b9", + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" }, "description": "\u30bb\u30f3\u30b5\u30fc\u30ea\u30bd\u30fc\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json index 7b9f046afc7..5eabace63fe 100644 --- a/homeassistant/components/nws/translations/ja.json +++ b/homeassistant/components/nws/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index 0a2c394592d..fd00b05cd2c 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "user": { @@ -8,8 +15,11 @@ "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "title": "NZBGet\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/octoprint/translations/id.json b/homeassistant/components/octoprint/translations/id.json index 7970b3f17fb..647df2d8c51 100644 --- a/homeassistant/components/octoprint/translations/id.json +++ b/homeassistant/components/octoprint/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "auth_failed": "Gagal mengambil kunci API aplikasi", "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -9,10 +10,16 @@ "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "OctoPrint Printer: {host}", + "progress": { + "get_api_key": "Buka antarmuka OctoPrint dan klik 'Izinkan' pada Permintaan Akses untuk 'Home Assistant'." + }, "step": { "user": { "data": { "host": "Host", + "path": "Jalur Navigasi", + "port": "Nomor Port", "ssl": "Gunakan SSL", "username": "Nama Pengguna" } diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index c8836fb4115..ed70b2113dc 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -13,7 +21,8 @@ "step": { "init": { "data": { - "ph_offset": "pH\u30aa\u30d5\u30bb\u30c3\u30c8(\u6b63\u307e\u305f\u306f\u8ca0)" + "ph_offset": "pH\u30aa\u30d5\u30bb\u30c3\u30c8(\u6b63\u307e\u305f\u306f\u8ca0)", + "polling_interval": "\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)" } } } diff --git a/homeassistant/components/onboarding/translations/ja.json b/homeassistant/components/onboarding/translations/ja.json index c36d54519fa..84a3ac79e4e 100644 --- a/homeassistant/components/onboarding/translations/ja.json +++ b/homeassistant/components/onboarding/translations/ja.json @@ -1,7 +1,7 @@ { "area": { "bedroom": "\u5bdd\u5ba4", - "kitchen": "\u30ad\u30c3\u30c1\u30f3", + "kitchen": "\u53f0\u6240", "living_room": "\u30ea\u30d3\u30f3\u30b0\u30eb\u30fc\u30e0" } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ja.json b/homeassistant/components/ondilo_ico/translations/ja.json index 5b012e09846..86fd09a27c3 100644 --- a/homeassistant/components/ondilo_ico/translations/ja.json +++ b/homeassistant/components/ondilo_ico/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 70e653ebda9..3213745d584 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "owserver": { "data": { diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index e8de619eb6f..acc567682de 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "auth": { "data": { @@ -17,18 +24,29 @@ }, "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, + "configure_profile": { + "title": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u8a2d\u5b9a" + }, + "device": { + "data": { + "host": "\u691c\u51fa\u3055\u308c\u305fONVIF\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" + }, "manual_input": { "data": { "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, "user": { "data": { "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" }, - "description": "\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3001\u30d7\u30ed\u30d5\u30a1\u30a4\u30ebS\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308bONVIF\u30c7\u30d0\u30a4\u30b9\u3092\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067\u691c\u7d22\u3057\u307e\u3059\u3002\n\n\u4e00\u90e8\u306e\u30e1\u30fc\u30ab\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u8a2d\u5b9a\u304b\u3089\u3001ONVIF\u3092\u7121\u52b9\u306b\u3057\u59cb\u3081\u3066\u3044\u307e\u3059\u3002\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a\u3067ONVIF\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3001\u30d7\u30ed\u30d5\u30a1\u30a4\u30ebS\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308bONVIF\u30c7\u30d0\u30a4\u30b9\u3092\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067\u691c\u7d22\u3057\u307e\u3059\u3002\n\n\u4e00\u90e8\u306e\u30e1\u30fc\u30ab\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u8a2d\u5b9a\u304b\u3089\u3001ONVIF\u3092\u7121\u52b9\u306b\u3057\u59cb\u3081\u3066\u3044\u307e\u3059\u3002\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a\u3067ONVIF\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, @@ -36,8 +54,10 @@ "step": { "onvif_devices": { "data": { + "extra_arguments": "\u8ffd\u52a0\u306eFFMPEG\u306e\u5f15\u6570", "rtsp_transport": "RTSP\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30e1\u30ab\u30cb\u30ba\u30e0" - } + }, + "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index f0986aaefc1..2bc2ed9c278 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -20,8 +20,10 @@ "step": { "init": { "data": { + "floor_temperature": "\u5e8a\u6e29\u5ea6", "read_precision": "\u7cbe\u5ea6\u3092\u8aad\u307f\u8fbc\u3080", - "set_precision": "\u7cbe\u5ea6\u3092\u8a2d\u5b9a\u3059\u308b" + "set_precision": "\u7cbe\u5ea6\u3092\u8a2d\u5b9a\u3059\u308b", + "temporary_override_mode": "\u4e00\u6642\u7684\u306a\u30bb\u30c3\u30c8\u30dd\u30a4\u30f3\u30c8\u306e\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u30e2\u30fc\u30c9" } } } diff --git a/homeassistant/components/openuv/translations/ja.json b/homeassistant/components/openuv/translations/ja.json index 69018ec5e5e..feccd2b1afa 100644 --- a/homeassistant/components/openuv/translations/ja.json +++ b/homeassistant/components/openuv/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" }, @@ -7,7 +10,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", - "elevation": "\u6a29\u9650\u306e\u6607\u683c", + "elevation": "\u6a19\u9ad8(Elevation)", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6" }, diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 0d1b8acc2fa..3d10a6221c7 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" + }, "step": { "user": { "data": { @@ -7,6 +14,7 @@ "language": "\u8a00\u8a9e", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", + "mode": "\u30e2\u30fc\u30c9", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", @@ -18,7 +26,8 @@ "step": { "init": { "data": { - "language": "\u8a00\u8a9e" + "language": "\u8a00\u8a9e", + "mode": "\u30e2\u30fc\u30c9" } } } diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json index cc5852ca726..fd2ca867d49 100644 --- a/homeassistant/components/ovo_energy/translations/ja.json +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{username}", "step": { "reauth": { diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index 1937781d0ae..1db987a2734 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "create_entry": { "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `'1A'`\n - Device ID: `'1B'`\n\niOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `'1A'`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json index 138920b69e4..d3ef9f7d17f 100644 --- a/homeassistant/components/ozw/translations/ja.json +++ b/homeassistant/components/ozw/translations/ja.json @@ -1,8 +1,19 @@ { "config": { "abort": { + "addon_info_failed": "OpenZWave\u306e\u30a2\u30c9\u30aa\u30f3\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_install_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "addon_set_config_failed": "OpenZWave\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "addon_start_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "progress": { + "install_addon": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { "hassio_confirm": { @@ -12,6 +23,10 @@ "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f" }, "on_supervisor": { + "data": { + "use_addon": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3059\u308b" + }, + "description": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f", "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" }, "start_addon": { diff --git a/homeassistant/components/panasonic_viera/translations/ja.json b/homeassistant/components/panasonic_viera/translations/ja.json index c72d0c34cc2..cdc07f9b45c 100644 --- a/homeassistant/components/panasonic_viera/translations/ja.json +++ b/homeassistant/components/panasonic_viera/translations/ja.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_pin_code": "\u5165\u529b\u3057\u305fPIN\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3057\u305f" + }, "step": { "pairing": { "data": { "pin": "PIN\u30b3\u30fc\u30c9" - } + }, + "description": "\u30c6\u30ec\u30d3\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30da\u30a2\u30ea\u30f3\u30b0" + }, + "user": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "name": "\u540d\u524d" + }, + "description": "\u30d1\u30ca\u30bd\u30cb\u30c3\u30af \u30d3\u30a8\u30e9TV\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30c6\u30ec\u30d3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index a3a6323d005..de14fbe14d0 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -14,6 +14,7 @@ "data": { "pin": "PIN\u30b3\u30fc\u30c9" }, + "description": "TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u30da\u30a2" }, "user": { diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index 1032e9fb39b..aab44165526 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "api_key": { "data": { @@ -10,9 +16,12 @@ "data": { "api_key": "API\u30ad\u30fc", "host": "\u30db\u30b9\u30c8", + "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8", - "statistics_only": "\u7d71\u8a08\u306e\u307f" + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "statistics_only": "\u7d71\u8a08\u306e\u307f", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } } diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 62fea558e72..6972961adeb 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { "default": "Plaato {device_type} \u540d\u524d **{device_name}** \u304c\u6b63\u5e38\u306b\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f\u3002" @@ -24,6 +26,7 @@ "device_name": "\u30c7\u30d0\u30a4\u30b9\u306b\u540d\u524d\u3092\u4ed8\u3051\u308b", "device_type": "Plaato\u30c7\u30d0\u30a4\u30b9\u306e\u30bf\u30a4\u30d7" }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", "title": "Plaato Wdebhookvices\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "webhook": { diff --git a/homeassistant/components/plant/translations/ja.json b/homeassistant/components/plant/translations/ja.json index 1e6829895b2..d7e8fa0a238 100644 --- a/homeassistant/components/plant/translations/ja.json +++ b/homeassistant/components/plant/translations/ja.json @@ -1,7 +1,8 @@ { "state": { "_": { - "ok": "OK" + "ok": "OK", + "problem": "\u554f\u984c" } }, "title": "\u30d7\u30e9\u30f3\u30c8\u30e2\u30cb\u30bf\u30fc" diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 0c4d7987c02..81bb84822a6 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -1,11 +1,16 @@ { "config": { "abort": { + "all_configured": "\u30ea\u30f3\u30af\u3055\u308c\u305f\u3059\u3079\u3066\u306e\u30b5\u30fc\u30d0\u30fc\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured": "\u3053\u306ePlex server\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "no_servers": "Plex\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ea\u30f3\u30af\u3055\u308c\u3066\u3044\u308b\u30b5\u30fc\u30d0\u30fc\u306f\u3042\u308a\u307e\u305b\u3093", + "not_found": "Plex server\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "ssl_error": "SSL\u8a3c\u660e\u66f8\u306e\u554f\u984c" }, "flow_title": "{name} ({host})", @@ -14,14 +19,25 @@ "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8", - "token": "\u30c8\u30fc\u30af\u30f3(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "token": "\u30c8\u30fc\u30af\u30f3(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } }, "select_server": { "data": { "server": "\u30b5\u30fc\u30d0\u30fc" }, + "description": "\u8907\u6570\u306e\u30b5\u30fc\u30d0\u30fc\u304c\u5229\u7528\u53ef\u80fd\u3067\u3059\u3002\u6b21\u306e\u3044\u305a\u308c\u304b\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044:", "title": "Plex\u30b5\u30fc\u30d0\u30fc\u3092\u9078\u629e" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u65b9\u6cd5" + } } } }, diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index d25e01dc68e..d3bc59f75a1 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -1,14 +1,38 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name}", "step": { "user": { + "data": { + "flow_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + }, "description": "\u30d7\u30ed\u30c0\u30af\u30c8:" }, "user_gateway": { "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9", "password": "Smile ID", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "username": "Smile \u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Smile\u306b\u63a5\u7d9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" } } } diff --git a/homeassistant/components/plum_lightpad/translations/ja.json b/homeassistant/components/plum_lightpad/translations/ja.json index bfe5b4dbc6c..8fba23eb117 100644 --- a/homeassistant/components/plum_lightpad/translations/ja.json +++ b/homeassistant/components/plum_lightpad/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 2f77aacd5f7..6d573895877 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -4,7 +4,8 @@ "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", - "no_flows": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + "no_flows": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/poolsense/translations/ja.json b/homeassistant/components/poolsense/translations/ja.json index 4b13a4f4c31..28d7e1d3d18 100644 --- a/homeassistant/components/poolsense/translations/ja.json +++ b/homeassistant/components/poolsense/translations/ja.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", + "title": "PoolSense" } } } diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index a27ebbb8072..53ebb4e0d75 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -1,10 +1,14 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "wrong_version": "PowerWall\u306b\u3001\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306e\u30bd\u30d5\u30c8\u30a6\u30a7\u30a2\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3092\u691c\u8a0e\u3059\u308b\u304b\u3001\u3053\u306e\u554f\u984c\u3092\u5831\u544a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{ip_address}", "step": { diff --git a/homeassistant/components/profiler/translations/ja.json b/homeassistant/components/profiler/translations/ja.json new file mode 100644 index 00000000000..c9c3cc04633 --- /dev/null +++ b/homeassistant/components/profiler/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ja.json b/homeassistant/components/progettihwsw/translations/ja.json index dc9a663ff69..b11538f447b 100644 --- a/homeassistant/components/progettihwsw/translations/ja.json +++ b/homeassistant/components/progettihwsw/translations/ja.json @@ -1,7 +1,32 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "relay_modes": { + "data": { + "relay_1": "\u30ea\u30ec\u30fc1", + "relay_10": "\u30ea\u30ec\u30fc10", + "relay_11": "\u30ea\u30ec\u30fc11", + "relay_12": "\u30ea\u30ec\u30fc12", + "relay_13": "\u30ea\u30ec\u30fc13", + "relay_14": "\u30ea\u30ec\u30fc14", + "relay_15": "\u30ea\u30ec\u30fc15", + "relay_16": "\u30ea\u30ec\u30fc16", + "relay_2": "\u30ea\u30ec\u30fc2", + "relay_3": "\u30ea\u30ec\u30fc3", + "relay_4": "\u30ea\u30ec\u30fc4", + "relay_5": "\u30ea\u30ec\u30fc5", + "relay_6": "\u30ea\u30ec\u30fc6", + "relay_7": "\u30ea\u30ec\u30fc7", + "relay_8": "\u30ea\u30ec\u30fc8", + "relay_9": "\u30ea\u30ec\u30fc9" + }, "title": "\u30ea\u30ec\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "user": { diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 23a04b45bd8..85defbb7e9c 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -1,14 +1,17 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "credential_error": "\u8cc7\u683c\u60c5\u5831\u306e\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "port_987_bind_error": "\u30dd\u30fc\u30c8 987\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "credential_timeout": "\u8cc7\u683c\u60c5\u5831\u30b5\u30fc\u30d3\u30b9\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002\u9001\u4fe1(submit)\u3092\u62bc\u3057\u3066\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN Code\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_ipaddress": "\u8a2d\u5b9a\u3057\u305f\u3044PlayStation4\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "step": { "creds": { @@ -22,6 +25,7 @@ "name": "\u540d\u524d", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, + "description": "PlayStation4\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002 PIN\u30b3\u30fc\u30c9(PIN CodePIN Code)\u306e\u5834\u5408\u306f\u3001PlayStation4\u672c\u4f53\u306e '\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001'\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3066\u3001'\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305f PIN\u30b3\u30fc\u30c9(PIN CodePIN Code) \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]\uff08(https://www.home-assistant.io/components/ps4/) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Play Station 4" }, "mode": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 431fa71a74d..245ad03a7e6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "step": { "user": { "data": { - "power": "\u5951\u7d04\u96fb\u529b (kW)" + "name": "\u30bb\u30f3\u30b5\u30fc\u540d", + "power": "\u5951\u7d04\u96fb\u529b (kW)", + "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", + "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" }, "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } @@ -14,8 +20,10 @@ "init": { "data": { "power": "\u5951\u7d04\u96fb\u529b (kW)", + "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" }, + "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c((hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/rachio/translations/ja.json b/homeassistant/components/rachio/translations/ja.json index 46ff1326f82..82aab7313b0 100644 --- a/homeassistant/components/rachio/translations/ja.json +++ b/homeassistant/components/rachio/translations/ja.json @@ -12,6 +12,16 @@ "user": { "data": { "api_key": "API\u30ad\u30fc" + }, + "description": "https://app.rach.io/ \u304b\u3089\u306eAPI Key\u304c\u5fc5\u8981\u3067\u3059\u3002Settings(\u8a2d\u5b9a)\u3092\u958b\u304d\u3001'GET API KEY'\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u30be\u30fc\u30f3 \u30b9\u30a4\u30c3\u30c1\u3092\u30a2\u30af\u30c6\u30a3\u30d6\u5316\u3059\u308b\u3068\u304d\u306e\u5b9f\u884c\u6642\u9593(\u5206)" } } } diff --git a/homeassistant/components/rainmachine/translations/ja.json b/homeassistant/components/rainmachine/translations/ja.json index 1383afca891..a7750784766 100644 --- a/homeassistant/components/rainmachine/translations/ja.json +++ b/homeassistant/components/rainmachine/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{ip}", "step": { "user": { @@ -14,5 +17,15 @@ "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u5165\u529b" } } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "\u30c7\u30d5\u30a9\u30eb\u30c8\u30be\u30fc\u30f3\u306e\u5b9f\u884c\u6642\u9593(\u79d2\u5358\u4f4d)" + }, + "title": "RainMachine\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ja.json b/homeassistant/components/recollect_waste/translations/ja.json index f2d82cfe432..c346781c1a6 100644 --- a/homeassistant/components/recollect_waste/translations/ja.json +++ b/homeassistant/components/recollect_waste/translations/ja.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u30d4\u30c3\u30af\u30a2\u30c3\u30d7\u30bf\u30a4\u30d7\u306b\u306f\u308f\u304b\u308a\u3084\u3059\u3044\u540d\u524d\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044(\u53ef\u80fd\u306a\u5834\u5408)" + }, + "title": "Recollect Waste\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index 16f6ef73e2c..6e3e7a8a615 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -1,10 +1,37 @@ { "config": { + "abort": { + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "setup_network": { "data": { + "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "\u63a5\u7d9a\u30a2\u30c9\u30ec\u30b9\u306e\u9078\u629e" + }, + "setup_serial": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "title": "\u30c7\u30d0\u30a4\u30b9" + }, + "setup_serial_manual_path": { + "data": { + "device": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "title": "\u30d1\u30b9" + }, + "user": { + "data": { + "type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + }, + "title": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e" } } }, @@ -17,5 +44,41 @@ "command": "\u30b3\u30de\u30f3\u30c9\u3092\u53d7\u4fe1: {subtype}", "status": "\u53d7\u4fe1\u30b9\u30c6\u30fc\u30bf\u30b9: {subtype}" } + }, + "options": { + "error": { + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_event_code": "\u7121\u52b9\u306a\u30a4\u30d9\u30f3\u30c8\u30b3\u30fc\u30c9", + "invalid_input_2262_off": "\u30b3\u30de\u30f3\u30c9 \u30aa\u30d5\u306e\u5165\u529b\u304c\u7121\u52b9", + "invalid_input_2262_on": "\u30b3\u30de\u30f3\u30c9 \u30aa\u30f3\u306e\u5165\u529b\u304c\u7121\u52b9", + "invalid_input_off_delay": "\u30aa\u30d5 \u30c7\u30a3\u30ec\u30a4\u306e\u5165\u529b\u304c\u7121\u52b9", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "\u81ea\u52d5\u8ffd\u52a0\u3092\u6709\u52b9\u306b\u3059\u308b", + "debug": "\u30c7\u30d0\u30c3\u30b0\u306e\u6709\u52b9\u5316", + "device": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", + "event_code": "\u30a4\u30d9\u30f3\u30c8\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8ffd\u52a0", + "remove_device": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" + }, + "title": "Rfxtrx\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "set_device_options": { + "data": { + "command_off": "\u30b3\u30de\u30f3\u30c9\u30aa\u30d5\u306e\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u5024", + "command_on": "\u30b3\u30de\u30f3\u30c9\u30aa\u30f3\u306e\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u5024", + "data_bit": "\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u6570", + "fire_event": "\u30c7\u30d0\u30a4\u30b9 \u30a4\u30d9\u30f3\u30c8\u3092\u6709\u52b9\u306b\u3059\u308b", + "off_delay": "\u30aa\u30d5\u9045\u5ef6", + "off_delay_enabled": "\u30aa\u30d5\u9045\u5ef6\u3092\u6709\u52b9\u306b\u3059\u308b", + "replace_device": "\u4ea4\u63db\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", + "signal_repetitions": "\u4fe1\u53f7\u306e\u30ea\u30d4\u30fc\u30c8\u6570", + "venetian_blind_mode": "Venetian blind\u30e2\u30fc\u30c9" + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index a20b33acd76..e2e6e6447af 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { @@ -9,5 +17,20 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + }, + "risco_to_ha": { + "data": { + "A": "\u30b0\u30eb\u30fc\u30d7A", + "B": "\u30b0\u30eb\u30fc\u30d7B", + "C": "\u30b0\u30eb\u30fc\u30d7C", + "D": "\u30b0\u30eb\u30fc\u30d7D" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index d54199c4691..94fa710a91a 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -1,13 +1,23 @@ { "config": { "abort": { - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", "title": "Roku" }, + "ssdp_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "Roku" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index 2d1b41b1309..d0278cbe9eb 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "not_irobot_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fiRobot\u793e\u306e\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" + "not_irobot_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fiRobot\u793e\u306e\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "short_blid": "BLID\u304c\u5207\u308a\u6368\u3066\u3089\u308c\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "flow_title": "{name} ({host})", "step": { @@ -35,6 +39,9 @@ }, "user": { "data": { + "blid": "BLID", + "continuous": "\u9023\u7d9a", + "delay": "\u9045\u5ef6", "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, @@ -42,5 +49,15 @@ "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" } } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "\u9023\u7d9a", + "delay": "\u9045\u5ef6" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index d3aaca838b2..03a5d275802 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "link": { "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/rpi_power/translations/ja.json b/homeassistant/components/rpi_power/translations/ja.json new file mode 100644 index 00000000000..26aee66af5b --- /dev/null +++ b/homeassistant/components/rpi_power/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u3053\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306b\u5fc5\u8981\u306a\u30b7\u30b9\u30c6\u30e0\u30af\u30e9\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30ab\u30fc\u30cd\u30eb\u304c\u6700\u65b0\u3067\u3001\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u30e9\u30ba\u30d9\u30ea\u30fc\u30d1\u30a4\u306e\u96fb\u6e90\u30c1\u30a7\u30c3\u30ab\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/ja.json b/homeassistant/components/ruckus_unleashed/translations/ja.json index 2981d97e3c6..5bd6c037226 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ja.json +++ b/homeassistant/components/ruckus_unleashed/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 75c6d8c2436..8cbb202fcef 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "id_missing": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index 2dc4cd3354a..7c91dd6b31c 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -20,6 +20,7 @@ "data": { "selected_gateway": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" }, + "description": "\u6b21\u306eScreenLogic gateways\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u69cb\u6210\u3059\u308b\u3082\u306e\u30921\u3064\u9078\u629e\u3059\u308b\u304b\u3001ScreenLogic gateways\u3092\u624b\u52d5\u3067\u69cb\u6210\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "ScreenLogic" } } @@ -27,6 +28,10 @@ "options": { "step": { "init": { + "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u9593\u306e\u79d2\u6570" + }, + "description": "{gateway_name} \u8a2d\u5b9a\u3092\u6307\u5b9a", "title": "ScreenLogic" } } diff --git a/homeassistant/components/season/translations/sensor.ja.json b/homeassistant/components/season/translations/sensor.ja.json index 2f524d4f910..bf4d103999e 100644 --- a/homeassistant/components/season/translations/sensor.ja.json +++ b/homeassistant/components/season/translations/sensor.ja.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "\u79cb", + "spring": "\u6625", + "summer": "\u590f", + "winter": "\u51ac" + }, "season__season__": { "autumn": "\u79cb", "spring": "\u6625", diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index e5df8e709bf..280ac0cb10f 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index a68fdc41be2..49cb304dad2 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { - "bad_dsn": "\u7121\u52b9\u306aDSN" + "bad_dsn": "\u7121\u52b9\u306aDSN", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/sharkiq/translations/ja.json b/homeassistant/components/sharkiq/translations/ja.json index a68aa531e29..a5f16e7104a 100644 --- a/homeassistant/components/sharkiq/translations/ja.json +++ b/homeassistant/components/sharkiq/translations/ja.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index bb77833d2a8..0703dbc3c7c 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "unsupported_firmware": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306e\u30d5\u30a1\u30fc\u30e0\u30a6\u30a7\u30a2\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{name}", "step": { "confirm_discovery": { @@ -14,7 +23,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u524d\u306b\u3001\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30dc\u30bf\u30f3\u3067\u30a6\u30a7\u30a4\u30af\u30a2\u30c3\u30d7\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/shopping_list/translations/ja.json b/homeassistant/components/shopping_list/translations/ja.json index e48b00fd05b..dd5609b8b32 100644 --- a/homeassistant/components/shopping_list/translations/ja.json +++ b/homeassistant/components/shopping_list/translations/ja.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "\u30b7\u30e7\u30c3\u30d4\u30f3\u30b0\u30ea\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "\u30b7\u30e7\u30c3\u30d4\u30f3\u30b0\u30ea\u30b9\u30c8" } } diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index 796b0964b9b..9286548ddb4 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -29,7 +29,8 @@ "port": "\u30dd\u30fc\u30c8", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", "zones": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30be\u30fc\u30f3\u6570" - } + }, + "title": "SIA\u30d9\u30fc\u30b9\u306e\u30a2\u30e9\u30fc\u30e0\u30b7\u30b9\u30c6\u30e0\u306e\u63a5\u7d9a\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index c71fc1261ed..ea151bfa868 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "error": { - "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f" + "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "input_auth_code": { @@ -21,7 +24,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { @@ -32,5 +36,15 @@ "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u30b3\u30fc\u30c9(Home Assistant UI\u3067\u4f7f\u7528)" + }, + "title": "SimpliSafe\u306e\u8a2d\u5b9a" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 6fc08d2e793..723b8fb9b2e 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -1,14 +1,27 @@ { "config": { + "abort": { + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + }, "flow_title": "{name}", "step": { "environment": { + "data": { + "environment": "\u74b0\u5883" + }, "description": "Smappee\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" }, "local": { "data": { "host": "\u30db\u30b9\u30c8" } + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } } diff --git a/homeassistant/components/smart_meter_texas/translations/ja.json b/homeassistant/components/smart_meter_texas/translations/ja.json index 38abb3ce5b6..2f714747433 100644 --- a/homeassistant/components/smart_meter_texas/translations/ja.json +++ b/homeassistant/components/smart_meter_texas/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json index 4b13a4f4c31..475ffb4fcf4 100644 --- a/homeassistant/components/smarthab/translations/ja.json +++ b/homeassistant/components/smarthab/translations/ja.json @@ -1,11 +1,16 @@ { "config": { + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "SmartHab\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index c1ef9364561..bab4e1838ef 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -12,12 +12,19 @@ "title": "Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b" }, "pat": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + }, "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "select_location": { + "data": { + "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" + }, "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { + "description": "SmartThings\u306f\u3001Home Assistant\u306b\u30d7\u30c3\u30b7\u30e5\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u9001\u4fe1\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3055\u308c\u307e\u3059:\n> {webhook_url}\n\n\u3053\u308c\u304c\u6b63\u3057\u304f\u306a\u3044\u5834\u5408\u306f\u3001\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u518d\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL\u306e\u78ba\u8a8d" } } diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json new file mode 100644 index 00000000000..2e214c0e6ad --- /dev/null +++ b/homeassistant/components/sms/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + }, + "title": "\u30e2\u30c7\u30e0\u306b\u63a5\u7d9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ja.json b/homeassistant/components/solaredge/translations/ja.json index 70652c898fe..fa5d4d0c715 100644 --- a/homeassistant/components/solaredge/translations/ja.json +++ b/homeassistant/components/solaredge/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "could_not_connect": "Solaredge API\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "site_not_active": "\u30b5\u30a4\u30c8\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/zh-Hans.json b/homeassistant/components/solaredge/translations/zh-Hans.json index 7f5039e9f93..eeddc9b59c9 100644 --- a/homeassistant/components/solaredge/translations/zh-Hans.json +++ b/homeassistant/components/solaredge/translations/zh-Hans.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" }, "error": { - "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 SolarEdge API", - "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "invalid_api_key": "API \u5bc6\u94a5\u65e0\u6548", "site_not_active": "\u672a\u6fc0\u6d3b" }, "step": { diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index fa1507b91a1..bb6bdaa1226 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" }, @@ -12,7 +14,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "SOMA Connect" } } } diff --git a/homeassistant/components/somfy/translations/ja.json b/homeassistant/components/somfy/translations/ja.json index 6aeb517fc16..3c25bd7bb8f 100644 --- a/homeassistant/components/somfy/translations/ja.json +++ b/homeassistant/components/somfy/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, "step": { "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 3e1d0719df2..3784edce53a 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -1,11 +1,28 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "description": "Sonarr\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { + "api_key": "API\u30ad\u30fc", + "base_path": "API\u3078\u306e\u30d1\u30b9", "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" + "port": "\u30dd\u30fc\u30c8", + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } } @@ -14,6 +31,7 @@ "step": { "init": { "data": { + "upcoming_days": "\u8868\u793a\u3059\u308b\u4eca\u5f8c\u306e\u65e5\u6570", "wanted_max_items": "\u8868\u793a\u3057\u305f\u3044\u30a2\u30a4\u30c6\u30e0\u306e\u6700\u5927\u6570" } } diff --git a/homeassistant/components/songpal/translations/ja.json b/homeassistant/components/songpal/translations/ja.json index ebac0e32cc9..5a0ebc5b3f8 100644 --- a/homeassistant/components/songpal/translations/ja.json +++ b/homeassistant/components/songpal/translations/ja.json @@ -1,12 +1,21 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "not_songpal_device": "Songpal\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name} ({host})", "step": { "init": { "description": "{name} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "endpoint": "\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8" + } } } } diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json new file mode 100644 index 00000000000..c9c3cc04633 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 5e048f9ce9a..9277adceeee 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index de984e58986..47749c58585 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, "create_entry": { "default": "Spotify\u306e\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" @@ -9,7 +10,15 @@ "step": { "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u5230\u9054\u53ef\u80fd" + } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json index f17835ca35a..7ddc6d40eca 100644 --- a/homeassistant/components/squeezebox/translations/ja.json +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_server_found": "LMS\u30b5\u30fc\u30d0\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "flow_title": "{host}", "step": { "edit": { diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 87d3ff85853..4c0530303dd 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -13,6 +13,7 @@ "user": { "data": { "id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "is_tou": "\u4f7f\u7528\u6642\u9593\u30d7\u30e9\u30f3\u3067\u3059", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } diff --git a/homeassistant/components/syncthru/translations/ja.json b/homeassistant/components/syncthru/translations/ja.json index dcb36d3d21c..9cf9c6fe4c3 100644 --- a/homeassistant/components/syncthru/translations/ja.json +++ b/homeassistant/components/syncthru/translations/ja.json @@ -1,12 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { + "invalid_url": "\u7121\u52b9\u306aURL", "syncthru_not_supported": "\u30c7\u30d0\u30a4\u30b9\u306fSyncThru\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" }, + "flow_title": "{name}", "step": { "confirm": { "data": { - "name": "\u540d\u524d" + "name": "\u540d\u524d", + "url": "Web\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u306eURL" + } + }, + "user": { + "data": { + "name": "\u540d\u524d", + "url": "Web\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u306eURL" } } } diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 024a2b786ad..6c975425efa 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -1,16 +1,31 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "reconfigure_successful": "\u518d\u8a2d\u5b9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "\u30b3\u30fc\u30c9" + } + }, "link": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "title": "Synology DSM" }, "reauth": { "data": { @@ -18,7 +33,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u7406\u7531: {details}", - "title": "Synology DSM\u518d\u8a8d\u8a3c\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" + "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "reauth_confirm": { "data": { @@ -32,7 +47,19 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "title": "Synology DSM" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u79d2)" } } } diff --git a/homeassistant/components/tado/translations/ja.json b/homeassistant/components/tado/translations/ja.json index 38abb3ce5b6..db7de0a783a 100644 --- a/homeassistant/components/tado/translations/ja.json +++ b/homeassistant/components/tado/translations/ja.json @@ -1,11 +1,31 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_homes": "\u3053\u306etado\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ea\u30f3\u30af\u3055\u308c\u3066\u3044\u308b\u5bb6\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Tado\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u63a5\u7d9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "\u30d5\u30a9\u30fc\u30eb\u30d0\u30c3\u30af\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u3002" + }, + "title": "Tado\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } } diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json index fa90ba9f383..353b3020e8c 100644 --- a/homeassistant/components/tasmota/translations/ja.json +++ b/homeassistant/components/tasmota/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { "invalid_discovery_topic": "(\u4e0d\u6b63\u306a)Invalid discovery topic prefix." }, @@ -7,7 +10,12 @@ "config": { "data": { "discovery_prefix": "(\u691c\u51fa)Discovery topic prefix" - } + }, + "description": "Tasmota\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Tasmota" + }, + "confirm": { + "description": "Tasmota\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index b8780258287..41803a6f62b 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "auth": { diff --git a/homeassistant/components/tibber/translations/ja.json b/homeassistant/components/tibber/translations/ja.json new file mode 100644 index 00000000000..f2fd8d2ac83 --- /dev/null +++ b/homeassistant/components/tibber/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + }, + "step": { + "user": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + }, + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/ja.json b/homeassistant/components/tile/translations/ja.json index bfe5b4dbc6c..7ee1ea080a0 100644 --- a/homeassistant/components/tile/translations/ja.json +++ b/homeassistant/components/tile/translations/ja.json @@ -1,10 +1,26 @@ { "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" + }, + "title": "\u30bf\u30a4\u30eb\u306e\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "\u975e\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30bf\u30a4\u30eb\u3092\u8868\u793a" } } } diff --git a/homeassistant/components/timer/translations/ja.json b/homeassistant/components/timer/translations/ja.json new file mode 100644 index 00000000000..d560f4da835 --- /dev/null +++ b/homeassistant/components/timer/translations/ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "active": "\u30a2\u30af\u30c6\u30a3\u30d6", + "idle": "\u30a2\u30a4\u30c9\u30eb", + "paused": "\u4e00\u6642\u505c\u6b62" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/en.json b/homeassistant/components/tolo/translations/en.json index c304f583b61..488c2f7ae69 100644 --- a/homeassistant/components/tolo/translations/en.json +++ b/homeassistant/components/tolo/translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network" }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/toon/translations/ja.json b/homeassistant/components/toon/translations/ja.json new file mode 100644 index 00000000000..ff8ff3b5c4f --- /dev/null +++ b/homeassistant/components/toon/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_agreements": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u306f\u3001Toon displays\u304c\u3042\u308a\u307e\u305b\u3093\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 2690f8bfccb..aeb6b3af691 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_locations": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u5229\u7528\u3067\u304d\u308b\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u305b\u3093\u3002TotalConnect\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "usercode": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u304c\u3053\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u306b\u306f\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059" }, "step": { @@ -22,6 +24,7 @@ }, "user": { "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } } diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index 6d5e2d060c8..4338175fda6 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001Traccar\u3067Webhook\u6a5f\u80fd\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306eURL\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044: `{webhook_url}`\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { + "description": "Traccar\u3092\u8a2d\u5b9a\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f", "title": "Traccar\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json index a7d809235c9..987acc64a53 100644 --- a/homeassistant/components/tradfri/translations/ja.json +++ b/homeassistant/components/tradfri/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" }, "error": { "cannot_authenticate": "\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306f\u3001Homekit\u306a\u3069\u306e\u4ed6\u306e\u30b5\u30fc\u30d0\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 3ead1384575..3352f4b4dd4 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" }, "step": { @@ -19,5 +20,14 @@ "title": "Transmission Client\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "step": { + "init": { + "data": { + "order": "\u30aa\u30fc\u30c0\u30fc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 0fe60e18781..ce6ca2789fc 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "login_error": "\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc ({code}): {msg}" }, + "flow_title": "Tuya\u306e\u69cb\u6210", "step": { "login": { "data": { diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json index a0ef9e88923..053e5c2b007 100644 --- a/homeassistant/components/twentemilieu/translations/ja.json +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_address": "Twente Milieu\u30b5\u30fc\u30d3\u30b9\u30a8\u30ea\u30a2\u306b\u4f4f\u6240\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + }, "step": { "user": { "data": { + "house_number": "\u5bb6\u5c4b\u756a\u53f7", "post_code": "\u90f5\u4fbf\u756a\u53f7" }, - "description": "\u3042\u306a\u305f\u306e\u4f4f\u6240\u306eTwente Milieu providing waste collection(\u30b4\u30df\u53ce\u96c6\u60c5\u5831)\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "\u3042\u306a\u305f\u306e\u4f4f\u6240\u306eTwente Milieu providing waste collection(\u30b4\u30df\u53ce\u96c6\u60c5\u5831)\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "title": "Twente Milieu" } } } diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 4336bbc01e7..45930c49e7b 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, "create_entry": { "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Twilio]({twilio_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-form-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index a5d84529d21..6214c863e55 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -29,15 +29,32 @@ "step": { "client_control": { "data": { - "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8" + "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8", + "dpi_restrictions": "DPI\u5236\u9650\u30b0\u30eb\u30fc\u30d7\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b" + }, + "description": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a2d\u5b9a\n\n\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u5236\u5fa1\u3057\u305f\u3044\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc\u306e\u30b9\u30a4\u30c3\u30c1\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "UniFi\u30aa\u30d7\u30b7\u30e7\u30f32/3" + }, + "device_tracker": { + "data": { + "detection_time": "\u6700\u5f8c\u306b\u898b\u305f\u3082\u306e\u304b\u3089\u96e2\u308c\u3066\u3044\u308b\u3068\u898b\u306a\u3055\u308c\u308b\u307e\u3067\u306e\u6642\u9593(\u79d2)", + "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", + "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)", + "track_wired_clients": "\u6709\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u542b\u3081\u308b" } }, "simple_options": { + "data": { + "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8", + "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", + "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)" + }, "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u7528\u306e\u5e2f\u57df\u5e45\u4f7f\u7528\u30bb\u30f3\u30b5\u30fc" + "allow_bandwidth_sensors": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u7528\u306e\u5e2f\u57df\u5e45\u4f7f\u7528\u30bb\u30f3\u30b5\u30fc", + "allow_uptime_sensors": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306e\u30a2\u30c3\u30d7\u30bf\u30a4\u30e0\u30bb\u30f3\u30b5\u30fc" }, "description": "\u7d71\u8a08\u30bb\u30f3\u30b5\u30fc\u306e\u8a2d\u5b9a", "title": "UniFi\u30aa\u30d7\u30b7\u30e7\u30f33/3" diff --git a/homeassistant/components/upb/translations/ja.json b/homeassistant/components/upb/translations/ja.json index 34a87ae39a5..ad3b7d003fc 100644 --- a/homeassistant/components/upb/translations/ja.json +++ b/homeassistant/components/upb/translations/ja.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { - "invalid_upb_file": "UPB UPStart\u306e\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u306a\u3044\u304b\u7121\u52b9\u3067\u3059\u3002\u30d5\u30a1\u30a4\u30eb\u540d\u3068\u30d1\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_upb_file": "UPB UPStart\u306e\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u306a\u3044\u304b\u7121\u52b9\u3067\u3059\u3002\u30d5\u30a1\u30a4\u30eb\u540d\u3068\u30d1\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { diff --git a/homeassistant/components/upcloud/translations/ja.json b/homeassistant/components/upcloud/translations/ja.json index 38abb3ce5b6..906dfcfb2cd 100644 --- a/homeassistant/components/upcloud/translations/ja.json +++ b/homeassistant/components/upcloud/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { @@ -8,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694(\u79d2\u3001\u6700\u5c0f30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/ja.json b/homeassistant/components/vacuum/translations/ja.json index 2d45d5772e2..74fb5660762 100644 --- a/homeassistant/components/vacuum/translations/ja.json +++ b/homeassistant/components/vacuum/translations/ja.json @@ -1,11 +1,18 @@ { + "device_automation": { + "trigger_type": { + "docked": "{entity_name} \u30c9\u30c3\u30ad\u30f3\u30b0\u6e08\u307f" + } + }, "state": { "_": { "cleaning": "\u30af\u30ea\u30fc\u30cb\u30f3\u30b0", "docked": "\u30c9\u30c3\u30ad\u30f3\u30b0", "error": "\u30a8\u30e9\u30fc", + "idle": "\u30a2\u30a4\u30c9\u30eb", "off": "\u30aa\u30d5", "on": "\u30aa\u30f3", + "paused": "\u4e00\u6642\u505c\u6b62", "returning": "\u30c9\u30c3\u30af\u306b\u623b\u308b" } }, diff --git a/homeassistant/components/velbus/translations/ja.json b/homeassistant/components/velbus/translations/ja.json index 9c79d49ccba..5534e240732 100644 --- a/homeassistant/components/velbus/translations/ja.json +++ b/homeassistant/components/velbus/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json index 3aef59492de..63bdce0f003 100644 --- a/homeassistant/components/vera/translations/ja.json +++ b/homeassistant/components/vera/translations/ja.json @@ -10,5 +10,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "Vera controller\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 5b8df8badba..5bd85b67e34 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -12,10 +12,12 @@ "installation": { "data": { "giid": "\u30a4\u30f3\u30b9\u30c8\u30ec\u30fc\u30b7\u30e7\u30f3" - } + }, + "description": "Home Assistant\u306f\u3001\u30de\u30a4\u30da\u30fc\u30b8\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u8907\u6570\u306eVerisure\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002Home Assistant\u306b\u8ffd\u52a0\u3059\u308b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm": { "data": { + "description": "Verisure MyPages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 738654030ad..66d7cf26ca9 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json index 8a4520bbf59..ff17008eb0f 100644 --- a/homeassistant/components/vilfo/translations/ja.json +++ b/homeassistant/components/vilfo/translations/ja.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "user": { "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "host": "\u30db\u30b9\u30c8" }, "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 6c7a89257bd..69f1b3094a9 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { @@ -37,7 +39,8 @@ "include_or_exclude": "\u30a2\u30d7\u30ea\u3092\u542b\u3081\u308b\u304b\u3001\u9664\u5916\u3057\u307e\u3059\u304b\uff1f", "volume_step": "\u30dc\u30ea\u30e5\u30fc\u30e0 \u30b9\u30c6\u30c3\u30d7\u30b5\u30a4\u30ba" }, - "description": "\u30b9\u30de\u30fc\u30c8\u30c6\u30ec\u30d3\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea\u3092\u9078\u629e\u3057\u3066\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3067\u304d\u307e\u3059\u3002" + "description": "\u30b9\u30de\u30fc\u30c8\u30c6\u30ec\u30d3\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea\u3092\u9078\u629e\u3057\u3066\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3067\u304d\u307e\u3059\u3002", + "title": "VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0" } } } diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json index 01d512c72e3..8066a84a4cd 100644 --- a/homeassistant/components/volumio/translations/ja.json +++ b/homeassistant/components/volumio/translations/ja.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u691c\u51fa\u3055\u308c\u305fVolumio\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "discovery_confirm": { "description": "Volumio (`{name}`) \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/wallbox/translations/he.json b/homeassistant/components/wallbox/translations/he.json index ca1c7a93c5c..6109bb22195 100644 --- a/homeassistant/components/wallbox/translations/he.json +++ b/homeassistant/components/wallbox/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 55122a8bff4..e86fc7f18cd 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -23,11 +23,15 @@ "init": { "data": { "avoid_ferries": "\u30d5\u30a7\u30ea\u30fc\u3092\u907f\u3051\u307e\u3059\u304b\uff1f", + "avoid_subscription_roads": "Vignette / Subscription\u3092\u5fc5\u8981\u3068\u3059\u308b\u9053\u8def\u3092\u907f\u3051\u307e\u3059\u304b\uff1f", "avoid_toll_roads": "\u6709\u6599\u9053\u8def\u3092\u907f\u3051\u307e\u3059\u304b\uff1f", + "excl_filter": "\u9078\u629e\u3055\u308c\u305f\u30eb\u30fc\u30c8\u306e\u8aac\u660e\u306b\u542b\u307e\u308c\u306a\u3044Substring", + "incl_filter": "\u9078\u629e\u3055\u308c\u305f\u30eb\u30fc\u30c8\u306e\u8aac\u660e\u306b\u542b\u307e\u308c\u308bSubstring", "realtime": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u306e\u79fb\u52d5\u6642\u9593\uff1f", "units": "\u5358\u4f4d", "vehicle_type": "\u8eca\u4e21\u30bf\u30a4\u30d7" - } + }, + "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json index 4e567596b31..f86e1e80520 100644 --- a/homeassistant/components/wemo/translations/ja.json +++ b/homeassistant/components/wemo/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, "step": { "confirm": { "description": "Wemo\u3092\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" @@ -8,7 +12,7 @@ }, "device_automation": { "trigger_type": { - "long_press": "Wemo\u30dc\u30bf\u30f3\u304c2\u79d2\u9593\u62bc\u3055\u308c\u307e\u3057\u305f" + "long_press": "Wemo\u30dc\u30bf\u30f3\u304c\u30012\u79d2\u9593\u62bc\u3055\u308c\u307e\u3057\u305f" } } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/ja.json b/homeassistant/components/wilight/translations/ja.json new file mode 100644 index 00000000000..afecf5f624f --- /dev/null +++ b/homeassistant/components/wilight/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index c60d8562235..e8380fae0b6 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -2,11 +2,15 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Netatmo\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002" + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, "create_entry": { "default": "\u9078\u629e\u3057\u305fWithings\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f\u3002" }, + "error": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{profile}", "step": { "pick_implementation": { @@ -17,6 +21,9 @@ "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d" }, "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" + }, + "reauth": { + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 16593c09912..a26ee4f8b66 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{name}", "step": { "user": { @@ -7,6 +14,18 @@ "host": "\u30db\u30b9\u30c8" }, "description": "WLED\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + }, + "zeroconf_confirm": { + "title": "\u767a\u898b\u3055\u308c\u305fWLED device" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "1\u3064\u306eLED\u30bb\u30b0\u30e1\u30f3\u30c8\u3067\u3082\u3001\u30de\u30b9\u30bf\u30fc\u30e9\u30a4\u30c8\u3092\u4fdd\u3064(keep)\u3002" + } } } } diff --git a/homeassistant/components/wled/translations/select.he.json b/homeassistant/components/wled/translations/select.he.json new file mode 100644 index 00000000000..6a2dfa5e45c --- /dev/null +++ b/homeassistant/components/wled/translations/select.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "\u05db\u05d1\u05d5\u05d9", + "1": "\u05de\u05d5\u05e4\u05e2\u05dc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.hu.json b/homeassistant/components/wled/translations/select.hu.json new file mode 100644 index 00000000000..415352b2e88 --- /dev/null +++ b/homeassistant/components/wled/translations/select.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Ki", + "1": "Be", + "2": "Am\u00edg az eszk\u00f6z \u00fajraindul" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/ja.json b/homeassistant/components/wolflink/translations/ja.json index 66f33d9411e..4154735e4a7 100644 --- a/homeassistant/components/wolflink/translations/ja.json +++ b/homeassistant/components/wolflink/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "device": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 229aa40a4ef..b6ae9e847ff 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -2,6 +2,8 @@ "state": { "wolflink__state": { "1_x_warmwasser": "1 x DHW", + "absenkbetrieb": "\u30bb\u30c3\u30c8\u30d0\u30c3\u30af\u30e2\u30fc\u30c9", + "absenkstop": "\u30bb\u30c3\u30c8\u30d0\u30c3\u30af \u30b9\u30c8\u30c3\u30d7", "aktiviert": "\u6709\u52b9\u5316", "antilegionellenfunktion": "\u30ec\u30b8\u30aa\u30cd\u30e9\u83cc\u5bfe\u7b56\u6a5f\u80fd", "at_abschaltung": "OT\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3", @@ -11,10 +13,15 @@ "automatik_aus": "\u81ea\u52d5\u30aa\u30d5", "automatik_ein": "\u81ea\u52d5\u30aa\u30f3", "deaktiviert": "\u975e\u6d3b\u6027", + "dhw_prior": "DHWPrior", "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", + "glt_betrieb": "BMS\u30e2\u30fc\u30c9", "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", + "heizung": "\u6696\u623f", + "initialisierung": "\u521d\u671f\u5316", + "kalibration": "\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", "parallelbetrieb": "\u30d1\u30e9\u30ec\u30eb\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json new file mode 100644 index 00000000000..7a9337c9332 --- /dev/null +++ b/homeassistant/components/xbox/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index fc1b1c265a2..f459b030e9b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "not_xiaomi_aqara": "Xiaomi Aqara Gateway\u3067\u306f\u306a\u304f\u3001\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u304c\u65e2\u77e5\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { "discovery_error": "Xiaomi Aqara Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Home Assistant\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u306eIP\u3092\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044", "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9", "invalid_key": "\u7121\u52b9\u306a\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 \u30ad\u30fc", "invalid_mac": "\u7121\u52b9\u306aMAC\u30a2\u30c9\u30ec\u30b9" diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 2b57152bebb..c8d1d3abc9f 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -1,11 +1,14 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "incomplete_info": "\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u305f\u3081\u306e\u60c5\u5831\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30db\u30b9\u30c8\u307e\u305f\u306f\u30c8\u30fc\u30af\u30f3\u304c\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "not_xiaomi_miio": "\u30c7\u30d0\u30a4\u30b9\u306f(\u307e\u3060) Xiaomi Miio\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_login_error": "Xiaomi Miio Cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a8d\u8a3c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_no_devices": "\u3053\u306eXiaomi Miio cloud\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index 3d4fc5381aa..1b901189bfc 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { diff --git a/homeassistant/components/zerproc/translations/ja.json b/homeassistant/components/zerproc/translations/ja.json new file mode 100644 index 00000000000..d1234b69652 --- /dev/null +++ b/homeassistant/components/zerproc/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 8fa2a910d5f..b8a76775aba 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -40,10 +40,13 @@ }, "config_panel": { "zha_alarm_options": { + "alarm_failed_tries": "\u30a2\u30e9\u30fc\u30e0\u3092\u30c8\u30ea\u30ac\u30fc\u3055\u305b\u308b\u305f\u3081\u306b\u9023\u7d9a\u3057\u3066\u5931\u6557\u3057\u305f\u30b3\u30fc\u30c9 \u30a8\u30f3\u30c8\u30ea\u306e\u6570", "alarm_master_code": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30de\u30b9\u30bf\u30fc\u30b3\u30fc\u30c9", "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { + "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", + "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, @@ -60,7 +63,7 @@ "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", - "open": "\u958b\u304f", + "open": "\u30aa\u30fc\u30d7\u30f3", "right": "\u53f3", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" @@ -68,7 +71,11 @@ "trigger_type": { "device_offline": "\u30c7\u30d0\u30a4\u30b9\u304c\u30aa\u30d5\u30e9\u30a4\u30f3", "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", - "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b" + "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b", + "remote_button_alt_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_short_press": "\"{subtype}\" \u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", + "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json index b4772630366..b52e9701a23 100644 --- a/homeassistant/components/zoneminder/translations/ja.json +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -2,14 +2,18 @@ "config": { "abort": { "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "create_entry": { "default": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u304c\u8ffd\u52a0\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "ZoneMinder", "step": { @@ -19,7 +23,9 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "path": "ZM\u30d1\u30b9", "path_zms": "ZMS\u30d1\u30b9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "title": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json index fe7ecaaca20..ff0afe58c0e 100644 --- a/homeassistant/components/zwave/translations/ja.json +++ b/homeassistant/components/zwave/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" @@ -24,8 +25,8 @@ "sleeping": "\u30b9\u30ea\u30fc\u30d7" }, "query_stage": { - "dead": " ({query_stage})", - "initializing": "\u521d\u671f\u5316\u4e2d ( {query_stage} )" + "dead": "\u30c7\u30c3\u30c9", + "initializing": "\u521d\u671f\u5316\u4e2d" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 81d41422fa9..1b79614b078 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -79,6 +79,7 @@ "trigger_type": { "event.notification.entry_control": "\u30a8\u30f3\u30c8\u30ea\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", "event.notification.notification": "\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", + "event.value_notification.basic": "{subtype} \u306a\u30d9\u30fc\u30b7\u30c3\u30af CC \u30a4\u30d9\u30f3\u30c8", "event.value_notification.central_scene": "{subtype} \u306e\u30bb\u30f3\u30c8\u30e9\u30eb \u30b7\u30fc\u30f3 \u30a2\u30af\u30b7\u30e7\u30f3", "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", From 0920e74aa20490a9cb44713362b893cb0dd9d19e Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Thu, 25 Nov 2021 00:24:27 +0000 Subject: [PATCH 0853/1452] Use DeviceEntryType in Metoffice (#60252) --- homeassistant/components/metoffice/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 2d762eb7c3b..5537af31540 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -104,7 +105,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def get_device_info(coordinates: str, name: str) -> DeviceInfo: """Return device registry information.""" return DeviceInfo( - entry_type="service", + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinates)}, manufacturer="Met Office", name=f"Met Office {name}", From 75057949d1a0d84cf906591b622381dd0f5599c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 02:30:02 +0100 Subject: [PATCH 0854/1452] Adjust async_step_discovery methods for BaseServiceInfo (#60285) Co-authored-by: epenet --- homeassistant/config_entries.py | 11 +++++----- tests/components/spotify/test_config_flow.py | 14 +++++++++++-- .../helpers/test_config_entry_oauth2_flow.py | 8 ++++++-- tests/test_config_entries.py | 20 +++++++++---------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9a6709820ff..9f1731e40d6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable, Mapping from contextvars import ContextVar +import dataclasses from enum import Enum import functools import logging @@ -1360,13 +1361,13 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_mqtt( self, discovery_info: MqttServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_ssdp( self, discovery_info: DiscoveryInfoType @@ -1378,19 +1379,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_usb( self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) @callback def async_create_entry( # pylint: disable=arguments-differ diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 8ff18882e8b..fb0279f9112 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from spotipy import SpotifyException from homeassistant import data_entry_flow, setup +from homeassistant.components import zeroconf from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET @@ -12,6 +13,15 @@ from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry +BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + hostname="mock_hostname", + name="mock_name", + port=None, + properties={}, + type="mock_type", +) + async def test_abort_if_no_configuration(hass): """Check flow aborts when no configuration is present.""" @@ -23,7 +33,7 @@ async def test_abort_if_no_configuration(hass): assert result["reason"] == "missing_configuration" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF} + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -35,7 +45,7 @@ async def test_zeroconf_abort_if_existing_entry(hass): MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF} + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 52dda703f1e..48868a3727b 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -220,7 +220,9 @@ async def test_step_discovery(hass, flow_handler, local_impl): ) result = await hass.config_entries.flow.async_init( - TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + TEST_DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=data_entry_flow.BaseServiceInfo(), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -242,7 +244,9 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): assert result["step_id"] == "pick_implementation" result = await hass.config_entries.flow.async_init( - TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + TEST_DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=data_entry_flow.BaseServiceInfo(), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 85d64de70a2..0b4d7fa8799 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -9,7 +9,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, callback -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -2350,13 +2350,13 @@ async def test_async_setup_update_entry(hass): @pytest.mark.parametrize( "discovery_source", ( - config_entries.SOURCE_DISCOVERY, - config_entries.SOURCE_SSDP, - config_entries.SOURCE_USB, - config_entries.SOURCE_HOMEKIT, - config_entries.SOURCE_DHCP, - config_entries.SOURCE_ZEROCONF, - config_entries.SOURCE_HASSIO, + (config_entries.SOURCE_DISCOVERY, {}), + (config_entries.SOURCE_SSDP, {}), + (config_entries.SOURCE_USB, BaseServiceInfo()), + (config_entries.SOURCE_HOMEKIT, BaseServiceInfo()), + (config_entries.SOURCE_DHCP, BaseServiceInfo()), + (config_entries.SOURCE_ZEROCONF, BaseServiceInfo()), + (config_entries.SOURCE_HASSIO, {}), ), ) async def test_flow_with_default_discovery(hass, manager, discovery_source): @@ -2382,7 +2382,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): # Create one to be in progress result = await manager.flow.async_init( - "comp", context={"source": discovery_source} + "comp", context={"source": discovery_source[0]}, data=discovery_source[1] ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -2403,7 +2403,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): entry = hass.config_entries.async_entries("comp")[0] assert entry.title == "yo" - assert entry.source == discovery_source + assert entry.source == discovery_source[0] assert entry.unique_id is None From 5b199bcc6dc66c0e535cd92b8150370228dca12e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Nov 2021 19:34:19 -0600 Subject: [PATCH 0855/1452] Use random effect from flux_led library (#60312) --- homeassistant/components/flux_led/const.py | 2 +- homeassistant/components/flux_led/light.py | 49 ++++++------------- .../components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/number.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_light.py | 43 ++++++++-------- 7 files changed, 40 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 188967dfdb9..639bac7165e 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -71,7 +71,7 @@ CONF_SPEED_PCT: Final = "speed_pct" CONF_TRANSITION: Final = "transition" -EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} +EFFECT_SPEED_SUPPORT_MODES: Final = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} CONF_CUSTOM_EFFECT_COLORS: Final = "custom_effect_colors" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index aecf1646726..5d89d878e3f 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -3,7 +3,6 @@ from __future__ import annotations import ast import logging -import random from typing import Any, Final, cast from flux_led.const import ATTR_ID, ATTR_IPADDR @@ -31,7 +30,6 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE, - EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_EFFECT, SUPPORT_TRANSITION, @@ -71,7 +69,6 @@ from .const import ( CONF_TRANSITION, DEFAULT_EFFECT_SPEED, DOMAIN, - EFFECT_SUPPORT_MODES, FLUX_LED_DISCOVERY, MODE_AUTO, MODE_RGB, @@ -86,9 +83,6 @@ from .util import _flux_color_mode_to_hass, _hass_color_modes _LOGGER = logging.getLogger(__name__) -SUPPORT_FLUX_LED: Final = SUPPORT_TRANSITION - - MODE_ATTRS = { ATTR_EFFECT, ATTR_COLOR_TEMP, @@ -229,6 +223,8 @@ async def async_setup_entry( class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): """Representation of a Flux light.""" + _attr_supported_features = SUPPORT_TRANSITION | SUPPORT_EFFECT + def __init__( self, coordinator: FluxLedUpdateCoordinator, @@ -240,17 +236,15 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(coordinator, unique_id, name) - self._attr_supported_features = SUPPORT_FLUX_LED self._attr_min_mireds = ( color_temperature_kelvin_to_mired(self._device.max_temp) + 1 ) # for rounding self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) self._attr_supported_color_modes = _hass_color_modes(self._device) - if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): - self._attr_supported_features |= SUPPORT_EFFECT - self._attr_effect_list = [*self._device.effect_list, EFFECT_RANDOM] - if custom_effect_colors: - self._attr_effect_list.append(EFFECT_CUSTOM) + custom_effects: list[str] = [] + if custom_effect_colors: + custom_effects.append(EFFECT_CUSTOM) + self._attr_effect_list = [*self._device.effect_list, *custom_effects] self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition @@ -321,14 +315,6 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): async def _async_set_effect(self, effect: str, brightness: int) -> None: """Set an effect.""" - # Random color effect - if effect == EFFECT_RANDOM: - await self._device.async_set_levels( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), - ) - return # Custom effect if effect == EFFECT_CUSTOM: if self._custom_effect_colors: @@ -364,8 +350,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): await self._async_set_effect(effect, brightness) return # Handle switch to CCT Color Mode - if ATTR_COLOR_TEMP in kwargs: - color_temp_mired = kwargs[ATTR_COLOR_TEMP] + if color_temp_mired := kwargs.get(ATTR_COLOR_TEMP): color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) if self.color_mode != COLOR_MODE_RGBWW: await self._device.async_set_white_temp(color_temp_kelvin, brightness) @@ -381,29 +366,23 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold) return # Handle switch to RGB Color Mode - if ATTR_RGB_COLOR in kwargs: - await self._device.async_set_levels( - *kwargs[ATTR_RGB_COLOR], brightness=brightness - ) + if rgb := kwargs.get(ATTR_RGB_COLOR): + await self._device.async_set_levels(*rgb, brightness=brightness) return # Handle switch to RGBW Color Mode - if ATTR_RGBW_COLOR in kwargs: + if rgbw := kwargs.get(ATTR_RGBW_COLOR): if ATTR_BRIGHTNESS in kwargs: - rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness) - else: - rgbw = kwargs[ATTR_RGBW_COLOR] + rgbw = rgbw_brightness(rgbw, brightness) await self._device.async_set_levels(*rgbw) return # Handle switch to RGBWW Color Mode - if ATTR_RGBWW_COLOR in kwargs: + if rgbcw := kwargs.get(ATTR_RGBWW_COLOR): if ATTR_BRIGHTNESS in kwargs: rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) - else: - rgbcw = kwargs[ATTR_RGBWW_COLOR] await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) return - if ATTR_WHITE in kwargs: - await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) + if (white := kwargs.get(ATTR_WHITE)) is not None: + await self._device.async_set_levels(w=white) return async def _async_adjust_brightness(self, brightness: int) -> None: diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index e42ad3b1afd..8276cde0ff9 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.33"], + "requirements": ["flux_led==0.24.34"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 3942f61e8fa..dcd0d816cee 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator -from .const import DOMAIN, EFFECT_SUPPORT_MODES +from .const import DOMAIN, EFFECT_SPEED_SUPPORT_MODES from .entity import FluxEntity from .util import _hass_color_modes @@ -27,7 +27,7 @@ async def async_setup_entry( coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] color_modes = _hass_color_modes(coordinator.device) - if not color_modes.intersection(EFFECT_SUPPORT_MODES): + if not color_modes.intersection(EFFECT_SPEED_SUPPORT_MODES): return async_add_entities( diff --git a/requirements_all.txt b/requirements_all.txt index e0c9a429cc4..ad49c6cb0c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.33 +flux_led==0.24.34 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7a5a0145f7..3fd29d7f6b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.33 +flux_led==0.24.34 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index b56e655af08..a02224364cc 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -40,7 +40,6 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, - EFFECT_RANDOM, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -202,7 +201,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -272,8 +271,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.async_set_levels.assert_called_once() - bulb.async_set_levels.reset_mock() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -308,7 +307,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -350,8 +349,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.async_set_levels.assert_called_once() - bulb.async_set_levels.reset_mock() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -427,7 +426,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgbw" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] assert attributes[ATTR_RGB_COLOR] == (255, 42, 42) @@ -494,8 +493,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.async_set_levels.assert_called_once() - bulb.async_set_levels.reset_mock() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -529,7 +528,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb", "white"] assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) @@ -578,8 +577,8 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.async_set_levels.assert_called_once() - bulb.async_set_levels.reset_mock() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -640,7 +639,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgbww" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgbww"] assert attributes[ATTR_HS_COLOR] == (3.237, 94.51) @@ -732,8 +731,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.async_set_levels.assert_called_once() - bulb.async_set_levels.reset_mock() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -781,7 +780,7 @@ async def test_white_light(hass: HomeAssistant) -> None: assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "brightness" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - assert ATTR_EFFECT_LIST not in attributes # single channel does not support effects + assert ATTR_EFFECT_LIST in attributes # single channel now supports effects await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -835,7 +834,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM, "custom"] + assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, "custom"] assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -917,7 +916,7 @@ async def test_rgb_light_custom_effects_invalid_colors( attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -946,7 +945,7 @@ async def test_rgb_light_custom_effect_via_service( attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgb" - assert attributes[ATTR_EFFECT_LIST] == [*bulb.effect_list, EFFECT_RANDOM] + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -1090,9 +1089,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_COLOR_MODE] == "onoff" - assert ( - ATTR_EFFECT_LIST not in attributes - ) # no support for effects with addressable yet + assert ATTR_EFFECT_LIST in attributes assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] await hass.services.async_call( From 62bf42e4211387642bdb0c38a61d7fa9fd5f414f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 02:35:15 +0100 Subject: [PATCH 0856/1452] Introduce SsdpServiceInfo for ssdp discovery (#60284) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 62 +++++++++++++++++++ .../components/arcam_fmj/test_config_flow.py | 24 ++++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index a4dbbbf4576..437712d555a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable +from dataclasses import dataclass, field from datetime import timedelta from enum import Enum from ipaddress import IPv4Address, IPv6Address @@ -20,9 +21,11 @@ from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import EVENT_HOMEASSISTANT_STOP, MATCH_ALL from homeassistant.core import HomeAssistant, callback as core_callback +from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass @@ -85,6 +88,65 @@ SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { _LOGGER = logging.getLogger(__name__) +@dataclass +class _HaServiceDescription: + """Keys added by HA.""" + + x_homeassistant_matching_domains: set[str] = field(default_factory=set) + + +@dataclass +class _SsdpServiceDescription: + """SSDP info with optional keys.""" + + ssdp_usn: str + ssdp_st: str + ssdp_location: str + ssdp_nt: str | None = None + ssdp_udn: str | None = None + ssdp_ext: str | None = None + ssdp_server: str | None = None + + +@dataclass +class _UpnpServiceDescription: + """UPnP info.""" + + upnp: dict[str, Any] + + +@dataclass +class SsdpServiceInfo( + _HaServiceDescription, + _SsdpServiceDescription, + _UpnpServiceDescription, + BaseServiceInfo, +): + """Prepared info from ssdp/upnp entries.""" + + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"ssdp"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + # Use a property if it is available, fallback to upnp data + if hasattr(self, name): + return getattr(self, name) + return self.upnp.get(name) + + @bind_hass async def async_register_callback( hass: HomeAssistant, diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 6c86b2bbf96..efcbb0b691c 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -33,16 +33,20 @@ MOCK_UPNP_DEVICE = f""" MOCK_UPNP_LOCATION = f"http://{MOCK_HOST}:8080/dd.xml" -MOCK_DISCOVER = { - ssdp.ATTR_UPNP_MANUFACTURER: "ARCAM", - ssdp.ATTR_UPNP_MODEL_NAME: " ", - ssdp.ATTR_UPNP_MODEL_NUMBER: "AVR450, AVR750", - ssdp.ATTR_UPNP_FRIENDLY_NAME: f"Arcam media client {MOCK_UUID}", - ssdp.ATTR_UPNP_SERIAL: "12343", - ssdp.ATTR_SSDP_LOCATION: f"http://{MOCK_HOST}:8080/dd.xml", - ssdp.ATTR_UPNP_UDN: MOCK_UDN, - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:MediaRenderer:1", -} +MOCK_DISCOVER = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{MOCK_HOST}:8080/dd.xml", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER: "ARCAM", + ssdp.ATTR_UPNP_MODEL_NAME: " ", + ssdp.ATTR_UPNP_MODEL_NUMBER: "AVR450, AVR750", + ssdp.ATTR_UPNP_FRIENDLY_NAME: f"Arcam media client {MOCK_UUID}", + ssdp.ATTR_UPNP_SERIAL: "12343", + ssdp.ATTR_UPNP_UDN: MOCK_UDN, + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:MediaRenderer:1", + }, +) @pytest.fixture(name="dummy_client", autouse=True) From fd8b43320dbc3b5618b14724d8de3e51859b2748 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Thu, 25 Nov 2021 12:09:30 +0100 Subject: [PATCH 0857/1452] Replace returned STATE_UNKNOWN by None (#60324) --- homeassistant/components/statistics/sensor.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 6905170f470..5dc11b92729 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -481,7 +481,7 @@ class StatisticsSensor(SensorEntity): ) age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() return area / age_range_seconds - return STATE_UNKNOWN + return None def _stat_average_step(self): if len(self.states) >= 2: @@ -493,7 +493,7 @@ class StatisticsSensor(SensorEntity): ) age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() return area / age_range_seconds - return STATE_UNKNOWN + return None def _stat_average_timeless(self): return self._stat_mean() @@ -501,19 +501,19 @@ class StatisticsSensor(SensorEntity): def _stat_change(self): if len(self.states) > 0: return self.states[-1] - self.states[0] - return STATE_UNKNOWN + return None def _stat_change_sample(self): if len(self.states) > 1: return (self.states[-1] - self.states[0]) / (len(self.states) - 1) - return STATE_UNKNOWN + return None def _stat_change_second(self): if len(self.states) > 1: age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() if age_range_seconds > 0: return (self.states[-1] - self.states[0]) / age_range_seconds - return STATE_UNKNOWN + return None def _stat_count(self): return len(self.states) @@ -521,37 +521,37 @@ class StatisticsSensor(SensorEntity): def _stat_datetime_newest(self): if len(self.states) > 0: return self.ages[-1] - return STATE_UNKNOWN + return None def _stat_datetime_oldest(self): if len(self.states) > 0: return self.ages[0] - return STATE_UNKNOWN + return None def _stat_distance_95_percent_of_values(self): if len(self.states) >= 2: return 2 * 1.96 * self._stat_standard_deviation() - return STATE_UNKNOWN + return None def _stat_distance_99_percent_of_values(self): if len(self.states) >= 2: return 2 * 2.58 * self._stat_standard_deviation() - return STATE_UNKNOWN + return None def _stat_distance_absolute(self): if len(self.states) > 0: return max(self.states) - min(self.states) - return STATE_UNKNOWN + return None def _stat_mean(self): if len(self.states) > 0: return statistics.mean(self.states) - return STATE_UNKNOWN + return None def _stat_median(self): if len(self.states) > 0: return statistics.median(self.states) - return STATE_UNKNOWN + return None def _stat_noisiness(self): if len(self.states) >= 2: @@ -559,7 +559,7 @@ class StatisticsSensor(SensorEntity): abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) ) return diff_sum / (len(self.states) - 1) - return STATE_UNKNOWN + return None def _stat_quantiles(self): if len(self.states) > self._quantile_intervals: @@ -571,29 +571,29 @@ class StatisticsSensor(SensorEntity): method=self._quantile_method, ) ] - return STATE_UNKNOWN + return None def _stat_standard_deviation(self): if len(self.states) >= 2: return statistics.stdev(self.states) - return STATE_UNKNOWN + return None def _stat_total(self): if len(self.states) > 0: return sum(self.states) - return STATE_UNKNOWN + return None def _stat_value_max(self): if len(self.states) > 0: return max(self.states) - return STATE_UNKNOWN + return None def _stat_value_min(self): if len(self.states) > 0: return min(self.states) - return STATE_UNKNOWN + return None def _stat_variance(self): if len(self.states) >= 2: return statistics.variance(self.states) - return STATE_UNKNOWN + return None From f2a44553d8729ad9d338e8428734fc2568387dec Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 25 Nov 2021 12:24:30 +0100 Subject: [PATCH 0858/1452] Fix image build issues (#60319) --- .github/workflows/builder.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 7b5a145cd09..aa1c2b2a3e1 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -131,7 +131,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.09.0 + uses: home-assistant/builder@2021.11.4 with: args: | $BUILD_ARGS \ @@ -184,7 +184,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.11.3 + uses: home-assistant/builder@2021.11.4 with: args: | $BUILD_ARGS \ @@ -291,7 +291,7 @@ jobs: function validate_image() { local image=${1} - if ! cas authenticate --signerID notary@home-assistant.io; then + if ! cas authenticate --signerID notary@home-assistant.io "${image}"; then echo "Invalid signature!" exit 1 fi From 3372288c887cde935a6b0d6a26f746fc19f80a51 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Thu, 25 Nov 2021 12:29:09 +0100 Subject: [PATCH 0859/1452] Add tolo sensor platform (#60308) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/tolo/manifest.json | 2 +- homeassistant/components/tolo/sensor.py | 76 +++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/tolo/sensor.py diff --git a/.coveragerc b/.coveragerc index 619b8468a07..bee0555d865 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1094,6 +1094,7 @@ omit = homeassistant/components/tolo/__init__.py homeassistant/components/tolo/climate.py homeassistant/components/tolo/light.py + homeassistant/components/tolo/sensor.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 5151449f1fd..978bfe64ebb 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["climate", "light"] +PLATFORMS = ["climate", "light", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index 4aa84f3f2d2..63e87ebf876 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tolo", "requirements": [ - "tololib==0.1.0b2" + "tololib==0.1.0b3" ], "codeowners": [ "@MatthiasLohr" diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py new file mode 100644 index 00000000000..23533f68784 --- /dev/null +++ b/homeassistant/components/tolo/sensor.py @@ -0,0 +1,76 @@ +"""TOLO Sauna (non-binary, general) sensors.""" + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up (non-binary, general) sensors for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + ToloWaterLevelSensor(coordinator, entry), + ToloTankTemperatureSensor(coordinator, entry), + ] + ) + + +class ToloWaterLevelSensor(ToloSaunaCoordinatorEntity, SensorEntity): + """Sensor for tank water level.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_name = "Water Level" + _attr_icon = "mdi:waves-arrow-up" + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Sauna tank water level sensor entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_water_level" + + @property + def native_value(self) -> int: + """Return current tank water level.""" + return self.coordinator.data.status.water_level_percent + + +class ToloTankTemperatureSensor(ToloSaunaCoordinatorEntity, SensorEntity): + """Sensor for tank temperature.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_name = "Tank Temperature" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_native_unit_of_measurement = TEMP_CELSIUS + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Sauna tank temperature sensor entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_tank_temperature" + + @property + def native_value(self) -> int: + """Return current tank temperature.""" + return self.coordinator.data.status.tank_temperature diff --git a/requirements_all.txt b/requirements_all.txt index ad49c6cb0c5..d51a8a3257e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2317,7 +2317,7 @@ tmb==0.0.4 todoist-python==8.0.0 # homeassistant.components.tolo -tololib==0.1.0b2 +tololib==0.1.0b3 # homeassistant.components.toon toonapi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fd29d7f6b6..30e3fd0b7e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.12 # homeassistant.components.tolo -tololib==0.1.0b2 +tololib==0.1.0b3 # homeassistant.components.toon toonapi==0.2.1 From 995f01cb686f350f5799c3783dfc31082a53d05c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 05:30:57 -0600 Subject: [PATCH 0860/1452] Fix exception in august if bridge is missing (#60316) --- homeassistant/components/august/__init__.py | 29 +++++++-------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 700b03a85da..474e69db435 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -272,19 +272,13 @@ class AugustData(AugustSubscriberMixin): def _remove_inoperative_doorbells(self): for doorbell in list(self.doorbells): device_id = doorbell.device_id - doorbell_is_operative = False - doorbell_detail = self._device_detail_by_id.get(device_id) - if doorbell_detail is None: - _LOGGER.info( - "The doorbell %s could not be setup because the system could not fetch details about the doorbell", - doorbell.device_name, - ) - else: - doorbell_is_operative = True - - if not doorbell_is_operative: - del self._doorbells_by_id[device_id] - del self._device_detail_by_id[device_id] + if self._device_detail_by_id.get(device_id): + continue + _LOGGER.info( + "The doorbell %s could not be setup because the system could not fetch details about the doorbell", + doorbell.device_name, + ) + del self._doorbells_by_id[device_id] def _remove_inoperative_locks(self): # Remove non-operative locks as there must @@ -292,7 +286,6 @@ class AugustData(AugustSubscriberMixin): # be usable for lock in list(self.locks): device_id = lock.device_id - lock_is_operative = False lock_detail = self._device_detail_by_id.get(device_id) if lock_detail is None: _LOGGER.info( @@ -304,14 +297,12 @@ class AugustData(AugustSubscriberMixin): "The lock %s could not be setup because it does not have a bridge (Connect)", lock.device_name, ) + del self._device_detail_by_id[device_id] # Bridge may come back online later so we still add the device since we will # have a pubnub subscription to tell use when it recovers else: - lock_is_operative = True - - if not lock_is_operative: - del self._locks_by_id[device_id] - del self._device_detail_by_id[device_id] + continue + del self._locks_by_id[device_id] def _save_live_attrs(lock_detail): From a78f0eae39cbc20406bb7a83e1af70237ac9bf81 Mon Sep 17 00:00:00 2001 From: refinedcranberry <3584606+refinedcranberry@users.noreply.github.com> Date: Thu, 25 Nov 2021 12:34:04 +0100 Subject: [PATCH 0861/1452] Add "nextchange" sensors to AVM FRITZ!Smarthome devices (#58274) --- homeassistant/components/fritzbox/climate.py | 2 +- homeassistant/components/fritzbox/model.py | 2 +- homeassistant/components/fritzbox/sensor.py | 63 +++++++++++++++++++- tests/components/fritzbox/__init__.py | 5 ++ tests/components/fritzbox/test_climate.py | 60 +++++++++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index bf2d857e30e..ef1285f0ec2 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -185,7 +185,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active if self.device.summer_active is not None: attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active - if ATTR_STATE_WINDOW_OPEN is not None: + if self.device.window_open is not None: attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open return attrs diff --git a/homeassistant/components/fritzbox/model.py b/homeassistant/components/fritzbox/model.py index fa6da56caeb..133638c1fe8 100644 --- a/homeassistant/components/fritzbox/model.py +++ b/homeassistant/components/fritzbox/model.py @@ -18,8 +18,8 @@ class FritzExtraAttributes(TypedDict): class ClimateExtraAttributes(FritzExtraAttributes, total=False): """TypedDict for climates extra attributes.""" - battery_low: bool battery_level: int + battery_low: bool holiday_mode: bool summer_mode: bool window_open: bool diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 6ffa8bc8560..0ee7c3e8563 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -3,10 +3,12 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from typing import Final from pyfritzhome.fritzhomedevice import FritzhomeDevice +from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -20,6 +22,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -28,6 +31,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.dt import utc_from_timestamp from . import FritzBoxEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN @@ -38,7 +43,7 @@ from .model import FritzEntityDescriptionMixinBase class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase): """Sensor description mixin for Fritz!Smarthome entities.""" - native_value: Callable[[FritzhomeDevice], float | int | None] + native_value: Callable[[FritzhomeDevice], StateType | datetime] @dataclass @@ -97,6 +102,60 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.energy / 1000 if device.energy else 0.0, ), + # Thermostat Sensors + FritzSensorEntityDescription( + key="comfort_temperature", + name="Comfort Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + suitable=lambda device: device.has_thermostat + and device.comfort_temperature is not None, + native_value=lambda device: device.comfort_temperature, # type: ignore[no-any-return] + ), + FritzSensorEntityDescription( + key="eco_temperature", + name="Eco Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + suitable=lambda device: device.has_thermostat + and device.eco_temperature is not None, + native_value=lambda device: device.eco_temperature, # type: ignore[no-any-return] + ), + FritzSensorEntityDescription( + key="nextchange_temperature", + name="Next Scheduled Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + suitable=lambda device: device.has_thermostat + and device.nextchange_temperature is not None, + native_value=lambda device: device.nextchange_temperature, # type: ignore[no-any-return] + ), + FritzSensorEntityDescription( + key="nextchange_time", + name="Next Scheduled Change Time", + device_class=DEVICE_CLASS_TIMESTAMP, + suitable=lambda device: device.has_thermostat + and device.nextchange_endperiod is not None, + native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod), + ), + FritzSensorEntityDescription( + key="nextchange_preset", + name="Next Scheduled Preset", + suitable=lambda device: device.has_thermostat + and device.nextchange_temperature is not None, + native_value=lambda device: PRESET_ECO + if device.nextchange_temperature == device.eco_temperature + else PRESET_COMFORT, + ), + FritzSensorEntityDescription( + key="scheduled_preset", + name="Current Scheduled Preset", + suitable=lambda device: device.has_thermostat + and device.nextchange_temperature is not None, + native_value=lambda device: PRESET_COMFORT + if device.nextchange_temperature == device.eco_temperature + else PRESET_ECO, + ), ) @@ -122,6 +181,6 @@ class FritzBoxSensor(FritzBoxEntity, SensorEntity): entity_description: FritzSensorEntityDescription @property - def native_value(self) -> float | int | None: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.native_value(self.device) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 27abd38f8cb..80052125f99 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from unittest.mock import Mock +from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.core import HomeAssistant @@ -86,6 +87,10 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): summer_active = "fake_summer" target_temperature = 19.5 window_open = "fake_window" + nextchange_temperature = 22.0 + nextchange_endperiod = 0 + nextchange_preset = PRESET_COMFORT + scheduled_preset = PRESET_ECO class FritzDeviceSensorMock(FritzDeviceBaseMock): diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 627c82f617a..175b67df858 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -85,6 +86,65 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert ATTR_STATE_CLASS not in state.attributes + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_comfort_temperature") + assert state + assert state.state == "22.0" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Comfort Temperature" + ) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_eco_temperature") + assert state + assert state.state == "16.0" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Eco Temperature" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get( + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_temperature" + ) + assert state + assert state.state == "22.0" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{CONF_FAKE_NAME} Next Scheduled Temperature" + ) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get( + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_change_time" + ) + assert state + assert state.state == "1970-01-01T00:00:00+00:00" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{CONF_FAKE_NAME} Next Scheduled Change Time" + ) + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset") + assert state + assert state.state == PRESET_COMFORT + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{CONF_FAKE_NAME} Next Scheduled Preset" + ) + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get( + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_current_scheduled_preset" + ) + assert state + assert state.state == PRESET_ECO + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{CONF_FAKE_NAME} Current Scheduled Preset" + ) + assert ATTR_STATE_CLASS not in state.attributes + async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" From 213be1f15ecd9cbb048a0928c5b89bb94768dd14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 05:41:32 -0600 Subject: [PATCH 0862/1452] Update flux_led to use async_set_brightness in the lib (#60315) --- homeassistant/components/flux_led/light.py | 57 ++----------------- .../components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/number.py | 6 +- homeassistant/components/flux_led/util.py | 5 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 2 + tests/components/flux_led/test_light.py | 40 ++++++------- 8 files changed, 38 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 5d89d878e3f..0bccc2eafb3 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -11,7 +11,6 @@ from flux_led.utils import ( rgbcw_brightness, rgbcw_to_rgbwc, rgbw_brightness, - rgbww_brightness, ) import voluptuous as vol @@ -24,12 +23,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_WHITE, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, COLOR_MODE_RGBWW, - COLOR_MODE_WHITE, PLATFORM_SCHEMA, SUPPORT_EFFECT, SUPPORT_TRANSITION, @@ -51,8 +45,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import ( - color_hs_to_RGB, - color_RGB_to_hs, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) @@ -79,7 +71,7 @@ from .const import ( TRANSITION_STROBE, ) from .entity import FluxOnOffEntity -from .util import _flux_color_mode_to_hass, _hass_color_modes +from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes _LOGGER = logging.getLogger(__name__) @@ -262,10 +254,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value.""" - # Note that we call color_RGB_to_hs and not color_RGB_to_hsv - # to get the unscaled value since this is what the frontend wants - # https://github.com/home-assistant/frontend/blob/e797c017614797bb11671496d6bd65863de22063/src/dialogs/more-info/controls/more-info-light.ts#L263 - rgb: tuple[int, int, int] = color_hs_to_RGB(*color_RGB_to_hs(*self._device.rgb)) + rgb: tuple[int, int, int] = self._device.rgb_unscaled return rgb @property @@ -280,12 +269,6 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): rgbcw: tuple[int, int, int, int, int] = self._device.rgbcw return rgbcw - @property - def rgbwc_color(self) -> tuple[int, int, int, int, int]: - """Return the rgbwc color value.""" - rgbwc: tuple[int, int, int, int, int] = self._device.rgbww - return rgbwc - @property def color_mode(self) -> str: """Return the color mode of the light.""" @@ -311,7 +294,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): if MODE_ATTRS.intersection(kwargs): await self._async_set_mode(**kwargs) return - await self._async_adjust_brightness(self._async_brightness(**kwargs)) + await self._device.async_set_brightness(self._async_brightness(**kwargs)) async def _async_set_effect(self, effect: str, brightness: int) -> None: """Set an effect.""" @@ -324,9 +307,10 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): self._custom_effect_transition, ) return - effect_brightness = round(brightness / 255 * 100) await self._device.async_set_effect( - effect, self._device.speed or DEFAULT_EFFECT_SPEED, effect_brightness + effect, + self._device.speed or DEFAULT_EFFECT_SPEED, + _effect_brightness(brightness), ) @callback @@ -385,35 +369,6 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): await self._device.async_set_levels(w=white) return - async def _async_adjust_brightness(self, brightness: int) -> None: - """Adjust brightness.""" - # Handle brightness adjustment in effect mode - if effect := self.effect: - await self._async_set_effect(effect, brightness) - return - if self.color_mode == COLOR_MODE_COLOR_TEMP: - await self._device.async_set_white_temp(self._device.color_temp, brightness) - return - # Handle brightness adjustment in RGB Color Mode - if self.color_mode == COLOR_MODE_RGB: - await self._device.async_set_levels(*self.rgb_color, brightness=brightness) - return - # Handle brightness adjustment in RGBW Color Mode - if self.color_mode == COLOR_MODE_RGBW: - await self._device.async_set_levels( - *rgbw_brightness(self.rgbw_color, brightness) - ) - return - # Handle brightness adjustment in RGBWW Color Mode - if self.color_mode == COLOR_MODE_RGBWW: - rgbwc = self.rgbwc_color - await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness)) - return - # Handle Brightness Only Color Mode - if self.color_mode in {COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS}: - await self._device.async_set_levels(w=brightness) - return - async def async_set_custom_effect( self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str ) -> None: diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 8276cde0ff9..64a41a9ce8a 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.34"], + "requirements": ["flux_led==0.24.35"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index dcd0d816cee..39cfb2ea094 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -15,7 +15,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator from .const import DOMAIN, EFFECT_SPEED_SUPPORT_MODES from .entity import FluxEntity -from .util import _hass_color_modes +from .util import _effect_brightness, _hass_color_modes async def async_setup_entry( @@ -68,7 +68,6 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set the flux speed value.""" current_effect = self._device.effect - current_brightness = self._device.brightness new_speed = int(value) if not current_effect: raise HomeAssistantError( @@ -76,8 +75,7 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): ) if self._device.original_addressable and not self._device.is_on: raise HomeAssistantError("Speed can only be adjusted when the light is on") - effect_brightness = round(current_brightness / 255 * 100) await self._device.async_set_effect( - current_effect, new_speed, effect_brightness + current_effect, new_speed, _effect_brightness(self._device.brightness) ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index a564cb81bd0..8a089c9ebb6 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -25,3 +25,8 @@ def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) - return COLOR_MODE_WHITE return COLOR_MODE_BRIGHTNESS return FLUX_COLOR_MODE_TO_HASS.get(flux_color_mode, COLOR_MODE_ONOFF) + + +def _effect_brightness(brightness: int) -> int: + """Convert hass brightness to effect brightness.""" + return round(brightness / 255 * 100) diff --git a/requirements_all.txt b/requirements_all.txt index d51a8a3257e..f63190bbd6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.34 +flux_led==0.24.35 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30e3fd0b7e7..4b7c224ec34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.34 +flux_led==0.24.35 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index ea02529d83c..e3eb4a0e200 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -64,6 +64,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.async_set_preset_pattern = AsyncMock() bulb.async_set_effect = AsyncMock() bulb.async_set_white_temp = AsyncMock() + bulb.async_set_brightness = AsyncMock() bulb.async_stop = AsyncMock() bulb.async_update = AsyncMock() bulb.async_turn_off = AsyncMock() @@ -76,6 +77,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.getRgbww = MagicMock(return_value=[255, 0, 0, 50, 0]) bulb.getRgbcw = MagicMock(return_value=[255, 0, 0, 0, 50]) bulb.rgb = (255, 0, 0) + bulb.rgb_unscaled = (255, 0, 0) bulb.rgbw = (255, 0, 0, 50) bulb.rgbww = (255, 0, 0, 50, 0) bulb.rgbcw = (255, 0, 0, 0, 50) diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index a02224364cc..e66492d2f43 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -240,8 +240,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -331,8 +331,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -391,8 +391,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.async_set_white_temp.assert_called_with(5000, 255) - bulb.async_set_white_temp.reset_mock() + bulb.async_set_brightness.assert_called_with(255) + bulb.async_set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -400,8 +400,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, blocking=True, ) - bulb.async_set_white_temp.assert_called_with(5000, 128) - bulb.async_set_white_temp.reset_mock() + bulb.async_set_brightness.assert_called_with(128) + bulb.async_set_brightness.reset_mock() async def test_rgbw_light(hass: HomeAssistant) -> None: @@ -451,8 +451,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(168, 0, 0, 33) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -553,8 +553,8 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -612,8 +612,8 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: }, blocking=True, ) - bulb.async_set_levels.assert_called_with(w=100) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() async def test_rgbcw_light(hass: HomeAssistant) -> None: @@ -663,8 +663,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(250, 0, 0, 49, 0) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() bulb.is_on = True await hass.services.async_call( @@ -751,8 +751,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.async_set_effect.assert_called_with("purple_fade", 50, 100) - bulb.async_set_effect.reset_mock() + bulb.async_set_brightness.assert_called_with(255) + bulb.async_set_brightness.reset_mock() async def test_white_light(hass: HomeAssistant) -> None: @@ -802,8 +802,8 @@ async def test_white_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.async_set_levels.assert_called_with(w=100) - bulb.async_set_levels.reset_mock() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: From 18a82e43a437cebd303e4eaf6615aa43a5ca286e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 14:13:03 +0100 Subject: [PATCH 0863/1452] CI: fix linters on partial runs with multiple components (#60331) Co-authored-by: epenet --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aa25f426037..f6d31aa19ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -230,6 +230,7 @@ jobs: pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure - name: Run bandit (partially) if: needs.changes.outputs.test_full_suite == 'false' + shell: bash run: | . venv/bin/activate pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure @@ -278,6 +279,7 @@ jobs: pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - name: Run black (partially) if: needs.changes.outputs.test_full_suite == 'false' + shell: bash run: | . venv/bin/activate pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure @@ -329,6 +331,7 @@ jobs: pre-commit run --hook-stage manual flake8 --all-files - name: Run flake8 (partially) if: needs.changes.outputs.test_full_suite == 'false' + shell: bash run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* @@ -596,6 +599,7 @@ jobs: pylint homeassistant - name: Run pylint (partially) if: needs.changes.outputs.test_full_suite == 'false' + shell: bash run: | . venv/bin/activate pylint homeassistant/components/${{ needs.changes.outputs.integrations_glob }} @@ -635,6 +639,7 @@ jobs: mypy homeassistant - name: Run mypy (partially) if: needs.changes.outputs.test_full_suite == 'false' + shell: bash run: | . venv/bin/activate mypy homeassistant/components/${{ needs.changes.outputs.integrations_glob }} From 9eed18f12170b966acfb6c9df681c22833783b8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 14:35:19 +0100 Subject: [PATCH 0864/1452] Use SsdpServiceInfo for ssdp tests (part 1) (#60320) Co-authored-by: epenet --- tests/components/harmony/test_config_flow.py | 37 +++++--- tests/components/heos/conftest.py | 24 +++--- tests/components/heos/test_config_flow.py | 22 +++-- tests/components/isy994/test_config_flow.py | 84 ++++++++++++------- tests/components/roku/__init__.py | 22 ++--- tests/components/roku/test_config_flow.py | 8 +- tests/components/songpal/test_config_flow.py | 28 ++++--- .../synology_dsm/test_config_flow.py | 56 ++++++++----- tests/components/unifi/test_config_flow.py | 65 ++++++++------ 9 files changed, 215 insertions(+), 131 deletions(-) diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 9195af40cf1..7725e9752f5 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import aiohttp from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY from homeassistant.const import CONF_HOST, CONF_NAME @@ -58,10 +59,14 @@ async def test_form_ssdp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "Harmony Hub", - "ssdp_location": "http://192.168.1.12:8088/description", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.12:8088/description", + upnp={ + "friendlyName": "Harmony Hub", + }, + ), ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -106,10 +111,14 @@ async def test_form_ssdp_fails_to_get_remote_id(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "Harmony Hub", - "ssdp_location": "http://192.168.1.12:8088/description", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.12:8088/description", + upnp={ + "friendlyName": "Harmony Hub", + }, + ), ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" @@ -139,10 +148,14 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "Harmony Hub", - "ssdp_location": "http://2.2.2.2:8088/description", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://2.2.2.2:8088/description", + upnp={ + "friendlyName": "Harmony Hub", + }, + ), ) assert result["type"] == "abort" diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 693e665d1ee..a6b5c11dc9e 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -146,16 +146,20 @@ def dispatcher_fixture() -> Dispatcher: @pytest.fixture(name="discovery_data") def discovery_data_fixture() -> dict: """Return mock discovery data for testing.""" - return { - ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-denon-com:device:AiosDevice:1", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Office", - ssdp.ATTR_UPNP_MANUFACTURER: "Denon", - ssdp.ATTR_UPNP_MODEL_NAME: "HEOS Drive", - ssdp.ATTR_UPNP_MODEL_NUMBER: "DWSA-10 4.0", - ssdp.ATTR_UPNP_SERIAL: None, - ssdp.ATTR_UPNP_UDN: "uuid:e61de70c-2250-1c22-0080-0005cdf512be", - } + return ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", + upnp={ + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-denon-com:device:AiosDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Office", + ssdp.ATTR_UPNP_MANUFACTURER: "Denon", + ssdp.ATTR_UPNP_MODEL_NAME: "HEOS Drive", + ssdp.ATTR_UPNP_MODEL_NUMBER: "DWSA-10 4.0", + ssdp.ATTR_UPNP_SERIAL: None, + ssdp.ATTR_UPNP_UDN: "uuid:e61de70c-2250-1c22-0080-0005cdf512be", + }, + ) @pytest.fixture(name="quick_selects") diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 76ff06e2a96..d1d940671b8 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -79,7 +79,9 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): assert DATA_DISCOVERED_HOSTS not in hass.data -async def test_discovery_shows_create_form(hass, controller, discovery_data): +async def test_discovery_shows_create_form( + hass, controller, discovery_data: ssdp.SsdpServiceInfo +): """Test discovery shows form to confirm setup and subsequent abort.""" await hass.config_entries.flow.async_init( @@ -91,9 +93,9 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): assert len(flows_in_progress) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == {"Office (127.0.0.1)": "127.0.0.1"} - port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port - discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" - discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + port = urlparse(discovery_data.ssdp_location).port + discovery_data.ssdp_location = f"http://127.0.0.2:{port}/" + discovery_data.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data @@ -109,7 +111,7 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): async def test_discovery_flow_aborts_already_setup( - hass, controller, discovery_data, config_entry + hass, controller, discovery_data: ssdp.SsdpServiceInfo, config_entry ): """Test discovery flow aborts when entry already setup.""" config_entry.add_to_hass(hass) @@ -120,12 +122,14 @@ async def test_discovery_flow_aborts_already_setup( assert result["reason"] == "single_instance_allowed" -async def test_discovery_sets_the_unique_id(hass, controller, discovery_data): +async def test_discovery_sets_the_unique_id( + hass, controller, discovery_data: ssdp.SsdpServiceInfo +): """Test discovery sets the unique id.""" - port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port - discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" - discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + port = urlparse(discovery_data.ssdp_location).port + discovery_data.ssdp_location = f"http://127.0.0.2:{port}/" + discovery_data.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index c2853454327..e9a4c5dc4fb 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -326,11 +326,15 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -341,11 +345,15 @@ async def test_form_ssdp(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -385,11 +393,15 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://3.3.3.3{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) await hass.async_block_till_done() @@ -412,11 +424,15 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3/{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://3.3.3.3/{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) await hass.async_block_till_done() @@ -439,11 +455,15 @@ async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) await hass.async_block_till_done() @@ -466,11 +486,15 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: f"https://3.3.3.3/{ISY_URL_POSTFIX}", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"https://3.3.3.3/{ISY_URL_POSTFIX}", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ), ) await hass.async_block_till_done() diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index ece58de763a..5ae81eb7b72 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -3,13 +3,9 @@ from http import HTTPStatus import re from socket import gaierror as SocketGIAError -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.components.roku.const import DOMAIN -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL, -) +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -24,11 +20,15 @@ SSDP_LOCATION = "http://192.168.1.160/" UPNP_FRIENDLY_NAME = "My Roku 3" UPNP_SERIAL = "1GU48T017973" -MOCK_SSDP_DISCOVERY_INFO = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL: UPNP_SERIAL, -} +MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={ + ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL: UPNP_SERIAL, + }, +) HOMEKIT_HOST = "192.168.1.161" diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 768f42548c0..459c90f536a 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -48,7 +48,7 @@ async def test_duplicate_error( assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -216,7 +216,7 @@ async def test_ssdp_cannot_connect( """Test we abort SSDP flow on connection error.""" mock_connection(aioclient_mock, error=True) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, @@ -231,7 +231,7 @@ async def test_ssdp_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) with patch( "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, @@ -252,7 +252,7 @@ async def test_ssdp_discovery( """Test the SSDP discovery flow.""" mock_connection(aioclient_mock) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index a1751bca676..4d58639a1d0 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -1,5 +1,6 @@ """Test the songpal config flow.""" import copy +import dataclasses from unittest.mock import patch from homeassistant.components import ssdp @@ -26,17 +27,21 @@ from tests.common import MockConfigEntry UDN = "uuid:1234" -SSDP_DATA = { - ssdp.ATTR_UPNP_UDN: UDN, - ssdp.ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, - ssdp.ATTR_SSDP_LOCATION: f"http://{HOST}:52323/dmr.xml", - "X_ScalarWebAPI_DeviceInfo": { - "X_ScalarWebAPI_BaseURL": ENDPOINT, - "X_ScalarWebAPI_ServiceList": { - "X_ScalarWebAPI_ServiceType": ["guide", "system", "audio", "avContent"], +SSDP_DATA = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{HOST}:52323/dmr.xml", + upnp={ + ssdp.ATTR_UPNP_UDN: UDN, + ssdp.ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, + "X_ScalarWebAPI_DeviceInfo": { + "X_ScalarWebAPI_BaseURL": ENDPOINT, + "X_ScalarWebAPI_ServiceList": { + "X_ScalarWebAPI_ServiceType": ["guide", "system", "audio", "avContent"], + }, }, }, -} +) def _flow_next(hass, flow_id): @@ -150,8 +155,9 @@ def _create_mock_config_entry(hass): async def test_ssdp_bravia(hass): """Test discovering a bravia TV.""" - ssdp_data = copy.deepcopy(SSDP_DATA) - ssdp_data["X_ScalarWebAPI_DeviceInfo"]["X_ScalarWebAPI_ServiceList"][ + ssdp_data = dataclasses.replace(SSDP_DATA) + ssdp_data.upnp = copy.deepcopy(ssdp_data.upnp) + ssdp_data.upnp["X_ScalarWebAPI_DeviceInfo"]["X_ScalarWebAPI_ServiceList"][ "X_ScalarWebAPI_ServiceType" ].append("videoScreen") result = await hass.config_entries.flow.async_init( diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 435ed3bdb4b..3907f9c42ec 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -387,11 +387,15 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX99", # MAC address, but SSDP does not have `-` - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.5:5000", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ssdp.ATTR_UPNP_SERIAL: "001132XXXX99", # MAC address, but SSDP does not have `-` + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" @@ -434,11 +438,15 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.5:5000", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reconfigure_successful" @@ -462,11 +470,15 @@ async def test_skip_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.5:5000", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -490,11 +502,15 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.5:5000", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 24628fae60e..151baee6176 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch import aiounifi from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.unifi.config_flow import async_discover_unifi from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -547,12 +548,16 @@ async def test_form_ssdp(hass): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "UniFi Dream Machine", - "modelDescription": "UniFi Dream Machine Pro", - "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", - "serialNumber": "e0:63:da:20:14:a9", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.208.1:41417/rootDesc.xml", + upnp={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "serialNumber": "e0:63:da:20:14:a9", + }, + ), ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -579,12 +584,16 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "UniFi Dream Machine", - "modelDescription": "UniFi Dream Machine Pro", - "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", - "serialNumber": "e0:63:da:20:14:a9", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.208.1:41417/rootDesc.xml", + upnp={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "serialNumber": "e0:63:da:20:14:a9", + }, + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -602,12 +611,16 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "UniFi Dream Machine", - "modelDescription": "UniFi Dream Machine Pro", - "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", - "serialNumber": "e0:63:da:20:14:a9", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.208.1:41417/rootDesc.xml", + upnp={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "serialNumber": "e0:63:da:20:14:a9", + }, + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -625,12 +638,16 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "UniFi Dream Machine New", - "modelDescription": "UniFi Dream Machine Pro", - "ssdp_location": "http://1.2.3.4:41417/rootDesc.xml", - "serialNumber": "e0:63:da:20:14:a9", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:41417/rootDesc.xml", + upnp={ + "friendlyName": "UniFi Dream Machine New", + "modelDescription": "UniFi Dream Machine Pro", + "serialNumber": "e0:63:da:20:14:a9", + }, + ), ) assert result["type"] == "form" assert result["step_id"] == "user" From 2f0ec0d7e5a6177e5296da5645fd1c7f9020251f Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Thu, 25 Nov 2021 14:41:57 +0100 Subject: [PATCH 0865/1452] Add tolo select platform (#60326) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/tolo/select.py | 51 +++++++++++++++++++ .../components/tolo/strings.select.json | 8 +++ .../tolo/translations/select.en.json | 8 +++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tolo/select.py create mode 100644 homeassistant/components/tolo/strings.select.json create mode 100644 homeassistant/components/tolo/translations/select.en.json diff --git a/.coveragerc b/.coveragerc index bee0555d865..18a4ddfbcf8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1094,6 +1094,7 @@ omit = homeassistant/components/tolo/__init__.py homeassistant/components/tolo/climate.py homeassistant/components/tolo/light.py + homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 978bfe64ebb..2daeedaf837 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["climate", "light", "sensor"] +PLATFORMS = ["climate", "light", "select", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/select.py b/homeassistant/components/tolo/select.py new file mode 100644 index 00000000000..1dc1f4f6163 --- /dev/null +++ b/homeassistant/components/tolo/select.py @@ -0,0 +1,51 @@ +"""TOLO Sauna Select controls.""" + +from __future__ import annotations + +from tololib.const import LampMode + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up select entities for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([ToloLampModeSelect(coordinator, entry)]) + + +class ToloLampModeSelect(ToloSaunaCoordinatorEntity, SelectEntity): + """TOLO Sauna lamp mode select.""" + + _attr_device_class = "tolo__lamp_mode" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_icon = "mdi:lightbulb-multiple-outline" + _attr_name = "Lamp Mode" + _attr_options = [lamp_mode.name.lower() for lamp_mode in LampMode] + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize lamp mode select entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_lamp_mode" + + @property + def current_option(self) -> str: + """Return current lamp mode.""" + return self.coordinator.data.settings.lamp_mode.name.lower() + + def select_option(self, option: str) -> None: + """Select lamp mode.""" + self.coordinator.client.set_lamp_mode(LampMode[option.upper()]) diff --git a/homeassistant/components/tolo/strings.select.json b/homeassistant/components/tolo/strings.select.json new file mode 100644 index 00000000000..c65caaf5d2d --- /dev/null +++ b/homeassistant/components/tolo/strings.select.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatic", + "manual": "manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.en.json b/homeassistant/components/tolo/translations/select.en.json new file mode 100644 index 00000000000..ba4b0d20dbc --- /dev/null +++ b/homeassistant/components/tolo/translations/select.en.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatic", + "manual": "manual" + } + } +} \ No newline at end of file From 8b001fd54d0ddc65fab546644dd6f7b2d98bd180 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 15:05:20 +0100 Subject: [PATCH 0866/1452] Adjust/Fix CI coverage reporting (#60329) --- .github/workflows/ci.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f6d31aa19ca..3f47af055ad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -703,7 +703,7 @@ jobs: --test-group-count ${{ needs.changes.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ --cov homeassistant \ - --cov-report= \ + --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ tests @@ -716,19 +716,22 @@ jobs: --timeout=9 \ --durations=10 \ -n auto \ - --dist=loadfile \ --cov homeassistant.components.${{ matrix.group }} \ - --cov-report= \ + --cov-report=xml \ + --cov-report=term-missing \ -o console_output_style=count \ -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage to Codecov (full coverage) if: needs.changes.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v2.1.0 + with: + files: coverage.xml - name: Upload coverage to Codecov (partial coverage) if: needs.changes.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v2.1.0 with: + files: coverage.xml flags: ${{ matrix.group }} - name: Check dirty run: | From 4c0d90ed41962447eb250818ca06e993bc0798cd Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Thu, 25 Nov 2021 15:13:55 +0100 Subject: [PATCH 0867/1452] Remove unknown attribute instead of marking STATE_UNKNOWN (#60325) --- homeassistant/components/statistics/sensor.py | 17 +++++------------ tests/components/statistics/test_sensor.py | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 5dc11b92729..e32ce804cb2 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -177,9 +177,9 @@ class StatisticsSensor(SensorEntity): self.states = deque(maxlen=self._samples_max_buffer_size) self.ages = deque(maxlen=self._samples_max_buffer_size) self.attributes = { - STAT_AGE_COVERAGE_RATIO: STATE_UNKNOWN, - STAT_BUFFER_USAGE_RATIO: STATE_UNKNOWN, - STAT_SOURCE_VALUE_VALID: STATE_UNKNOWN, + STAT_AGE_COVERAGE_RATIO: None, + STAT_BUFFER_USAGE_RATIO: None, + STAT_SOURCE_VALUE_VALID: None, } self._state_characteristic_fn = getattr( self, f"_stat_{self._state_characteristic}" @@ -319,15 +319,8 @@ class StatisticsSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" - extra_attr = {} - if self._samples_max_age is not None: - extra_attr = { - STAT_AGE_COVERAGE_RATIO: self.attributes[STAT_AGE_COVERAGE_RATIO] - } return { - **extra_attr, - STAT_BUFFER_USAGE_RATIO: self.attributes[STAT_BUFFER_USAGE_RATIO], - STAT_SOURCE_VALUE_VALID: self.attributes[STAT_SOURCE_VALUE_VALID], + key: value for key, value in self.attributes.items() if value is not None } @property @@ -449,7 +442,7 @@ class StatisticsSensor(SensorEntity): 2, ) else: - self.attributes[STAT_AGE_COVERAGE_RATIO] = STATE_UNKNOWN + self.attributes[STAT_AGE_COVERAGE_RATIO] = None def _update_value(self): """Front to call the right statistical characteristics functions. diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 91e5f07497b..f1f895b2b1f 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -328,7 +328,7 @@ class TestStatisticsSensor(unittest.TestCase): state = self.hass.states.get("sensor.test") assert state.state == STATE_UNKNOWN assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) - assert state.attributes.get("age_coverage_ratio") == STATE_UNKNOWN + assert state.attributes.get("age_coverage_ratio") is None def test_precision_0(self): """Test correct result with precision=0 as integer.""" From 635d875b1d4338155ea36066d8a899949b524ce5 Mon Sep 17 00:00:00 2001 From: Till Skrodzki Date: Thu, 25 Nov 2021 15:53:34 +0100 Subject: [PATCH 0868/1452] Only accept valid hvac actions sent via mqtt (#59919) * Only accept valid hvac actions sent via mqtt * Only accept valid hvac actions sent via mqtt * Fix existing action test and remove old one * Remote None as valid hvac action being accepted * Change loglevel of wrong hvac action received --- homeassistant/components/mqtt/climate.py | 13 +++++-- tests/components/mqtt/test_climate.py | 43 ++++++++++++++---------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index b4b1875c4df..e1f63252495 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -13,6 +13,7 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_ACTIONS, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, FAN_AUTO, @@ -405,9 +406,15 @@ class MqttClimate(MqttEntity, ClimateEntity): def handle_action_received(msg): """Handle receiving action via MQTT.""" payload = render_template(msg, CONF_ACTION_TEMPLATE) - - self._action = payload - self.async_write_ha_state() + if payload in CURRENT_HVAC_ACTIONS: + self._action = payload + self.async_write_ha_state() + else: + _LOGGER.warning( + "Invalid %s action: %s", + CURRENT_HVAC_ACTIONS, + payload, + ) add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 323d75ae091..61f04db99d9 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -8,6 +8,8 @@ import voluptuous as vol from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( + ATTR_HVAC_ACTION, + CURRENT_HVAC_ACTIONS, DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -432,6 +434,28 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock): assert state.attributes.get("current_temperature") == 47 +async def test_handle_action_received(hass, mqtt_mock): + """Test getting the action received via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["climate"]["action_topic"] = "action" + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + # Cycle through valid modes and also check for wrong input such as "None" (str(None)) + async_fire_mqtt_message(hass, "action", "None") + state = hass.states.get(ENTITY_CLIMATE) + hvac_action = state.attributes.get(ATTR_HVAC_ACTION) + assert hvac_action is None + # Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action + actions = ["off", "heating", "cooling", "drying", "idle", "fan"] + assert all(elem in actions for elem in CURRENT_HVAC_ACTIONS) + for action in actions: + async_fire_mqtt_message(hass, "action", action) + state = hass.states.get(ENTITY_CLIMATE) + hvac_action = state.attributes.get(ATTR_HVAC_ACTION) + assert hvac_action == action + + async def test_set_away_mode_pessimistic(hass, mqtt_mock): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -492,21 +516,6 @@ async def test_set_away_mode(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "away" -async def test_set_hvac_action(hass, mqtt_mock): - """Test setting of the HVAC action.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config["climate"]["action_topic"] = "action" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hvac_action") is None - - async_fire_mqtt_message(hass, "action", "cool") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hvac_action") == "cool" - - async def test_set_hold_pessimistic(hass, mqtt_mock): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -779,9 +788,9 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): assert state.attributes.get("current_temperature") == 74656 # Action - async_fire_mqtt_message(hass, "action", '"cool"') + async_fire_mqtt_message(hass, "action", '"cooling"') state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hvac_action") == "cool" + assert state.attributes.get("hvac_action") == "cooling" async def test_set_with_templates(hass, mqtt_mock, caplog): From 57fd632cd98d27328cb17f7aa93c67338233d573 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Thu, 25 Nov 2021 15:54:08 +0100 Subject: [PATCH 0869/1452] Add missing MQTT lock.open (#60022) * Add missing MQTT lock.open from: https://community.home-assistant.io/t/mqtt-lock-open/232823/13 based on https://github.com/home-assistant/core/pull/48008 * Update homeassistant/components/mqtt/lock.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/lock.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/lock.py Co-authored-by: Erik Montnemery * removed `STATE_OPEN` from tests * Apply suggestions from code review * Format code * Update lock.py * Update test_lock.py * Update test_lock.py Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/lock.py | 27 +++++- tests/components/mqtt/test_lock.py | 125 +++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 5410af5d38c..b43a00166ae 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -4,7 +4,7 @@ import functools import voluptuous as vol from homeassistant.components import lock -from homeassistant.components.lock import LockEntity +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -19,6 +19,7 @@ from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_hel CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" +CONF_PAYLOAD_OPEN = "payload_open" CONF_STATE_LOCKED = "state_locked" CONF_STATE_UNLOCKED = "state_unlocked" @@ -27,6 +28,7 @@ DEFAULT_NAME = "MQTT Lock" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" +DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_STATE_LOCKED = "LOCKED" DEFAULT_STATE_UNLOCKED = "UNLOCKED" @@ -43,6 +45,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, + vol.Optional(CONF_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -145,6 +148,11 @@ class MqttLock(MqttEntity, LockEntity): """Return true if we do optimistic updates.""" return self._optimistic + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN if CONF_PAYLOAD_OPEN in self._config else 0 + async def async_lock(self, **kwargs): """Lock the device. @@ -178,3 +186,20 @@ class MqttLock(MqttEntity, LockEntity): # Optimistically assume that the lock has changed state. self._state = False self.async_write_ha_state() + + async def async_open(self, **kwargs): + """Open the door latch. + + This method is a coroutine. + """ + await mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + self._config[CONF_PAYLOAD_OPEN], + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + # Optimistically assume that the lock unlocks when opened. + self._state = False + self.async_write_ha_state() diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 97f524d5d82..8d76e46f32b 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -6,12 +6,18 @@ import pytest from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, + SERVICE_OPEN, SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, + SUPPORT_OPEN, ) from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, +) from homeassistant.setup import async_setup_component from .test_common import ( @@ -69,6 +75,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert not state.attributes.get(ATTR_SUPPORTED_FEATURES) async_fire_mqtt_message(hass, "state-topic", "LOCKED") @@ -278,6 +285,122 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock): + """Test open function of the lock without state topic.""" + assert await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( + hass, mqtt_mock +): + """Test open function of the lock without state topic.""" + assert await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "optimistic": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( From 6b9c2d829541b42c68ef0f0bdb907f0a91600b04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 16:03:53 +0100 Subject: [PATCH 0870/1452] Add shorthand attribute support to Camera platform (#59837) --- homeassistant/components/amcrest/camera.py | 2 +- homeassistant/components/camera/__init__.py | 43 ++++++++++++++------ homeassistant/components/demo/camera.py | 38 +++++------------ homeassistant/components/hyperion/camera.py | 4 +- homeassistant/components/motioneye/camera.py | 2 +- homeassistant/components/nest/camera_sdm.py | 2 +- homeassistant/components/netatmo/camera.py | 6 +-- homeassistant/components/uvc/camera.py | 2 +- tests/components/motioneye/test_camera.py | 4 +- 9 files changed, 52 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 8333ece1030..e250b8ef59b 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -412,7 +412,7 @@ class AmcrestCam(Camera): f"{serial_number}-{self._resolution}-{self._channel}" ) _LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id) - self.is_streaming = self._get_video() + self._attr_is_streaming = self._get_video() self._is_recording = self._get_recording() self._motion_detection_enabled = self._get_motion_detection() self._audio_enabled = self._get_audio() diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 8534120a77f..fd05d3c2095 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -369,9 +369,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class Camera(Entity): """The base class for camera entities.""" + # Entity Properties + _attr_brand: str | None = None + _attr_frame_interval: float = MIN_STREAM_INTERVAL + _attr_frontend_stream_type: str | None + _attr_is_on: bool = True + _attr_is_recording: bool = False + _attr_is_streaming: bool = False + _attr_model: str | None = None + _attr_motion_detection_enabled: bool = False + _attr_should_poll: bool = False # No need to poll cameras + _attr_state: None = None # State is determined by is_on + _attr_supported_features: int = 0 + def __init__(self) -> None: """Initialize a camera.""" - self.is_streaming: bool = False self.stream: Stream | None = None self.stream_options: dict[str, str] = {} self.content_type: str = DEFAULT_CONTENT_TYPE @@ -379,45 +391,47 @@ class Camera(Entity): self._warned_old_signature = False self.async_update_token() - @property - def should_poll(self) -> bool: - """No need to poll cameras.""" - return False - @property def entity_picture(self) -> str: """Return a link to the camera feed as entity picture.""" + if self._attr_entity_picture is not None: + return self._attr_entity_picture return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) @property def supported_features(self) -> int: """Flag supported features.""" - return 0 + return self._attr_supported_features @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return False + return self._attr_is_recording + + @property + def is_streaming(self) -> bool: + """Return true if the device is streaming.""" + return self._attr_is_streaming @property def brand(self) -> str | None: """Return the camera brand.""" - return None + return self._attr_brand @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return False + return self._attr_motion_detection_enabled @property def model(self) -> str | None: """Return the camera model.""" - return None + return self._attr_model @property def frame_interval(self) -> float: """Return the interval between frames of the mjpeg stream.""" - return MIN_STREAM_INTERVAL + return self._attr_frame_interval @property def frontend_stream_type(self) -> str | None: @@ -427,6 +441,8 @@ class Camera(Entity): frontend which camera attributes and player to use. The default type is to use HLS, and components can override to change the type. """ + if hasattr(self, "_attr_frontend_stream_type"): + return self._attr_frontend_stream_type if not self.supported_features & SUPPORT_STREAM: return None return STREAM_TYPE_HLS @@ -508,6 +524,7 @@ class Camera(Entity): return await self.handle_async_still_stream(request, self.frame_interval) @property + @final def state(self) -> str: """Return the camera state.""" if self.is_recording: @@ -519,7 +536,7 @@ class Camera(Entity): @property def is_on(self) -> bool: """Return true if on.""" - return True + return self._attr_is_on def turn_off(self) -> None: """Turn off camera.""" diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 572a5bf331e..5131741617e 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -24,13 +24,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoCamera(Camera): """The representation of a Demo camera.""" + _attr_is_streaming = True + _attr_motion_detection_enabled = False + _attr_supported_features = SUPPORT_ON_OFF + def __init__(self, name, content_type): """Initialize demo camera component.""" super().__init__() - self._name = name + self._attr_name = name self.content_type = content_type - self._motion_status = False - self.is_streaming = True self._images_index = 0 async def async_camera_image( @@ -43,42 +45,24 @@ class DemoCamera(Camera): return await self.hass.async_add_executor_job(image_path.read_bytes) - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def supported_features(self): - """Camera support turn on/off features.""" - return SUPPORT_ON_OFF - - @property - def is_on(self): - """Whether camera is on (streaming).""" - return self.is_streaming - - @property - def motion_detection_enabled(self): - """Camera Motion Detection Status.""" - return self._motion_status - async def async_enable_motion_detection(self): """Enable the Motion detection in base station (Arm).""" - self._motion_status = True + self._attr_motion_detection_enabled = True self.async_write_ha_state() async def async_disable_motion_detection(self): """Disable the motion detection in base station (Disarm).""" - self._motion_status = False + self._attr_motion_detection_enabled = False self.async_write_ha_state() async def async_turn_off(self): """Turn off camera.""" - self.is_streaming = False + self._attr_is_streaming = False + self._attr_is_on = False self.async_write_ha_state() async def async_turn_on(self): """Turn on camera.""" - self.is_streaming = True + self._attr_is_streaming = True + self._attr_is_on = True self.async_write_ha_state() diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index c1763c3c21c..4b6492559ea 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -186,7 +186,7 @@ class HyperionCamera(Camera): return False self._image_stream_clients += 1 - self.is_streaming = True + self._attr_is_streaming = True self.async_write_ha_state() return True @@ -196,7 +196,7 @@ class HyperionCamera(Camera): if not self._image_stream_clients: await self._client.async_send_image_stream_stop() - self.is_streaming = False + self._attr_is_streaming = False self.async_write_ha_state() @asynccontextmanager diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index dac5dd7093f..7d9be209c4e 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -159,7 +159,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) # motionEye cameras are always streaming or unavailable. - self.is_streaming = True + self._attr_is_streaming = True MotionEyeEntity.__init__( self, diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 71798eb40c3..5385eb42b26 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -79,7 +79,7 @@ class NestCamera(Camera): self._event_id: str | None = None self._event_image_bytes: bytes | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None - self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits self._placeholder_image: bytes | None = None @property diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 6cb7457f8f6..380571aba70 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -172,13 +172,13 @@ class NetatmoCamera(NetatmoBase, Camera): if data["home_id"] == self._home_id and data["camera_id"] == self._id: if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): - self.is_streaming = False + self._attr_is_streaming = False self._status = "off" elif data[WEBHOOK_PUSH_TYPE] in ( "NACamera-on", WEBHOOK_NACAMERA_CONNECTION, ): - self.is_streaming = True + self._attr_is_streaming = True self._status = "on" elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] @@ -273,7 +273,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._sd_status = camera.get("sd_status") self._alim_status = camera.get("alim_status") self._is_local = camera.get("is_local") - self.is_streaming = bool(self._status == "on") + self._attr_is_streaming = bool(self._status == "on") if self._model == "NACamera": # Smart Indoor Camera self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 77ff6a30f95..fa914de1d6a 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -86,7 +86,7 @@ class UnifiVideoCamera(Camera): self._uuid = uuid self._name = name self._password = password - self.is_streaming = False + self._attr_is_streaming = False self._connect_addr = None self._camera = None self._motion_status = False diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index c1144256ae2..15462f6c592 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -75,7 +75,7 @@ async def test_setup_camera(hass: HomeAssistant) -> None: entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) assert entity_state - assert entity_state.state == "idle" + assert entity_state.state == "streaming" assert entity_state.attributes.get("friendly_name") == TEST_CAMERA_NAME @@ -192,7 +192,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N await setup_mock_motioneye_config_entry(hass, client=client) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) assert entity_state - assert entity_state.state == "idle" + assert entity_state.state == "streaming" cameras = copy.deepcopy(TEST_CAMERAS) cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False From 3399c90936b38a09935fe456dffab220b703faa1 Mon Sep 17 00:00:00 2001 From: Redah <54580455+redahb@users.noreply.github.com> Date: Thu, 25 Nov 2021 16:40:26 +0100 Subject: [PATCH 0871/1452] Add Notifications for Android TV icon support (#60159) * Add icon support * Sort imports * Sort imports correctly * Satisfy pylint Co-authored-by: Erik Montnemery --- homeassistant/components/nfandroidtv/const.py | 24 +++++--- .../components/nfandroidtv/notify.py | 56 +++++++++++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/nfandroidtv/const.py b/homeassistant/components/nfandroidtv/const.py index 332c1754771..12449a9b046 100644 --- a/homeassistant/components/nfandroidtv/const.py +++ b/homeassistant/components/nfandroidtv/const.py @@ -17,12 +17,20 @@ ATTR_TRANSPARENCY = "transparency" ATTR_COLOR = "color" ATTR_BKGCOLOR = "bkgcolor" ATTR_INTERRUPT = "interrupt" -ATTR_FILE = "file" -# Attributes contained in file -ATTR_FILE_URL = "url" -ATTR_FILE_PATH = "path" -ATTR_FILE_USERNAME = "username" -ATTR_FILE_PASSWORD = "password" -ATTR_FILE_AUTH = "auth" +ATTR_IMAGE = "image" +# Attributes contained in image +ATTR_IMAGE_URL = "url" +ATTR_IMAGE_PATH = "path" +ATTR_IMAGE_USERNAME = "username" +ATTR_IMAGE_PASSWORD = "password" +ATTR_IMAGE_AUTH = "auth" +ATTR_ICON = "icon" +# Attributes contained in icon +ATTR_ICON_URL = "url" +ATTR_ICON_PATH = "path" +ATTR_ICON_USERNAME = "username" +ATTR_ICON_PASSWORD = "password" +ATTR_ICON_AUTH = "auth" # Any other value or absence of 'auth' lead to basic authentication being used -ATTR_FILE_AUTH_DIGEST = "digest" +ATTR_IMAGE_AUTH_DIGEST = "digest" +ATTR_ICON_AUTH_DIGEST = "digest" diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index c769770ae43..479bbc40891 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import ATTR_ICON, CONF_HOST, CONF_TIMEOUT +from homeassistant.const import CONF_HOST, CONF_TIMEOUT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -24,14 +24,21 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_COLOR, ATTR_DURATION, - ATTR_FILE, - ATTR_FILE_AUTH, - ATTR_FILE_AUTH_DIGEST, - ATTR_FILE_PASSWORD, - ATTR_FILE_PATH, - ATTR_FILE_URL, - ATTR_FILE_USERNAME, ATTR_FONTSIZE, + ATTR_ICON, + ATTR_ICON_AUTH, + ATTR_ICON_AUTH_DIGEST, + ATTR_ICON_PASSWORD, + ATTR_ICON_PATH, + ATTR_ICON_URL, + ATTR_ICON_USERNAME, + ATTR_IMAGE, + ATTR_IMAGE_AUTH, + ATTR_IMAGE_AUTH_DIGEST, + ATTR_IMAGE_PASSWORD, + ATTR_IMAGE_PATH, + ATTR_IMAGE_URL, + ATTR_IMAGE_USERNAME, ATTR_INTERRUPT, ATTR_POSITION, ATTR_TRANSPARENCY, @@ -158,22 +165,23 @@ class NFAndroidTVNotificationService(BaseNotificationService): _LOGGER.warning( "Invalid interrupt-value: %s", str(data.get(ATTR_INTERRUPT)) ) - filedata = data.get(ATTR_FILE) if data else None - if filedata is not None: - if ATTR_ICON in filedata: - icon = self.load_file( - url=filedata[ATTR_ICON], - local_path=filedata.get(ATTR_FILE_PATH), - username=filedata.get(ATTR_FILE_USERNAME), - password=filedata.get(ATTR_FILE_PASSWORD), - auth=filedata.get(ATTR_FILE_AUTH), - ) + imagedata = data.get(ATTR_IMAGE) if data else None + if imagedata is not None: image_file = self.load_file( - url=filedata.get(ATTR_FILE_URL), - local_path=filedata.get(ATTR_FILE_PATH), - username=filedata.get(ATTR_FILE_USERNAME), - password=filedata.get(ATTR_FILE_PASSWORD), - auth=filedata.get(ATTR_FILE_AUTH), + url=imagedata.get(ATTR_IMAGE_URL), + local_path=imagedata.get(ATTR_IMAGE_PATH), + username=imagedata.get(ATTR_IMAGE_USERNAME), + password=imagedata.get(ATTR_IMAGE_PASSWORD), + auth=imagedata.get(ATTR_IMAGE_AUTH), + ) + icondata = data.get(ATTR_ICON) if data else None + if icondata is not None: + icon = self.load_file( + url=icondata.get(ATTR_ICON_URL), + local_path=icondata.get(ATTR_ICON_PATH), + username=icondata.get(ATTR_ICON_USERNAME), + password=icondata.get(ATTR_ICON_PASSWORD), + auth=icondata.get(ATTR_ICON_AUTH), ) self.notify.send( message, @@ -203,7 +211,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): if username is not None and password is not None: # Use digest or basic authentication auth_: HTTPDigestAuth | HTTPBasicAuth - if ATTR_FILE_AUTH_DIGEST == auth: + if auth in (ATTR_IMAGE_AUTH_DIGEST, ATTR_ICON_AUTH_DIGEST): auth_ = HTTPDigestAuth(username, password) else: auth_ = HTTPBasicAuth(username, password) From 24687243785accb741af12d6e7a9bdd1041016a4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 16:45:35 +0100 Subject: [PATCH 0872/1452] Use SsdpServiceInfo for ssdp tests (part 2) (#60322) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 2 +- tests/components/axis/test_config_flow.py | 112 +++++++++++------- tests/components/nanoleaf/test_config_flow.py | 16 ++- .../components/octoprint/test_config_flow.py | 30 +++-- tests/components/yeelight/test_config_flow.py | 14 ++- 5 files changed, 106 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 437712d555a..72d450fa569 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -101,7 +101,7 @@ class _SsdpServiceDescription: ssdp_usn: str ssdp_st: str - ssdp_location: str + ssdp_location: str | None = None ssdp_nt: str | None = None ssdp_udn: str | None = None ssdp_ext: str | None = None diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 2261a462113..6263c62be42 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -5,7 +5,7 @@ import pytest import respx from homeassistant import data_entry_flow -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( CONF_EVENTS, @@ -263,32 +263,36 @@ async def test_reauth_flow_update_configuration(hass): ), ( SOURCE_SSDP, - { - "st": "urn:axis-com:service:BasicService:1", - "usn": f"uuid:Upnp-BasicDevice-1_0-{MAC}::urn:axis-com:service:BasicService:1", - "ext": "", - "server": "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP devices/1.8.7", - "deviceType": "urn:schemas-upnp-org:device:Basic:1", - "friendlyName": f"AXIS M1065-LW - {MAC}", - "manufacturer": "AXIS", - "manufacturerURL": "http://www.axis.com/", - "modelDescription": "AXIS M1065-LW Network Camera", - "modelName": "AXIS M1065-LW", - "modelNumber": "M1065-LW", - "modelURL": "http://www.axis.com/", - "serialNumber": MAC, - "UDN": f"uuid:Upnp-BasicDevice-1_0-{MAC}", - "serviceList": { - "service": { - "serviceType": "urn:axis-com:service:BasicService:1", - "serviceId": "urn:axis-com:serviceId:BasicServiceId", - "controlURL": "/upnp/control/BasicServiceId", - "eventSubURL": "/upnp/event/BasicServiceId", - "SCPDURL": "/scpd_basic.xml", - } + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "st": "urn:axis-com:service:BasicService:1", + "usn": f"uuid:Upnp-BasicDevice-1_0-{MAC}::urn:axis-com:service:BasicService:1", + "ext": "", + "server": "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP devices/1.8.7", + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "friendlyName": f"AXIS M1065-LW - {MAC}", + "manufacturer": "AXIS", + "manufacturerURL": "http://www.axis.com/", + "modelDescription": "AXIS M1065-LW Network Camera", + "modelName": "AXIS M1065-LW", + "modelNumber": "M1065-LW", + "modelURL": "http://www.axis.com/", + "serialNumber": MAC, + "UDN": f"uuid:Upnp-BasicDevice-1_0-{MAC}", + "serviceList": { + "service": { + "serviceType": "urn:axis-com:service:BasicService:1", + "serviceId": "urn:axis-com:serviceId:BasicServiceId", + "controlURL": "/upnp/control/BasicServiceId", + "eventSubURL": "/upnp/event/BasicServiceId", + "SCPDURL": "/scpd_basic.xml", + } + }, + "presentationURL": f"http://{DEFAULT_HOST}:80/", }, - "presentationURL": f"http://{DEFAULT_HOST}:80/", - }, + ), ), ( SOURCE_ZEROCONF, @@ -354,11 +358,15 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ), ( SOURCE_SSDP, - { - "friendlyName": f"AXIS M1065-LW - {MAC}", - "serialNumber": MAC, - "presentationURL": f"http://{DEFAULT_HOST}:80/", - }, + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": f"http://{DEFAULT_HOST}:80/", + }, + ), ), ( SOURCE_ZEROCONF, @@ -403,11 +411,15 @@ async def test_discovered_device_already_configured( ), ( SOURCE_SSDP, - { - "friendlyName": f"AXIS M1065-LW - {MAC}", - "serialNumber": MAC, - "presentationURL": "http://2.3.4.5:8080/", - }, + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": "http://2.3.4.5:8080/", + }, + ), 8080, ), ( @@ -474,11 +486,15 @@ async def test_discovery_flow_updated_configuration( ), ( SOURCE_SSDP, - { - "friendlyName": "", - "serialNumber": "01234567890", - "presentationURL": "", - }, + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "friendlyName": "", + "serialNumber": "01234567890", + "presentationURL": "", + }, + ), ), ( SOURCE_ZEROCONF, @@ -518,11 +534,15 @@ async def test_discovery_flow_ignore_non_axis_device( ), ( SOURCE_SSDP, - { - "friendlyName": f"AXIS M1065-LW - {MAC}", - "serialNumber": MAC, - "presentationURL": "http://169.254.3.4:80/", - }, + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": "http://169.254.3.4:80/", + }, + ), ), ( SOURCE_ZEROCONF, diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 9faf48b625c..3b2cd3777cb 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -7,7 +7,7 @@ from aionanoleaf import InvalidToken, NanoleafException, Unauthorized, Unavailab import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant @@ -462,11 +462,15 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "_host": TEST_HOST, - "nl-devicename": TEST_NAME, - "nl-deviceid": TEST_DEVICE_ID, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "_host": TEST_HOST, + "nl-devicename": TEST_NAME, + "nl-deviceid": TEST_DEVICE_ID, + }, + ), ) assert result["type"] == "form" diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index b55d43fda9e..57e89955d58 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyoctoprintapi import ApiError, DiscoverySettings from homeassistant import config_entries, data_entry_flow -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.components.octoprint.const import DOMAIN from homeassistant.core import HomeAssistant @@ -235,11 +235,15 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "presentationURL": "http://192.168.1.123:80/discovery/device.xml", - "port": 80, - "UDN": "uuid:83747482", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "presentationURL": "http://192.168.1.123:80/discovery/device.xml", + "port": 80, + "UDN": "uuid:83747482", + }, + ), ) assert result["type"] == "form" assert not result["errors"] @@ -512,11 +516,15 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "presentationURL": "http://192.168.1.123:80/discovery/device.xml", - "port": 80, - "UDN": "uuid:83747482", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + "presentationURL": "http://192.168.1.123:80/discovery/device.xml", + "port": 80, + "UDN": "uuid:83747482", + }, + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8628e9620e9..b3596c4442a 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.yeelight.config_flow import MODEL_UNKNOWN, CannotConnect from homeassistant.components.yeelight.const import ( CONF_DETECTED_MODEL, @@ -52,6 +52,12 @@ DEFAULT_CONFIG = { CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, } +SSDP_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp=CAPABILITIES, +) + async def test_discovery(hass: HomeAssistant): """Test setting up discovery.""" @@ -627,7 +633,7 @@ async def test_discovered_ssdp(hass): 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 + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=SSDP_INFO ) await hass.async_block_till_done() @@ -656,7 +662,7 @@ async def test_discovered_ssdp(hass): 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 + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=SSDP_INFO ) await hass.async_block_till_done() @@ -719,7 +725,7 @@ async def test_discovered_zeroconf(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=CAPABILITIES, + data=SSDP_INFO, ) await hass.async_block_till_done() From f72e9aea1cd54254e6918c765753c1194d13c519 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 17:35:15 +0100 Subject: [PATCH 0873/1452] CI: Only carry forward full-suite test coverage (#60344) --- .github/workflows/ci.yaml | 1 + codecov.yml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3f47af055ad..551c9861bd9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -727,6 +727,7 @@ jobs: uses: codecov/codecov-action@v2.1.0 with: files: coverage.xml + flags: full-suite - name: Upload coverage to Codecov (partial coverage) if: needs.changes.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v2.1.0 diff --git a/codecov.yml b/codecov.yml index 5c0750e659f..8dee6ee3686 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,4 +12,9 @@ comment: false # we need to carry forward. flag_management: default_rules: - carryforward: true + carryforward: false + individual_flags: + - name: full-suite + paths: + - ".*" + carryforward: true From 67684d68ff7c370e87399d91ea4efd1304a1f1ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 11:39:37 -0600 Subject: [PATCH 0874/1452] Fix slow yeelight discovery test (#60346) --- tests/components/yeelight/test_config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index b3596c4442a..b101fd3413d 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -126,7 +126,9 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant): alternate_bulb.capabilities["id"] = "0x000000000099999" alternate_bulb.capabilities["location"] = "yeelight://4.4.4.4" - with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=alternate_bulb): + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=alternate_bulb + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.async_block_till_done() @@ -138,7 +140,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant): assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(), _patch_discovery_interval(): + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() await hass.async_block_till_done() From 624d866239a8e70fd621a08859cc26ea23263268 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:41:49 +0100 Subject: [PATCH 0875/1452] SSDP attributes can be present but empty (#60340) Co-authored-by: epenet --- homeassistant/components/hue/config_flow.py | 2 +- homeassistant/components/upnp/config_flow.py | 6 +++--- homeassistant/components/wilight/config_flow.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index fd743213d7f..b1bed30478c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -207,7 +207,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_hue_bridge") if ( - ssdp.ATTR_SSDP_LOCATION not in discovery_info + not discovery_info.get(ssdp.ATTR_SSDP_LOCATION) or ssdp.ATTR_UPNP_SERIAL not in discovery_info ): return self.async_abort(reason="not_hue_bridge") diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 80c126edbec..58dee0c7021 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -41,9 +41,9 @@ def _is_complete_discovery(discovery_info: Mapping[str, Any]) -> bool: """Test if discovery is complete and usable.""" return ( ssdp.ATTR_UPNP_UDN in discovery_info - and ssdp.ATTR_SSDP_ST in discovery_info - and ssdp.ATTR_SSDP_LOCATION in discovery_info - and ssdp.ATTR_SSDP_USN in discovery_info + and discovery_info.get(ssdp.ATTR_SSDP_ST) + and discovery_info.get(ssdp.ATTR_SSDP_LOCATION) + and discovery_info.get(ssdp.ATTR_SSDP_USN) ) diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 4bfc331a543..a5e14ebfc6b 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -53,7 +53,7 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a discovered WiLight.""" # Filter out basic information if ( - ssdp.ATTR_SSDP_LOCATION not in discovery_info + not discovery_info.get(ssdp.ATTR_SSDP_LOCATION) or ssdp.ATTR_UPNP_MANUFACTURER not in discovery_info or ssdp.ATTR_UPNP_SERIAL not in discovery_info or ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info From f292691b7bc54f04eb2dc0b1b0e87300fdd501fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:46:20 +0100 Subject: [PATCH 0876/1452] Use SsdpServiceInfo for ssdp tests (part 3) (#60334) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 18 +++ tests/components/deconz/test_config_flow.py | 69 +++++---- tests/components/directv/__init__.py | 9 +- tests/components/directv/test_config_flow.py | 19 +-- tests/components/dlna_dmr/test_config_flow.py | 139 ++++++++++-------- tests/components/fritz/test_config_flow.py | 27 ++-- tests/components/fritzbox/test_config_flow.py | 32 ++-- .../components/huawei_lte/test_config_flow.py | 29 ++-- tests/components/hyperion/test_config_flow.py | 82 ++++++----- tests/components/keenetic_ndms2/__init__.py | 14 +- .../keenetic_ndms2/test_config_flow.py | 27 ++-- tests/components/netgear/test_config_flow.py | 32 ++-- .../components/samsungtv/test_config_flow.py | 57 ++++--- tests/components/syncthru/test_config_flow.py | 20 ++- .../yamaha_musiccast/test_config_flow.py | 42 ++++-- tests/components/zha/test_config_flow.py | 22 +-- 16 files changed, 374 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 72d450fa569..298757eeccd 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -146,6 +146,24 @@ class SsdpServiceInfo( return getattr(self, name) return self.upnp.get(name) + def get(self, name: str, default: Any = None) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"ssdp"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + if hasattr(self, name): + return getattr(self, name) + return self.upnp.get(name, default) + @bind_hass async def async_register_callback( diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4380b8c6021..11b0a3bdb31 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pydeconz +from homeassistant.components import ssdp from homeassistant.components.deconz.config_flow import ( CONF_MANUAL_INPUT, CONF_SERIAL, @@ -17,11 +18,7 @@ from homeassistant.components.deconz.const import ( CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER_URL, - ATTR_UPNP_SERIAL, -) +from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( SOURCE_HASSIO, SOURCE_REAUTH, @@ -412,11 +409,15 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): """Test that config flow for one discovered bridge works.""" result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ATTR_UPNP_SERIAL: BRIDGEID, + }, + ), context={"source": SOURCE_SSDP}, ) @@ -446,7 +447,11 @@ async def test_flow_ssdp_bad_discovery(hass, aioclient_mock): """Test that SSDP discovery aborts if manufacturer URL is wrong.""" result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ATTR_UPNP_MANUFACTURER_URL: "other"}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ATTR_UPNP_MANUFACTURER_URL: "other"}, + ), context={"source": SOURCE_SSDP}, ) @@ -464,11 +469,15 @@ async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://2.3.4.5:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ATTR_UPNP_SERIAL: BRIDGEID, + }, + ), context={"source": SOURCE_SSDP}, ) await hass.async_block_till_done() @@ -485,11 +494,15 @@ async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ATTR_UPNP_SERIAL: BRIDGEID, + }, + ), context={"source": SOURCE_SSDP}, ) @@ -508,11 +521,15 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ATTR_UPNP_SERIAL: BRIDGEID, + }, + ), context={"source": SOURCE_SSDP}, ) diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 790121ddca1..584a7c95509 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,8 +1,8 @@ """Tests for the DirecTV component.""" from http import HTTPStatus +from homeassistant.components import ssdp from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN -from homeassistant.components.ssdp import ATTR_SSDP_LOCATION from homeassistant.const import CONF_HOST, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -15,7 +15,12 @@ SSDP_LOCATION = "http://127.0.0.1/" UPNP_SERIAL = "RID-028877455858" MOCK_CONFIG = {DOMAIN: [{CONF_HOST: HOST}]} -MOCK_SSDP_DISCOVERY_INFO = {ATTR_SSDP_LOCATION: SSDP_LOCATION} +MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={}, +) MOCK_USER_INPUT = {CONF_HOST: HOST} diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 646b863a114..f80e0d781ef 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DirecTV config flow.""" +import dataclasses from unittest.mock import patch from aiohttp import ClientError as HTTPClientError @@ -43,7 +44,7 @@ async def test_show_ssdp_form( """Test that the ssdp confirmation form is served.""" mock_connection(aioclient_mock) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -77,7 +78,7 @@ async def test_ssdp_cannot_connect( """Test we abort SSDP flow on connection error.""" aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, @@ -94,7 +95,7 @@ async def test_ssdp_confirm_cannot_connect( """Test we abort SSDP flow on connection error.""" aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP, CONF_HOST: HOST, CONF_NAME: HOST}, @@ -128,7 +129,7 @@ async def test_ssdp_device_exists_abort( """Test we abort SSDP flow if DirecTV receiver already configured.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, @@ -145,8 +146,8 @@ async def test_ssdp_with_receiver_id_device_exists_abort( """Test we abort SSDP flow if DirecTV receiver already configured.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() - discovery_info[ATTR_UPNP_SERIAL] = UPNP_SERIAL + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + discovery_info.upnp[ATTR_UPNP_SERIAL] = UPNP_SERIAL result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, @@ -180,7 +181,7 @@ async def test_ssdp_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) with patch( "homeassistant.components.directv.config_flow.DIRECTV.update", side_effect=Exception, @@ -199,7 +200,7 @@ async def test_ssdp_confirm_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) with patch( "homeassistant.components.directv.config_flow.DIRECTV.update", side_effect=Exception, @@ -249,7 +250,7 @@ async def test_full_ssdp_flow_implementation( """Test the full SSDP flow from start to finish.""" mock_connection(aioclient_mock) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 6ff718290b2..c497d45a4f2 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -1,6 +1,7 @@ """Test the DLNA config flow.""" from __future__ import annotations +import dataclasses from unittest.mock import Mock from async_upnp_client import UpnpDevice, UpnpError @@ -23,7 +24,6 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import DiscoveryInfoType from .conftest import ( MOCK_DEVICE_LOCATION, @@ -52,40 +52,43 @@ MOCK_CONFIG_IMPORT_DATA = { MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE" -MOCK_DISCOVERY: DiscoveryInfoType = { - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, - ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, - ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, - ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, - ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, - ssdp.ATTR_UPNP_SERVICE_LIST: { - "service": [ - { - "SCPDURL": "/AVTransport/scpd.xml", - "controlURL": "/AVTransport/control.xml", - "eventSubURL": "/AVTransport/event.xml", - "serviceId": "urn:upnp-org:serviceId:AVTransport", - "serviceType": "urn:schemas-upnp-org:service:AVTransport:1", - }, - { - "SCPDURL": "/ConnectionManager/scpd.xml", - "controlURL": "/ConnectionManager/control.xml", - "eventSubURL": "/ConnectionManager/event.xml", - "serviceId": "urn:upnp-org:serviceId:ConnectionManager", - "serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1", - }, - { - "SCPDURL": "/RenderingControl/scpd.xml", - "controlURL": "/RenderingControl/control.xml", - "eventSubURL": "/RenderingControl/event.xml", - "serviceId": "urn:upnp-org:serviceId:RenderingControl", - "serviceType": "urn:schemas-upnp-org:service:RenderingControl:1", - }, - ] +MOCK_DISCOVERY = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={ + ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + ssdp.ATTR_UPNP_SERVICE_LIST: { + "service": [ + { + "SCPDURL": "/AVTransport/scpd.xml", + "controlURL": "/AVTransport/control.xml", + "eventSubURL": "/AVTransport/event.xml", + "serviceId": "urn:upnp-org:serviceId:AVTransport", + "serviceType": "urn:schemas-upnp-org:service:AVTransport:1", + }, + { + "SCPDURL": "/ConnectionManager/scpd.xml", + "controlURL": "/ConnectionManager/control.xml", + "eventSubURL": "/ConnectionManager/event.xml", + "serviceId": "urn:upnp-org:serviceId:ConnectionManager", + "serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1", + }, + { + "SCPDURL": "/RenderingControl/scpd.xml", + "controlURL": "/RenderingControl/control.xml", + "eventSubURL": "/RenderingControl/event.xml", + "serviceId": "urn:upnp-org:serviceId:RenderingControl", + "serviceType": "urn:schemas-upnp-org:service:RenderingControl:1", + }, + ] + }, }, - ssdp.ATTR_HA_MATCHING_DOMAINS: {DLNA_DOMAIN}, -} + x_homeassistant_matching_domains=(DLNA_DOMAIN,), +) async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: @@ -545,13 +548,17 @@ async def test_ssdp_flow_existing( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, - ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, - ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, - ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + upnp={ + ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -581,14 +588,17 @@ async def test_ssdp_flow_upnp_udn( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, - ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, - ssdp.ATTR_UPNP_UDN: "DIFFERENT_ROOT_DEVICE", - ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, - ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={ + ssdp.ATTR_UPNP_UDN: "DIFFERENT_ROOT_DEVICE", + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -598,8 +608,9 @@ async def test_ssdp_flow_upnp_udn( async def test_ssdp_missing_services(hass: HomeAssistant) -> None: """Test SSDP ignores devices that are missing required services.""" # No services defined at all - discovery = dict(MOCK_DISCOVERY) - del discovery[ssdp.ATTR_UPNP_SERVICE_LIST] + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = discovery.upnp.copy() + del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -609,11 +620,12 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: assert result["reason"] == "not_dmr" # AVTransport service is missing - discovery = dict(MOCK_DISCOVERY) - discovery[ssdp.ATTR_UPNP_SERVICE_LIST] = { + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = discovery.upnp.copy() + discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = { "service": [ service - for service in discovery[ssdp.ATTR_UPNP_SERVICE_LIST]["service"] + for service in discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST]["service"] if service.get("serviceId") != "urn:upnp-org:serviceId:AVTransport" ] } @@ -626,8 +638,9 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: """Test SSDP discovery ignores certain devices.""" - discovery = dict(MOCK_DISCOVERY) - discovery[ssdp.ATTR_HA_MATCHING_DOMAINS] = {DLNA_DOMAIN, "other_domain"} + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.x_homeassistant_matching_domains = {DLNA_DOMAIN, "other_domain"} + assert discovery.x_homeassistant_matching_domains result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -636,8 +649,11 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "alternative_integration" - discovery = dict(MOCK_DISCOVERY) - discovery[ssdp.ATTR_UPNP_DEVICE_TYPE] = "urn:schemas-upnp-org:device:ZonePlayer:1" + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = discovery.upnp.copy() + discovery.upnp[ + ssdp.ATTR_UPNP_DEVICE_TYPE + ] = "urn:schemas-upnp-org:device:ZonePlayer:1" result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -652,9 +668,10 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: ("LG Electronics.", "LG TV"), ("Royal Philips Electronics", "Philips TV DMR"), ]: - discovery = dict(MOCK_DISCOVERY) - discovery[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer - discovery[ssdp.ATTR_UPNP_MODEL_NAME] = model + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = discovery.upnp.copy() + discovery.upnp[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer + discovery.upnp[ssdp.ATTR_UPNP_MODEL_NAME] = model result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 2d276293baa..3981a3d2685 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -1,9 +1,11 @@ """Tests for AVM Fritz!Box config flow.""" +import dataclasses from unittest.mock import patch from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import pytest +from homeassistant.components import ssdp from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -14,11 +16,7 @@ from homeassistant.components.fritz.const import ( ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, ) -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_UDN, -) +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import ( SOURCE_IMPORT, SOURCE_REAUTH, @@ -51,11 +49,15 @@ MOCK_DEVICE_INFO = { ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, } MOCK_IMPORT_CONFIG = {CONF_HOST: MOCK_HOST, CONF_USERNAME: "username"} -MOCK_SSDP_DATA = { - ATTR_SSDP_LOCATION: f"https://{MOCK_IP}:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "fake_name", - ATTR_UPNP_UDN: "uuid:only-a-test", -} +MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"https://{MOCK_IP}:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "fake_name", + ATTR_UPNP_UDN: "uuid:only-a-test", + }, +) MOCK_REQUEST = b'xxxxxxxxxxxxxxxxxxxxxxxx0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2FakeFritzUser\n' @@ -407,8 +409,9 @@ async def test_ssdp_already_in_progress_host( assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" - MOCK_NO_UNIQUE_ID = MOCK_SSDP_DATA.copy() - del MOCK_NO_UNIQUE_ID[ATTR_UPNP_UDN] + MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) + MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy() + del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 6d62122a871..ce38a1a38c8 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box config flow.""" +import dataclasses from unittest import mock from unittest.mock import Mock, patch @@ -6,12 +7,9 @@ from pyfritzhome import LoginError import pytest from requests.exceptions import HTTPError +from homeassistant.components import ssdp from homeassistant.components.fritzbox.const import DOMAIN -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_UDN, -) +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -26,11 +24,15 @@ from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import MockConfigEntry MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] -MOCK_SSDP_DATA = { - ATTR_SSDP_LOCATION: "https://fake_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, - ATTR_UPNP_UDN: "uuid:only-a-test", -} +MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, + ATTR_UPNP_UDN: "uuid:only-a-test", + }, +) @pytest.fixture(name="fritz") @@ -201,8 +203,9 @@ async def test_ssdp(hass: HomeAssistant, fritz: Mock): async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery without friendly name.""" - MOCK_NO_NAME = MOCK_SSDP_DATA.copy() - del MOCK_NO_NAME[ATTR_UPNP_FRIENDLY_NAME] + MOCK_NO_NAME = dataclasses.replace(MOCK_SSDP_DATA) + MOCK_NO_NAME.upnp = MOCK_NO_NAME.upnp.copy() + del MOCK_NO_NAME.upnp[ATTR_UPNP_FRIENDLY_NAME] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) @@ -300,8 +303,9 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" - MOCK_NO_UNIQUE_ID = MOCK_SSDP_DATA.copy() - del MOCK_NO_UNIQUE_ID[ATTR_UPNP_UDN] + MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) + MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy() + del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 4eb9557c6a7..52b46c57b98 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -177,19 +177,22 @@ async def test_ssdp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context=context, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.100.1:60957/rootDesc.xml", - ssdp.ATTR_SSDP_ST: "upnp:rootdevice", - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", - ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", - ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", - ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", - ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", - ssdp.ATTR_UPNP_PRESENTATION_URL: url, - ssdp.ATTR_UPNP_SERIAL: "00000000", - ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="upnp:rootdevice", + ssdp_location="http://192.168.100.1:60957/rootDesc.xml", + upnp={ + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", + ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", + ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", + ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", + ssdp.ATTR_UPNP_PRESENTATION_URL: url, + ssdp.ATTR_UPNP_SERIAL: "00000000", + ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 7260d589e71..5d2e77e8adb 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable +import dataclasses from typing import Any from unittest.mock import AsyncMock, Mock, patch from hyperion import const from homeassistant import data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.hyperion.const import ( CONF_AUTH_ID, CONF_CREATE_TOKEN, @@ -65,39 +67,41 @@ TEST_REQUEST_TOKEN_FAIL = { "error": "Token request timeout or denied", } -TEST_SSDP_SERVICE_INFO = { - "ssdp_location": f"http://{TEST_HOST}:{TEST_PORT_UI}/description.xml", - "ssdp_st": "upnp:rootdevice", - "deviceType": "urn:schemas-upnp-org:device:Basic:1", - "friendlyName": f"Hyperion ({TEST_HOST})", - "manufacturer": "Hyperion Open Source Ambient Lighting", - "manufacturerURL": "https://www.hyperion-project.org", - "modelDescription": "Hyperion Open Source Ambient Light", - "modelName": "Hyperion", - "modelNumber": "2.0.0-alpha.8", - "modelURL": "https://www.hyperion-project.org", - "serialNumber": f"{TEST_SYSINFO_ID}", - "UDN": f"uuid:{TEST_SYSINFO_ID}", - "ports": { - "jsonServer": f"{TEST_PORT}", - "sslServer": "8092", - "protoBuffer": "19445", - "flatBuffer": "19400", +TEST_SSDP_SERVICE_INFO = ssdp.SsdpServiceInfo( + ssdp_st="upnp:rootdevice", + ssdp_location=f"http://{TEST_HOST}:{TEST_PORT_UI}/description.xml", + ssdp_usn=f"uuid:{TEST_SYSINFO_ID}", + ssdp_ext="", + ssdp_server="Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8", + upnp={ + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "friendlyName": f"Hyperion ({TEST_HOST})", + "manufacturer": "Hyperion Open Source Ambient Lighting", + "manufacturerURL": "https://www.hyperion-project.org", + "modelDescription": "Hyperion Open Source Ambient Light", + "modelName": "Hyperion", + "modelNumber": "2.0.0-alpha.8", + "modelURL": "https://www.hyperion-project.org", + "serialNumber": f"{TEST_SYSINFO_ID}", + "UDN": f"uuid:{TEST_SYSINFO_ID}", + "ports": { + "jsonServer": f"{TEST_PORT}", + "sslServer": "8092", + "protoBuffer": "19445", + "flatBuffer": "19400", + }, + "presentationURL": "index.html", + "iconList": { + "icon": { + "mimetype": "image/png", + "height": "100", + "width": "100", + "depth": "32", + "url": "img/hyperion/ssdp_icon.png", + } + }, }, - "presentationURL": "index.html", - "iconList": { - "icon": { - "mimetype": "image/png", - "height": "100", - "width": "100", - "depth": "32", - "url": "img/hyperion/ssdp_icon.png", - } - }, - "ssdp_usn": f"uuid:{TEST_SYSINFO_ID}", - "ssdp_ext": "", - "ssdp_server": "Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8", -} +) async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry: @@ -639,8 +643,9 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: """Check an SSDP flow where no id is provided.""" client = create_mock_client() - bad_data = {**TEST_SSDP_SERVICE_INFO} - del bad_data["serialNumber"] + bad_data = dataclasses.replace(TEST_SSDP_SERVICE_INFO) + bad_data.upnp = bad_data.upnp.copy() + del bad_data.upnp["serialNumber"] with patch( "homeassistant.components.hyperion.client.HyperionClient", return_value=client @@ -656,8 +661,9 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: """Check an SSDP flow with bad json port.""" client = create_mock_client() - bad_data: dict[str, Any] = {**TEST_SSDP_SERVICE_INFO} - bad_data["ports"]["jsonServer"] = "not_a_port" + bad_data = dataclasses.replace(TEST_SSDP_SERVICE_INFO) + bad_data.upnp = bad_data.upnp.copy() + bad_data.upnp["ports"]["jsonServer"] = "not_a_port" with patch( "homeassistant.components.hyperion.client.HyperionClient", return_value=client @@ -676,8 +682,8 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: client = create_mock_client() client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) - bad_data = {**TEST_SSDP_SERVICE_INFO} - bad_data["ssdp_location"] = f"http://{TEST_HOST}:not_a_port/description.xml" + bad_data = dataclasses.replace(TEST_SSDP_SERVICE_INFO) + bad_data.ssdp_location = f"http://{TEST_HOST}:not_a_port/description.xml" with patch( "homeassistant.components.hyperion.client.HyperionClient", return_value=client diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py index 9f96e56cdd0..f6bc72cb1e4 100644 --- a/tests/components/keenetic_ndms2/__init__.py +++ b/tests/components/keenetic_ndms2/__init__.py @@ -29,8 +29,12 @@ MOCK_OPTIONS = { const.CONF_INTERFACES: ["Home", "VPS0"], } -MOCK_SSDP_DISCOVERY_INFO = { - ssdp.ATTR_SSDP_LOCATION: SSDP_LOCATION, - ssdp.ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, -} +MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={ + ssdp.ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, + }, +) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 23c1bead25e..4d417705ac3 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -1,5 +1,6 @@ """Test Keenetic NDMS2 setup process.""" +import dataclasses from unittest.mock import Mock, patch from ndms2_client import ConnectionException @@ -164,7 +165,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: async def test_ssdp_works(hass: HomeAssistant, connect) -> None: """Test host already configured and discovered.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_SSDP}, @@ -200,7 +201,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_SSDP}, @@ -221,7 +222,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_SSDP}, @@ -245,10 +246,8 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: new_ip = "10.10.10.10" - discovery_info = { - **MOCK_SSDP_DISCOVERY_INFO, - ssdp.ATTR_SSDP_LOCATION: f"http://{new_ip}/", - } + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + discovery_info.ssdp_location = f"http://{new_ip}/" result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, @@ -264,10 +263,9 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: """Discovered device has no UDN.""" - discovery_info = { - **MOCK_SSDP_DISCOVERY_INFO, - } - discovery_info.pop(ssdp.ATTR_UPNP_UDN) + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + discovery_info.upnp = {**discovery_info.upnp} + discovery_info.upnp.pop(ssdp.ATTR_UPNP_UDN) result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, @@ -282,10 +280,9 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: """Discovered device does not look like a keenetic router.""" - discovery_info = { - **MOCK_SSDP_DISCOVERY_INFO, - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Suspicious device", - } + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + discovery_info.upnp = {**discovery_info.upnp} + discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Suspicious device" result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_SSDP}, diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index b81171196c0..b26dce8d936 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -211,12 +211,16 @@ async def test_ssdp_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: SSDP_URL_SLL, - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_URL_SLL, + upnp={ + ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", + ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + ssdp.ATTR_UPNP_SERIAL: SERIAL, + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -227,12 +231,16 @@ async def test_ssdp(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: SSDP_URL, - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_URL, + upnp={ + ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", + ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + ssdp.ATTR_UPNP_SERIAL: SERIAL, + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 2443955ac14..c2a258b2afa 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -7,7 +7,7 @@ from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -24,7 +24,6 @@ from homeassistant.components.samsungtv.const import ( TIMEOUT_WEBSOCKET, ) from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME, @@ -63,27 +62,39 @@ MOCK_IMPORT_WSDATA = { CONF_PORT: 8002, } MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} -MOCK_SSDP_DATA = { - ATTR_SSDP_LOCATION: "https://fake_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", -} -MOCK_SSDP_DATA_NOPREFIX = { - ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake2_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake2_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", -} -MOCK_SSDP_DATA_WRONGMODEL = { - ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", - ATTR_UPNP_MODEL_NAME: "HW-Qfake", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", -} +MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake_model", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) +MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://fake2_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "fake2_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake2_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake2_model", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", + }, +) +MOCK_SSDP_DATA_WRONGMODEL = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://fake2_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "fake2_name", + ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MODEL_NAME: "HW-Qfake", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", + }, +) MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( ip="fake_host", macaddress="aa:bb:cc:dd:ee:ff", hostname="fake_hostname" ) diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 8ac91770741..7a91ff7a735 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -130,14 +130,18 @@ async def test_ssdp(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.2:5200/Printer.xml", - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:Printer:1", - ssdp.ATTR_UPNP_MANUFACTURER: "Samsung Electronics", - ssdp.ATTR_UPNP_PRESENTATION_URL: url, - ssdp.ATTR_UPNP_SERIAL: "00000000", - ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.2:5200/Printer.xml", + upnp={ + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:Printer:1", + ssdp.ATTR_UPNP_MANUFACTURER: "Samsung Electronics", + ssdp.ATTR_UPNP_PRESENTATION_URL: url, + ssdp.ATTR_UPNP_SERIAL: "00000000", + ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 31d63bac158..6229f25f908 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -308,11 +308,15 @@ async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha, mock_get_source_ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "123456789", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1/desc.xml", + upnp={ + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "123456789", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -326,11 +330,15 @@ async def test_ssdp_discovery_successful_add_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "1234567890", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1/desc.xml", + upnp={ + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "1234567890", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -364,11 +372,15 @@ async def test_ssdp_discovery_existing_device_update( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "1234567890", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1/desc.xml", + upnp={ + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "1234567890", + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 12d9f6e1690..fe03f759304 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -8,12 +8,8 @@ import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries -from homeassistant.components import usb, zeroconf -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER_URL, - ATTR_UPNP_SERIAL, -) +from homeassistant.components import ssdp, usb, zeroconf +from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import ( CONF_BAUDRATE, @@ -281,11 +277,15 @@ async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass): """Test usb flow -- deconz discovered.""" result = await hass.config_entries.flow.async_init( "deconz", - data={ - ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", - ATTR_UPNP_MANUFACTURER_URL: "http://www.dresden-elektronik.de", - ATTR_UPNP_SERIAL: "0000000000000000", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: "http://www.dresden-elektronik.de", + ATTR_UPNP_SERIAL: "0000000000000000", + }, + ), context={"source": SOURCE_SSDP}, ) await hass.async_block_till_done() From b724672dd858fe368111eea4c8b8b8f4410e8614 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:47:57 +0100 Subject: [PATCH 0877/1452] Fix overridden state methods - camera (#60352) --- .../components/homekit_controller/camera.py | 5 ----- homeassistant/components/push/camera.py | 21 +++++-------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index a0b15087356..820574e3ffd 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -18,11 +18,6 @@ class HomeKitCamera(AccessoryEntity, Camera): """Define the homekit characteristics the entity is tracking.""" return [] - @property - def state(self): - """Return the current state of the camera.""" - return "idle" - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index b5ce846d52f..b216eabbd85 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -10,12 +10,7 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - STATE_IDLE, - STATE_RECORDING, - Camera, -) +from homeassistant.components.camera import PLATFORM_SCHEMA, STATE_IDLE, Camera from homeassistant.components.camera.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.core import callback @@ -99,7 +94,6 @@ class PushCamera(Camera): self._last_trip = None self._filename = None self._expired_listener = None - self._state = STATE_IDLE self._timeout = timeout self.queue = deque([], buffer_size) self._current_image = None @@ -125,15 +119,10 @@ class PushCamera(Camera): """HTTP field containing the image file.""" return self._image_field - @property - def state(self): - """Return current state of the camera.""" - return self._state - async def update_image(self, image, filename): """Update the camera image.""" - if self._state == STATE_IDLE: - self._state = STATE_RECORDING + if self.state == STATE_IDLE: + self._attr_is_recording = True self._last_trip = dt_util.utcnow() self.queue.clear() @@ -143,7 +132,7 @@ class PushCamera(Camera): @callback def reset_state(now): """Set state to idle after no new images for a period of time.""" - self._state = STATE_IDLE + self._attr_is_recording = False self._expired_listener = None _LOGGER.debug("Reset state") self.async_write_ha_state() @@ -162,7 +151,7 @@ class PushCamera(Camera): ) -> bytes | None: """Return a still image response.""" if self.queue: - if self._state == STATE_IDLE: + if self.state == STATE_IDLE: self.queue.rotate(1) self._current_image = self.queue[0] From 78305ac6ae66d03a1cff7c853c61381c56ad29fc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:48:17 +0100 Subject: [PATCH 0878/1452] Fix slow config_flow test in AVM Fritz!SmartHome (#60348) --- tests/components/fritzbox/test_config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index ce38a1a38c8..a3b258d405e 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -38,7 +38,9 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.config_flow.Fritzhome") as fritz: + with patch("homeassistant.components.fritzbox.async_setup_entry"), patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ) as fritz: yield fritz From 03d1efab46b76a9ded3150f9596cadbad1dffc70 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Thu, 25 Nov 2021 11:04:06 -0700 Subject: [PATCH 0879/1452] Add Balboa Spa integration (#59234) --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/balboa/__init__.py | 102 +++++++ homeassistant/components/balboa/climate.py | 161 +++++++++++ .../components/balboa/config_flow.py | 99 +++++++ homeassistant/components/balboa/const.py | 35 +++ homeassistant/components/balboa/entity.py | 57 ++++ homeassistant/components/balboa/manifest.json | 13 + homeassistant/components/balboa/strings.json | 28 ++ .../components/balboa/translations/en.json | 28 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/balboa/__init__.py | 167 +++++++++++ tests/components/balboa/test_climate.py | 272 ++++++++++++++++++ tests/components/balboa/test_config_flow.py | 167 +++++++++++ tests/components/balboa/test_init.py | 43 +++ 17 files changed, 1182 insertions(+) create mode 100644 homeassistant/components/balboa/__init__.py create mode 100644 homeassistant/components/balboa/climate.py create mode 100644 homeassistant/components/balboa/config_flow.py create mode 100644 homeassistant/components/balboa/const.py create mode 100644 homeassistant/components/balboa/entity.py create mode 100644 homeassistant/components/balboa/manifest.json create mode 100644 homeassistant/components/balboa/strings.json create mode 100644 homeassistant/components/balboa/translations/en.json create mode 100644 tests/components/balboa/__init__.py create mode 100644 tests/components/balboa/test_climate.py create mode 100644 tests/components/balboa/test_config_flow.py create mode 100644 tests/components/balboa/test_init.py diff --git a/.coveragerc b/.coveragerc index 18a4ddfbcf8..2fcdebffccc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -90,6 +90,8 @@ omit = homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* homeassistant/components/baidu/tts.py + homeassistant/components/balboa/__init__.py + homeassistant/components/balboa/entity.py homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index 48a3b5ed02b..6804400f6a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -67,6 +67,7 @@ homeassistant/components/axis/* @Kane610 homeassistant/components/azure_devops/* @timmo001 homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_service_bus/* @hfurubotten +homeassistant/components/balboa/* @garbled1 homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py new file mode 100644 index 00000000000..0922218aa5c --- /dev/null +++ b/homeassistant/components/balboa/__init__.py @@ -0,0 +1,102 @@ +"""The Balboa Spa Client integration.""" +import asyncio +from datetime import timedelta +import time + +from pybalboa import BalboaSpaWifi + +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.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +from .const import ( + _LOGGER, + CONF_SYNC_TIME, + DEFAULT_SYNC_TIME, + DOMAIN, + PLATFORMS, + SIGNAL_UPDATE, +) + +SYNC_TIME_INTERVAL = timedelta(days=1) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Balboa Spa from a config entry.""" + host = entry.data[CONF_HOST] + + _LOGGER.debug("Attempting to connect to %s", host) + spa = BalboaSpaWifi(host) + + connected = await spa.connect() + if not connected: + _LOGGER.error("Failed to connect to spa at %s", host) + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa + + # send config requests, and then listen until we are configured. + await spa.send_mod_ident_req() + await spa.send_panel_req(0, 1) + + async def _async_balboa_update_cb(): + """Primary update callback called from pybalboa.""" + _LOGGER.debug("Primary update callback triggered") + async_dispatcher_send(hass, SIGNAL_UPDATE.format(entry.entry_id)) + + # set the callback so we know we have new data + spa.new_data_cb = _async_balboa_update_cb + + _LOGGER.debug("Starting listener and monitor tasks") + hass.loop.create_task(spa.listen()) + await spa.spa_configured() + asyncio.create_task(spa.check_connection_status()) + + # At this point we have a configured spa. + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + # call update_listener on startup and for options change as well. + await async_setup_time_sync(hass, entry) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + _LOGGER.debug("Disconnecting from spa") + spa = hass.data[DOMAIN][entry.entry_id] + await spa.disconnect() + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Set up the time sync.""" + if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME): + return + + _LOGGER.debug("Setting up daily time sync") + spa = hass.data[DOMAIN][entry.entry_id] + + async def sync_time(): + _LOGGER.debug("Syncing time with Home Assistant") + await spa.set_time(time.strptime(str(dt_util.now()), "%Y-%m-%d %H:%M:%S.%f%z")) + + await sync_time() + entry.async_on_unload( + async_track_time_interval(hass, sync_time, SYNC_TIME_INTERVAL) + ) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py new file mode 100644 index 00000000000..567c65d6388 --- /dev/null +++ b/homeassistant/components/balboa/climate.py @@ -0,0 +1,161 @@ +"""Support for Balboa Spa Wifi adaptor.""" +from __future__ import annotations + +import asyncio + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.exceptions import HomeAssistantError + +from .const import CLIMATE, CLIMATE_SUPPORTED_FANSTATES, CLIMATE_SUPPORTED_MODES, DOMAIN +from .entity import BalboaEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the spa climate device.""" + async_add_entities( + [ + BalboaSpaClimate( + hass, + entry, + hass.data[DOMAIN][entry.entry_id], + CLIMATE, + ) + ], + ) + + +class BalboaSpaClimate(BalboaEntity, ClimateEntity): + """Representation of a Balboa Spa Climate device.""" + + _attr_icon = "mdi:hot-tub" + _attr_fan_modes = CLIMATE_SUPPORTED_FANSTATES + _attr_hvac_modes = CLIMATE_SUPPORTED_MODES + + def __init__(self, hass, entry, client, devtype, num=None): + """Initialize the climate entity.""" + super().__init__(hass, entry, client, devtype, num) + self._balboa_to_ha_blower_map = { + self._client.BLOWER_OFF: FAN_OFF, + self._client.BLOWER_LOW: FAN_LOW, + self._client.BLOWER_MEDIUM: FAN_MEDIUM, + self._client.BLOWER_HIGH: FAN_HIGH, + } + self._ha_to_balboa_blower_map = { + value: key for key, value in self._balboa_to_ha_blower_map.items() + } + self._balboa_to_ha_heatmode_map = { + self._client.HEATMODE_READY: HVAC_MODE_HEAT, + self._client.HEATMODE_RNR: HVAC_MODE_AUTO, + self._client.HEATMODE_REST: HVAC_MODE_OFF, + } + self._ha_heatmode_to_balboa_map = { + value: key for key, value in self._balboa_to_ha_heatmode_map.items() + } + scale = self._client.get_tempscale() + self._attr_preset_modes = self._client.get_heatmode_stringlist() + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + if self._client.have_blower(): + self._attr_supported_features |= SUPPORT_FAN_MODE + self._attr_min_temp = self._client.tmin[self._client.TEMPRANGE_LOW][scale] + self._attr_max_temp = self._client.tmax[self._client.TEMPRANGE_HIGH][scale] + self._attr_temperature_unit = TEMP_FAHRENHEIT + self._attr_precision = PRECISION_WHOLE + if self._client.get_tempscale() == self._client.TSCALE_C: + self._attr_temperature_unit = TEMP_CELSIUS + self._attr_precision = PRECISION_HALVES + + @property + def hvac_mode(self) -> str: + """Return the current HVAC mode.""" + mode = self._client.get_heatmode() + return self._balboa_to_ha_heatmode_map[mode] + + @property + def hvac_action(self) -> str: + """Return the current operation mode.""" + state = self._client.get_heatstate() + if state >= self._client.ON: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + + @property + def fan_mode(self) -> str: + """Return the current fan mode.""" + fanmode = self._client.get_blower() + return self._balboa_to_ha_blower_map.get(fanmode, FAN_OFF) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._client.get_curtemp() + + @property + def target_temperature(self): + """Return the target temperature we try to reach.""" + return self._client.get_settemp() + + @property + def preset_mode(self): + """Return current preset mode.""" + return self._client.get_heatmode(True) + + async def async_set_temperature(self, **kwargs): + """Set a new target temperature.""" + scale = self._client.get_tempscale() + newtemp = kwargs[ATTR_TEMPERATURE] + if newtemp > self._client.tmax[self._client.TEMPRANGE_LOW][scale]: + await self._client.change_temprange(self._client.TEMPRANGE_HIGH) + await asyncio.sleep(1) + if newtemp < self._client.tmin[self._client.TEMPRANGE_HIGH][scale]: + await self._client.change_temprange(self._client.TEMPRANGE_LOW) + await asyncio.sleep(1) + await self._client.send_temp_change(newtemp) + + async def async_set_preset_mode(self, preset_mode) -> None: + """Set new preset mode.""" + modelist = self._client.get_heatmode_stringlist() + self._async_validate_mode_or_raise(preset_mode) + if preset_mode not in modelist: + raise HomeAssistantError(f"{preset_mode} is not a valid preset mode") + await self._client.change_heatmode(modelist.index(preset_mode)) + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + await self._client.change_blower(self._ha_to_balboa_blower_map[fan_mode]) + + def _async_validate_mode_or_raise(self, mode): + """Check that the mode can be set.""" + if mode == self._client.HEATMODE_RNR: + raise HomeAssistantError(f"{mode} can only be reported but not set") + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode. + + OFF = Rest + AUTO = Ready in Rest (can't be set, only reported) + HEAT = Ready + """ + mode = self._ha_heatmode_to_balboa_map[hvac_mode] + self._async_validate_mode_or_raise(mode) + await self._client.change_heatmode(self._ha_heatmode_to_balboa_map[hvac_mode]) diff --git a/homeassistant/components/balboa/config_flow.py b/homeassistant/components/balboa/config_flow.py new file mode 100644 index 00000000000..1c91376d76e --- /dev/null +++ b/homeassistant/components/balboa/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow for Balboa Spa Client integration.""" +from pybalboa import BalboaSpaWifi +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_HOST +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac + +from .const import _LOGGER, CONF_SYNC_TIME, DOMAIN + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + _LOGGER.debug("Attempting to connect to %s", data[CONF_HOST]) + spa = BalboaSpaWifi(data[CONF_HOST]) + connected = await spa.connect() + _LOGGER.debug("Got connected = %d", connected) + if not connected: + raise CannotConnect + + # send config requests, and then listen until we are configured. + await spa.send_mod_ident_req() + await spa.send_panel_req(0, 1) + + hass.loop.create_task(spa.listen()) + + await spa.spa_configured() + + macaddr = format_mac(spa.get_macaddr()) + model = spa.get_model_name() + await spa.disconnect() + + return {"title": model, "formatted_mac": macaddr} + + +class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Balboa Spa Client config flow.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return BalboaSpaClientOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info["formatted_mac"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class BalboaSpaClientOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Balboa Spa Client options.""" + + def __init__(self, config_entry): + """Initialize Balboa Spa Client options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage Balboa Spa Client options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_SYNC_TIME, + default=self.config_entry.options.get(CONF_SYNC_TIME, False), + ): bool, + } + ), + ) diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py new file mode 100644 index 00000000000..121c6be3bfa --- /dev/null +++ b/homeassistant/components/balboa/const.py @@ -0,0 +1,35 @@ +"""Constants for the Balboa Spa Client integration.""" +import logging + +from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) +from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_OFF + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "balboa" + +CLIMATE_SUPPORTED_FANSTATES = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] +CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] +CONF_SYNC_TIME = "sync_time" +DEFAULT_SYNC_TIME = False +FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] +PLATFORMS = ["climate"] + +AUX = "Aux" +CIRC_PUMP = "Circ Pump" +CLIMATE = "Climate" +FILTER = "Filter" +LIGHT = "Light" +MISTER = "Mister" +PUMP = "Pump" +TEMP_RANGE = "Temp Range" + +SIGNAL_UPDATE = "balboa_update_{}" diff --git a/homeassistant/components/balboa/entity.py b/homeassistant/components/balboa/entity.py new file mode 100644 index 00000000000..016beadac5c --- /dev/null +++ b/homeassistant/components/balboa/entity.py @@ -0,0 +1,57 @@ +"""Base class for Balboa Spa Client integration.""" +import time + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import SIGNAL_UPDATE + + +class BalboaEntity(Entity): + """Abstract class for all Balboa platforms. + + Once you connect to the spa's port, it continuously sends data (at a rate + of about 5 per second!). The API updates the internal states of things + from this stream, and all we have to do is read the values out of the + accessors. + """ + + _attr_should_poll = False + + def __init__(self, hass, entry, client, devtype, num=None): + """Initialize the spa entity.""" + self._client = client + self._device_name = self._client.get_model_name() + self._type = devtype + self._num = num + self._entry = entry + self._attr_unique_id = f'{self._device_name}-{self._type}{self._num or ""}-{self._client.get_macaddr().replace(":","")[-6:]}' + self._attr_name = f'{self._device_name}: {self._type}{self._num or ""}' + self._attr_device_info = DeviceInfo( + name=self._device_name, + manufacturer="Balboa Water Group", + model=self._client.get_model_name(), + sw_version=self._client.get_ssid(), + connections={(CONNECTION_NETWORK_MAC, self._client.get_macaddr())}, + ) + + async def async_added_to_hass(self) -> None: + """Set up a listener for the entity.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE.format(self._entry.entry_id), + self.async_write_ha_state, + ) + ) + + @property + def assumed_state(self) -> bool: + """Return whether the state is based on actual reading from device.""" + return (self._client.lastupd + 5 * 60) < time.time() + + @property + def available(self) -> bool: + """Return whether the entity is available or not.""" + return self._client.connected diff --git a/homeassistant/components/balboa/manifest.json b/homeassistant/components/balboa/manifest.json new file mode 100644 index 00000000000..aa52bee230d --- /dev/null +++ b/homeassistant/components/balboa/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "balboa", + "name": "Balboa Spa Client", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/balboa", + "requirements": [ + "pybalboa==0.13" + ], + "codeowners": [ + "@garbled1" + ], + "iot_class": "local_push" +} diff --git a/homeassistant/components/balboa/strings.json b/homeassistant/components/balboa/strings.json new file mode 100644 index 00000000000..68bd4ddef7b --- /dev/null +++ b/homeassistant/components/balboa/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Balboa Wi-Fi device", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant" + } + } + } + } +} diff --git a/homeassistant/components/balboa/translations/en.json b/homeassistant/components/balboa/translations/en.json new file mode 100644 index 00000000000..15d8aa2b47e --- /dev/null +++ b/homeassistant/components/balboa/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Balboa Wi-Fi device", + "data": { + "host": "Host" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6c199b47e32..0c0ea7964e0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -35,6 +35,7 @@ FLOWS = [ "awair", "axis", "azure_devops", + "balboa", "blebox", "blink", "bmw_connected_drive", diff --git a/requirements_all.txt b/requirements_all.txt index f63190bbd6e..3cfcf26657c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1371,6 +1371,9 @@ pyatome==0.1.1 # homeassistant.components.apple_tv pyatv==0.8.2 +# homeassistant.components.balboa +pybalboa==0.13 + # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b7c224ec34..45a20c67ff2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -837,6 +837,9 @@ pyatmo==6.2.0 # homeassistant.components.apple_tv pyatv==0.8.2 +# homeassistant.components.balboa +pybalboa==0.13 + # homeassistant.components.blackbird pyblackbird==0.5 diff --git a/tests/components/balboa/__init__.py b/tests/components/balboa/__init__.py new file mode 100644 index 00000000000..13c8b6240a7 --- /dev/null +++ b/tests/components/balboa/__init__.py @@ -0,0 +1,167 @@ +"""Test the Balboa Spa Client integration.""" +import asyncio +from unittest.mock import patch + +from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +BALBOA_DEFAULT_PORT = 4257 +TEST_HOST = "balboatest.localdomain" + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Mock integration setup.""" + config_entry = MockConfigEntry( + domain=BALBOA_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.balboa.BalboaSpaWifi", + new=BalboaMock, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def init_integration_mocked(hass: HomeAssistant) -> MockConfigEntry: + """Mock integration setup.""" + config_entry = MockConfigEntry( + domain=BALBOA_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.connect", + new=BalboaMock.connect, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.listen_until_configured", + new=BalboaMock.listen_until_configured, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.listen", + new=BalboaMock.listen, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.check_connection_status", + new=BalboaMock.check_connection_status, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.send_panel_req", + new=BalboaMock.send_panel_req, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.send_mod_ident_req", + new=BalboaMock.send_mod_ident_req, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.spa_configured", + new=BalboaMock.spa_configured, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_model_name", + new=BalboaMock.get_model_name, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +class BalboaMock: + """Mock pybalboa library.""" + + def __init__(self, hostname, port=BALBOA_DEFAULT_PORT): + """Mock init.""" + self.host = hostname + self.port = port + self.connected = False + self.new_data_cb = None + self.lastupd = 0 + self.connected = False + self.fake_action = False + + async def connect(self): + """Connect to the spa.""" + self.connected = True + return True + + async def broken_connect(self): + """Connect to the spa.""" + self.connected = False + return False + + async def disconnect(self): + """Stop talking to the spa.""" + self.connected = False + + async def send_panel_req(self, arg_ba, arg_bb): + """Send a panel request, 2 bytes of data.""" + self.fake_action = False + return + + async def send_mod_ident_req(self): + """Ask for the module identification.""" + self.fake_action = False + return + + @staticmethod + def get_macaddr(): + """Return the macaddr of the spa wifi.""" + return "ef:ef:ef:c0:ff:ee" + + def get_model_name(self): + """Return the model name.""" + self.fake_action = False + return "FakeSpa" + + @staticmethod + def get_ssid(): + """Return the software version.""" + return "V0.0" + + @staticmethod + async def set_time(new_time, timescale=None): + """Set time on spa to new_time with optional timescale.""" + return + + async def listen(self): + """Listen to the spa babble forever.""" + while True: + if not self.connected: + # sleep and hope the checker fixes us + await asyncio.sleep(5) + continue + + # fake it + await asyncio.sleep(5) + + async def check_connection_status(self): + """Set this up to periodically check the spa connection and fix.""" + self.fake_action = False + while True: + # fake it + await asyncio.sleep(15) + + async def spa_configured(self): + """Check if the spa has been configured.""" + self.fake_action = False + return + + async def int_new_data_cb(self): + """Call false internal data callback.""" + + if self.new_data_cb is None: + return + await self.new_data_cb() # pylint: disable=not-callable + + async def listen_until_configured(self, maxiter=20): + """Listen to the spa babble until we are configured.""" + if not self.connected: + return False + return True diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py new file mode 100644 index 00000000000..53eb0307beb --- /dev/null +++ b/tests/components/balboa/test_climate.py @@ -0,0 +1,272 @@ +"""Tests of the climate entity of the balboa integration.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN, SIGNAL_UPDATE +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from . import init_integration_mocked + +from tests.components.climate import common + +FAN_SETTINGS = [ + FAN_OFF, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, +] + +HVAC_SETTINGS = [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, +] + +ENTITY_CLIMATE = "climate.fakespa_climate" + + +async def test_spa_defaults(hass: HomeAssistant): + """Test supported features flags.""" + + await _setup_climate_test(hass) + + state = hass.states.get(ENTITY_CLIMATE) + + assert ( + state.attributes["supported_features"] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + ) + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_MIN_TEMP] == 10.0 + assert state.attributes[ATTR_MAX_TEMP] == 40.0 + assert state.attributes[ATTR_PRESET_MODE] == "Ready" + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +async def test_spa_defaults_fake_tscale(hass: HomeAssistant): + """Test supported features flags.""" + + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_tempscale", return_value=1 + ): + await _setup_climate_test(hass) + + state = hass.states.get(ENTITY_CLIMATE) + + assert ( + state.attributes["supported_features"] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + ) + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_MIN_TEMP] == 10.0 + assert state.attributes[ATTR_MAX_TEMP] == 40.0 + assert state.attributes[ATTR_PRESET_MODE] == "Ready" + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +async def test_spa_with_blower(hass: HomeAssistant): + """Test supported features flags.""" + + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.have_blower", return_value=True + ): + config_entry = await _setup_climate_test(hass) + + # force a refresh + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_CLIMATE) + + assert ( + state.attributes["supported_features"] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE + ) + + for fan_state in range(4): + # set blower + state = await _patch_blower(hass, config_entry, fan_state) + assert state.attributes[ATTR_FAN_MODE] == FAN_SETTINGS[fan_state] + + # test the nonsense checks + for fan_state in (None, 70): + state = await _patch_blower(hass, config_entry, fan_state) + assert state.attributes[ATTR_FAN_MODE] == FAN_OFF + + +async def test_spa_temperature(hass: HomeAssistant): + """Test spa temperature settings.""" + + config_entry = await _setup_climate_test(hass) + + # flip the spa into F + # set temp to a valid number + state = await _patch_spa_settemp(hass, config_entry, 0, 100.0) + assert state.attributes.get(ATTR_TEMPERATURE) == 38.0 + + +async def test_spa_temperature_unit(hass: HomeAssistant): + """Test temperature unit conversions.""" + + with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): + config_entry = await _setup_climate_test(hass) + + state = await _patch_spa_settemp(hass, config_entry, 0, 15.4) + assert state.attributes.get(ATTR_TEMPERATURE) == 15.0 + + +async def test_spa_hvac_modes(hass: HomeAssistant): + """Test hvac modes.""" + + config_entry = await _setup_climate_test(hass) + + # try out the different heat modes + for heat_mode in range(2): + state = await _patch_spa_heatmode(hass, config_entry, heat_mode) + modes = state.attributes.get(ATTR_HVAC_MODES) + assert [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] == modes + assert state.state == HVAC_SETTINGS[heat_mode] + + with pytest.raises(HomeAssistantError): + await _patch_spa_heatmode(hass, config_entry, 2) + + +async def test_spa_hvac_action(hass: HomeAssistant): + """Test setting of the HVAC action.""" + + config_entry = await _setup_climate_test(hass) + + # try out the different heat states + state = await _patch_spa_heatstate(hass, config_entry, 1) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + state = await _patch_spa_heatstate(hass, config_entry, 0) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +async def test_spa_preset_modes(hass: HomeAssistant): + """Test the various preset modes.""" + + config_entry = await _setup_climate_test(hass) + + state = hass.states.get(ENTITY_CLIMATE) + modes = state.attributes.get(ATTR_PRESET_MODES) + assert ["Ready", "Rest", "Ready in Rest"] == modes + + # Put it in Ready and Rest + modelist = ["Ready", "Rest"] + for mode in modelist: + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", + return_value=modelist.index(mode), + ): + await common.async_set_preset_mode(hass, mode, ENTITY_CLIMATE) + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes[ATTR_PRESET_MODE] == modelist.index(mode) + + # put it in RNR and test assertion + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", + return_value=2, + ), pytest.raises(HomeAssistantError): + await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE) + + +# Helpers +async def _patch_blower(hass, config_entry, fan_state): + """Patch the blower state.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_blower", + return_value=fan_state, + ): + if fan_state is not None and fan_state <= len(FAN_SETTINGS): + await common.async_set_fan_mode(hass, FAN_SETTINGS[fan_state]) + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_settemp(hass, config_entry, tscale, settemp): + """Patch the settemp.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_tempscale", + return_value=tscale, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_settemp", + return_value=settemp, + ): + await common.async_set_temperature( + hass, temperature=settemp, entity_id=ENTITY_CLIMATE + ) + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_heatmode(hass, config_entry, heat_mode): + """Patch the heatmode.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", + return_value=heat_mode, + ): + await common.async_set_hvac_mode(hass, HVAC_SETTINGS[heat_mode], ENTITY_CLIMATE) + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_heatstate(hass, config_entry, heat_state): + """Patch the heatmode.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_heatstate", + return_value=heat_state, + ): + await common.async_set_hvac_mode( + hass, HVAC_SETTINGS[heat_state], ENTITY_CLIMATE + ) + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _setup_climate_test(hass): + """Prepare the test.""" + config_entry = await init_integration_mocked(hass) + await async_setup_component(hass, BALBOA_DOMAIN, config_entry) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py new file mode 100644 index 00000000000..fc12289d90a --- /dev/null +++ b/tests/components/balboa/test_config_flow.py @@ -0,0 +1,167 @@ +"""Test the Balboa Spa Client config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import BalboaMock + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_HOST: "1.1.1.1", +} +TEST_ID = "FakeBalboa" + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", + new=BalboaMock.connect, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", + new=BalboaMock.disconnect, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.listen", + new=BalboaMock.listen, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_mod_ident_req", + new=BalboaMock.send_mod_ident_req, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_panel_req", + new=BalboaMock.send_panel_req, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.spa_configured", + new=BalboaMock.spa_configured, + ), patch( + "homeassistant.components.balboa.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", + new=BalboaMock.broken_connect, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", + new=BalboaMock.disconnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_unknown_error(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured(hass: HomeAssistant) -> None: + """Test when provided credentials are already configured.""" + MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", + new=BalboaMock.connect, + ), patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", + new=BalboaMock.disconnect, + ), patch( + "homeassistant.components.balboa.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test specifying non default settings using options flow.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID) + config_entry.add_to_hass(hass) + + # Rather than mocking out 15 or so functions, we just need to mock + # the entire library, otherwise it will get stuck in a listener and + # the various loops in pybalboa. + with patch( + "homeassistant.components.balboa.config_flow.BalboaSpaWifi", + new=BalboaMock, + ), patch( + "homeassistant.components.balboa.BalboaSpaWifi", + new=BalboaMock, + ): + 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) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SYNC_TIME: True}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_SYNC_TIME: True} diff --git a/tests/components/balboa/test_init.py b/tests/components/balboa/test_init.py new file mode 100644 index 00000000000..ac0dea3b007 --- /dev/null +++ b/tests/components/balboa/test_init.py @@ -0,0 +1,43 @@ +"""Tests of the initialization of the balboa integration.""" + +from unittest.mock import patch + +from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import TEST_HOST, BalboaMock, init_integration + +from tests.common import MockConfigEntry + + +async def test_setup_entry(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + config_entry = await init_integration(hass) + + assert config_entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_fails(hass): + """Validate that setup entry also configure the client.""" + config_entry = MockConfigEntry( + domain=BALBOA_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.connect", + new=BalboaMock.broken_connect, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY From fb909eca8a643447f0a8355c0125a053a58ce77f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 25 Nov 2021 19:32:26 +0100 Subject: [PATCH 0880/1452] Fix slow config_flow test in bond (#60355) --- tests/components/bond/test_config_flow.py | 41 ++++++++++++----------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index e3f441ab56f..4a6efa8f89b 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -194,20 +194,21 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant): async def test_zeroconf_form(hass: core.HomeAssistant): """Test we get the discovery form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( - host="test-host", - hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", - port=None, - properties={}, - type="mock_type", - ), - ) - assert result["type"] == "form" - assert result["errors"] == {} + with patch_bond_version(), patch_bond_token(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + assert result["type"] == "form" + assert result["errors"] == {} with patch_bond_version( return_value={"bondid": "test-bond-id"} @@ -326,13 +327,12 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): type="mock_type", ), ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["host"] == "updated-host" - - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 1 async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistant): @@ -442,9 +442,10 @@ async def _help_test_form_unexpected_error( error: Exception, ): """Test we handle unexpected error gracefully.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source}, data=initial_input - ) + with patch_bond_token(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=initial_input + ) with patch_bond_version( return_value={"bond_id": "test-bond-id"} From e5d8c69a927e8451e7cb50b950265fb211445d73 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 20:53:49 +0100 Subject: [PATCH 0881/1452] CI: Move bandit into the other linters job (#60357) --- .github/workflows/ci.yaml | 67 +++++++++------------------------------ 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 551c9861bd9..d7a63848b3b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -186,55 +186,6 @@ jobs: . venv/bin/activate pre-commit install-hooks - lint-bandit: - name: Check bandit - runs-on: ubuntu-latest - needs: - - changes - - prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v2.1.7 - with: - path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v2.1.7 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Run bandit (fully) - if: needs.changes.outputs.test_full_suite == 'true' - run: | - . venv/bin/activate - pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure - - name: Run bandit (partially) - if: needs.changes.outputs.test_full_suite == 'false' - shell: bash - run: | - . venv/bin/activate - pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure - lint-black: name: Check black runs-on: ubuntu-latest @@ -379,7 +330,9 @@ jobs: lint-other: name: Check other linters runs-on: ubuntu-latest - needs: prepare-base + needs: + - changes + - prepare-base steps: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 @@ -452,7 +405,6 @@ jobs: - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" - - name: Check Dockerfile uses: docker://hadolint/hadolint:v1.18.2 with: @@ -462,6 +414,18 @@ jobs: with: args: hadolint Dockerfile.dev + - name: Run bandit (fully) + if: needs.changes.outputs.test_full_suite == 'true' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure + - name: Run bandit (partially) + if: needs.changes.outputs.test_full_suite == 'false' + shell: bash + run: | + . venv/bin/activate + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure + hassfest: name: Check hassfest runs-on: ubuntu-latest @@ -651,7 +615,6 @@ jobs: - changes - gen-requirements-all - hassfest - - lint-bandit - lint-black - lint-other - lint-isort From d504c1e3e8b7e301b2f19bc5eed1312451d0cc73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 14:20:34 -0600 Subject: [PATCH 0882/1452] Add support for flux_led 0xA2 devices (#60361) --- homeassistant/components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/number.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 3 +-- tests/components/flux_led/test_number.py | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 64a41a9ce8a..d0a75205cdd 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.35"], + "requirements": ["flux_led==0.24.37"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 39cfb2ea094..583b8cea0a8 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -73,7 +73,7 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): raise HomeAssistantError( "Speed can only be adjusted when an effect is active" ) - if self._device.original_addressable and not self._device.is_on: + if not self._device.speed_adjust_off and not self._device.is_on: raise HomeAssistantError("Speed can only be adjusted when the light is on") await self._device.async_set_effect( current_effect, new_speed, _effect_brightness(self._device.brightness) diff --git a/requirements_all.txt b/requirements_all.txt index 3cfcf26657c..0b68310c1a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.35 +flux_led==0.24.37 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45a20c67ff2..4b10036dc97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.35 +flux_led==0.24.37 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index e3eb4a0e200..47aeb6be491 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -89,8 +89,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.speed = 50 bulb.model = "Smart Bulb (0x35)" bulb.version_num = 8 - bulb.original_addressable = False - bulb.addressable = False + bulb.speed_adjust_off = True bulb.rgbwcapable = True bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 88cecd48cd7..c6167ebcedc 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -121,7 +121,7 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - bulb.original_addressable = True + bulb.speed_adjust_off = False bulb.raw_state = bulb.raw_state._replace( model_num=0xA1 ) # Original addressable model From f2e03420d19f02bcda7b9f1ae988a17398b9a1c4 Mon Sep 17 00:00:00 2001 From: cvroque <65680394+cvroque@users.noreply.github.com> Date: Thu, 25 Nov 2021 17:24:46 -0300 Subject: [PATCH 0883/1452] Add commands to Tuya Vacuum (sd) (#60351) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/vacuum.py | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 672001dd3fa..c530b83ce49 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -236,6 +236,7 @@ class DPCode(str, Enum): PM25_STATE = "pm25_state" PM25_VALUE = "pm25_value" POWDER_SET = "powder_set" # Powder + POWER = "power" POWER_GO = "power_go" PRESENCE_STATE = "presence_state" PRESSURE_STATE = "pressure_state" diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 5602413615c..4b7c7660556 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -13,12 +13,15 @@ from homeassistant.components.vacuum import ( STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_START, SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, StateVacuumEntity, ) from homeassistant.config_entries import ConfigEntry @@ -93,9 +96,15 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): if DPCode.SWITCH_CHARGE in self.device.status: self._supported_features |= SUPPORT_RETURN_HOME + if DPCode.SEEK in self.device.status: + self._supported_features |= SUPPORT_LOCATE + if DPCode.STATUS in self.device.status: self._supported_features |= SUPPORT_STATE | SUPPORT_STATUS + if DPCode.POWER in self.device.status: + self._supported_features |= SUPPORT_TURN_ON | SUPPORT_TURN_OFF + if DPCode.POWER_GO in self.device.status: self._supported_features |= SUPPORT_STOP | SUPPORT_START @@ -144,12 +153,20 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): """Flag supported features.""" return self._supported_features - def start(self, **kwargs: Any) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" + self._send_command([{"code": DPCode.POWER, "value": True}]) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._send_command([{"code": DPCode.POWER, "value": False}]) + + def start(self, **kwargs: Any) -> None: + """Start the device.""" self._send_command([{"code": DPCode.POWER_GO, "value": True}]) def stop(self, **kwargs: Any) -> None: - """Turn the device off.""" + """Stop the device.""" self._send_command([{"code": DPCode.POWER_GO, "value": False}]) def pause(self, **kwargs: Any) -> None: @@ -161,7 +178,7 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): self._send_command([{"code": DPCode.MODE, "value": "chargego"}]) def locate(self, **kwargs: Any) -> None: - """Return device to dock.""" + """Locate the device.""" self._send_command([{"code": DPCode.SEEK, "value": True}]) def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: From 9b4fb44feaf638c88bcfa87a85f5ea0bd09cf710 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 21:45:14 +0100 Subject: [PATCH 0884/1452] CI: GitHub Annotate slow tests in partial test runs (#60359) * CI: GitHub Annotate slow tests in partial test runs * Correct line regex --- .github/workflows/ci.yaml | 5 +++++ .github/workflows/matchers/pytest-slow.json | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .github/workflows/matchers/pytest-slow.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7a63848b3b..7da47d390d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -653,6 +653,9 @@ jobs: # However this plugin is fairly new and doesn't run correctly # on a non-GitHub environment. pip install pytest-github-actions-annotate-failures==0.1.3 + - name: Register pytest slow test problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Run pytest (fully) if: needs.changes.outputs.test_full_suite == 'true' run: | @@ -683,6 +686,8 @@ jobs: --cov-report=xml \ --cov-report=term-missing \ -o console_output_style=count \ + --durations=0 \ + --durations-min=1 \ -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/matchers/pytest-slow.json b/.github/workflows/matchers/pytest-slow.json new file mode 100644 index 00000000000..31f565a594a --- /dev/null +++ b/.github/workflows/matchers/pytest-slow.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^=+ slowest durations =+$" + }, + { + "regexp": "^((.*s)\\s(call|setup|teardown)\\s+(.*)::(.*))$", + "message": 1, + "file": 2, + "loop": true + } + ] + } + ] +} From 7014f60f42365ecf793091273752e7ec9346d115 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 21:51:08 +0100 Subject: [PATCH 0885/1452] CI: Add partial run support to pyupgrade (#60362) --- .github/workflows/ci.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7da47d390d9..063044a5602 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -365,10 +365,17 @@ jobs: echo "Failed to restore pre-commit environment from cache" exit 1 - - name: Run pyupgrade + - name: Run pyupgrade (fully) + if: needs.changes.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure + - name: Run pyupgrade (partially) + if: needs.changes.outputs.test_full_suite == 'false' + shell: bash + run: | + . venv/bin/activate + pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure - name: Register yamllint problem matcher run: | From 4360fb733f3fd4ae603a3f734f7c9577aa9db31f Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Thu, 25 Nov 2021 22:02:59 +0100 Subject: [PATCH 0886/1452] Add tolo button platform (#60345) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/tolo/button.py | 54 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tolo/button.py diff --git a/.coveragerc b/.coveragerc index 2fcdebffccc..f394a1729bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1094,6 +1094,7 @@ omit = homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py homeassistant/components/tolo/__init__.py + homeassistant/components/tolo/button.py homeassistant/components/tolo/climate.py homeassistant/components/tolo/light.py homeassistant/components/tolo/select.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 2daeedaf837..02fece7b3b9 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["climate", "light", "select", "sensor"] +PLATFORMS = ["button", "climate", "light", "select", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/button.py b/homeassistant/components/tolo/button.py new file mode 100644 index 00000000000..9dd7752b0c9 --- /dev/null +++ b/homeassistant/components/tolo/button.py @@ -0,0 +1,54 @@ +"""TOLO Sauna Button controls.""" + +from tololib.const import LampMode + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up buttons for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + ToloLampNextColorButton(coordinator, entry), + ] + ) + + +class ToloLampNextColorButton(ToloSaunaCoordinatorEntity, ButtonEntity): + """Button for switching to the next lamp color.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_icon = "mdi:palette" + _attr_name = "Next Color" + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize lamp next color button entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_lamp_next_color" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + self.coordinator.data.status.lamp_on + and self.coordinator.data.settings.lamp_mode == LampMode.MANUAL + ) + + def press(self) -> None: + """Execute action when lamp change color button was pressed.""" + self.coordinator.client.lamp_change_color() From de78c4f0f8fc545eb593977bbc2484aa905b7460 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 25 Nov 2021 22:23:48 +0100 Subject: [PATCH 0887/1452] Import Callable from collections.abc (#60354) --- homeassistant/components/azure_devops/sensor.py | 3 ++- homeassistant/components/fritz/sensor.py | 3 ++- homeassistant/components/fronius/__init__.py | 3 ++- homeassistant/components/homekit/type_sensors.py | 3 ++- homeassistant/components/lookin/climate.py | 4 ++-- homeassistant/components/met/__init__.py | 3 ++- homeassistant/components/rdw/binary_sensor.py | 2 +- homeassistant/components/rdw/sensor.py | 2 +- homeassistant/components/speedtestdotnet/const.py | 3 ++- homeassistant/components/tuya/const.py | 2 +- homeassistant/components/vicare/__init__.py | 2 +- homeassistant/components/vicare/sensor.py | 2 +- homeassistant/components/wled/sensor.py | 2 +- 13 files changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index dc81cf53171..d249e8a8088 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -1,8 +1,9 @@ """Support for Azure DevOps sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any from aioazuredevops.builds import DevOpsBuild diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 951085dbdbc..869e135c2c8 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -1,10 +1,11 @@ """AVM FRITZ!Box binary sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta import logging -from typing import Any, Callable, Literal +from typing import Any, Literal from fritzconnection.core.exceptions import ( FritzActionError, diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index d114d732f1f..f605e57bac2 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging -from typing import Callable, TypeVar +from typing import TypeVar from pyfronius import Fronius, FroniusError diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index e43a899a9b2..148b705b23f 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,8 +1,9 @@ """Class to hold all sensor accessories.""" from __future__ import annotations +from collections.abc import Callable import logging -from typing import Callable, NamedTuple +from typing import NamedTuple from pyhap.const import CATEGORY_SENSOR diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 7bb48350eef..cab4b0968eb 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -1,10 +1,10 @@ """The lookin integration climate platform.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine from datetime import timedelta import logging -from typing import Any, Callable, Final, cast +from typing import Any, Final, cast from aiolookin import Climate, MeteoSensor, SensorID diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 4010b0e0b79..47573a76151 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,11 +1,12 @@ """The met component.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging from random import randrange from types import MappingProxyType -from typing import Any, Callable +from typing import Any import metno diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 75077a53966..80f4a425212 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -1,8 +1,8 @@ """Support for RDW binary sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable from vehicle import Vehicle diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index f9b1dd09847..f2c8d93a8a2 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -1,9 +1,9 @@ """Support for RDW sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import date -from typing import Callable from vehicle import Vehicle diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 57beaf99eb9..323d17cdd84 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,8 +1,9 @@ """Constants used by Speedtest.net.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable, Final +from typing import Final from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index c530b83ce49..dc781ad8e42 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,9 +1,9 @@ """Constants for the Tuya integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass, field from enum import Enum -from typing import Callable from tuya_iot import TuyaCloudOpenAPIEndpoint diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index d4e4db3fbe9..a79dbf0657d 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,9 +1,9 @@ """The ViCare integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import Callable from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 50ad64ab0c8..6397236f299 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -1,10 +1,10 @@ """Viessmann ViCare sensor device.""" from __future__ import annotations +from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass import logging -from typing import Callable from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareUtils import ( diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index d1c1bc2d96c..271cba9055b 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,9 +1,9 @@ """Support for WLED sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Callable from wled import Device as WLEDDevice From ba7b5681e6a8f74f8d929163a066c216ef6bd1e3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 25 Nov 2021 22:34:47 +0100 Subject: [PATCH 0888/1452] Fix slow config_flow test in Dune HD (#60366) --- tests/components/dunehd/test_config_flow.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index c8e7b0f7f3e..73732236077 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -16,7 +16,9 @@ DUNEHD_STATE = {"protocol_version": "4", "player_state": "navigator"} async def test_import(hass): """Test that the import works.""" - with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + with patch("homeassistant.components.dunehd.async_setup_entry"), patch( + "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME ) @@ -108,7 +110,9 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): """Test that the user step works.""" - with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + with patch("homeassistant.components.dunehd.async_setup_entry"), patch( + "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) @@ -120,7 +124,9 @@ async def test_create_entry(hass): async def test_create_entry_with_ipv6_address(hass): """Test that the user step works with device IPv6 address..""" - with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + with patch("homeassistant.components.dunehd.async_setup_entry"), patch( + "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, From db0104c2c9c0fe16c6a13d86093cb0da680a8ed7 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Thu, 25 Nov 2021 22:39:39 +0100 Subject: [PATCH 0889/1452] Add tolo binary_sensor platform (#60365) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 2 +- .../components/tolo/binary_sensor.py | 72 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tolo/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index f394a1729bf..b0f3baf33cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1094,6 +1094,7 @@ omit = homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py homeassistant/components/tolo/__init__.py + homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py homeassistant/components/tolo/climate.py homeassistant/components/tolo/light.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 02fece7b3b9..3776c4bd61c 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["button", "climate", "light", "select", "sensor"] +PLATFORMS = ["binary_sensor", "button", "climate", "light", "select", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/binary_sensor.py b/homeassistant/components/tolo/binary_sensor.py new file mode 100644 index 00000000000..777e2f16942 --- /dev/null +++ b/homeassistant/components/tolo/binary_sensor.py @@ -0,0 +1,72 @@ +"""TOLO Sauna binary sensors.""" + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensors for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + ToloFlowInBinarySensor(coordinator, entry), + ToloFlowOutBinarySensor(coordinator, entry), + ] + ) + + +class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): + """Water In Valve Sensor.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_name = "Water In Valve" + _attr_device_class = DEVICE_CLASS_OPENING + _attr_icon = "mdi:water-plus-outline" + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Water In Valve entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_flow_in" + + @property + def is_on(self) -> bool: + """Return if flow in valve is open.""" + return self.coordinator.data.status.flow_in + + +class ToloFlowOutBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): + """Water Out Valve Sensor.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_name = "Water Out Valve" + _attr_device_class = DEVICE_CLASS_OPENING + _attr_icon = "mdi:water-minus-outline" + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO Water Out Valve entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_flow_out" + + @property + def is_on(self) -> bool: + """Return if flow out valve is open.""" + return self.coordinator.data.status.flow_out From 7613c6fd4cca17f8e933e4c53a1e49b5a739d529 Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Thu, 25 Nov 2021 21:41:56 +0000 Subject: [PATCH 0890/1452] Add configuration_url to AdGuard Home integration (#60356) --- homeassistant/components/adguard/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 6dc0ea63a2b..e408338cd57 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -6,7 +6,7 @@ import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -197,6 +197,14 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this AdGuard Home instance.""" + if self._entry.source == SOURCE_HASSIO: + config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" + else: + if self.adguard.tls: + config_url = f"https://{self.adguard.host}:{self.adguard.port}" + else: + config_url = f"http://{self.adguard.host}:{self.adguard.port}" + return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ @@ -207,4 +215,5 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( DATA_ADGUARD_VERSION ), + configuration_url=config_url, ) From 16eb85bfc85c65b6053bf224db6239fbee561b6d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:26:28 +0100 Subject: [PATCH 0891/1452] Fix slow config_flow test in squeezebox (#60373) --- tests/components/squeezebox/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index df6d27572b8..70f5f1233d7 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Logitech Squeezebox config flow.""" + from http import HTTPStatus from unittest.mock import patch @@ -215,7 +216,7 @@ async def test_dhcp_discovery_no_server_found(hass): with patch( "homeassistant.components.squeezebox.config_flow.async_discover", mock_failed_discover, - ): + ), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, From db0c82b88eb28bda03dfae9f56c1e707fced2fea Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:00:37 +0100 Subject: [PATCH 0892/1452] Fix slow config_flow test in Twinkly (#60374) --- tests/components/twinkly/test_config_flow.py | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index afbe608219f..46566bdf54b 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -11,23 +11,24 @@ from homeassistant.components.twinkly.const import ( DOMAIN as TWINKLY_DOMAIN, ) -from tests.components.twinkly import TEST_MODEL, ClientMock +from . import TEST_MODEL, ClientMock async def test_invalid_host(hass): """Test the failure when invalid host provided.""" - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_ENTRY_HOST: "dummy"}, - ) + client = ClientMock() + client.is_offline = True + with patch("twinkly_client.TwinklyClient", return_value=client): + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ENTRY_HOST: "dummy"}, + ) assert result["type"] == "form" assert result["step_id"] == "user" From b61375e5cb45d4e5e63d96a0a83da69a37619f6b Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:03:52 +0000 Subject: [PATCH 0893/1452] Add configuration_url to Waze Travel Time (#60376) --- homeassistant/components/waze_travel_time/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 7a37a012680..9df95daf9ee 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -169,6 +169,7 @@ class WazeTravelTime(SensorEntity): entry_type=DeviceEntryType.SERVICE, name="Waze", identifiers={(DOMAIN, DOMAIN)}, + configuration_url="https://www.waze.com", ) def __init__(self, unique_id, name, origin, destination, waze_data): From af8e1143b95befe908f5b455c8b5587879832b9e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:07:23 +0100 Subject: [PATCH 0894/1452] Fix slow config_flow test in upnp (#60377) --- tests/components/upnp/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 7c68bfb1db1..f8c6110db7e 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -189,7 +189,10 @@ async def ssdp_no_discovery(): ) as mock_register, patch( "homeassistant.components.ssdp.async_get_discovery_info_by_st", return_value=[], - ) as mock_get_info: + ) as mock_get_info, patch( + "homeassistant.components.upnp.config_flow.SSDP_SEARCH_TIMEOUT", + 0.1, + ): yield (mock_register, mock_get_info) From dab2b17a175a7da83c97032049958025743536bc Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:10:20 +0000 Subject: [PATCH 0895/1452] Added configuration_url for Met Eireann integration (#60380) --- homeassistant/components/met_eireann/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 51bfa5b360a..ae09bc5845e 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -192,4 +192,5 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): identifiers={(DOMAIN,)}, manufacturer="Met Éireann", model="Forecast", + configuration_url="https://www.met.ie", ) From 25f8d4a189036352df31f3f83eb97d2fed2b5efa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:13:27 +0100 Subject: [PATCH 0896/1452] Upgrade pylint to 2.12.1 (#60375) --- homeassistant/components/alert/__init__.py | 2 +- homeassistant/components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 2 +- homeassistant/components/demo/light.py | 2 +- .../components/devolo_home_network/config_flow.py | 1 - homeassistant/components/dhcp/__init__.py | 2 +- .../components/digital_ocean/binary_sensor.py | 2 +- homeassistant/components/digital_ocean/switch.py | 2 +- homeassistant/components/linode/binary_sensor.py | 2 +- homeassistant/components/linode/switch.py | 2 +- homeassistant/components/litejet/light.py | 2 +- homeassistant/components/litejet/scene.py | 2 +- homeassistant/components/litejet/switch.py | 2 +- homeassistant/components/london_air/sensor.py | 4 ++-- homeassistant/components/pushbullet/notify.py | 2 +- homeassistant/components/pushbullet/sensor.py | 10 +++++++--- homeassistant/components/serial_pm/sensor.py | 4 ++-- homeassistant/components/spotify/media_player.py | 2 +- .../components/xiaomi_miio/binary_sensor.py | 2 +- homeassistant/components/zabbix/sensor.py | 12 ++++++------ homeassistant/components/zwave/__init__.py | 2 +- .../components/zwave_js/discovery_data_template.py | 1 - pyproject.toml | 2 ++ requirements_test.txt | 2 +- 24 files changed, 36 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index e72a1bcffa4..e5ee7ee6911 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -211,7 +211,7 @@ class Alert(ToggleEntity): ) @property - def state(self): + def state(self): # pylint: disable=overridden-final-method """Return the alert status.""" if self._firing: if self._ack: diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 858c39c4db9..2ef4586f537 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BloomSkySensor(BinarySensorEntity): """Representation of a single binary sensor in a BloomSky device.""" - def __init__(self, bs, device, sensor_name): + def __init__(self, bs, device, sensor_name): # pylint: disable=invalid-name """Initialize a BloomSky binary sensor.""" self._bloomsky = bs self._device_id = device["DeviceID"] diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 288a1767c7e..9a14145d1d8 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -79,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BloomSkySensor(SensorEntity): """Representation of a single sensor in a BloomSky device.""" - def __init__(self, bs, device, sensor_name): + def __init__(self, bs, device, sensor_name): # pylint: disable=invalid-name """Initialize a BloomSky sensor.""" self._bloomsky = bs self._device_id = device["DeviceID"] diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 29e7e1395ee..d14f7ffba0e 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -103,7 +103,7 @@ class DemoLight(LightEntity): state, available=False, brightness=180, - ct=None, + ct=None, # pylint: disable=invalid-name effect_list=None, effect=None, hs_color=None, diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index c3e91a6ec65..05d88aa1045 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -83,7 +83,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["SN"]) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_HOST] = discovery_info[zeroconf.ATTR_HOST] self.context["title_placeholders"] = { PRODUCT: discovery_info[zeroconf.ATTR_PROPERTIES]["Product"], diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a45de7b0c16..65f1759b54e 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -62,7 +62,7 @@ _LOGGER = logging.getLogger(__name__) class DhcpServiceInfo(BaseServiceInfo): """Prepared info from dhcp entries.""" - ip: str # pylint: disable=invalid-name + ip: str hostname: str macaddress: str diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 172c50d20b2..af74e1ffb13 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DigitalOceanBinarySensor(BinarySensorEntity): """Representation of a Digital Ocean droplet sensor.""" - def __init__(self, do, droplet_id): + def __init__(self, do, droplet_id): # pylint: disable=invalid-name """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do self._droplet_id = droplet_id diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index b323d5be5b4..3ba60c4c457 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DigitalOceanSwitch(SwitchEntity): """Representation of a Digital Ocean droplet switch.""" - def __init__(self, do, droplet_id): + def __init__(self, do, droplet_id): # pylint: disable=invalid-name """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do self._droplet_id = droplet_id diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 4f602f5b81a..9e482283042 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -51,7 +51,7 @@ class LinodeBinarySensor(BinarySensorEntity): _attr_device_class = DEVICE_CLASS_MOVING - def __init__(self, li, node_id): + def __init__(self, li, node_id): # pylint: disable=invalid-name """Initialize a new Linode sensor.""" self._linode = li self._node_id = node_id diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index a2a31ae62a8..cd2efe9c4da 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LinodeSwitch(SwitchEntity): """Representation of a Linode Node switch.""" - def __init__(self, li, node_id): + def __init__(self, li, node_id): # pylint: disable=invalid-name """Initialize a new Linode sensor.""" self._linode = li self._node_id = node_id diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 172e46c441a..742cc341754 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" - def __init__(self, config_entry, lj, i, name): + def __init__(self, config_entry, lj, i, name): # pylint: disable=invalid-name """Initialize a LiteJet light.""" self._config_entry = config_entry self._lj = lj diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 2f2ab244e1e..8b285d58ef1 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LiteJetScene(Scene): """Representation of a single LiteJet scene.""" - def __init__(self, entry_id, lj, i, name): + def __init__(self, entry_id, lj, i, name): # pylint: disable=invalid-name """Initialize the scene.""" self._entry_id = entry_id self._lj = lj diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 343d8393f1c..0feabfbf864 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LiteJetSwitch(SwitchEntity): """Representation of a single LiteJet switch.""" - def __init__(self, entry_id, lj, i, name): + def __init__(self, entry_id, lj, i, name): # pylint: disable=invalid-name """Initialize a LiteJet switch.""" self._entry_id = entry_id self._lj = lj diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 4c9e7a4c4fb..fb522054c5f 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -94,10 +94,10 @@ class AirSensor(SensorEntity): ICON = "mdi:cloud-outline" - def __init__(self, name, APIdata): + def __init__(self, name, api_data): """Initialize the sensor.""" self._name = name - self._api_data = APIdata + self._api_data = api_data self._site_data = None self._state = None self._updated = None diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index a64517d2f48..6f851f8000e 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -41,7 +41,7 @@ def get_service(hass, config, discovery_info=None): class PushBulletNotificationService(BaseNotificationService): """Implement the notification service for Pushbullet.""" - def __init__(self, pb): + def __init__(self, pb): # pylint: disable=invalid-name """Initialize the service.""" self.pushbullet = pb self.pbtargets = {} diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index f7c946a91d9..e58296e56fe 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -95,7 +95,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class PushBulletNotificationSensor(SensorEntity): """Representation of a Pushbullet Sensor.""" - def __init__(self, pb, description: SensorEntityDescription): + def __init__( + self, + pb, # pylint: disable=invalid-name + description: SensorEntityDescription, + ): """Initialize the Pushbullet sensor.""" self.entity_description = description self.pushbullet = pb @@ -118,10 +122,10 @@ class PushBulletNotificationSensor(SensorEntity): class PushBulletNotificationProvider: """Provider for an account, leading to one or more sensors.""" - def __init__(self, pb): + def __init__(self, pushbullet): """Start to retrieve pushes from the given Pushbullet instance.""" - self.pushbullet = pb + self.pushbullet = pushbullet self._data = None self.listener = None self.thread = threading.Thread(target=self.retrieve_pushes) diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 9332f268308..bffb052f653 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -58,12 +58,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ParticulateMatterSensor(SensorEntity): """Representation of an Particulate matter sensor.""" - def __init__(self, pmDataCollector, name, pmname): + def __init__(self, pm_data_collector, name, pmname): """Initialize a new PM sensor.""" self._name = name self._pmname = pmname self._state = None - self._collector = pmDataCollector + self._collector = pm_data_collector @property def name(self): diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index efb5e0f6b4a..7f4865c21aa 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -234,7 +234,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self, session: OAuth2Session, spotify: Spotify, - me: dict, + me: dict, # pylint: disable=invalid-name user_id: str, name: str, ) -> None: diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index ef172bbbbc5..26fa82b7df2 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,9 +1,9 @@ """Support for Xiaomi Miio binary sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 1b3cf40eedd..36207842285 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -79,10 +79,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ZabbixTriggerCountSensor(SensorEntity): """Get the active trigger count for all Zabbix monitored hosts.""" - def __init__(self, zApi, name="Zabbix"): + def __init__(self, zapi, name="Zabbix"): """Initialize Zabbix sensor.""" self._name = name - self._zapi = zApi + self._zapi = zapi self._state = None self._attributes = {} @@ -121,9 +121,9 @@ class ZabbixTriggerCountSensor(SensorEntity): class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor): """Get the active trigger count for a single Zabbix monitored host.""" - def __init__(self, zApi, hostid, name=None): + def __init__(self, zapi, hostid, name=None): """Initialize Zabbix sensor.""" - super().__init__(zApi, name) + super().__init__(zapi, name) self._hostid = hostid if not name: self._name = self._zapi.host.get(hostids=self._hostid, output="extend")[0][ @@ -145,9 +145,9 @@ class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor): class ZabbixMultipleHostTriggerCountSensor(ZabbixTriggerCountSensor): """Get the active trigger count for specified Zabbix monitored hosts.""" - def __init__(self, zApi, hostids, name=None): + def __init__(self, zapi, hostids, name=None): """Initialize Zabbix sensor.""" - super().__init__(zApi, name) + super().__init__(zapi, name) self._hostids = hostids if not name: host_names = self._zapi.host.get(hostids=self._hostids, output="extend") diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index b5a77c82050..ef3ab223248 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -56,7 +56,7 @@ from .const import ( DOMAIN, ) from .discovery_schemas import DISCOVERY_SCHEMAS -from .migration import ( # noqa: F401 pylint: disable=unused-import +from .migration import ( # noqa: F401 async_add_migration_entity_value, async_get_migration_data, async_is_ozw_migrated, diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 77dd6de6ce3..3c5ef220ae9 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -444,7 +444,6 @@ class FanSpeedDataTemplate: Empty lists are not permissible. """ - # pylint: disable=no-self-use raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index 32c87227940..d5be195d2b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ good-names = [ "k", "Run", "T", + "ip", ] [tool.pylint."MESSAGES CONTROL"] @@ -113,6 +114,7 @@ score = false ignored-classes = [ "_CountingAttr", # for attrs ] +mixin-class-rgx = ".*[Mm]ix[Ii]n" [tool.pylint.FORMAT] expected-line-ending-format = "LF" diff --git a/requirements_test.txt b/requirements_test.txt index 7f7e36ac8d9..4e08d5b320f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 pre-commit==2.15.0 -pylint==2.11.1 +pylint==2.12.1 pipdeptree==2.2.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 22bdd385837d256c4e79cc008512d4bc3839c19e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:13:52 +0100 Subject: [PATCH 0897/1452] Use SsdpServiceInfo for SOURCE_SSDP tests (part 4) (#60339) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 20 ++- tests/components/denonavr/test_config_flow.py | 60 +++++---- tests/components/hue/test_config_flow.py | 104 ++++++++++----- .../components/konnected/test_config_flow.py | 123 +++++++++++------- tests/components/upnp/conftest.py | 24 ++-- tests/components/upnp/test_config_flow.py | 14 +- tests/components/wilight/__init__.py | 54 +++++--- tests/components/wilight/test_config_flow.py | 15 ++- 8 files changed, 265 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 298757eeccd..afcb2211ea6 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable +from collections.abc import Awaitable, Iterator from dataclasses import dataclass, field from datetime import timedelta from enum import Enum @@ -144,7 +144,7 @@ class SsdpServiceInfo( # Use a property if it is available, fallback to upnp data if hasattr(self, name): return getattr(self, name) - return self.upnp.get(name) + return self.upnp[name] def get(self, name: str, default: Any = None) -> Any: """ @@ -164,6 +164,22 @@ class SsdpServiceInfo( return getattr(self, name) return self.upnp.get(name, default) + def __iter__(self) -> Iterator[str]: + """ + Implement iter(self) on upnp data. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + "accessed discovery_info.__iter__() instead of discovery_info.upnp.__iter__(); this will fail in version 2022.6", + exclude_integrations={"ssdp"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + return self.upnp.__iter__() + @bind_hass async def async_register_callback( diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index b38c43775f9..ff811e0d235 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -300,12 +300,16 @@ async def test_config_flow_ssdp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, - ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL, - ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, - ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=TEST_SSDP_LOCATION, + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, + ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL, + ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, + }, + ), ) assert result["type"] == "form" @@ -336,12 +340,16 @@ async def test_config_flow_ssdp_not_denon(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_UPNP_MANUFACTURER: "NotSupported", - ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL, - ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, - ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=TEST_SSDP_LOCATION, + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER: "NotSupported", + ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL, + ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, + }, + ), ) assert result["type"] == "abort" @@ -357,10 +365,14 @@ async def test_config_flow_ssdp_missing_info(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, - ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=TEST_SSDP_LOCATION, + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, + }, + ), ) assert result["type"] == "abort" @@ -376,12 +388,16 @@ async def test_config_flow_ssdp_ignored_model(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, - ssdp.ATTR_UPNP_MODEL_NAME: TEST_IGNORED_MODEL, - ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, - ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=TEST_SSDP_LOCATION, + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER, + ssdp.ATTR_UPNP_MODEL_NAME: TEST_IGNORED_MODEL, + ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER, + }, + ), ) assert result["type"] == "abort" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 9d5fc280ff6..079c7db7d8b 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -333,11 +333,15 @@ async def test_bridge_ssdp(hass, mf_url, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url, - ssdp.ATTR_UPNP_SERIAL: "1234", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0/", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url, + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), ) assert result["type"] == "form" @@ -349,7 +353,11 @@ async def test_bridge_ssdp_discover_other_bridge(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, + ), ) assert result["type"] == "abort" @@ -361,12 +369,16 @@ async def test_bridge_ssdp_emulated_hue(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0/", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), ) assert result["type"] == "abort" @@ -378,10 +390,14 @@ async def test_bridge_ssdp_missing_location(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), ) assert result["type"] == "abort" @@ -393,10 +409,14 @@ async def test_bridge_ssdp_missing_serial(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0/", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + }, + ), ) assert result["type"] == "abort" @@ -408,12 +428,16 @@ async def test_bridge_ssdp_espalexa(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0/", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), ) assert result["type"] == "abort" @@ -430,11 +454,15 @@ async def test_bridge_ssdp_already_configured(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0/", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), ) assert result["type"] == "abort" @@ -587,11 +615,15 @@ async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.1.1.1/", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff", + }, + ), ) assert result["type"] == "abort" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 466359c3dc3..7397f03c1fc 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import konnected +from homeassistant.components import konnected, ssdp from homeassistant.components.konnected import config_flow from tests.common import MockConfigEntry @@ -113,11 +113,15 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ), ) assert result["type"] == "form" @@ -134,11 +138,15 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ), ) assert result["type"] == "abort" @@ -149,9 +157,12 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={}, + ), ) assert result["type"] == "abort" @@ -161,11 +172,15 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - "manufacturer": "SHOULD_FAIL", - "modelName": config_flow.KONN_MODEL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={ + "manufacturer": "SHOULD_FAIL", + "modelName": config_flow.KONN_MODEL, + }, + ), ) assert result["type"] == "abort" @@ -175,11 +190,15 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": "SHOULD_FAIL", - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": "SHOULD_FAIL", + }, + ), ) assert result["type"] == "abort" @@ -195,11 +214,15 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.2.3.4:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.2.3.4:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ), ) assert result["type"] == "abort" @@ -317,11 +340,15 @@ async def test_import_ssdp_host_user_finish(hass, mock_panel): ssdp_result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://0.0.0.0:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL_PRO, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL_PRO, + }, + ), ) assert ssdp_result["type"] == "abort" assert ssdp_result["reason"] == "already_in_progress" @@ -360,11 +387,15 @@ async def test_ssdp_already_configured(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://0.0.0.0:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL_PRO, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://0.0.0.0:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL_PRO, + }, + ), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -436,11 +467,15 @@ async def test_ssdp_host_update(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - "ssdp_location": "http://1.1.1.1:1234/Device.xml", - "manufacturer": config_flow.KONN_MANUFACTURER, - "modelName": config_flow.KONN_MODEL_PRO, - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://1.1.1.1:1234/Device.xml", + upnp={ + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL_PRO, + }, + ), ) assert result["type"] == "abort" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index f8c6110db7e..6f6f2905869 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -30,17 +30,19 @@ 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, -} +TEST_DISCOVERY = ssdp.SsdpServiceInfo( + ssdp_usn=TEST_USN, + ssdp_st=TEST_ST, + ssdp_location=TEST_LOCATION, + upnp={ + ssdp.ATTR_UPNP_UDN: TEST_UDN, + "usn": TEST_USN, + "location": TEST_LOCATION, + "_host": TEST_HOSTNAME, + "_udn": TEST_UDN, + "friendlyName": TEST_FRIENDLY_NAME, + }, +) class MockDevice: diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index a704232ef84..0771e51f890 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -66,12 +66,14 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - 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. - }, + data=ssdp.SsdpServiceInfo( + ssdp_usn=TEST_USN, + ssdp_st=TEST_ST, + ssdp_location=TEST_LOCATION, + upnp={ + # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. + }, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_discovery" diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index dd7d83876f8..dbcfbbaaa8c 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -2,8 +2,8 @@ from pywilight.const import DOMAIN +from homeassistant.components import ssdp from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME, ATTR_UPNP_MODEL_NUMBER, @@ -33,28 +33,40 @@ UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" UPNP_MANUFACTURER_NOT_WILIGHT = "Test" CONF_COMPONENTS = "components" -MOCK_SSDP_DISCOVERY_INFO_P_B = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, - ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL: UPNP_SERIAL, -} +MOCK_SSDP_DISCOVERY_INFO_P_B = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={ + ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: UPNP_SERIAL, + }, +) -MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT, - ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, - ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, -} +MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={ + ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, + }, +) -MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, - ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, -} +MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_LOCATION, + upnp={ + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, + }, +) async def setup_integration( diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 4835167715d..326224b02c9 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WiLight config flow.""" +import dataclasses from unittest.mock import patch import pytest @@ -55,7 +56,7 @@ def mock_dummy_get_components_from_model_wrong(): async def test_show_ssdp_form(hass: HomeAssistant) -> None: """Test that the ssdp confirmation form is served.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_P_B) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -71,7 +72,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -83,7 +84,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -97,7 +98,7 @@ async def test_ssdp_not_wilight_abort_3( ) -> None: """Test that the ssdp aborts not_wilight.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_P_B) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -111,7 +112,7 @@ async def test_ssdp_not_supported_abort( ) -> None: """Test that the ssdp aborts not_supported.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_P_B) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -134,7 +135,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_P_B) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, @@ -148,7 +149,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: """Test the full SSDP flow from start to finish.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO_P_B) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) From c9dc3a61af14259485006e8d6b3826ffa62a69ae Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:14:15 +0100 Subject: [PATCH 0898/1452] Fix slow config_flow test in devolo Home Network (#60364) --- .../devolo_home_network/test_config_flow.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 0be07be9a00..1af3c39bca2 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -102,11 +102,15 @@ async def test_zeroconf(hass: HomeAssistant): == DISCOVERY_INFO["hostname"].split(".", maxsplit=1)[0] ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() assert result2["title"] == "test" assert result2["data"] == { @@ -131,13 +135,17 @@ async def test_abort_if_configued(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_IP_ADDRESS: IP, - }, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: IP, + }, + ) + await hass.async_block_till_done() # Abort on concurrent user flow result = await hass.config_entries.flow.async_init( From fabc55cbc401a9d9b66c887e4ee87fea039ea57b Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:14:55 +0000 Subject: [PATCH 0899/1452] Added configuration_url to Met.no integration (#60378) --- homeassistant/components/met/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 43aeaad5872..53c372030f1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -286,4 +286,5 @@ class MetWeather(CoordinatorEntity, WeatherEntity): identifiers={(DOMAIN,)}, # type: ignore[arg-type] manufacturer="Met.no", model="Forecast", + configuration_url="https://www.met.no/en", ) From 78b47019f96eb0b6ee15d059e02cfd2ec81c157d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Nov 2021 00:12:49 +0000 Subject: [PATCH 0900/1452] [ci skip] Translation update --- .../components/adguard/translations/ja.json | 1 + .../components/almond/translations/ja.json | 1 + .../components/balboa/translations/ca.json | 28 +++++++++++ .../components/balboa/translations/de.json | 28 +++++++++++ .../components/balboa/translations/en.json | 46 +++++++++---------- .../components/bond/translations/ja.json | 3 +- .../components/braviatv/translations/ja.json | 4 +- .../cloudflare/translations/ja.json | 6 ++- .../components/deconz/translations/ja.json | 1 + .../components/elgato/translations/ja.json | 1 + .../forked_daapd/translations/ja.json | 1 + .../components/fronius/translations/nl.json | 20 ++++++++ .../components/gios/translations/ja.json | 3 ++ .../components/homekit/translations/ja.json | 7 ++- .../homekit_controller/translations/ja.json | 13 ++++++ .../hvv_departures/translations/ja.json | 15 +++++- .../components/insteon/translations/ja.json | 26 +++++++++-- .../components/knx/translations/nl.json | 13 ++++++ .../components/konnected/translations/nl.json | 1 + .../components/konnected/translations/ru.json | 1 + .../lutron_caseta/translations/id.json | 6 +++ .../components/motioneye/translations/ja.json | 2 +- .../components/mqtt/translations/ja.json | 2 +- .../components/mysensors/translations/id.json | 1 + .../components/mysensors/translations/ja.json | 9 ++++ .../components/nest/translations/ja.json | 2 +- .../components/netatmo/translations/id.json | 1 + .../nfandroidtv/translations/id.json | 3 +- .../nmap_tracker/translations/id.json | 12 +++++ .../components/nzbget/translations/ja.json | 9 ++++ .../components/onewire/translations/ja.json | 6 ++- .../components/onvif/translations/id.json | 6 ++- .../opengarage/translations/id.json | 1 + .../components/openuv/translations/id.json | 4 ++ .../panasonic_viera/translations/ja.json | 2 +- .../philips_js/translations/ja.json | 2 +- .../components/plugwise/translations/ja.json | 6 ++- .../pvpc_hourly_pricing/translations/id.json | 15 ++++++ .../rainforest_eagle/translations/id.json | 4 +- .../components/rdw/translations/id.json | 10 +++- .../components/remote/translations/ja.json | 15 ++++++ .../components/renault/translations/id.json | 1 + .../components/roon/translations/ja.json | 3 +- .../components/samsungtv/translations/ja.json | 1 + .../components/sensor/translations/ja.json | 10 +++- .../components/shelly/translations/id.json | 3 ++ .../simplisafe/translations/id.json | 4 +- .../components/smappee/translations/ja.json | 3 ++ .../speedtestdotnet/translations/ja.json | 14 +++++- .../components/spotify/translations/ja.json | 1 + .../stookalert/translations/id.json | 7 +++ .../components/switchbot/translations/id.json | 1 + .../components/syncthing/translations/id.json | 1 + .../components/tile/translations/ja.json | 3 +- .../components/tolo/translations/ca.json | 23 ++++++++++ .../components/tolo/translations/de.json | 23 ++++++++++ .../components/tolo/translations/et.json | 23 ++++++++++ .../components/tolo/translations/id.json | 23 ++++++++++ .../components/tolo/translations/ja.json | 23 ++++++++++ .../components/tolo/translations/nl.json | 23 ++++++++++ .../components/tolo/translations/no.json | 23 ++++++++++ .../components/tolo/translations/ru.json | 23 ++++++++++ .../tolo/translations/select.ca.json | 8 ++++ .../tolo/translations/select.de.json | 8 ++++ .../components/tolo/translations/zh-Hant.json | 23 ++++++++++ .../totalconnect/translations/id.json | 4 +- .../components/tplink/translations/id.json | 11 ++++- .../components/tractive/translations/id.json | 1 + .../components/tradfri/translations/id.json | 1 + .../transmission/translations/ja.json | 1 + .../components/tuya/translations/ja.json | 9 +++- .../uptimerobot/translations/id.json | 6 ++- .../components/vera/translations/ja.json | 3 +- .../components/vizio/translations/ja.json | 3 +- .../components/wilight/translations/ja.json | 4 +- .../wled/translations/select.nl.json | 9 ++++ .../components/wolflink/translations/ja.json | 6 ++- .../wolflink/translations/sensor.id.json | 3 ++ .../wolflink/translations/sensor.ja.json | 2 + 79 files changed, 614 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/balboa/translations/ca.json create mode 100644 homeassistant/components/balboa/translations/de.json create mode 100644 homeassistant/components/fronius/translations/nl.json create mode 100644 homeassistant/components/knx/translations/nl.json create mode 100644 homeassistant/components/tolo/translations/ca.json create mode 100644 homeassistant/components/tolo/translations/de.json create mode 100644 homeassistant/components/tolo/translations/et.json create mode 100644 homeassistant/components/tolo/translations/id.json create mode 100644 homeassistant/components/tolo/translations/ja.json create mode 100644 homeassistant/components/tolo/translations/nl.json create mode 100644 homeassistant/components/tolo/translations/no.json create mode 100644 homeassistant/components/tolo/translations/ru.json create mode 100644 homeassistant/components/tolo/translations/select.ca.json create mode 100644 homeassistant/components/tolo/translations/select.de.json create mode 100644 homeassistant/components/tolo/translations/zh-Hant.json create mode 100644 homeassistant/components/wled/translations/select.nl.json diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index 0ea8436d990..b06b18d61e8 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -9,6 +9,7 @@ }, "step": { "hassio_confirm": { + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308bAdGuard Home\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAdGuard Home" }, "user": { diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index 807df670b92..c91407e7ecf 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -8,6 +8,7 @@ }, "step": { "hassio_confirm": { + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308b\u3001Almond\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAlmond" }, "pick_implementation": { diff --git a/homeassistant/components/balboa/translations/ca.json b/homeassistant/components/balboa/translations/ca.json new file mode 100644 index 00000000000..139f706c878 --- /dev/null +++ b/homeassistant/components/balboa/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Connexi\u00f3 amb dispositiu Wi-Fi Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Mantingues l'hora del client Balboa Spa sincronitzada amb Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/de.json b/homeassistant/components/balboa/translations/de.json new file mode 100644 index 00000000000..7b5961040e7 --- /dev/null +++ b/homeassistant/components/balboa/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Verbinde dich mit dem Balboa Wi-Fi Ger\u00e4t" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Synchronisiere die Zeit deines Balboa Spa-Clients mit Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/en.json b/homeassistant/components/balboa/translations/en.json index 15d8aa2b47e..bad5167fc5e 100644 --- a/homeassistant/components/balboa/translations/en.json +++ b/homeassistant/components/balboa/translations/en.json @@ -1,28 +1,28 @@ { - "config": { - "step": { - "user": { - "title": "Connect to the Balboa Wi-Fi device", - "data": { - "host": "Host" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Connect to the Balboa Wi-Fi device" + } } - } }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "data": { - "sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant" + "options": { + "step": { + "init": { + "data": { + "sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant" + } + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json index 47c72db38a5..c9c41c61e43 100644 --- a/homeassistant/components/bond/translations/ja.json +++ b/homeassistant/components/bond/translations/ja.json @@ -13,7 +13,8 @@ "confirm": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" - } + }, + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index dbaaef07641..737af228a37 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -22,7 +22,7 @@ "host": "\u30db\u30b9\u30c8" }, "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Sony Bravia TV" + "title": "Sony Bravia\u30c6\u30ec\u30d3" } } }, @@ -32,7 +32,7 @@ "data": { "ignored_sources": "\u7121\u8996\u3055\u308c\u305f\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8" }, - "title": "Sony Bravia TV\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + "title": "Sony Bravia \u30c6\u30ec\u30d3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 85e39392a9c..430a979d6bd 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -21,12 +21,14 @@ "records": { "data": { "records": "\u30ec\u30b3\u30fc\u30c9" - } + }, + "title": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3059\u308b\u30ec\u30b3\u30fc\u30c9\u3092\u9078\u629e" }, "user": { "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" - } + }, + "title": "Cloudflare\u306b\u63a5\u7d9a" }, "zone": { "data": { diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 57fa9849784..b8d54820183 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -13,6 +13,7 @@ "flow_title": "{host}", "step": { "hassio_confirm": { + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308bdeCONZ gateway\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306e\u3001deCONZ Zigbee gateway" }, "link": { diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index 88196db6d69..830ad27b671 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -17,6 +17,7 @@ "description": "Elgato Key Light\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" }, "zeroconf_confirm": { + "description": "\u30b7\u30ea\u30a2\u30eb\u756a\u53f7 '{serial_number}' \u306e\u3001Elgato Light\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "Elgato Light device\u3092\u767a\u898b" } } diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 3490e61f98c..669646a4ef0 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { + "forbidden": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002forked-daapd network\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/fronius/translations/nl.json b/homeassistant/components/fronius/translations/nl.json new file mode 100644 index 00000000000..a6aa710148b --- /dev/null +++ b/homeassistant/components/fronius/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Configureer het IP-adres of de lokale hostnaam van uw Fronius-apparaat.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index 7d5f1e2dc27..8145ba9d64d 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 6a8c3d81d7e..d7481b4d011 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "port_name_in_use": "\u540c\u3058\u540d\u524d\u3084\u30dd\u30fc\u30c8\u3092\u6301\u3064\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3084\u30d6\u30ea\u30c3\u30b8\u304c\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, "step": { "pairing": { "description": "\u201cHomeKit Pairing\u201d\u306e\"\u901a\u77e5\"\u306e\u6307\u793a\u306b\u5f93\u3063\u3066\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002", @@ -9,6 +12,7 @@ "data": { "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3" }, + "description": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u5185\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc \u30e2\u30fc\u30c9\u306e\u5225\u306e \u3001HomeKit\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u3001\u5404 \u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2 \u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3 \u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30fc\u30c8\u3001\u30ed\u30c3\u30af\u3001\u304a\u3088\u3073\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u306e\u9078\u629e" } } @@ -17,6 +21,7 @@ "step": { "advanced": { "data": { + "auto_start": "\u81ea\u52d5\u8d77\u52d5(homekit.start\u30b5\u30fc\u30d3\u30b9\u3092\u624b\u52d5\u3067\u547c\u3073\u51fa\u3059\u5834\u5408\u306f\u7121\u52b9\u306b\u3059\u308b)", "devices": "\u30c7\u30d0\u30a4\u30b9(\u30c8\u30ea\u30ac\u30fc)" }, "description": "\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9\u3054\u3068\u306b\u3001\u30d7\u30ed\u30b0\u30e9\u30e0\u53ef\u80fd\u306a\u30b9\u30a4\u30c3\u30c1\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u304c\u767a\u751f\u3059\u308b\u3068\u3001HomeKit\u306f\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3084\u30b7\u30fc\u30f3\u3092\u5b9f\u884c\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002" @@ -33,7 +38,7 @@ "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "mode": "\u30e2\u30fc\u30c9" }, - "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001TV\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001\u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 960b0eff659..a6b675cb131 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -33,6 +33,19 @@ } }, "device_automation": { + "trigger_subtype": { + "button1": "\u30dc\u30bf\u30f31", + "button10": "\u30dc\u30bf\u30f310", + "button2": "\u30dc\u30bf\u30f32", + "button3": "\u30dc\u30bf\u30f33", + "button4": "\u30dc\u30bf\u30f34", + "button5": "\u30dc\u30bf\u30f35", + "button6": "\u30dc\u30bf\u30f36", + "button7": "\u30dc\u30bf\u30f37", + "button8": "\u30dc\u30bf\u30f38", + "button9": "\u30dc\u30bf\u30f39", + "doorbell": "\u30c9\u30a2\u30d9\u30eb" + }, "trigger_type": { "double_press": "\"{subtype}\" \u30922\u56de\u62bc\u3059", "long_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u305f\u307e\u307e", diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json index 6d28f551904..030a6a99725 100644 --- a/homeassistant/components/hvv_departures/translations/ja.json +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -8,12 +8,25 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "station": { + "data": { + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3/\u30a2\u30c9\u30ec\u30b9" + }, + "title": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3/\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b" + }, + "station_select": { + "data": { + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3/\u30a2\u30c9\u30ec\u30b9" + }, + "title": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3/\u30a2\u30c9\u30ec\u30b9\u306e\u9078\u629e" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "HVV API\u306b\u63a5\u7d9a" } } }, diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 8d36bf959f8..4699565cae2 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -13,7 +13,9 @@ "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" - } + }, + "description": "Insteon Hub Version 1(2014\u4ee5\u524d)\u306e\u8a2d\u5b9a\u3092\u884c\u3044\u307e\u3059\u3002", + "title": "Insteon Hub Version 1" }, "hubv2": { "data": { @@ -21,7 +23,9 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "Insteon Hub Version 2\u306e\u8a2d\u5b9a\u3092\u884c\u3044\u307e\u3059\u3002", + "title": "Insteon Hub Version 2" }, "plm": { "data": { @@ -33,6 +37,7 @@ "data": { "modem_type": "\u30e2\u30c7\u30e0\u306e\u7a2e\u985e\u3002" }, + "description": "Insteon modem type\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "Insteon" } } @@ -44,6 +49,11 @@ }, "step": { "add_override": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9(\u4f8b: 1a2b3c)", + "cat": "\u30c7\u30d0\u30a4\u30b9\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x10)", + "subcat": "\u30c7\u30d0\u30a4\u30b9 \u30b5\u30d6\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x0a)" + }, "title": "Insteon" }, "add_x10": { @@ -65,14 +75,24 @@ }, "init": { "data": { - "add_x10": "X10 \u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + "add_x10": "X10 \u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002", + "change_hub_config": "Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059\u3002", + "remove_x10": "X10\u30c7\u30d0\u30a4\u30b9\u524a\u9664\u3092\u524a\u9664\u3057\u307e\u3059\u3002" }, + "description": "\u8a2d\u5b9a\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "Insteon" }, "remove_override": { + "data": { + "address": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e" + }, "title": "Insteon" }, "remove_x10": { + "data": { + "address": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e" + }, + "description": "X10\u30c7\u30d0\u30a4\u30b9\u306e\u524a\u9664", "title": "Insteon" } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json new file mode 100644 index 00000000000..ac8a07a747a --- /dev/null +++ b/homeassistant/components/knx/translations/nl.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "tunnel": { + "data": { + "host": "Host", + "port": "Poort", + "route_back": "Route Back / NAT Mode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 0387dc8c7b0..1680431a35b 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken", "not_konn_panel": "Geen herkend Konnected.io apparaat", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index 4357c924572..79cc78e6a9a 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -3,6 +3,7 @@ "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.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/lutron_caseta/translations/id.json b/homeassistant/components/lutron_caseta/translations/id.json index 409cea59060..7789d784d23 100644 --- a/homeassistant/components/lutron_caseta/translations/id.json +++ b/homeassistant/components/lutron_caseta/translations/id.json @@ -55,6 +55,12 @@ "open_3": "Buka 3", "open_4": "Buka 4", "open_all": "Buka semua", + "raise": "Angkat", + "raise_1": "Angkat 1", + "raise_2": "Angkat 2", + "raise_3": "Angkat 3", + "raise_4": "Angkat 4", + "raise_all": "Angkat semua", "stop": "Hentikan (favorit)", "stop_1": "Hentikan 1", "stop_2": "Hentikan 2", diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 311ad7d0957..5c0dfa7df8a 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "\u30a2\u30c9\u30aa\u30f3: {addon}\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u308b\u3001motionEye\u30b5\u30fc\u30d3\u30b9\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306b\u3001Home Assistant\u3092\u8a2d\u3057\u307e\u3059\u304b\uff1f", + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308bmotionEye service\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306emotionEye" }, "user": { diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 7d1c1cac97f..8b4c83306b1 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -22,7 +22,7 @@ "data": { "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b" }, - "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u63d0\u4f9b\u3059\u308bMQTT broker\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", + "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308bMQTT broker\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", "title": "HomeAssistant\u30a2\u30c9\u30aa\u30f3\u3092\u4ecb\u3057\u305fMQTT Broker" } } diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index eae16a87784..93b10bb8c2c 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -36,6 +36,7 @@ "mqtt_required": "Integrasi MQTT belum disiapkan", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", + "same_topic": "Topik subscribe dan publish sama", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 1cf50abc3a9..9ffdac2832b 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -8,11 +8,15 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_device": "\u7121\u52b9\u306a\u30c7\u30d0\u30a4\u30b9", "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_persistence_file": "\u7121\u52b9\u306a\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", + "invalid_publish_topic": "\u30d1\u30d6\u30ea\u30c3\u30af \u30c8\u30d4\u30c3\u30af\u304c\u7121\u52b9\u3067\u3059", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -25,11 +29,14 @@ "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", "invalid_persistence_file": "\u7121\u52b9\u306a\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb", "invalid_port": "\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7", + "invalid_publish_topic": "\u30d1\u30d6\u30ea\u30c3\u30af \u30c8\u30d4\u30c3\u30af\u304c\u7121\u52b9\u3067\u3059", "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", + "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { @@ -37,6 +44,8 @@ "data": { "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "retain": "mqtt retain(\u4fdd\u6301)", + "topic_in_prefix": "\u30a4\u30f3\u30d7\u30c3\u30c8 \u30c8\u30d4\u30c3\u30af\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(topic_in_prefix)", + "topic_out_prefix": "\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8 \u30c8\u30d4\u30c3\u30af\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(topic_out_prefix)", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, "description": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 39608c80465..77fb72e6b9e 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -43,7 +43,7 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Nest \u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/netatmo/translations/id.json b/homeassistant/components/netatmo/translations/id.json index bcdd35b0152..a71d2f3412e 100644 --- a/homeassistant/components/netatmo/translations/id.json +++ b/homeassistant/components/netatmo/translations/id.json @@ -23,6 +23,7 @@ "device_automation": { "trigger_subtype": { "away": "keluar", + "hg": "perlindungan kebekuan", "schedule": "jadwal" }, "trigger_type": { diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json index c7d0ba20f4e..fd70679d5a4 100644 --- a/homeassistant/components/nfandroidtv/translations/id.json +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -13,7 +13,8 @@ "host": "Host", "name": "Nama" }, - "description": "Integrasi ini memerlukan aplikasi Notifikasi untuk Android TV.\n\nUntuk Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nUntuk Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nAnda harus mengatur reservasi DHCP di router Anda (lihat manual pengguna router Anda) atau alamat IP statis pada perangkat. Jika tidak, perangkat akhirnya akan menjadi tidak tersedia." + "description": "Integrasi ini memerlukan aplikasi Notifikasi untuk Android TV.\n\nUntuk Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nUntuk Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nAnda harus mengatur reservasi DHCP di router Anda (lihat manual pengguna router Anda) atau alamat IP statis pada perangkat. Jika tidak, perangkat akhirnya akan menjadi tidak tersedia.", + "title": "Notifikasi untuk Android TV/Fire TV" } } } diff --git a/homeassistant/components/nmap_tracker/translations/id.json b/homeassistant/components/nmap_tracker/translations/id.json index b9cd85326ae..e1894aff49b 100644 --- a/homeassistant/components/nmap_tracker/translations/id.json +++ b/homeassistant/components/nmap_tracker/translations/id.json @@ -3,9 +3,15 @@ "abort": { "already_configured": "Lokasi sudah dikonfigurasi" }, + "error": { + "invalid_hosts": "Host Tidak Valid" + }, "step": { "user": { "data": { + "exclude": "Alamat jaringan (dipisahkan koma) untuk dikecualikan dari pemindaian", + "home_interval": "Jumlah menit minimum antara pemindaian perangkat aktif (menghemat baterai)", + "hosts": "Alamat jaringan (dipisahkan koma) untuk pemindaian", "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap" }, "description": "Konfigurasikan host untuk dipindai oleh Nmap. Alamat jaringan dan pengecualian dapat berupa alamat IP (192.168.1.1), jaringan IP (192.168.0.0/24), atau rentang IP (192.168.1.0-32)." @@ -13,10 +19,16 @@ } }, "options": { + "error": { + "invalid_hosts": "Host Tidak Valid" + }, "step": { "init": { "data": { "consider_home": "Waktu tunggu dalam detik untuk pelacak perangkat agar mempertimbangkan perangkat sebagai 'tidak di rumah' jika tidak ditemukan", + "exclude": "Alamat jaringan (dipisahkan koma) untuk dikecualikan dari pemindaian", + "home_interval": "Jumlah menit minimum antara pemindaian perangkat aktif (menghemat baterai)", + "hosts": "Alamat jaringan (dipisahkan koma) untuk pemindaian", "interval_seconds": "Interval pindai", "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap", "track_new_devices": "Lacak perangkat baru" diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index fd00b05cd2c..248bce2b77d 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -22,5 +22,14 @@ "title": "NZBGet\u306b\u63a5\u7d9a" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u5ea6(\u79d2)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 3213745d584..4078af9a0a3 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -4,14 +4,16 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "invalid_path": "\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "step": { "owserver": { "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - } + }, + "title": "owserver\u306e\u8a73\u7d30\u8a2d\u5b9a" }, "user": { "data": { diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json index 6fcb49dcd99..77e1353270f 100644 --- a/homeassistant/components/onvif/translations/id.json +++ b/homeassistant/components/onvif/translations/id.json @@ -25,7 +25,8 @@ "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" - } + }, + "title": "Konfigurasikan perangkat ONVIF" }, "configure_profile": { "data": { @@ -49,6 +50,9 @@ "title": "Konfigurasikan perangkat ONVIF" }, "user": { + "data": { + "auto": "Cari secara otomatis" + }, "description": "Dengan mengklik kirim, kami akan mencari perangkat ONVIF pada jaringan Anda yang mendukung Profil S.\n\nBeberapa produsen mulai menonaktifkan ONVIF secara default. Pastikan ONVIF diaktifkan dalam konfigurasi kamera Anda.", "title": "Penyiapan perangkat ONVIF" } diff --git a/homeassistant/components/opengarage/translations/id.json b/homeassistant/components/opengarage/translations/id.json index 49f5e6b2a75..d8cd6a0e66e 100644 --- a/homeassistant/components/opengarage/translations/id.json +++ b/homeassistant/components/opengarage/translations/id.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "device_key": "Kunci perangkat", "host": "Host", "port": "Port", "verify_ssl": "Verifikasi sertifikat SSL" diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 8851a532b99..568e92d1999 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -21,6 +21,10 @@ "options": { "step": { "init": { + "data": { + "from_window": "Indeks UV awal untuk jendela perlindungan", + "to_window": "Indeks UV akhir untuk jendela perlindungan" + }, "title": "Konfigurasikan OpenUV" } } diff --git a/homeassistant/components/panasonic_viera/translations/ja.json b/homeassistant/components/panasonic_viera/translations/ja.json index cdc07f9b45c..381978f5cbd 100644 --- a/homeassistant/components/panasonic_viera/translations/ja.json +++ b/homeassistant/components/panasonic_viera/translations/ja.json @@ -22,7 +22,7 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9", "name": "\u540d\u524d" }, - "description": "\u30d1\u30ca\u30bd\u30cb\u30c3\u30af \u30d3\u30a8\u30e9TV\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "Panasonic Viera TV\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u30c6\u30ec\u30d3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index de14fbe14d0..3e7976af153 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "\u30c6\u30ec\u30d3\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u30da\u30a2" }, "user": { diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index d3bc59f75a1..f7e89c00f6a 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -14,7 +14,8 @@ "data": { "flow_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" }, - "description": "\u30d7\u30ed\u30c0\u30af\u30c8:" + "description": "\u30d7\u30ed\u30c0\u30af\u30c8:", + "title": "Plugwise type" }, "user_gateway": { "data": { @@ -33,7 +34,8 @@ "init": { "data": { "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" - } + }, + "description": "Plugwise\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json index 9a8a18a7543..5705a2a4fcb 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/id.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -7,6 +7,21 @@ "user": { "data": { "name": "Nama Sensor", + "power": "Daya terkontrak (kW)", + "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", + "tariff": "Tarif yang berlaku menurut zona geografis" + }, + "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Penyiapan sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Daya terkontrak (kW)", + "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", "tariff": "Tarif yang berlaku menurut zona geografis" }, "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", diff --git a/homeassistant/components/rainforest_eagle/translations/id.json b/homeassistant/components/rainforest_eagle/translations/id.json index 80db8f3182d..06e9c5644b6 100644 --- a/homeassistant/components/rainforest_eagle/translations/id.json +++ b/homeassistant/components/rainforest_eagle/translations/id.json @@ -11,7 +11,9 @@ "step": { "user": { "data": { - "host": "Host" + "cloud_id": "ID cloud", + "host": "Host", + "install_code": "Kode instalasi" } } } diff --git a/homeassistant/components/rdw/translations/id.json b/homeassistant/components/rdw/translations/id.json index a8a19d90504..2fbe6f9509f 100644 --- a/homeassistant/components/rdw/translations/id.json +++ b/homeassistant/components/rdw/translations/id.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "unknown_license_plate": "Plat nomor tidak dikenal" + }, + "step": { + "user": { + "data": { + "license_plate": "Plat nomor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ja.json b/homeassistant/components/remote/translations/ja.json index 3b55598aaba..4669a228b1a 100644 --- a/homeassistant/components/remote/translations/ja.json +++ b/homeassistant/components/remote/translations/ja.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u30c8\u30b0\u30eb {entity_name}", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" + }, + "trigger_type": { + "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/renault/translations/id.json b/homeassistant/components/renault/translations/id.json index e1b1f3fc893..3fb9f05cd0c 100644 --- a/homeassistant/components/renault/translations/id.json +++ b/homeassistant/components/renault/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "kamereon_no_account": "Tidak dapat menemukan akun Kamereon", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 03a5d275802..dba8ccc9237 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -15,7 +15,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "Roon server\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3001 \u30db\u30b9\u30c8\u540d\u307e\u305f\u306f\u3001IP\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 8cbb202fcef..f57daa22df8 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -17,6 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { + "description": "\u30c7\u30d0\u30a4\u30b9}\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", "title": "Samsung TV" }, "reauth_confirm": { diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 9d002ebe8af..1cf40e43e8b 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -3,6 +3,8 @@ "condition_type": { "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_current": "\u73fe\u5728\u306e {entity_name} \u96fb\u6d41", + "is_energy": "\u73fe\u5728\u306e {entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc", "is_frequency": "\u73fe\u5728\u306e {entity_name} \u983b\u5ea6(frequency)", "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -14,11 +16,14 @@ "is_pm25": "\u73fe\u5728\u306e {entity_name} PM2.5\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", - "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb" + "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_voltage": "\u73fe\u5728\u306e {entity_name} \u96fb\u5727" }, "trigger_type": { "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", + "current": "{entity_name} \u73fe\u5728\u306e\u5909\u5316", + "energy": "{entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc\u306e\u5909\u5316", "frequency": "{entity_name} \u983b\u5ea6(frequency)\u306e\u5909\u66f4", "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", @@ -30,7 +35,8 @@ "pm25": "{entity_name} PM2.5\u6fc3\u5ea6\u306e\u5909\u5316", "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", - "volatile_organic_compounds": "{entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u6fc3\u5ea6\u306e\u5909\u5316" + "volatile_organic_compounds": "{entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u6fc3\u5ea6\u306e\u5909\u5316", + "voltage": "{entity_name} \u96fb\u5727\u306e\u5909\u5316" } }, "state": { diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index f4ddf62aa21..9ed75694de0 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -37,7 +37,10 @@ "button4": "Tombol keempat" }, "trigger_type": { + "btn_down": "Tombol \"{subtype}\" ditekan", + "btn_up": "Tombol \"{subtype}\" dilepas", "double": "{subtype} diklik dua kali", + "double_push": "Push ganda {subtype}", "long": "{subtype} diklik lama", "long_push": "Push lama {subtype}", "long_single": "{subtype} diklik lama kemudian diklik sekali", diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 910798405a6..a1be0833ba4 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, "error": { "identifier_exists": "Akun sudah terdaftar", @@ -15,6 +16,7 @@ "data": { "auth_code": "Kode Otorisasi" }, + "description": "Masukkan kode otorisasi dari URL aplikasi web SimpliSafe:", "title": "Selesaikan Otorisasi" }, "mfa": { diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 723b8fb9b2e..c481989b6d5 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -22,6 +22,9 @@ }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "zeroconf_confirm": { + "title": "Smappee device\u3092\u767a\u898b" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index c9c3cc04633..4e254367593 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -1,12 +1,24 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "wrong_server_id": "\u30b5\u30fc\u30d0\u30fcID\u304c\u7121\u52b9\u3067\u3059" }, "step": { "user": { "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "\u81ea\u52d5\u66f4\u65b0\u3092\u7121\u52b9\u306b\u3059\u308b", + "scan_interval": "\u66f4\u65b0\u983b\u5ea6(\u5206)", + "server_name": "\u30c6\u30b9\u30c8\u30b5\u30fc\u30d0\u30fc\u306e\u9078\u629e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 47749c58585..d5304c1db0d 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -12,6 +12,7 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { + "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/stookalert/translations/id.json b/homeassistant/components/stookalert/translations/id.json index fa8cd415378..a5af255739a 100644 --- a/homeassistant/components/stookalert/translations/id.json +++ b/homeassistant/components/stookalert/translations/id.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "province": "Provinsi" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index 02f90ba89c3..4619f421811 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -3,6 +3,7 @@ "abort": { "already_configured_device": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi.", "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/syncthing/translations/id.json b/homeassistant/components/syncthing/translations/id.json index 2aa4f701c95..d8f2a76c41d 100644 --- a/homeassistant/components/syncthing/translations/id.json +++ b/homeassistant/components/syncthing/translations/id.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "title": "Siapkan integrasi Syncthing", "token": "Token", "url": "URL", "verify_ssl": "Verifikasi sertifikat SSL" diff --git a/homeassistant/components/tile/translations/ja.json b/homeassistant/components/tile/translations/ja.json index 7ee1ea080a0..9dd693379bd 100644 --- a/homeassistant/components/tile/translations/ja.json +++ b/homeassistant/components/tile/translations/ja.json @@ -21,7 +21,8 @@ "init": { "data": { "show_inactive": "\u975e\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30bf\u30a4\u30eb\u3092\u8868\u793a" - } + }, + "title": "\u30bf\u30a4\u30eb\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/tolo/translations/ca.json b/homeassistant/components/tolo/translations/ca.json new file mode 100644 index 00000000000..06d201141a5 --- /dev/null +++ b/homeassistant/components/tolo/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Introdueix el nom d'amfitri\u00f3 o l'adre\u00e7a IP del dispositiu TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/de.json b/homeassistant/components/tolo/translations/de.json new file mode 100644 index 00000000000..6002d2ada8b --- /dev/null +++ b/homeassistant/components/tolo/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Gib den Hostnamen oder die IP-Adresse deines TOLO Sauna-Ger\u00e4ts ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/et.json b/homeassistant/components/tolo/translations/et.json new file mode 100644 index 00000000000..57d59b85713 --- /dev/null +++ b/homeassistant/components/tolo/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Sisesta oma TOLO Sauna seadme hostinimi v\u00f5i IP-aadress." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/id.json b/homeassistant/components/tolo/translations/id.json new file mode 100644 index 00000000000..53ea0e46cb1 --- /dev/null +++ b/homeassistant/components/tolo/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Masukkan nama host atau alamat IP perangkat TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/ja.json b/homeassistant/components/tolo/translations/ja.json new file mode 100644 index 00000000000..ce963469912 --- /dev/null +++ b/homeassistant/components/tolo/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "TOLO Sauna device\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/nl.json b/homeassistant/components/tolo/translations/nl.json new file mode 100644 index 00000000000..f65f6bae7c1 --- /dev/null +++ b/homeassistant/components/tolo/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Voer de hostnaam of het IP-adres van uw TOLO Sauna-apparaat in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/no.json b/homeassistant/components/tolo/translations/no.json new file mode 100644 index 00000000000..20311dd8f69 --- /dev/null +++ b/homeassistant/components/tolo/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Skriv inn vertsnavnet eller IP-adressen til TOLO Sauna-enheten." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/ru.json b/homeassistant/components/tolo/translations/ru.json new file mode 100644 index 00000000000..82fdffdb8b1 --- /dev/null +++ b/homeassistant/components/tolo/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.ca.json b/homeassistant/components/tolo/translations/select.ca.json new file mode 100644 index 00000000000..91b7948786f --- /dev/null +++ b/homeassistant/components/tolo/translations/select.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "autom\u00e0tic", + "manual": "manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.de.json b/homeassistant/components/tolo/translations/select.de.json new file mode 100644 index 00000000000..70c28bbd4d9 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "Automatisch", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/zh-Hant.json b/homeassistant/components/tolo/translations/zh-Hant.json new file mode 100644 index 00000000000..d887eb212a1 --- /dev/null +++ b/homeassistant/components/tolo/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8f38\u5165 TOLO Sauna \u8a2d\u5099\u4e4b\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json index b1bc5573021..0c2cbbfc6e2 100644 --- a/homeassistant/components/totalconnect/translations/id.json +++ b/homeassistant/components/totalconnect/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "no_locations": "Tidak ada lokasi yang tersedia untuk pengguna ini, periksa pengaturan TotalConnect", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { @@ -11,7 +12,8 @@ "step": { "locations": { "data": { - "location": "Lokasi" + "location": "Lokasi", + "usercode": "Kode pengguna" }, "description": "Masukkan kode pengguna untuk pengguna ini di lokasi {location_id}", "title": "Lokasi Kode Pengguna" diff --git a/homeassistant/components/tplink/translations/id.json b/homeassistant/components/tplink/translations/id.json index 8a04fd63fed..2a435ac1ac1 100644 --- a/homeassistant/components/tplink/translations/id.json +++ b/homeassistant/components/tplink/translations/id.json @@ -13,10 +13,19 @@ "confirm": { "description": "Ingin menyiapkan perangkat cerdas TP-Link?" }, + "discovery_confirm": { + "description": "Ingin menyiapkan {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } } } diff --git a/homeassistant/components/tractive/translations/id.json b/homeassistant/components/tractive/translations/id.json index 80f6595e215..91843f1c37f 100644 --- a/homeassistant/components/tractive/translations/id.json +++ b/homeassistant/components/tractive/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_failed_existing": "Tidak dapat memperbarui entri konfigurasi, hapus integrasi dan siapkan kembali.", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { diff --git a/homeassistant/components/tradfri/translations/id.json b/homeassistant/components/tradfri/translations/id.json index 8ff5fe257eb..f24fdac1980 100644 --- a/homeassistant/components/tradfri/translations/id.json +++ b/homeassistant/components/tradfri/translations/id.json @@ -5,6 +5,7 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung" }, "error": { + "cannot_authenticate": "Tidak dapat mengautentikasi, apakah Gateway dipasangkan dengan server lain seperti misalnya Homekit?", "cannot_connect": "Gagal terhubung", "invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.", "timeout": "Waktu tunggu memvalidasi kode telah habis." diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 3352f4b4dd4..39fdf77a407 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -25,6 +25,7 @@ "step": { "init": { "data": { + "limit": "\u30ea\u30df\u30c3\u30c8", "order": "\u30aa\u30fc\u30c0\u30fc" } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index ce6ca2789fc..025f1a41b18 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -54,18 +54,25 @@ "data": { "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", "curr_temp_divider": "\u73fe\u5728\u306e\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", + "min_temp": "\u6700\u5c0f\u76ee\u6a19\u6e29\u5ea6(\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6700\u5c0f\u304a\u3088\u3073\u6700\u5927 = 0\u3092\u4f7f\u7528)", "set_temp_divided": "\u8a2d\u5b9a\u6e29\u5ea6\u30b3\u30de\u30f3\u30c9\u306b\u533a\u5207\u3089\u308c\u305f\u6e29\u5ea6\u5024\u3092\u4f7f\u7528", "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8", "temp_divider": "\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", "temp_step_override": "\u76ee\u6a19\u6e29\u5ea6\u30b9\u30c6\u30c3\u30d7", + "tuya_max_coltemp": "\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u5831\u544a\u3055\u308c\u305f\u6700\u5927\u8272\u6e29\u5ea6", "unit_of_measurement": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d" }, + "description": "{device_type} \u30c7\u30d0\u30a4\u30b9 `{device_name}` \u306e\u8868\u793a\u60c5\u5831\u3092\u8abf\u6574\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u69cb\u6210\u3057\u307e\u3059", "title": "Tuya\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, "init": { "data": { - "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)" + "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)", + "list_devices": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u6b04\u306e\u307e\u307e\u306b\u3057\u3066\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3059", + "query_device": "\u30b9\u30c6\u30fc\u30bf\u30b9\u306e\u66f4\u65b0\u3092\u9ad8\u901f\u5316\u3059\u308b\u305f\u3081\u306b\u30af\u30a8\u30ea\u306e\u65b9\u6cd5(query method)\u3092\u4f7f\u7528\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059", + "query_interval": "\u30af\u30a8\u30ea\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2)" }, + "description": "\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694\u306e\u5024\u3092\u4f4e\u304f\u8a2d\u5b9a\u3057\u3059\u304e\u306a\u3044\u3067\u304f\u3060\u3055\u3044\u3002\u30b3\u30fc\u30eb\u306b\u5931\u6557\u3057\u3066\u30ed\u30b0\u306b\u30a8\u30e9\u30fc\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u751f\u6210\u3055\u308c\u307e\u3059\u3002", "title": "Tuya\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/uptimerobot/translations/id.json b/homeassistant/components/uptimerobot/translations/id.json index 763ed330bbe..dac2e7e2814 100644 --- a/homeassistant/components/uptimerobot/translations/id.json +++ b/homeassistant/components/uptimerobot/translations/id.json @@ -2,12 +2,14 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "reauth_failed_existing": "Tidak dapat memperbarui entri konfigurasi, hapus integrasi dan siapkan kembali.", "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_api_key": "Kunci API tidak valid", + "reauth_failed_matching_account": "Kunci API yang Anda berikan tidak cocok dengan ID akun untuk konfigurasi yang ada.", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { @@ -15,12 +17,14 @@ "data": { "api_key": "Kunci API" }, + "description": "Anda perlu menyediakan kunci API hanya-baca yang baru dari UptimeRobot", "title": "Autentikasi Ulang Integrasi" }, "user": { "data": { "api_key": "Kunci API" - } + }, + "description": "Anda perlu menyediakan kunci API hanya-baca dari UptimeRobot" } } } diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json index 63bdce0f003..bef1a45ba32 100644 --- a/homeassistant/components/vera/translations/ja.json +++ b/homeassistant/components/vera/translations/ja.json @@ -7,7 +7,8 @@ "user": { "data": { "vera_controller_url": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL" - } + }, + "description": "Vera\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL\u3092\u4ee5\u4e0b\u306b\u793a\u3057\u307e\u3059: http://192.168.1.161:3480 \u306e\u3088\u3046\u306b\u306a\u3063\u3066\u3044\u308b\u306f\u305a\u3067\u3059\u3002" } } }, diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 69f1b3094a9..5c4c3ccdc14 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -12,7 +12,8 @@ "pair_tv": { "data": { "pin": "PIN\u30b3\u30fc\u30c9" - } + }, + "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u306e\u5b8c\u4e86" }, "pairing_complete": { "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" diff --git a/homeassistant/components/wilight/translations/ja.json b/homeassistant/components/wilight/translations/ja.json index afecf5f624f..48023a1ca57 100644 --- a/homeassistant/components/wilight/translations/ja.json +++ b/homeassistant/components/wilight/translations/ja.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "not_wilight_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001WiLight\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { "confirm": { + "description": "WiLight {name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f\n\n \u5bfe\u5fdc\u3057\u3066\u3044\u308b: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wled/translations/select.nl.json b/homeassistant/components/wled/translations/select.nl.json new file mode 100644 index 00000000000..f2ac9da54cb --- /dev/null +++ b/homeassistant/components/wled/translations/select.nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Uit", + "1": "Aan", + "2": "Totdat het apparaat opnieuw opstart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/ja.json b/homeassistant/components/wolflink/translations/ja.json index 4154735e4a7..9d87a7bf27a 100644 --- a/homeassistant/components/wolflink/translations/ja.json +++ b/homeassistant/components/wolflink/translations/ja.json @@ -12,13 +12,15 @@ "device": { "data": { "device_name": "\u30c7\u30d0\u30a4\u30b9" - } + }, + "title": "WOLF device\u306e\u9078\u629e" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "WOLF SmartSet\u306e\u63a5\u7d9a" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index 12b755a9c24..bcef8fe6d2c 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -40,6 +40,9 @@ "solarbetrieb": "Mode surya", "sparbetrieb": "Mode ekonomi", "sparen": "Ekonomi", + "standby": "Siaga", + "start": "Mulai", + "storung": "Kesalahan", "urlaubsmodus": "Mode liburan", "ventilprufung": "Uji katup" } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index b6ae9e847ff..36d59ff0836 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -17,6 +17,8 @@ "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", + "frostschutz": "\u971c\u9632\u6b62", + "gasdruck": "\u30ac\u30b9\u5727", "glt_betrieb": "BMS\u30e2\u30fc\u30c9", "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", "heizung": "\u6696\u623f", From 2a35ae2c0ab74ba6119f79b2788005e2e0d6be62 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 25 Nov 2021 18:21:39 -0600 Subject: [PATCH 0901/1452] Update rokuecp to 0.8.4 (#57259) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roku/test_config_flow.py | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 81e3af86bb5..7756d917e73 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.8.1"], + "requirements": ["rokuecp==0.8.4"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 0b68310c1a8..d5f1528dffe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2073,7 +2073,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.8.1 +rokuecp==0.8.4 # homeassistant.components.roomba roombapy==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b10036dc97..5a3f9cd9449 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ rflink==0.0.58 ring_doorbell==0.7.1 # homeassistant.components.roku -rokuecp==0.8.1 +rokuecp==0.8.4 # homeassistant.components.roomba roombapy==1.6.4 diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 459c90f536a..8aa015d3e01 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -105,15 +105,19 @@ async def test_form_cannot_connect( assert result["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error(hass: HomeAssistant) -> None: +async def test_form_unknown_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test we handle unknown error.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) user_input = {CONF_HOST: HOST} with patch( - "homeassistant.components.roku.config_flow.Roku.update", + "homeassistant.components.roku.config_flow.Roku._request", side_effect=Exception, ) as mock_validate_input: result = await hass.config_entries.flow.async_configure( @@ -152,9 +156,11 @@ async def test_homekit_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort homekit flow on unknown error.""" + mock_connection(aioclient_mock) + discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) with patch( - "homeassistant.components.roku.config_flow.Roku.update", + "homeassistant.components.roku.config_flow.Roku._request", side_effect=Exception, ): result = await hass.config_entries.flow.async_init( @@ -231,9 +237,11 @@ async def test_ssdp_unknown_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" + mock_connection(aioclient_mock) + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) with patch( - "homeassistant.components.roku.config_flow.Roku.update", + "homeassistant.components.roku.config_flow.Roku._request", side_effect=Exception, ): result = await hass.config_entries.flow.async_init( From 79d6d795578670bf1bb387af165383532355994b Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:23:14 +0000 Subject: [PATCH 0902/1452] Add configuration_url to Nanoleaf integration (#60372) --- homeassistant/components/nanoleaf/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index d181aeb12d8..c6efed91787 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -19,4 +19,5 @@ class NanoleafEntity(Entity): model=nanoleaf.model, name=nanoleaf.name, sw_version=nanoleaf.firmware_version, + configuration_url=f"http://{nanoleaf.host}", ) From 18e822b7b6b6ed234320ea19883b39fc5eecb8c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 21:55:17 -0600 Subject: [PATCH 0903/1452] Bump sqlalchemy to 1.4.27 (#60383) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index e986c104f8b..a6a746f019e 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.26"], + "requirements": ["sqlalchemy==1.4.27"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index debcea9a737..dfc58474366 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.26"], + "requirements": ["sqlalchemy==1.4.27"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8774bdd947a..6e992e60d7b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.26.0 scapy==2.4.5 -sqlalchemy==1.4.26 +sqlalchemy==1.4.27 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index d5f1528dffe..0dbec70d059 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2221,7 +2221,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.26 +sqlalchemy==1.4.27 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a3f9cd9449..27d302842b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1315,7 +1315,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.26 +sqlalchemy==1.4.27 # homeassistant.components.srp_energy srpenergy==1.3.2 From 53f15307566421d5c138299105bb14e82642d9a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Nov 2021 22:33:01 -0600 Subject: [PATCH 0904/1452] Fix flux_led effect brightness in A2,A3 models (#60386) - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.24.37...0.24.38 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d0a75205cdd..5145dd8ba16 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.37"], + "requirements": ["flux_led==0.24.38"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0dbec70d059..9edff340fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.37 +flux_led==0.24.38 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d302842b7..190871e9c92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.37 +flux_led==0.24.38 # homeassistant.components.homekit fnvhash==0.1.0 From 3f741d4295e288e9b443d3c126e7e5a587efe41d Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Fri, 26 Nov 2021 00:19:26 -0800 Subject: [PATCH 0905/1452] Bump python-smarttub dependency to 0.0.28 (#60391) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 713f2a7f7a1..5972d755e11 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.27"], + "requirements": ["python-smarttub==0.0.28"], "quality_scale": "platinum", "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9edff340fb9..bfbbbabbc88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1931,7 +1931,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.27 +python-smarttub==0.0.28 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 190871e9c92..c48900dd0a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1157,7 +1157,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.27 +python-smarttub==0.0.28 # homeassistant.components.songpal python-songpal==0.12 From 061691508b88bc7e0cf49523a6c24ff5e785a333 Mon Sep 17 00:00:00 2001 From: PlusPlus-ua Date: Fri, 26 Nov 2021 10:35:19 +0200 Subject: [PATCH 0906/1452] Fixed handling of zero values in TuyaNumberEntity (#60393) --- homeassistant/components/tuya/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 634f4858186..c4063d22e22 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -286,7 +286,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): value = self.device.status.get(self.entity_description.key) # Scale integer/float value - if value and isinstance(self._type_data, IntegerTypeData): + if value is not None and isinstance(self._type_data, IntegerTypeData): return self._type_data.scale_value(value) return None From 249cac2901af594fb3753e8a146ad73afe8be221 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Nov 2021 10:03:51 +0100 Subject: [PATCH 0907/1452] Remove myself as codeowner from Hue (#60395) --- CODEOWNERS | 2 +- homeassistant/components/hue/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6804400f6a4..c3e623ca482 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -232,7 +232,7 @@ homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle -homeassistant/components/hue/* @balloob @frenck @marcelveldt +homeassistant/components/hue/* @balloob @marcelveldt homeassistant/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 75cdea853e7..02734df1481 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -22,7 +22,7 @@ "models": ["BSB002"] }, "zeroconf": ["_hue._tcp.local."], - "codeowners": ["@balloob", "@frenck", "@marcelveldt"], + "codeowners": ["@balloob", "@marcelveldt"], "quality_scale": "platinum", "iot_class": "local_push" } From 88068fa97fd61d4be32b8f0c279b5e5f514d71de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Nov 2021 11:10:39 +0100 Subject: [PATCH 0908/1452] Remove unused OrderedEnum (#60392) --- homeassistant/util/__init__.py | 28 ----------------------- tests/util/test_init.py | 41 ---------------------------------- 2 files changed, 69 deletions(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 81f1389b47c..b71fd8fa5e4 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -162,34 +162,6 @@ def get_random_string(length: int = 10) -> str: return "".join(generator.choice(source_chars) for _ in range(length)) -class OrderedEnum(enum.Enum): - """Taken from Python 3.4.0 docs.""" - - def __ge__(self, other: ENUM_T) -> bool: - """Return the greater than element.""" - if self.__class__ is other.__class__: - return bool(self.value >= other.value) - return NotImplemented - - def __gt__(self, other: ENUM_T) -> bool: - """Return the greater element.""" - if self.__class__ is other.__class__: - return bool(self.value > other.value) - return NotImplemented - - def __le__(self, other: ENUM_T) -> bool: - """Return the lower than element.""" - if self.__class__ is other.__class__: - return bool(self.value <= other.value) - return NotImplemented - - def __lt__(self, other: ENUM_T) -> bool: - """Return the lower element.""" - if self.__class__ is other.__class__: - return bool(self.value < other.value) - return NotImplemented - - class Throttle: """A class for throttling the execution of tasks. diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 0634f3cb6cf..b2ede0c881b 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -120,47 +120,6 @@ def test_ensure_unique_string(): assert util.ensure_unique_string("Beer", ["Wine", "Soda"]) == "Beer" -def test_ordered_enum(): - """Test the ordered enum class.""" - - class TestEnum(util.OrderedEnum): - """Test enum that can be ordered.""" - - FIRST = 1 - SECOND = 2 - THIRD = 3 - - assert TestEnum.SECOND >= TestEnum.FIRST - assert TestEnum.SECOND >= TestEnum.SECOND - assert TestEnum.SECOND < TestEnum.THIRD - - assert TestEnum.SECOND > TestEnum.FIRST - assert TestEnum.SECOND <= TestEnum.SECOND - assert TestEnum.SECOND <= TestEnum.THIRD - - assert TestEnum.SECOND > TestEnum.FIRST - assert TestEnum.SECOND <= TestEnum.SECOND - assert TestEnum.SECOND <= TestEnum.THIRD - - assert TestEnum.SECOND >= TestEnum.FIRST - assert TestEnum.SECOND >= TestEnum.SECOND - assert TestEnum.SECOND < TestEnum.THIRD - - # Python will raise a TypeError if the <, <=, >, >= methods - # raise a NotImplemented error. - with pytest.raises(TypeError): - TestEnum.FIRST < 1 - - with pytest.raises(TypeError): - TestEnum.FIRST <= 1 - - with pytest.raises(TypeError): - TestEnum.FIRST > 1 - - with pytest.raises(TypeError): - TestEnum.FIRST >= 1 - - def test_throttle(): """Test the add cooldown decorator.""" calls1 = [] From 3d5432b799aa576a18ed9f5cbc9557b84a881b3f Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Fri, 26 Nov 2021 10:15:03 +0000 Subject: [PATCH 0909/1452] Add configuration_url to AccuWeather integration (#60381) --- homeassistant/components/accuweather/weather.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index a17c1a090b1..c97cc44aea3 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -73,6 +73,12 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): identifiers={(DOMAIN, coordinator.location_key)}, manufacturer=MANUFACTURER, name=NAME, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url="http://accuweather.com/en/" + f"_/_/{coordinator.location_key}/" + f"weather-forecast/{coordinator.location_key}/", ) @property From dc98791963418746e20d088ff298528b2c4b12fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 26 Nov 2021 12:06:50 +0100 Subject: [PATCH 0910/1452] Bump awesomeversion from 21.10.1 to 21.11.0 (#60401) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6e992e60d7b..d5d73be25b2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.22.12 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==21.10.1 +awesomeversion==21.11.0 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/requirements.txt b/requirements.txt index a7e86ca6dea..8009c50a7e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.0 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==21.10.1 +awesomeversion==21.11.0 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/setup.py b/setup.py index af23dc52159..a616102018d 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ REQUIRES = [ "async_timeout==4.0.0", "attrs==21.2.0", "atomicwrites==1.4.0", - "awesomeversion==21.10.1", + "awesomeversion==21.11.0", 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2021.5.30", From ad9c3a47cfbb5f45dec0b4af2878eb8f42fdaf92 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Nov 2021 13:21:11 +0100 Subject: [PATCH 0911/1452] Correct validation of conditions in scripts and automations (#60403) --- homeassistant/helpers/script.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6a727aefb4d..a0f53a06291 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -262,11 +262,7 @@ async def async_validate_action_config( config = platform.ACTION_SCHEMA(config) # type: ignore elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION: - if config[CONF_CONDITION] == "device": - platform = await device_automation.async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "condition" - ) - config = platform.CONDITION_SCHEMA(config) # type: ignore + config = await condition.async_validate_condition_config(hass, config) # type: ignore elif action_type == cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: config[CONF_WAIT_FOR_TRIGGER] = await async_validate_trigger_config( From ea102f71a665d8a4573785f0765085caeea62257 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Nov 2021 14:12:59 +0100 Subject: [PATCH 0912/1452] Correct validation of repeats in scripts and automations (#60318) * Correct validation of repeats in scripts and automations * Improve validation test --- homeassistant/helpers/script.py | 2 +- tests/helpers/test_script.py | 53 ++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a0f53a06291..293a8a9455f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -270,7 +270,7 @@ async def async_validate_action_config( ) elif action_type == cv.SCRIPT_ACTION_REPEAT: - config[CONF_SEQUENCE] = await async_validate_actions_config( + config[CONF_REPEAT][CONF_SEQUENCE] = await async_validate_actions_config( hass, config[CONF_REPEAT][CONF_SEQUENCE] ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a3bf128d3c4..6c64b64d6de 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3,7 +3,9 @@ import asyncio from contextlib import contextmanager from datetime import timedelta +from functools import reduce import logging +import operator from types import MappingProxyType from unittest import mock from unittest.mock import AsyncMock, patch @@ -23,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback from homeassistant.exceptions import ConditionError, ServiceNotFound -from homeassistant.helpers import config_validation as cv, script, trace +from homeassistant.helpers import config_validation as cv, script, template, trace from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -3021,6 +3023,15 @@ async def test_set_redefines_variable(hass, caplog): async def test_validate_action_config(hass): """Validate action config.""" + + def templated_device_action(message): + return { + "device_id": "abcd", + "domain": "mobile_app", + "message": f"{message} {{{{ 5 + 5}}}}", + "type": "notify", + } + configs = { cv.SCRIPT_ACTION_CALL_SERVICE: {"service": "light.turn_on"}, cv.SCRIPT_ACTION_DELAY: {"delay": 5}, @@ -3031,24 +3042,22 @@ async def test_validate_action_config(hass): cv.SCRIPT_ACTION_CHECK_CONDITION: { "condition": "{{ states.light.kitchen.state == 'on' }}" }, - cv.SCRIPT_ACTION_DEVICE_AUTOMATION: { - "domain": "light", - "entity_id": "light.kitchen", - "device_id": "abcd", - "type": "turn_on", - }, + cv.SCRIPT_ACTION_DEVICE_AUTOMATION: templated_device_action("device"), cv.SCRIPT_ACTION_ACTIVATE_SCENE: {"scene": "scene.relax"}, cv.SCRIPT_ACTION_REPEAT: { - "repeat": {"count": 3, "sequence": [{"event": "repeat_event"}]} + "repeat": { + "count": 3, + "sequence": [templated_device_action("repeat_event")], + } }, cv.SCRIPT_ACTION_CHOOSE: { "choose": [ { "condition": "{{ states.light.kitchen.state == 'on' }}", - "sequence": [{"event": "choose_event"}], + "sequence": [templated_device_action("choose_event")], } ], - "default": [{"event": "choose_default_event"}], + "default": [templated_device_action("choose_default_event")], }, cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: { "wait_for_trigger": [ @@ -3057,9 +3066,17 @@ async def test_validate_action_config(hass): }, cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}}, } + expected_templates = { + cv.SCRIPT_ACTION_CHECK_CONDITION: None, + cv.SCRIPT_ACTION_DEVICE_AUTOMATION: [[]], + cv.SCRIPT_ACTION_REPEAT: [["repeat", "sequence", 0]], + cv.SCRIPT_ACTION_CHOOSE: [["choose", 0, "sequence", 0], ["default", 0]], + cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: None, + } for key in cv.ACTION_TYPE_SCHEMAS: assert key in configs, f"No validate config test found for {key}" + assert key in expected_templates or key in script.STATIC_VALIDATION_ACTION_TYPES # Verify we raise if we don't know the action type with patch( @@ -3068,13 +3085,27 @@ async def test_validate_action_config(hass): ), pytest.raises(ValueError): await script.async_validate_action_config(hass, {}) + # Verify each action can validate + validated_config = {} for action_type, config in configs.items(): assert cv.determine_script_action(config) == action_type try: - await script.async_validate_action_config(hass, config) + validated_config[action_type] = await script.async_validate_action_config( + hass, config + ) except vol.Invalid as err: assert False, f"{action_type} config invalid: {err}" + # Verify non-static actions have validated + for action_type, paths_to_templates in expected_templates.items(): + if paths_to_templates is None: + continue + for path_to_template in paths_to_templates: + device_action = reduce( + operator.getitem, path_to_template, validated_config[action_type] + ) + assert isinstance(device_action["message"], template.Template) + async def test_embedded_wait_for_trigger_in_automation(hass): """Test an embedded wait for trigger.""" From 296b73874054eec31d91f0db1b9a625ad5d47c32 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 26 Nov 2021 14:19:39 +0100 Subject: [PATCH 0913/1452] Add button to trigger ota firmware update for Shelly devices (#58757) Co-authored-by: Shay Levy --- homeassistant/components/shelly/__init__.py | 75 ++++++++++++++- homeassistant/components/shelly/button.py | 101 ++++++++++++++++++++ homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/switch.py | 1 + tests/components/shelly/conftest.py | 24 ++++- tests/components/shelly/test_button.py | 70 ++++++++++++++ 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/shelly/button.py create mode 100644 tests/components/shelly/test_button.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index c85728d3ee6..4109130ab80 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -29,6 +29,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, + ATTR_BETA, ATTR_CHANNEL, ATTR_CLICK_TYPE, ATTR_DEVICE, @@ -66,6 +67,7 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ "binary_sensor", + "button", "climate", "cover", "light", @@ -73,7 +75,7 @@ BLOCK_PLATFORMS: Final = [ "switch", ] BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] -RPC_PLATFORMS: Final = ["binary_sensor", "light", "sensor", "switch"] +RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) COAP_SCHEMA: Final = vol.Schema( @@ -424,6 +426,41 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger or schedule an ota update.""" + update_data = self.device.status["update"] + _LOGGER.debug("OTA update service - update_data: %s", update_data) + + if not update_data["has_update"] and not beta: + _LOGGER.warning("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get("beta_version"): + _LOGGER.warning( + "No OTA update on beta channel available for device %s", self.name + ) + return + + if update_data["status"] == "updating": + _LOGGER.warning("OTA update already in progress for %s", self.name) + return + + new_version = update_data["new_version"] + if beta: + new_version = update_data["beta_version"] + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.exception("Error while perform ota update: %s", err) + _LOGGER.debug("Result of OTA update call: %s", result) + def shutdown(self) -> None: """Shutdown the wrapper.""" self.device.shutdown() @@ -661,6 +698,42 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger an ota update.""" + + update_data = self.device.status["sys"]["available_updates"] + _LOGGER.debug("OTA update service - update_data: %s", update_data) + + if not bool(update_data) or (not update_data.get("stable") and not beta): + _LOGGER.warning("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get(ATTR_BETA): + _LOGGER.warning( + "No OTA update on beta channel available for device %s", self.name + ) + return + + new_version = update_data.get("stable", {"version": ""})["version"] + if beta: + new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] + + assert self.device.shelly + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) + async def shutdown(self) -> None: """Shutdown the wrapper.""" await self.device.shutdown() diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py new file mode 100644 index 00000000000..7890eefd30c --- /dev/null +++ b/homeassistant/components/shelly/button.py @@ -0,0 +1,101 @@ +"""Button for Shelly.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import BlockDeviceWrapper, RpcDeviceWrapper +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set buttons for device.""" + wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None + if get_device_entry_gen(config_entry) == 2: + if rpc_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ].get(RPC): + wrapper = cast(RpcDeviceWrapper, rpc_wrapper) + else: + if block_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ].get(BLOCK): + wrapper = cast(BlockDeviceWrapper, block_wrapper) + + if wrapper is not None: + async_add_entities( + [ + ShellyOtaUpdateStableButton(wrapper, config_entry), + ShellyOtaUpdateBetaButton(wrapper, config_entry), + ] + ) + + +class ShellyOtaUpdateBaseButton(ButtonEntity): + """Defines a Shelly OTA update base button.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__( + self, + wrapper: RpcDeviceWrapper | BlockDeviceWrapper, + entry: ConfigEntry, + name: str, + beta_channel: bool, + icon: str, + ) -> None: + """Initialize Shelly OTA update button.""" + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} + ) + + if isinstance(wrapper, RpcDeviceWrapper): + device_name = get_rpc_device_name(wrapper.device) + else: + device_name = get_block_device_name(wrapper.device) + + self._attr_name = f"{device_name} {name}" + self._attr_unique_id = slugify(self._attr_name) + self._attr_icon = icon + + self.beta_channel = beta_channel + self.entry = entry + self.wrapper = wrapper + + async def async_press(self) -> None: + """Triggers the OTA update service.""" + await self.wrapper.async_trigger_ota_update(beta=self.beta_channel) + + +class ShellyOtaUpdateStableButton(ShellyOtaUpdateBaseButton): + """Defines a Shelly OTA update stable channel button.""" + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update button.""" + super().__init__(wrapper, entry, "OTA Update", False, "mdi:package-up") + + +class ShellyOtaUpdateBetaButton(ShellyOtaUpdateBaseButton): + """Defines a Shelly OTA update beta channel button.""" + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update button.""" + super().__init__(wrapper, entry, "OTA Update Beta", True, "mdi:flask-outline") + self._attr_entity_registry_enabled_default = False diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index fd06e88cdf1..8ef3ed5f1ac 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -90,6 +90,8 @@ ATTR_CHANNEL: Final = "channel" ATTR_DEVICE: Final = "device" ATTR_GENERATION: Final = "generation" CONF_SUBTYPE: Final = "subtype" +ATTR_BETA: Final = "beta" +CONF_OTA_BETA_CHANNEL: Final = "ota_beta_channel" BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"} diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 0291258b511..9114587a910 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -74,6 +74,7 @@ async def async_setup_rpc_entry( ) -> None: """Set up entities for RPC device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch") switch_ids = [] diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index a0d4a27bbc4..16dc8fdae9e 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -71,8 +71,25 @@ MOCK_SHELLY = { "num_outputs": 2, } -MOCK_STATUS = { +MOCK_STATUS_COAP = { + "update": { + "status": "pending", + "has_update": True, + "beta_version": "some_beta_version", + "new_version": "some_new_version", + "old_version": "some_old_version", + }, +} + + +MOCK_STATUS_RPC = { "switch:0": {"output": True}, + "sys": { + "available_updates": { + "beta": {"version": "some_beta_version"}, + "stable": {"version": "some_beta_version"}, + } + }, } @@ -117,8 +134,10 @@ async def coap_wrapper(hass): blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS, shelly=MOCK_SHELLY, + status=MOCK_STATUS_COAP, firmware_version="some fw string", update=AsyncMock(), + trigger_ota_update=AsyncMock(), initialized=True, ) @@ -150,9 +169,10 @@ async def rpc_wrapper(hass): config=MOCK_CONFIG, event={}, shelly=MOCK_SHELLY, - status=MOCK_STATUS, + status=MOCK_STATUS_RPC, firmware_version="some fw string", update=AsyncMock(), + trigger_ota_update=AsyncMock(), initialized=True, shutdown=AsyncMock(), ) diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py new file mode 100644 index 00000000000..5ceed08b9d9 --- /dev/null +++ b/tests/components/shelly/test_button.py @@ -0,0 +1,70 @@ +"""Tests for Shelly button platform.""" +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get + + +async def test_block_button(hass: HomeAssistant, coap_wrapper): + """Test block device OTA button.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) + ) + await hass.async_block_till_done() + + # stable channel button + state = hass.states.get("button.test_name_ota_update") + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + # beta channel button + entity_registry = async_get(hass) + entry = entity_registry.async_get("button.test_name_ota_update_beta") + state = hass.states.get("button.test_name_ota_update_beta") + + assert entry + assert state is None + + +async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): + """Test rpc device OTA button.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) + ) + await hass.async_block_till_done() + + # stable channel button + state = hass.states.get("button.test_name_ota_update") + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + # beta channel button + entity_registry = async_get(hass) + entry = entity_registry.async_get("button.test_name_ota_update_beta") + state = hass.states.get("button.test_name_ota_update_beta") + + assert entry + assert state is None From 5a72c9f7c3817737d916816d257c1b55c62357a6 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 26 Nov 2021 16:44:49 -0500 Subject: [PATCH 0914/1452] Rename various usages of UniFi to better identify only UniFi Network (#59568) * Renames various usages of UniFi to better indentify only UniFi Network * Apply suggestions from code review Co-authored-by: Robert Svensson * Missed renames * Updates more locations * Removes instances of application/controller * Missed a spot * Updates all UniFi Controller instances * Fixes typo * Reverts changes to translations Co-authored-by: Robert Svensson --- homeassistant/components/unifi/__init__.py | 12 ++++---- homeassistant/components/unifi/config_flow.py | 18 +++++------ homeassistant/components/unifi/const.py | 2 +- homeassistant/components/unifi/controller.py | 30 ++++++++++++------- .../components/unifi/device_tracker.py | 4 +-- homeassistant/components/unifi/errors.py | 6 ++-- homeassistant/components/unifi/manifest.json | 4 +-- homeassistant/components/unifi/sensor.py | 8 ++--- homeassistant/components/unifi/services.py | 4 +-- homeassistant/components/unifi/services.yaml | 4 +-- homeassistant/components/unifi/strings.json | 14 ++++----- homeassistant/components/unifi/switch.py | 8 ++--- .../components/unifi/unifi_entity_base.py | 4 +-- tests/components/unifi/__init__.py | 2 +- tests/components/unifi/conftest.py | 2 +- tests/components/unifi/test_config_flow.py | 2 +- tests/components/unifi/test_controller.py | 4 +-- tests/components/unifi/test_device_tracker.py | 4 +-- tests/components/unifi/test_init.py | 6 ++-- tests/components/unifi/test_sensor.py | 2 +- tests/components/unifi/test_switch.py | 2 +- 21 files changed, 75 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index eb026643515..180f1d6752f 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,4 +1,4 @@ -"""Integration to UniFi controllers and its various features.""" +"""Integration to UniFi Network and its various features.""" from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -20,7 +20,7 @@ STORAGE_VERSION = 1 async def async_setup(hass, config): - """Component doesn't support configuration through configuration.yaml.""" + """Integration doesn't support configuration through configuration.yaml.""" hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) await wireless_clients.async_load() @@ -28,7 +28,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): - """Set up the UniFi component.""" + """Set up the UniFi Network integration.""" hass.data.setdefault(UNIFI_DOMAIN, {}) # Flat configuration was introduced with 2021.3 @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) ) - LOGGER.debug("UniFi config options %s", config_entry.options) + LOGGER.debug("UniFi Network config options %s", config_entry.options) if controller.mac is None: return True @@ -64,8 +64,8 @@ async def async_setup_entry(hass, config_entry): configuration_url=controller.api.url, connections={(CONNECTION_NETWORK_MAC, controller.mac)}, default_manufacturer=ATTR_MANUFACTURER, - default_model="UniFi Controller", - default_name="UniFi Controller", + default_model="UniFi Network", + default_name="UniFi Network", ) return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index ea737599b20..210216cfe3c 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,8 +1,8 @@ -"""Config flow for UniFi. +"""Config flow for UniFi Network integration. Provides user initiated configuration flow. -Discovery of controllers hosted on UDM and UDM Pro devices through SSDP. -Reauthentication when issue with credentials are reported. +Discovery of UniFi Network instances hosted on UDM and UDM Pro devices +through SSDP. Reauthentication when issue with credentials are reported. Configuration of options through options flow. """ import socket @@ -56,7 +56,7 @@ MODEL_PORTS = { class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): - """Handle a UniFi config flow.""" + """Handle a UniFi Network config flow.""" VERSION = 1 @@ -67,7 +67,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return UnifiOptionsFlowHandler(config_entry) def __init__(self): - """Initialize the UniFi flow.""" + """Initialize the UniFi Network flow.""" self.config = {} self.site_ids = {} self.site_names = {} @@ -242,16 +242,16 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): class UnifiOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Unifi options.""" + """Handle Unifi Network options.""" def __init__(self, config_entry): - """Initialize UniFi options flow.""" + """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) self.controller = None async def async_step_init(self, user_input=None): - """Manage the UniFi options.""" + """Manage the UniFi Network options.""" self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients @@ -416,7 +416,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_discover_unifi(hass): - """Discover UniFi address.""" + """Discover UniFi Network address.""" try: return await hass.async_add_executor_job(socket.gethostbyname, "unifi") except socket.gaierror: diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 94e2fad35ed..406b8d23a18 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -1,4 +1,4 @@ -"""Constants for the UniFi component.""" +"""Constants for the UniFi Network integration.""" import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 14df45e3aeb..26463563db2 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,4 +1,4 @@ -"""UniFi Controller abstraction.""" +"""UniFi Network abstraction.""" from __future__ import annotations import asyncio @@ -90,7 +90,7 @@ DEVICE_CONNECTED = ( class UniFiController: - """Manages a single UniFi Controller.""" + """Manages a single UniFi Network instance.""" def __init__(self, hass, config_entry): """Initialize the system.""" @@ -198,7 +198,7 @@ class UniFiController: if signal == SIGNAL_CONNECTION_STATE: if data == STATE_DISCONNECTED and self.available: - LOGGER.warning("Lost connection to UniFi controller") + LOGGER.warning("Lost connection to UniFi Network") if (data == STATE_RUNNING and not self.available) or ( data == STATE_DISCONNECTED and self.available @@ -209,7 +209,7 @@ class UniFiController: if not self.available: self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) else: - LOGGER.info("Connected to UniFi controller") + LOGGER.info("Connected to UniFi Network") elif signal == SIGNAL_DATA and data: @@ -301,7 +301,7 @@ class UniFiController: unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) async def async_setup(self): - """Set up a UniFi controller.""" + """Set up a UniFi Network instance.""" try: self.api = await get_controller( self.hass, @@ -413,11 +413,11 @@ class UniFiController: def reconnect(self, log=False) -> None: """Prepare to reconnect UniFi session.""" if log: - LOGGER.info("Will try to reconnect to UniFi controller") + LOGGER.info("Will try to reconnect to UniFi Network") self.hass.loop.create_task(self.async_reconnect()) async def async_reconnect(self) -> None: - """Try to reconnect UniFi session.""" + """Try to reconnect UniFi Network session.""" try: async with async_timeout.timeout(5): await self.api.login() @@ -494,7 +494,11 @@ async def get_controller( return controller except aiounifi.Unauthorized as err: - LOGGER.warning("Connected to UniFi at %s but not registered: %s", host, err) + LOGGER.warning( + "Connected to UniFi Network at %s but not registered: %s", + host, + err, + ) raise AuthenticationRequired from err except ( @@ -503,13 +507,17 @@ async def get_controller( aiounifi.ServiceUnavailable, aiounifi.RequestError, ) as err: - LOGGER.error("Error connecting to the UniFi controller at %s: %s", host, err) + LOGGER.error("Error connecting to the UniFi Network at %s: %s", host, err) raise CannotConnect from err except aiounifi.LoginRequired as err: - LOGGER.warning("Connected to UniFi at %s but login required: %s", host, err) + LOGGER.warning( + "Connected to UniFi Network at %s but login required: %s", + host, + err, + ) raise AuthenticationRequired from err except aiounifi.AiounifiException as err: - LOGGER.exception("Unknown UniFi communication error occurred: %s", err) + LOGGER.exception("Unknown UniFi Network communication error occurred: %s", err) raise AuthenticationRequired from err diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index abf37a02fa8..035d8b0ae87 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,4 +1,4 @@ -"""Track both clients and devices using UniFi controllers.""" +"""Track both clients and devices using UniFi Network.""" from datetime import timedelta from aiounifi.api import SOURCE_DATA, SOURCE_EVENT @@ -72,7 +72,7 @@ WIRELESS_CONNECTION = ( async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up device tracker for UniFi component.""" + """Set up device tracker for UniFi Network integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py index c90c4956312..c3b2bb23d8e 100644 --- a/homeassistant/components/unifi/errors.py +++ b/homeassistant/components/unifi/errors.py @@ -1,9 +1,9 @@ -"""Errors for the UniFi component.""" +"""Errors for the UniFi Network integration.""" from homeassistant.exceptions import HomeAssistantError class UnifiException(HomeAssistantError): - """Base class for UniFi exceptions.""" + """Base class for UniFi Network exceptions.""" class AlreadyConfigured(UnifiException): @@ -19,7 +19,7 @@ class CannotConnect(UnifiException): class LoginRequired(UnifiException): - """Component got logged out.""" + """Integration got logged out.""" class UserLevel(UnifiException): diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index ae8ebd767af..7dbd86c928a 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifi", - "name": "Ubiquiti UniFi", + "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ @@ -23,4 +23,4 @@ } ], "iot_class": "local_push" -} \ No newline at end of file +} diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index c1181b029a5..be5b2d03e1c 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -1,4 +1,4 @@ -"""Sensor platform for UniFi integration. +"""Sensor platform for UniFi Network integration. Support for bandwidth sensors of network clients. Support for uptime sensors of network clients. @@ -21,7 +21,7 @@ UPTIME_SENSOR = "uptime" async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up sensors for UniFi integration.""" + """Set up sensors for UniFi Network integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = { RX_SENSOR: set(), @@ -82,7 +82,7 @@ def add_uptime_entities(controller, async_add_entities, clients): class UniFiBandwidthSensor(UniFiClient, SensorEntity): - """UniFi bandwidth sensor base class.""" + """UniFi Network bandwidth sensor base class.""" DOMAIN = DOMAIN @@ -127,7 +127,7 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): class UniFiUpTimeSensor(UniFiClient, SensorEntity): - """UniFi uptime sensor.""" + """UniFi Network client uptime sensor.""" DOMAIN = DOMAIN TYPE = UPTIME_SENSOR diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index ca15cc83194..edc982d73dd 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -1,4 +1,4 @@ -"""UniFi services.""" +"""UniFi Network services.""" import voluptuous as vol @@ -47,7 +47,7 @@ def async_setup_services(hass) -> None: @callback def async_unload_services(hass) -> None: - """Unload UniFi services.""" + """Unload UniFi Network services.""" for service in SUPPORTED_SERVICES: hass.services.async_remove(UNIFI_DOMAIN, service) diff --git a/homeassistant/components/unifi/services.yaml b/homeassistant/components/unifi/services.yaml index 7f06adc88a2..c6a4de3072a 100644 --- a/homeassistant/components/unifi/services.yaml +++ b/homeassistant/components/unifi/services.yaml @@ -1,6 +1,6 @@ reconnect_client: name: Reconnect wireless client - description: Try to get wireless client to reconnect to UniFi network + description: Try to get wireless client to reconnect to UniFi Network fields: device_id: name: Device @@ -11,5 +11,5 @@ reconnect_client: integration: unifi remove_clients: - name: Remove clients from the UniFi Controller + name: Remove clients from the UniFi Network description: Clean up clients that has only been associated with the controller for a short period of time. diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index d625ff79117..476a0bfdd61 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -3,7 +3,7 @@ "flow_title": "{site} ({host})", "step": { "user": { - "title": "Set up UniFi Controller", + "title": "Set up UniFi Network", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", @@ -20,7 +20,7 @@ "unknown_client_mac": "No client available on that MAC address" }, "abort": { - "already_configured": "Controller site is already configured", + "already_configured": "UniFi Network site is already configured", "configuration_updated": "Configuration updated.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } @@ -30,14 +30,14 @@ "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", - "ignore_wired_bug": "Disable UniFi wired bug logic", + "ignore_wired_bug": "Disable UniFi Network wired bug logic", "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" }, "description": "Configure device tracking", - "title": "UniFi options 1/3" + "title": "UniFi Network options 1/3" }, "client_control": { "data": { @@ -46,7 +46,7 @@ "dpi_restrictions": "Allow control of DPI restriction groups" }, "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", - "title": "UniFi options 2/3" + "title": "UniFi Network options 2/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]", "block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]" }, - "description": "Configure UniFi integration" + "description": "Configure UniFi Network integration" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "Uptime sensors for network clients" }, "description": "Configure statistics sensors", - "title": "UniFi options 3/3" + "title": "UniFi Network options 3/3" } } } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 370d48d5adf..075718b4da5 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,4 +1,4 @@ -"""Switch platform for UniFi integration. +"""Switch platform for UniFi Network integration. Support for controlling power supply of clients which are powered over Ethernet (POE). Support for controlling network access of clients selected in option flow. @@ -36,7 +36,7 @@ CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up switches for UniFi component. + """Set up switches for UniFi Network integration. Switches are controlling network access and switch ports with POE. """ @@ -374,6 +374,6 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"unifi_controller_{self._item.site_id}")}, manufacturer=ATTR_MANUFACTURER, - model="UniFi Controller", - name="UniFi Controller", + model="UniFi Network", + name="UniFi Network", ) diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index e3b6e4f9970..25e10ab13ec 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,4 +1,4 @@ -"""Base class for UniFi entities.""" +"""Base class for UniFi Network entities.""" import logging from typing import Any @@ -18,7 +18,7 @@ class UniFiBase(Entity): TYPE = "" def __init__(self, item, controller) -> None: - """Set up UniFi entity base. + """Set up UniFi Network entity base. Register mac to controller entities to cover disabled entities. """ diff --git a/tests/components/unifi/__init__.py b/tests/components/unifi/__init__.py index e75b2778d2b..3e26a8e6ea7 100644 --- a/tests/components/unifi/__init__.py +++ b/tests/components/unifi/__init__.py @@ -1 +1 @@ -"""Tests for the UniFi component.""" +"""Tests for the UniFi Network integration.""" diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 81af3f7243f..42e9db6b958 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,4 +1,4 @@ -"""Fixtures for UniFi methods.""" +"""Fixtures for UniFi Network methods.""" from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 151baee6176..8e0e687345d 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test UniFi config flow.""" +"""Test UniFi Network config flow.""" import socket from unittest.mock import patch diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 0f3447b3dd9..738cb28e1e3 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,4 +1,4 @@ -"""Test UniFi Controller.""" +"""Test UniFi Network.""" import asyncio from copy import deepcopy @@ -171,7 +171,7 @@ async def setup_unifi_integration( unique_id="1", config_entry_id=DEFAULT_CONFIG_ENTRY_ID, ): - """Create the UniFi controller.""" + """Create the UniFi Network instance.""" assert await async_setup_component(hass, UNIFI_DOMAIN, {}) config_entry = MockConfigEntry( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 384db693f1c..4014062ee27 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,4 +1,4 @@ -"""The tests for the UniFi device tracker platform.""" +"""The tests for the UniFi Network device tracker platform.""" from datetime import timedelta from unittest.mock import patch @@ -900,7 +900,7 @@ async def test_wireless_client_go_wired_issue( ): """Test the solution to catch wireless device go wired UniFi issue. - UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. + UniFi Network has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ client = { "essid": "ssid", diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 85733d6d686..b2d37ad7ee3 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,4 +1,4 @@ -"""Test UniFi setup process.""" +"""Test UniFi Network integration setup process.""" from unittest.mock import AsyncMock, patch from homeassistant.components import unifi @@ -59,8 +59,8 @@ async def test_controller_mac(hass): ) assert device.configuration_url == "https://123:443" assert device.manufacturer == "Ubiquiti Networks" - assert device.model == "UniFi Controller" - assert device.name == "UniFi Controller" + assert device.model == "UniFi Network" + assert device.name == "UniFi Network" assert device.sw_version is None diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 2df279090f3..3794c46988d 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,4 +1,4 @@ -"""UniFi sensor platform tests.""" +"""UniFi Network sensor platform tests.""" from datetime import datetime from unittest.mock import patch diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 796434c5cd9..29640b7a4b4 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,4 +1,4 @@ -"""UniFi switch platform tests.""" +"""UniFi Network switch platform tests.""" from copy import deepcopy from unittest.mock import patch From ec7b1e574f00e4f5581f3056d130fb2a4ffe2d3c Mon Sep 17 00:00:00 2001 From: bcelary Date: Sat, 27 Nov 2021 01:07:30 +0100 Subject: [PATCH 0915/1452] Use mysensors child description as entity name (#60420) * using description for instance name if not empty * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Martin Hjelmare --- homeassistant/components/mysensors/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 0e6c5231bc8..35b15bc3595 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -131,6 +131,10 @@ class MySensorsDevice: @property def name(self) -> str: """Return the name of this entity.""" + child = self._child + + if child.description: + return str(child.description) return f"{self.node_name} {self.child_id}" @property From 96313bbbe04469cec0a7dc40f5dbc2f95990c5f2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 27 Nov 2021 00:12:17 +0000 Subject: [PATCH 0916/1452] [ci skip] Translation update --- .../components/abode/translations/ja.json | 2 +- .../accuweather/translations/ja.json | 2 +- .../components/adax/translations/ja.json | 2 +- .../components/adguard/translations/ja.json | 2 +- .../advantage_air/translations/ja.json | 2 +- .../components/agent_dvr/translations/ja.json | 2 +- .../components/airnow/translations/ja.json | 2 +- .../components/airthings/translations/ja.json | 2 +- .../components/airtouch4/translations/ja.json | 2 +- .../components/airvisual/translations/ja.json | 2 +- .../alarmdecoder/translations/ja.json | 2 +- .../components/almond/translations/ja.json | 2 +- .../components/ambee/translations/ja.json | 2 +- .../components/arcam_fmj/translations/ja.json | 2 +- .../components/asuswrt/translations/ja.json | 2 +- .../components/atag/translations/ja.json | 2 +- .../components/august/translations/ja.json | 2 +- .../components/aurora/translations/ja.json | 2 +- .../components/axis/translations/ja.json | 2 +- .../azure_devops/translations/ja.json | 2 +- .../components/balboa/translations/et.json | 28 +++++++++ .../components/balboa/translations/ja.json | 28 +++++++++ .../components/balboa/translations/no.json | 28 +++++++++ .../components/balboa/translations/pl.json | 28 +++++++++ .../components/balboa/translations/ru.json | 28 +++++++++ .../balboa/translations/zh-Hant.json | 28 +++++++++ .../components/blebox/translations/ja.json | 3 +- .../components/blink/translations/ja.json | 2 +- .../bmw_connected_drive/translations/ja.json | 2 +- .../components/bond/translations/ja.json | 2 +- .../components/bosch_shc/translations/ja.json | 2 +- .../components/braviatv/translations/ja.json | 2 +- .../components/broadlink/translations/ja.json | 4 +- .../components/brother/translations/ja.json | 2 +- .../components/brunt/translations/ja.json | 2 +- .../components/brunt/translations/pl.json | 29 +++++++++ .../components/bsblan/translations/ja.json | 2 +- .../components/button/translations/pl.json | 2 +- .../components/canary/translations/ja.json | 2 +- .../components/climacell/translations/ja.json | 2 +- .../cloudflare/translations/ja.json | 2 +- .../components/coinbase/translations/ja.json | 2 +- .../components/control4/translations/ja.json | 11 +++- .../coolmaster/translations/ja.json | 2 +- .../coronavirus/translations/ja.json | 2 +- .../components/daikin/translations/ja.json | 4 +- .../device_tracker/translations/ja.json | 4 ++ .../devolo_home_network/translations/ja.json | 2 +- .../components/dexcom/translations/ja.json | 2 +- .../components/directv/translations/ja.json | 2 +- .../components/dlna_dmr/translations/ja.json | 4 +- .../components/doorbird/translations/ja.json | 2 +- .../components/dsmr/translations/ja.json | 4 +- .../components/dunehd/translations/ja.json | 2 +- .../components/econet/translations/ja.json | 4 +- .../components/efergy/translations/ja.json | 2 +- .../components/elgato/translations/ja.json | 4 +- .../components/elkm1/translations/ja.json | 3 +- .../components/emonitor/translations/ja.json | 2 +- .../components/enocean/translations/ja.json | 9 ++- .../enphase_envoy/translations/ja.json | 2 +- .../environment_canada/translations/ja.json | 2 +- .../components/epson/translations/ja.json | 2 +- .../evil_genius_labs/translations/ja.json | 2 +- .../components/ezviz/translations/ja.json | 2 +- .../faa_delays/translations/ja.json | 2 +- .../components/firmata/translations/ja.json | 2 +- .../flick_electric/translations/ja.json | 2 +- .../components/flipr/translations/ja.json | 2 +- .../components/flo/translations/ja.json | 2 +- .../components/flume/translations/ja.json | 2 +- .../components/flux_led/translations/ja.json | 2 +- .../forked_daapd/translations/ja.json | 7 ++- .../components/foscam/translations/ja.json | 2 +- .../components/freebox/translations/ja.json | 2 +- .../freedompro/translations/ja.json | 2 +- .../components/fritz/translations/ja.json | 4 +- .../fritzbox_callmonitor/translations/ja.json | 1 + .../components/fronius/translations/ja.json | 2 +- .../components/fronius/translations/pl.json | 20 ++++++ .../garages_amsterdam/translations/ja.json | 2 +- .../components/gios/translations/ja.json | 2 +- .../components/glances/translations/ja.json | 2 +- .../components/goalzero/translations/ja.json | 2 +- .../components/gogogate2/translations/ja.json | 5 +- .../google_travel_time/translations/ja.json | 2 +- .../components/guardian/translations/ja.json | 2 +- .../components/harmony/translations/ja.json | 2 +- .../components/heos/translations/ja.json | 2 +- .../components/hlk_sw16/translations/ja.json | 2 +- .../components/homekit/translations/ja.json | 4 +- .../homematicip_cloud/translations/ja.json | 2 +- .../components/hue/translations/ja.json | 5 +- .../components/hue/translations/pl.json | 8 ++- .../huisbaasje/translations/ja.json | 2 +- .../humidifier/translations/ja.json | 3 +- .../translations/ja.json | 2 +- .../hvv_departures/translations/ja.json | 2 +- .../components/hyperion/translations/ja.json | 4 +- .../components/ialarm/translations/ja.json | 2 +- .../components/iaqualink/translations/ja.json | 2 +- .../components/insteon/translations/ja.json | 6 +- .../components/iotawatt/translations/ja.json | 2 +- .../components/ipp/translations/ja.json | 7 ++- .../components/isy994/translations/ja.json | 3 +- .../components/jellyfin/translations/ja.json | 2 +- .../components/juicenet/translations/ja.json | 5 +- .../keenetic_ndms2/translations/ja.json | 2 +- .../components/kmtronic/translations/ja.json | 2 +- .../components/knx/translations/ja.json | 2 +- .../components/knx/translations/pl.json | 63 +++++++++++++++++++ .../components/kodi/translations/ja.json | 6 +- .../components/konnected/translations/ja.json | 4 +- .../components/konnected/translations/pl.json | 1 + .../kostal_plenticore/translations/ja.json | 2 +- .../litterrobot/translations/ja.json | 2 +- .../components/lookin/translations/ja.json | 4 +- .../components/luftdaten/translations/ja.json | 2 +- .../lutron_caseta/translations/ja.json | 16 ++++- .../components/mazda/translations/ja.json | 2 +- .../components/melcloud/translations/ja.json | 2 +- .../components/metoffice/translations/ja.json | 2 +- .../components/mikrotik/translations/ja.json | 2 +- .../components/mill/translations/ja.json | 2 +- .../components/mill/translations/pl.json | 16 ++++- .../minecraft_server/translations/ja.json | 2 +- .../modem_callerid/translations/ja.json | 2 +- .../modern_forms/translations/ja.json | 4 +- .../components/monoprice/translations/ja.json | 2 +- .../motion_blinds/translations/ja.json | 2 +- .../components/motioneye/translations/ja.json | 2 +- .../components/mqtt/translations/ja.json | 24 ++++--- .../components/mullvad/translations/ja.json | 2 +- .../components/mutesync/translations/ja.json | 2 +- .../components/myq/translations/ja.json | 2 +- .../components/mysensors/translations/ja.json | 4 +- .../components/nam/translations/ja.json | 2 +- .../components/nam/translations/pl.json | 5 +- .../components/nanoleaf/translations/ja.json | 4 +- .../components/nexia/translations/ja.json | 2 +- .../nfandroidtv/translations/ja.json | 2 +- .../nightscout/translations/ja.json | 2 +- .../components/nuheat/translations/ja.json | 2 +- .../components/nuki/translations/ja.json | 2 +- .../components/nut/translations/ja.json | 4 +- .../components/nws/translations/ja.json | 2 +- .../components/nzbget/translations/ja.json | 2 +- .../components/octoprint/translations/ja.json | 4 +- .../components/omnilogic/translations/ja.json | 2 +- .../components/onewire/translations/ja.json | 2 +- .../components/onvif/translations/ja.json | 14 ++++- .../opengarage/translations/ja.json | 2 +- .../opentherm_gw/translations/ja.json | 2 +- .../openweathermap/translations/ja.json | 2 +- .../ovo_energy/translations/ja.json | 2 +- .../p1_monitor/translations/ja.json | 2 +- .../panasonic_viera/translations/ja.json | 4 +- .../philips_js/translations/ja.json | 2 +- .../components/pi_hole/translations/ja.json | 2 +- .../components/picnic/translations/ja.json | 2 +- .../components/plaato/translations/ja.json | 1 + .../components/plex/translations/ja.json | 7 ++- .../components/plugwise/translations/ja.json | 2 +- .../plum_lightpad/translations/ja.json | 2 +- .../components/powerwall/translations/ja.json | 2 +- .../progettihwsw/translations/ja.json | 2 +- .../components/prosegur/translations/ja.json | 2 +- .../components/ps4/translations/ja.json | 2 +- .../components/rachio/translations/ja.json | 2 +- .../rainforest_eagle/translations/ja.json | 2 +- .../components/rdw/translations/ja.json | 2 +- .../components/rfxtrx/translations/ja.json | 4 +- .../components/risco/translations/ja.json | 2 +- .../translations/ja.json | 2 +- .../components/roku/translations/ja.json | 2 +- .../components/roomba/translations/ja.json | 4 +- .../ruckus_unleashed/translations/ja.json | 2 +- .../components/samsungtv/translations/ja.json | 2 +- .../screenlogic/translations/ja.json | 2 +- .../components/sense/translations/ja.json | 2 +- .../components/sensor/translations/pl.json | 4 +- .../components/sharkiq/translations/ja.json | 4 +- .../components/shelly/translations/ja.json | 2 +- .../components/sma/translations/ja.json | 2 +- .../components/smappee/translations/ja.json | 2 +- .../smart_meter_texas/translations/ja.json | 2 +- .../components/smarthab/translations/ja.json | 1 + .../components/sms/translations/ja.json | 2 +- .../components/solarlog/translations/ja.json | 2 +- .../components/soma/translations/ja.json | 2 +- .../somfy_mylink/translations/ja.json | 4 +- .../components/sonarr/translations/ja.json | 2 +- .../components/songpal/translations/ja.json | 2 +- .../squeezebox/translations/ja.json | 2 +- .../srp_energy/translations/ja.json | 2 +- .../components/subaru/translations/ja.json | 4 +- .../surepetcare/translations/ja.json | 2 +- .../components/switchbot/translations/ja.json | 4 +- .../components/syncthing/translations/ja.json | 2 +- .../components/syncthru/translations/ja.json | 3 +- .../synology_dsm/translations/ja.json | 3 +- .../system_bridge/translations/ja.json | 2 +- .../components/tado/translations/ja.json | 2 +- .../components/tibber/translations/ja.json | 6 +- .../components/tolo/translations/ja.json | 2 +- .../components/tolo/translations/pl.json | 23 +++++++ .../tolo/translations/select.et.json | 8 +++ .../tolo/translations/select.ja.json | 8 +++ .../tolo/translations/select.no.json | 8 +++ .../tolo/translations/select.pl.json | 8 +++ .../tolo/translations/select.ru.json | 8 +++ .../tolo/translations/select.zh-Hant.json | 8 +++ .../components/toon/translations/ja.json | 5 ++ .../components/tplink/translations/ja.json | 2 +- .../components/tradfri/translations/ja.json | 2 +- .../transmission/translations/ja.json | 2 +- .../components/tuya/translations/ja.json | 4 +- .../twentemilieu/translations/ja.json | 2 +- .../components/twinkly/translations/ja.json | 2 +- .../components/unifi/translations/en.json | 14 ++--- .../components/unifi/translations/ja.json | 2 +- .../components/upb/translations/ja.json | 2 +- .../components/upcloud/translations/ja.json | 2 +- .../components/upnp/translations/ja.json | 3 + .../uptimerobot/translations/ja.json | 2 +- .../components/velbus/translations/ja.json | 2 +- .../components/venstar/translations/ja.json | 2 +- .../components/vilfo/translations/ja.json | 2 +- .../components/vizio/translations/ja.json | 4 +- .../vlc_telnet/translations/ja.json | 4 +- .../components/volumio/translations/ja.json | 2 +- .../components/wallbox/translations/ja.json | 2 +- .../water_heater/translations/ja.json | 1 + .../waze_travel_time/translations/ja.json | 2 +- .../components/whirlpool/translations/ja.json | 2 +- .../components/withings/translations/ja.json | 1 + .../components/wled/translations/ja.json | 4 +- .../wled/translations/select.pl.json | 9 +++ .../components/wolflink/translations/ja.json | 2 +- .../wolflink/translations/sensor.ja.json | 1 + .../xiaomi_miio/translations/ja.json | 2 +- .../components/yeelight/translations/ja.json | 2 +- .../components/youless/translations/ja.json | 2 +- .../components/zha/translations/ja.json | 2 +- .../zoneminder/translations/ja.json | 4 +- .../components/zwave_js/translations/ja.json | 8 +-- 246 files changed, 742 insertions(+), 280 deletions(-) create mode 100644 homeassistant/components/balboa/translations/et.json create mode 100644 homeassistant/components/balboa/translations/ja.json create mode 100644 homeassistant/components/balboa/translations/no.json create mode 100644 homeassistant/components/balboa/translations/pl.json create mode 100644 homeassistant/components/balboa/translations/ru.json create mode 100644 homeassistant/components/balboa/translations/zh-Hant.json create mode 100644 homeassistant/components/brunt/translations/pl.json create mode 100644 homeassistant/components/fronius/translations/pl.json create mode 100644 homeassistant/components/knx/translations/pl.json create mode 100644 homeassistant/components/tolo/translations/pl.json create mode 100644 homeassistant/components/tolo/translations/select.et.json create mode 100644 homeassistant/components/tolo/translations/select.ja.json create mode 100644 homeassistant/components/tolo/translations/select.no.json create mode 100644 homeassistant/components/tolo/translations/select.pl.json create mode 100644 homeassistant/components/tolo/translations/select.ru.json create mode 100644 homeassistant/components/tolo/translations/select.zh-Hant.json create mode 100644 homeassistant/components/wled/translations/select.pl.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index 964ee5dee7e..cd498691f4b 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -5,7 +5,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_mfa_code": "\u7121\u52b9\u306aMFA\u30b3\u30fc\u30c9" }, diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 63c8561abd2..00c659fade9 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "requests_exceeded": "Accuweather API\u3078\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u6570\u304c\u8a31\u53ef\u3055\u308c\u305f\u6570\u3092\u8d85\u3048\u307e\u3057\u305f\u3002\u6642\u9593\u3092\u7f6e\u304f\u304b\u3001API\u30ad\u30fc\u3092\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json index 7fea6e56932..880b37db94d 100644 --- a/homeassistant/components/adax/translations/ja.json +++ b/homeassistant/components/adax/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/adguard/translations/ja.json b/homeassistant/components/adguard/translations/ja.json index b06b18d61e8..33f24b1d8e5 100644 --- a/homeassistant/components/adguard/translations/ja.json +++ b/homeassistant/components/adguard/translations/ja.json @@ -5,7 +5,7 @@ "existing_instance_updated": "\u65e2\u5b58\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/advantage_air/translations/ja.json b/homeassistant/components/advantage_air/translations/ja.json index b47eaa705a6..9cc6ef69737 100644 --- a/homeassistant/components/advantage_air/translations/ja.json +++ b/homeassistant/components/advantage_air/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/agent_dvr/translations/ja.json b/homeassistant/components/agent_dvr/translations/ja.json index 5da28535c9d..091f9a89475 100644 --- a/homeassistant/components/agent_dvr/translations/ja.json +++ b/homeassistant/components/agent_dvr/translations/ja.json @@ -5,7 +5,7 @@ }, "error": { "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index 1cd0b8398e8..d535f7fc044 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_location": "\u305d\u306e\u5834\u6240\u306b\u5bfe\u3059\u308b\u7d50\u679c\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/airthings/translations/ja.json b/homeassistant/components/airthings/translations/ja.json index a959fc54980..2621fb437a7 100644 --- a/homeassistant/components/airthings/translations/ja.json +++ b/homeassistant/components/airthings/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/airtouch4/translations/ja.json b/homeassistant/components/airtouch4/translations/ja.json index 454d0434603..9cc4a7464e1 100644 --- a/homeassistant/components/airtouch4/translations/ja.json +++ b/homeassistant/components/airtouch4/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_units": "AirTouch 4 Groups\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index 09565aabc62..eafcdc7378d 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "general_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "location_not_found": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index b710d58d10d..7e508ba6ad5 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -7,7 +7,7 @@ "default": "AlarmDecoder\u3068\u306e\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "protocol": { diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index c91407e7ecf..4d51bbc2f13 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index 3a308ec4b70..e320189e010 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -4,7 +4,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" }, "step": { diff --git a/homeassistant/components/arcam_fmj/translations/ja.json b/homeassistant/components/arcam_fmj/translations/ja.json index 7c14e270f34..053954d79b9 100644 --- a/homeassistant/components/arcam_fmj/translations/ja.json +++ b/homeassistant/components/arcam_fmj/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{host}", "step": { diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 7517a342d4b..ab253e324ce 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "pwd_and_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u306e\u307f\u63d0\u4f9b", "pwd_or_ssh": "\u30d1\u30b9\u30ef\u30fc\u30c9\u307e\u305f\u306fSSH\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3092\u63d0\u4f9b\u3057\u3066\u304f\u3060\u3055\u3044", diff --git a/homeassistant/components/atag/translations/ja.json b/homeassistant/components/atag/translations/ja.json index f90666e5f39..5aecec86168 100644 --- a/homeassistant/components/atag/translations/ja.json +++ b/homeassistant/components/atag/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unauthorized": "\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u3067\u8a8d\u8a3c\u8981\u6c42\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, "step": { diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index 4b602e71948..d1b28dfd52d 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/aurora/translations/ja.json b/homeassistant/components/aurora/translations/ja.json index 504b77e6906..86e38f981f9 100644 --- a/homeassistant/components/aurora/translations/ja.json +++ b/homeassistant/components/aurora/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/axis/translations/ja.json b/homeassistant/components/axis/translations/ja.json index 66761cc0dc6..bb9fa910122 100644 --- a/homeassistant/components/axis/translations/ja.json +++ b/homeassistant/components/axis/translations/ja.json @@ -8,7 +8,7 @@ "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/azure_devops/translations/ja.json b/homeassistant/components/azure_devops/translations/ja.json index a358aadbe06..659caaa9317 100644 --- a/homeassistant/components/azure_devops/translations/ja.json +++ b/homeassistant/components/azure_devops/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "project_error": "\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, diff --git a/homeassistant/components/balboa/translations/et.json b/homeassistant/components/balboa/translations/et.json new file mode 100644 index 00000000000..264855023f9 --- /dev/null +++ b/homeassistant/components/balboa/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "\u00dchendu Balboa Wi-Fi seadmega" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Hoia oma Balboa Spa kliendi aeg Home Assistantiga s\u00fcnkroonis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/ja.json b/homeassistant/components/balboa/translations/ja.json new file mode 100644 index 00000000000..f6e799cd6af --- /dev/null +++ b/homeassistant/components/balboa/translations/ja.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "Balboa Wi-Fi device\u306b\u63a5\u7d9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Balboa Spa Client's\u306e\u6642\u9593\u3092\u3001Home Assistant\u3068\u540c\u671f\u3055\u305b\u307e\u3059" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/no.json b/homeassistant/components/balboa/translations/no.json new file mode 100644 index 00000000000..46f2c9284b8 --- /dev/null +++ b/homeassistant/components/balboa/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + }, + "title": "Koble til Balboa Wi-Fi-enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Hold Balboa Spa-klientens tid synkronisert med Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pl.json b/homeassistant/components/balboa/translations/pl.json new file mode 100644 index 00000000000..cc5429ad077 --- /dev/null +++ b/homeassistant/components/balboa/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "title": "Po\u0142\u0105cz si\u0119 z urz\u0105dzeniem Balboa Wi-Fi" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Synchronizacja czasu klienta Balboa Spa z Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/ru.json b/homeassistant/components/balboa/translations/ru.json new file mode 100644 index 00000000000..07732f3796e --- /dev/null +++ b/homeassistant/components/balboa/translations/ru.json @@ -0,0 +1,28 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f Balboa Spa \u0441 Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/zh-Hant.json b/homeassistant/components/balboa/translations/zh-Hant.json new file mode 100644 index 00000000000..78aa2e7c0b9 --- /dev/null +++ b/homeassistant/components/balboa/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u9023\u7dda\u81f3 Balboa Wi-Fi \u88dd\u7f6e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "\u5c07 Balboa Spa \u5ba2\u6236\u7aef\u6642\u9593\u8207 Home Assistant \u4fdd\u6301\u540c\u6b65" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index 0c77aff5b87..866204dc3da 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -1,10 +1,11 @@ { "config": { "abort": { + "address_already_configured": "{address} \u306b\u306f\u3059\u3067\u306bBleBox\u30c7\u30d0\u30a4\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index cdc7f16105b..40724c01d42 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 7e0b57d32f2..5e33b26f423 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json index c9c41c61e43..0a39acc8cb2 100644 --- a/homeassistant/components/bond/translations/ja.json +++ b/homeassistant/components/bond/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 7525cf016f4..fb466338238 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002Bosch Smart Home Controller\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b(LED\u304c\u70b9\u6ec5)\u3053\u3068\u3068\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u3044\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "session_error": "\u30bb\u30c3\u30b7\u30e7\u30f3\u30a8\u30e9\u30fc: API\u304c\u3001OK\u4ee5\u5916\u306e\u7d50\u679c\u3092\u8fd4\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 737af228a37..835e67e7b9f 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -5,7 +5,7 @@ "no_ip_control": "\u30c6\u30ec\u30d3\u3067IP\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30c6\u30ec\u30d3\u304c\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unsupported_model": "\u304a\u4f7f\u3044\u306e\u30c6\u30ec\u30d3\u306e\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/broadlink/translations/ja.json b/homeassistant/components/broadlink/translations/ja.json index 47985c1d2ee..37c08070d18 100644 --- a/homeassistant/components/broadlink/translations/ja.json +++ b/homeassistant/components/broadlink/translations/ja.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index f9673cf5d9c..c77ad218ea9 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -5,7 +5,7 @@ "unsupported_model": "\u3053\u306e\u30d7\u30ea\u30f3\u30bf\u30fc\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "snmp_error": "SNMP\u30b5\u30fc\u30d0\u30fc\u304c\u30aa\u30d5\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host": "\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json index 631d5e54f47..a0c477443b8 100644 --- a/homeassistant/components/brunt/translations/ja.json +++ b/homeassistant/components/brunt/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/brunt/translations/pl.json b/homeassistant/components/brunt/translations/pl.json new file mode 100644 index 00000000000..61c5f95269a --- /dev/null +++ b/homeassistant/components/brunt/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a ponownie has\u0142o dla: {username}:", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja integracji Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index ff3dcf38b74..4dda07de639 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/button/translations/pl.json b/homeassistant/components/button/translations/pl.json index e5af8b8c29b..81dcd88a091 100644 --- a/homeassistant/components/button/translations/pl.json +++ b/homeassistant/components/button/translations/pl.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "press": "naci\u015bnij przycisk {entity_name}" + "press": "Naci\u015bnij przycisk {entity_name}" }, "trigger_type": { "pressed": "{entity_name} zosta\u0142 naci\u015bni\u0119ty" diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 6b5b2546122..9f9903b86e4 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -5,7 +5,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index 6f8f3592891..5114f8e9881 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 430a979d6bd..87e08b4ad88 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_zone": "\u7121\u52b9\u306a\u30be\u30fc\u30f3" }, diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index 64bf9340450..55333777aa2 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/control4/translations/ja.json b/homeassistant/components/control4/translations/ja.json index 1c9668a0085..ae0824a804f 100644 --- a/homeassistant/components/control4/translations/ja.json +++ b/homeassistant/components/control4/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -18,5 +18,14 @@ "description": "Control4\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8a73\u7d30\u3068\u3001\u30ed\u30fc\u30ab\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694\u306e\u79d2\u6570" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json index 2cbcf95a7de..06a6ca34b95 100644 --- a/homeassistant/components/coolmaster/translations/ja.json +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_units": "CoolMasterNet\u306e\u30db\u30b9\u30c8\u306bHVAC\u30e6\u30cb\u30c3\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { diff --git a/homeassistant/components/coronavirus/translations/ja.json b/homeassistant/components/coronavirus/translations/ja.json index 0a0b23917fa..113db73a700 100644 --- a/homeassistant/components/coronavirus/translations/ja.json +++ b/homeassistant/components/coronavirus/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/ja.json b/homeassistant/components/daikin/translations/ja.json index 476fd0840c0..6b210a056c9 100644 --- a/homeassistant/components/daikin/translations/ja.json +++ b/homeassistant/components/daikin/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "api_password": "\u7121\u52b9\u306a\u8a8d\u8a3c\u3002API\u30ad\u30fc\u307e\u305f\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u3044\u305a\u308c\u304b\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/device_tracker/translations/ja.json b/homeassistant/components/device_tracker/translations/ja.json index f53b61ed316..4609f6f9f3e 100644 --- a/homeassistant/components/device_tracker/translations/ja.json +++ b/homeassistant/components/device_tracker/translations/ja.json @@ -1,5 +1,9 @@ { "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u306f\u3001\u5728\u5b85\u3067\u3059", + "is_not_home": "{entity_name} \u306f\u3001\u4e0d\u5728\u3067\u3059" + }, "trigger_type": { "enters": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u306b\u5165\u308b", "leaves": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u304b\u3089\u96e2\u308c\u308b" diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index 2d7f1b072ad..ee08879f713 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -5,7 +5,7 @@ "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{product} ({name})", diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index c07b80928c6..21cc971beec 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/directv/translations/ja.json b/homeassistant/components/directv/translations/ja.json index 2c0ae17f39b..413861bce12 100644 --- a/homeassistant/components/directv/translations/ja.json +++ b/homeassistant/components/directv/translations/ja.json @@ -5,7 +5,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 1d999efe833..9edb9156534 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", @@ -11,7 +11,7 @@ "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 9d6b6246d68..059ae70c39c 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -5,7 +5,7 @@ "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index f7acb5d9961..53c7d5c1050 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_communicate": "\u901a\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "setup_network": { diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index 461af46c93e..2bf0ff94c48 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" }, "step": { diff --git a/homeassistant/components/econet/translations/ja.json b/homeassistant/components/econet/translations/ja.json index a552bfb6977..4c29b8d305d 100644 --- a/homeassistant/components/econet/translations/ja.json +++ b/homeassistant/components/econet/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json index c9c542a6c6e..98dd06ab4f0 100644 --- a/homeassistant/components/efergy/translations/ja.json +++ b/homeassistant/components/efergy/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index 830ad27b671..8280fd46ecc 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{serial_number}", "step": { diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 7475aada4a9..0131bbb9fef 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -10,6 +10,7 @@ "data": { "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "prefix": "\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(ElkM1\u304c1\u3064\u3057\u304b\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" diff --git a/homeassistant/components/emonitor/translations/ja.json b/homeassistant/components/emonitor/translations/ja.json index c05a3c64696..feeb93f5c86 100644 --- a/homeassistant/components/emonitor/translations/ja.json +++ b/homeassistant/components/emonitor/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json index ea71065763b..e0ec74d778f 100644 --- a/homeassistant/components/enocean/translations/ja.json +++ b/homeassistant/components/enocean/translations/ja.json @@ -4,16 +4,21 @@ "invalid_dongle_path": "\u30c9\u30f3\u30b0\u30eb\u30d1\u30b9\u304c\u7121\u52b9", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, + "error": { + "invalid_dongle_path": "\u3053\u306e\u30d1\u30b9\u306b\u6709\u52b9\u306a\u30c9\u30f3\u30b0\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, "step": { "detect": { "data": { "path": "USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9" - } + }, + "title": "ENOcean dongle\u306e\u30d1\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "manual": { "data": { "path": "USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9" - } + }, + "title": "ENOcean dongle\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index 1fd206d2331..e14d0fe713b 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json index 0fcf4e4f2b4..e9057e7a48b 100644 --- a/homeassistant/components/environment_canada/translations/ja.json +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -2,7 +2,7 @@ "config": { "error": { "bad_station_id": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID\u304c\u7121\u52b9\u3001\u6b20\u843d\u3057\u3066\u3044\u308b\u3001\u307e\u305f\u306f\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID \u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u5185\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "error_response": "\u30ab\u30ca\u30c0\u74b0\u5883\u304b\u3089\u306e\u5fdc\u7b54\u30a8\u30e9\u30fc", "too_many_attempts": "\u30ab\u30ca\u30c0\u74b0\u5883\u7701\u3078\u306e\u63a5\u7d9a\u306f\u30ec\u30fc\u30c8\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u300260\u79d2\u5f8c\u306b\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/epson/translations/ja.json b/homeassistant/components/epson/translations/ja.json index 435b1eb8355..e48317bcee3 100644 --- a/homeassistant/components/epson/translations/ja.json +++ b/homeassistant/components/epson/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "powered_off": "\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u306f\u5165\u3063\u3066\u3044\u307e\u3059\u304b\uff1f\u521d\u671f\u8a2d\u5b9a\u3092\u884c\u3046\u305f\u3081\u306b\u306f\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30bf\u30fc\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u304a\u304f\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/ja.json b/homeassistant/components/evil_genius_labs/translations/ja.json index 677ed4b719c..db894cded4e 100644 --- a/homeassistant/components/evil_genius_labs/translations/ja.json +++ b/homeassistant/components/evil_genius_labs/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/ezviz/translations/ja.json b/homeassistant/components/ezviz/translations/ja.json index 70a66a09299..4780b5fd10a 100644 --- a/homeassistant/components/ezviz/translations/ja.json +++ b/homeassistant/components/ezviz/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" }, diff --git a/homeassistant/components/faa_delays/translations/ja.json b/homeassistant/components/faa_delays/translations/ja.json index c3dd00812e6..144133ae2a0 100644 --- a/homeassistant/components/faa_delays/translations/ja.json +++ b/homeassistant/components/faa_delays/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u3053\u306e\u7a7a\u6e2f\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_airport": "\u7a7a\u6e2f\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/firmata/translations/ja.json b/homeassistant/components/firmata/translations/ja.json index 34802a0807b..c0253537836 100644 --- a/homeassistant/components/firmata/translations/ja.json +++ b/homeassistant/components/firmata/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" } } } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ja.json b/homeassistant/components/flick_electric/translations/ja.json index 8373068fb2a..7a595dcd56a 100644 --- a/homeassistant/components/flick_electric/translations/ja.json +++ b/homeassistant/components/flick_electric/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/flipr/translations/ja.json b/homeassistant/components/flipr/translations/ja.json index 7a159542dfc..7d87c4a3c39 100644 --- a/homeassistant/components/flipr/translations/ja.json +++ b/homeassistant/components/flipr/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_flipr_id_found": "\u73fe\u5728\u3001\u3042\u306a\u305f\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u95a2\u9023\u4ed8\u3051\u3089\u308c\u3066\u3044\u308bflipr id\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u307e\u305a\u306f\u3001Flipr\u306e\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u3067\u52d5\u4f5c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/flo/translations/ja.json b/homeassistant/components/flo/translations/ja.json index 5bd6c037226..a9d2ddfd3ac 100644 --- a/homeassistant/components/flo/translations/ja.json +++ b/homeassistant/components/flo/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json index 8c51c78880c..9e279f94596 100644 --- a/homeassistant/components/flume/translations/ja.json +++ b/homeassistant/components/flume/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/flux_led/translations/ja.json b/homeassistant/components/flux_led/translations/ja.json index 7e0f053305f..3b6a34d7e5b 100644 --- a/homeassistant/components/flux_led/translations/ja.json +++ b/homeassistant/components/flux_led/translations/ja.json @@ -6,7 +6,7 @@ "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{model} {id} ({ipaddr})", "step": { diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 669646a4ef0..41f1167d063 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "not_forked_daapd": "\u30c7\u30d0\u30a4\u30b9\u306f\u3001forked-daapd server\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { "forbidden": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002forked-daapd network\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "websocket_not_enabled": "forked-daapd server\u306eWebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" @@ -29,7 +31,8 @@ "data": { "librespot_java_port": "librespot-java\u30d1\u30a4\u30d7\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7528\u30dd\u30fc\u30c8(\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u5834\u5408)", "max_playlists": "\u30bd\u30fc\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u6700\u5927\u6570", - "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570" + "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570", + "tts_volume": "TTS\u30dc\u30ea\u30e5\u30fc\u30e0(\u7bc4\u56f2\u306f\u3001[0,1]\u306e\u5c0f\u6570\u70b9)" } } } diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 81c10c8f353..5a02ae5f446 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_response": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u7121\u52b9\u306a\u5fdc\u7b54", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json index 11811d5a067..fa11e1c4822 100644 --- a/homeassistant/components/freebox/translations/ja.json +++ b/homeassistant/components/freebox/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/freedompro/translations/ja.json b/homeassistant/components/freedompro/translations/ja.json index 6e5184bfdef..22e7047f496 100644 --- a/homeassistant/components/freedompro/translations/ja.json +++ b/homeassistant/components/freedompro/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 1994cfe3401..77700224d19 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -8,8 +8,8 @@ "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{name}", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ja.json b/homeassistant/components/fritzbox_callmonitor/translations/ja.json index 2a11bc7a198..1c3294403ec 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ja.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "insufficient_permissions": "\u30e6\u30fc\u30b6\u30fc\u306b\u3001AVM FRITZ!Box\u306e\u8a2d\u5b9a\u3068\u96fb\u8a71\u5e33\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u6a29\u9650\u304c\u4e0d\u8db3\u3057\u3066\u3044\u307e\u3059\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { diff --git a/homeassistant/components/fronius/translations/ja.json b/homeassistant/components/fronius/translations/ja.json index dc120a17f6a..f5d2a03874e 100644 --- a/homeassistant/components/fronius/translations/ja.json +++ b/homeassistant/components/fronius/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/fronius/translations/pl.json b/homeassistant/components/fronius/translations/pl.json new file mode 100644 index 00000000000..b98f78aa82f --- /dev/null +++ b/homeassistant/components/fronius/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Skonfiguruj adres IP lub lokaln\u0105 nazw\u0119 hosta urz\u0105dzenia Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/ja.json b/homeassistant/components/garages_amsterdam/translations/ja.json index 2f0e09c36bb..778dd7077a1 100644 --- a/homeassistant/components/garages_amsterdam/translations/ja.json +++ b/homeassistant/components/garages_amsterdam/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index 8145ba9d64d..faaf4644f10 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/glances/translations/ja.json b/homeassistant/components/glances/translations/ja.json index 2de3d643a33..0267110e0d0 100644 --- a/homeassistant/components/glances/translations/ja.json +++ b/homeassistant/components/glances/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "wrong_version": "\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3\u306e\u307f)" }, "step": { diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index b93b6b4a6fc..3e2e33bc302 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/gogogate2/translations/ja.json b/homeassistant/components/gogogate2/translations/ja.json index f963a861282..d1c4eb62b92 100644 --- a/homeassistant/components/gogogate2/translations/ja.json +++ b/homeassistant/components/gogogate2/translations/ja.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{device} ({ip_address})", @@ -15,6 +15,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "\u4ee5\u4e0b\u306b\u5fc5\u8981\u306a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Gogogate2\u307e\u305f\u306fismartgate\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index 38dac479d9d..2fb8ae2883c 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index f49267f73f3..57378efab44 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "discovery_confirm": { diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index 7f040e4f684..2730ab3ba1a 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 5502f2e8183..0464bf98979 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/hlk_sw16/translations/ja.json b/homeassistant/components/hlk_sw16/translations/ja.json index 5bd6c037226..a9d2ddfd3ac 100644 --- a/homeassistant/components/hlk_sw16/translations/ja.json +++ b/homeassistant/components/hlk_sw16/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index d7481b4d011..e46c4d1999a 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -24,7 +24,8 @@ "auto_start": "\u81ea\u52d5\u8d77\u52d5(homekit.start\u30b5\u30fc\u30d3\u30b9\u3092\u624b\u52d5\u3067\u547c\u3073\u51fa\u3059\u5834\u5408\u306f\u7121\u52b9\u306b\u3059\u308b)", "devices": "\u30c7\u30d0\u30a4\u30b9(\u30c8\u30ea\u30ac\u30fc)" }, - "description": "\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9\u3054\u3068\u306b\u3001\u30d7\u30ed\u30b0\u30e9\u30e0\u53ef\u80fd\u306a\u30b9\u30a4\u30c3\u30c1\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u304c\u767a\u751f\u3059\u308b\u3068\u3001HomeKit\u306f\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3084\u30b7\u30fc\u30f3\u3092\u5b9f\u884c\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002" + "description": "\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9\u3054\u3068\u306b\u3001\u30d7\u30ed\u30b0\u30e9\u30e0\u53ef\u80fd\u306a\u30b9\u30a4\u30c3\u30c1\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u304c\u767a\u751f\u3059\u308b\u3068\u3001HomeKit\u306f\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3084\u30b7\u30fc\u30f3\u3092\u5b9f\u884c\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002", + "title": "\u9ad8\u5ea6\u306a\u8a2d\u5b9a" }, "cameras": { "data": { @@ -46,6 +47,7 @@ "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3", "mode": "\u30e2\u30fc\u30c9" }, + "description": "HomeKit \u306f\u3001\u30d6\u30ea\u30c3\u30b8\u307e\u305f\u306f\u5358\u4e00\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u516c\u958b\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002TV\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9\u3092\u6301\u3064\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u304c\u6b63\u5e38\u306b\u6a5f\u80fd\u3059\u308b\u305f\u3081\u306b\u306f\u3001\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059\u3002\"\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3(Domains to include)\"\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306f\u3001 HomeKit \u306b\u542b\u307e\u308c\u307e\u3059\u3002\u6b21\u306e\u753b\u9762\u3067\u3053\u306e\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3001\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3067\u304d\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "yaml": { diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 90f56fcf7b6..f68e51c7893 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "connection_aborted": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "connection_aborted": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index cd9db3eba6a..ed2161e9fbc 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -4,7 +4,7 @@ "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue bridge\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", "no_bridges": "Hue bridge\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "not_hue_bridge": "Hue bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", @@ -55,7 +55,8 @@ "step": { "init": { "data": { - "allow_hue_scenes": "Hue\u30b7\u30fc\u30f3\u3092\u8a31\u53ef" + "allow_hue_scenes": "Hue\u30b7\u30fc\u30f3\u3092\u8a31\u53ef", + "allow_unreachable": "\u5230\u9054\u3067\u304d\u306a\u304b\u3063\u305f\u96fb\u7403(bulbs)\u304c\u305d\u306e\u72b6\u614b\u3092\u6b63\u3057\u304f\u5831\u544a\u3067\u304d\u308b\u3088\u3046\u306b\u3059\u308b" } } } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index ea3e402a59b..c1109444bdb 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -51,11 +51,16 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { + "double_short_release": "Przycisk \"{subtype}\" zwolniony", + "initial_press": "Przycisk \"{subtype}\" wci\u015bni\u0119ty pocz\u0105tkowo", + "long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim przyci\u015bni\u0119ciu", "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", - "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione" + "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione", + "repeat": "Przycisk \"{subtype}\" przytrzymany", + "short_release": "Przycisk \"{subtype}\" zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu" } }, "options": { @@ -63,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Zezwalaj na grupowanie Hue", + "allow_hue_scenes": "Zezwalaj na sceny dla Hue", "allow_unreachable": "Zezwalaj nieosi\u0105galnym \u017car\u00f3wkom na poprawne raportowanie ich stanu" } } diff --git a/homeassistant/components/huisbaasje/translations/ja.json b/homeassistant/components/huisbaasje/translations/ja.json index 2f714747433..323de60808b 100644 --- a/homeassistant/components/huisbaasje/translations/ja.json +++ b/homeassistant/components/huisbaasje/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/humidifier/translations/ja.json b/homeassistant/components/humidifier/translations/ja.json index b03f4389d2c..2c17d1e3f61 100644 --- a/homeassistant/components/humidifier/translations/ja.json +++ b/homeassistant/components/humidifier/translations/ja.json @@ -23,5 +23,6 @@ "off": "\u30aa\u30d5", "on": "\u30aa\u30f3" } - } + }, + "title": "\u52a0\u6e7f\u5668" } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json index e5266579ed1..8df2288c362 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ja.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json index 030a6a99725..1dd658cedbf 100644 --- a/homeassistant/components/hvv_departures/translations/ja.json +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index 0289e0a8a3f..2d5b66adc3f 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -6,12 +6,12 @@ "auth_new_token_not_granted_error": "\u65b0\u3057\u304f\u4f5c\u6210\u3057\u305f\u30c8\u30fc\u30af\u30f3\u304cHyperion UI\u3067\u627f\u8a8d\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "auth_new_token_not_work_error": "\u65b0\u3057\u304f\u4f5c\u6210\u3055\u308c\u305f\u30c8\u30fc\u30af\u30f3\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "auth_required_error": "\u627f\u8a8d\u304c\u5fc5\u8981\u304b\u3069\u3046\u304b\u3092\u5224\u65ad\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_id": "Hyperion Ambilight\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u305d\u306eID\u3092\u30ec\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, "step": { diff --git a/homeassistant/components/ialarm/translations/ja.json b/homeassistant/components/ialarm/translations/ja.json index 1cbb749800b..6077685b7f4 100644 --- a/homeassistant/components/ialarm/translations/ja.json +++ b/homeassistant/components/ialarm/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 8d2a44e9136..cb4afb4d130 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 4699565cae2..22a9d0a2b2b 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { @@ -44,7 +44,7 @@ }, "options": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { diff --git a/homeassistant/components/iotawatt/translations/ja.json b/homeassistant/components/iotawatt/translations/ja.json index 3f176af1693..973f21fdcb7 100644 --- a/homeassistant/components/iotawatt/translations/ja.json +++ b/homeassistant/components/iotawatt/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 8b1d1ae0e2d..3e9159ce3a2 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_upgrade": "\u63a5\u7d9a\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9(connection upgrade)\u304c\u5fc5\u8981\u306a\u305f\u3081\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "ipp_version_error": "IPP\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30d7\u30ea\u30f3\u30bf\u30fc\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "unique_id_required": "\u691c\u51fa\u306b\u5fc5\u8981\u306a\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "connection_upgrade": "\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002SSL/TLS\u30aa\u30d7\u30b7\u30e7\u30f3\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066(option checked)\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { @@ -25,6 +25,7 @@ "title": "\u30d7\u30ea\u30f3\u30bf\u30fc\u3092\u30ea\u30f3\u30af\u3059\u308b" }, "zeroconf_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", "title": "\u691c\u51fa\u3055\u308c\u305f\u30d7\u30ea\u30f3\u30bf\u30fc" } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index e915abeffe9..20ed55f9fb2 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" @@ -15,6 +15,7 @@ "data": { "host": "URL", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "tls": "ISY\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306eTLS\u30d0\u30fc\u30b8\u30e7\u30f3\u3002", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 \u4f8b: http://192.168.10.100:80", diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 2b14d614f02..69cc9a5279d 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/juicenet/translations/ja.json b/homeassistant/components/juicenet/translations/ja.json index b140d6731e5..245fe0cdadc 100644 --- a/homeassistant/components/juicenet/translations/ja.json +++ b/homeassistant/components/juicenet/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -12,7 +12,8 @@ "user": { "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" - } + }, + "title": "JuiceNet\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/ja.json b/homeassistant/components/keenetic_ndms2/translations/ja.json index 471263e916c..90fc5e60155 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ja.json +++ b/homeassistant/components/keenetic_ndms2/translations/ja.json @@ -6,7 +6,7 @@ "not_keenetic_ndms2": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306fKeenetic router\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/kmtronic/translations/ja.json b/homeassistant/components/kmtronic/translations/ja.json index 84d24f2db6d..2b345d6ffcf 100644 --- a/homeassistant/components/kmtronic/translations/ja.json +++ b/homeassistant/components/kmtronic/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 0ad86b88945..23614ebcbc2 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -5,7 +5,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "manual_tunnel": { diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json new file mode 100644 index 00000000000..c987e2cc937 --- /dev/null +++ b/homeassistant/components/knx/translations/pl.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Nazwa hosta lub adres IP", + "individual_address": "Indywidualny adres dla po\u0142\u0105czenia", + "port": "Port", + "route_back": "Tryb Route Back / NAT" + }, + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + }, + "routing": { + "data": { + "individual_address": "Indywidualny adres dla po\u0142\u0105czenia routingowego", + "multicast_group": "Grupa multicast u\u017cyta do routingu", + "multicast_port": "Port multicast u\u017cyty do routingu" + }, + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu." + }, + "tunnel": { + "data": { + "gateway": "Po\u0142\u0105czenie tunelowe KNX" + }, + "description": "Prosz\u0119 wybra\u0107 bram\u0119 z listy." + }, + "type": { + "data": { + "connection_type": "Typ po\u0142\u0105czenia KNX" + }, + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \n AUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramy. \n TUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \n ROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "Typ po\u0142\u0105czenia KNX", + "individual_address": "Domy\u015blny adres indywidualny", + "multicast_group": "Grupa multicast u\u017cywana do routingu i wykrywania", + "multicast_port": "Port multicast u\u017cywany do routingu i wykrywania", + "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119", + "state_updater": "Zezw\u00f3l globalnie na odczyt stan\u00f3w z magistrali KNX" + } + }, + "tunnel": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "route_back": "Tryb Route Back / NAT" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index f3d32306703..c7cf892bb2b 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u4e00\u610f(unique)\u306aID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index fcb7275820b..162d7e25d1e 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_konn_panel": "\u8a8d\u8b58\u3055\u308c\u305f\u3001Konnected.io\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "confirm": { diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index f6e9a2dbfbc..7352bc82344 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json index 63c2dcb9928..d4f08a1dd5c 100644 --- a/homeassistant/components/kostal_plenticore/translations/ja.json +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index 580a8bb3a3d..b4c39a6b251 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/lookin/translations/ja.json b/homeassistant/components/lookin/translations/ja.json index d4bad858c63..345a338d19d 100644 --- a/homeassistant/components/lookin/translations/ja.json +++ b/homeassistant/components/lookin/translations/ja.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/luftdaten/translations/ja.json b/homeassistant/components/luftdaten/translations/ja.json index ec793c99368..15dc417c156 100644 --- a/homeassistant/components/luftdaten/translations/ja.json +++ b/homeassistant/components/luftdaten/translations/ja.json @@ -2,7 +2,7 @@ "config": { "error": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_sensor": "\u30bb\u30f3\u30b5\u30fc\u304c\u5229\u7528\u3067\u304d\u306a\u3044\u304b\u3001\u7121\u52b9\u3067\u3059" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 1b42430e02c..64d42df1c1d 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_lutron_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Lutron\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} ({host})", "step": { @@ -38,6 +38,12 @@ "group_1_button_2": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "group_2_button_1": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", "group_2_button_2": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "lower": "\u4e0b\u3052\u308b", + "lower_1": "\u4e0b1", + "lower_2": "\u4e0b2", + "lower_3": "\u4e0b3", + "lower_4": "\u4e0b4", + "lower_all": "\u3059\u3079\u3066\u4e0b\u3052\u308b", "off": "\u30aa\u30d5", "on": "\u30aa\u30f3", "open_1": "\u30aa\u30fc\u30d7\u30f31", @@ -45,6 +51,12 @@ "open_3": "\u30aa\u30fc\u30d7\u30f33", "open_4": "\u30aa\u30fc\u30d7\u30f34", "open_all": "\u3059\u3079\u3066\u958b\u304f", + "raise": "\u4e0a\u3052\u308b", + "raise_1": "\u4e0a1", + "raise_2": "\u4e0a2", + "raise_3": "\u4e0a3", + "raise_4": "\u4e0a4", + "raise_all": "\u3059\u3079\u3066\u4e0a\u3052\u308b", "stop": "\u505c\u6b62(\u304a\u6c17\u306b\u5165\u308a)", "stop_1": "\u505c\u6b62 1", "stop_2": "\u505c\u6b62 2", diff --git a/homeassistant/components/mazda/translations/ja.json b/homeassistant/components/mazda/translations/ja.json index f285bda19b7..3bf5b7f88b3 100644 --- a/homeassistant/components/mazda/translations/ja.json +++ b/homeassistant/components/mazda/translations/ja.json @@ -6,7 +6,7 @@ }, "error": { "account_locked": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f\u3002\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index 41d0b6c48a1..17675d40bfc 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/metoffice/translations/ja.json b/homeassistant/components/metoffice/translations/ja.json index 0fb2b51574a..a64d35406de 100644 --- a/homeassistant/components/metoffice/translations/ja.json +++ b/homeassistant/components/metoffice/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index 0949c1110d2..93cde1c8391 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "name_exists": "\u540d\u524d\u304c\u5b58\u5728\u3057\u307e\u3059" }, diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json index fe6edde5862..9250a503b58 100644 --- a/homeassistant/components/mill/translations/ja.json +++ b/homeassistant/components/mill/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "cloud": { diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json index a9ec418ddb3..73abdedbfbc 100644 --- a/homeassistant/components/mill/translations/pl.json +++ b/homeassistant/components/mill/translations/pl.json @@ -7,11 +7,25 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { - "user": { + "cloud": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" } + }, + "local": { + "data": { + "ip_address": "Adres IP" + }, + "description": "Lokalny adres IP urz\u0105dzenia." + }, + "user": { + "data": { + "connection_type": "Wybierz typ po\u0142\u0105czenia", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wybierz typ po\u0142\u0105czenia. Lokalnie wymaga grzejnik\u00f3w 3 generacji" } } } diff --git a/homeassistant/components/minecraft_server/translations/ja.json b/homeassistant/components/minecraft_server/translations/ja.json index 82f75825aca..e12d8967aab 100644 --- a/homeassistant/components/minecraft_server/translations/ja.json +++ b/homeassistant/components/minecraft_server/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u30b5\u30fc\u30d0\u30fc\u3067Minecraft\u306e\u30d0\u30fc\u30b8\u30e7\u30f31.7\u4ee5\u4e0a\u306e\u3082\u306e\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3082\u3042\u308f\u305b\u3066\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u30b5\u30fc\u30d0\u30fc\u3067Minecraft\u306e\u30d0\u30fc\u30b8\u30e7\u30f31.7\u4ee5\u4e0a\u306e\u3082\u306e\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3082\u3042\u308f\u305b\u3066\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_ip": "IP\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059(MAC\u30a2\u30c9\u30ec\u30b9\u3092\u7279\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f)\u3002\u4fee\u6b63\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "invalid_port": "\u30dd\u30fc\u30c8\u306f1024\u301c65535\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4fee\u6b63\u3057\u3066\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index 595c04d13fd..ab5ab732931 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -6,7 +6,7 @@ "no_devices_found": "\u6b8b\u308a\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "usb_confirm": { diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json index f06fea88935..bf36d307a09 100644 --- a/homeassistant/components/modern_forms/translations/ja.json +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/monoprice/translations/ja.json b/homeassistant/components/monoprice/translations/ja.json index 24bfd2a4ba7..b456edabd26 100644 --- a/homeassistant/components/monoprice/translations/ja.json +++ b/homeassistant/components/monoprice/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index 1e84edbfeea..ffc66c45cb7 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/motioneye/translations/ja.json b/homeassistant/components/motioneye/translations/ja.json index 5c0dfa7df8a..8f9d963019f 100644 --- a/homeassistant/components/motioneye/translations/ja.json +++ b/homeassistant/components/motioneye/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_url": "\u7121\u52b9\u306aURL", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 8b4c83306b1..78b002e492c 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -5,12 +5,12 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "broker": { "data": { - "broker": "\u30d6\u30ed\u30fc\u30ab\u30fc", + "broker": "Broker", "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", @@ -23,7 +23,7 @@ "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b" }, "description": "\u30a2\u30c9\u30aa\u30f3 {addon} \u304c\u3001\u63d0\u4f9b\u3059\u308bMQTT broker\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f", - "title": "HomeAssistant\u30a2\u30c9\u30aa\u30f3\u3092\u4ecb\u3057\u305fMQTT Broker" + "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u3092\u4ecb\u3057\u305fMQTT Broker" } } }, @@ -45,12 +45,14 @@ }, "options": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "bad_birth": "(\u7121\u52b9\u306a)Invalid birth topic.", + "bad_will": "(\u7121\u52b9\u306a)Invalid will topic.", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "broker": { "data": { - "broker": "\u30d6\u30ed\u30fc\u30ab\u30fc", + "broker": "Broker", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" @@ -60,11 +62,17 @@ }, "options": { "data": { - "birth_enable": "\u30d0\u30fc\u30b9(birth)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", - "birth_payload": "\u30d0\u30fc\u30b9(Birth)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9", + "birth_enable": "Birth message Enable(\u6709\u52b9\u5316)", + "birth_payload": "Birth message payload(\u30da\u30a4\u30ed\u30fc\u30c9)", + "birth_qos": "Birth message QoS", + "birth_retain": "Birth message retain(\u4fdd\u6301)", + "birth_topic": "Birth message topic", "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b", "will_enable": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u6709\u52b9\u5316", - "will_payload": "\u30a6\u30a3\u30eb(will)\u30e1\u30c3\u30bb\u30fc\u30b8 \u30da\u30a4\u30ed\u30fc\u30c9" + "will_payload": "Will message payload(\u30da\u30a4\u30ed\u30fc\u30c9)", + "will_qos": "Will message QoS", + "will_retain": "Will message retain(\u4fdd\u6301)", + "will_topic": "Will message topic" }, "description": "\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(Discovery(\u691c\u51fa)) - \u691c\u51fa\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408(\u63a8\u5968)\u3001Home Assistant\u306f\u3001MQTT broker\u306b\u8a2d\u5b9a\u3092\u516c\u958b\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3084\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3057\u307e\u3059\u3002\u691c\u51fa\u3092\u7121\u52b9\u306b\u3057\u305f\u5834\u5408\u306f\u3001\u3059\u3079\u3066\u306e\u8a2d\u5b9a\u3092\u624b\u52d5\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\u30d0\u30fc\u30b9(Birth(\u8a95\u751f))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u8a95\u751f\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304cMQTT broker\u306b\u3001(\u518d)\u63a5\u7d9a\u3059\u308b\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\n\u30a6\u30a3\u30eb(Will(\u610f\u601d))\u30e1\u30c3\u30bb\u30fc\u30b8 - \u30a6\u30a3\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001Home Assistant\u304c\u30d6\u30ed\u30fc\u30ab\u30fc(broker)\u3078\u306e\u63a5\u7d9a\u3092\u5931\u3046\u305f\u3073\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f\u3001\u30af\u30ea\u30fc\u30f3\u306a\u63a5\u7d9a(Home Assistant\u306e\u30b7\u30e3\u30c3\u30c8\u30c0\u30a6\u30f3\u306a\u3069)\u306e\u5834\u5408\u3068\u3001\u30af\u30ea\u30fc\u30f3\u3067\u306f\u306a\u3044\u63a5\u7d9a(Home Assistant\u306e\u30af\u30e9\u30c3\u30b7\u30e5\u3084\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u5931\u3063\u305f\u5834\u5408)\u306e\u3069\u3061\u3089\u306e\u5834\u5408\u3067\u3042\u3063\u3066\u3082\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002", "title": "MQTT\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json index b0e1e55764b..fc1791527f9 100644 --- a/homeassistant/components/mullvad/translations/ja.json +++ b/homeassistant/components/mullvad/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/mutesync/translations/ja.json b/homeassistant/components/mutesync/translations/ja.json index 8de0724a2fa..70e09f0ae6d 100644 --- a/homeassistant/components/mutesync/translations/ja.json +++ b/homeassistant/components/mutesync/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u8a2d\u5b9a\u3067\u3001m\u00fctesync\u306e\u8a8d\u8a3c\u3092\u6709\u52b9\u306b\u3059\u308b > \u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/myq/translations/ja.json b/homeassistant/components/myq/translations/ja.json index bc7fff471d9..e57bd9bc245 100644 --- a/homeassistant/components/myq/translations/ja.json +++ b/homeassistant/components/myq/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 9ffdac2832b..e62dc657dbb 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "duplicate_persistence_file": "\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb\u304c\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "duplicate_topic": "\u30c8\u30d4\u30c3\u30af\u306f\u65e2\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", @@ -21,7 +21,7 @@ }, "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "duplicate_persistence_file": "\u6c38\u7d9a(Persistence)\u30d5\u30a1\u30a4\u30eb\u304c\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "duplicate_topic": "\u30c8\u30d4\u30c3\u30af\u306f\u65e2\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 72e86724094..2125c9e3e38 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -7,7 +7,7 @@ "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index 94124da2e32..870578b3d4d 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -3,14 +3,15 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia. Usu\u0144 intergracj\u0119 i skonfiguruj j\u0105 ponownie. " }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Czy chcesz skonfigurowa\u0107 Nettigo Air Monitor {host}?" diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index 0b2f371d77e..824c4193f87 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_allowing_new_tokens": "Nanoleaf\u306f\u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u8a31\u53ef\u3057\u3066\u3044\u306a\u3044\u306e\u3067\u3001\u4e0a\u8a18\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json index 6c2441f2cbd..f06358ef78c 100644 --- a/homeassistant/components/nexia/translations/ja.json +++ b/homeassistant/components/nexia/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index 85a319fc5bd..c3cd4fee235 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/nightscout/translations/ja.json b/homeassistant/components/nightscout/translations/ja.json index 50a92184f36..d0a88be1985 100644 --- a/homeassistant/components/nightscout/translations/ja.json +++ b/homeassistant/components/nightscout/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nuheat/translations/ja.json b/homeassistant/components/nuheat/translations/ja.json index ab7b02e57dd..8f2eb2cc09a 100644 --- a/homeassistant/components/nuheat/translations/ja.json +++ b/homeassistant/components/nuheat/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index d6ae15ab371..6f54d0d8b4b 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -4,7 +4,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 7441f4c8169..707a8bd8faa 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { @@ -33,7 +33,7 @@ }, "options": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json index 5eabace63fe..16b3b37c6e0 100644 --- a/homeassistant/components/nws/translations/ja.json +++ b/homeassistant/components/nws/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index 248bce2b77d..c6b485976a7 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -5,7 +5,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json index 891120b3600..f38635da564 100644 --- a/homeassistant/components/octoprint/translations/ja.json +++ b/homeassistant/components/octoprint/translations/ja.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "OctoPrint Printer: {host}", diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index ed70b2113dc..c8f97ac3a40 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 4078af9a0a3..2aa624fbfb6 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_path": "\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "step": { diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index acc567682de..388332d6e03 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -2,17 +2,21 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_h264": "\u5229\u7528\u53ef\u80fd\u306aH264\u30b9\u30c8\u30ea\u30fc\u30e0\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u69cb\u6210\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_mac": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "onvif_error": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "auth": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "\u8a8d\u8a3c\u306e\u8a2d\u5b9a" }, "configure": { "data": { @@ -25,6 +29,10 @@ "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" }, "configure_profile": { + "data": { + "include": "\u30ab\u30e1\u30e9\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u4f5c\u6210" + }, + "description": "{resolution} \u89e3\u50cf\u5ea6\u3067 {profile} \u306e\u30ab\u30e1\u30e9 \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f5c\u6210\u3057\u307e\u3059\u304b\uff1f", "title": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u8a2d\u5b9a" }, "device": { diff --git a/homeassistant/components/opengarage/translations/ja.json b/homeassistant/components/opengarage/translations/ja.json index ea25099aca4..2c1576048bf 100644 --- a/homeassistant/components/opengarage/translations/ja.json +++ b/homeassistant/components/opengarage/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 2bc2ed9c278..0a7a15093ea 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -2,7 +2,7 @@ "config": { "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" }, "step": { diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 3d10a6221c7..1253ba5af21 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" }, "step": { diff --git a/homeassistant/components/ovo_energy/translations/ja.json b/homeassistant/components/ovo_energy/translations/ja.json index fd2ca867d49..c6dd8f5b0be 100644 --- a/homeassistant/components/ovo_energy/translations/ja.json +++ b/homeassistant/components/ovo_energy/translations/ja.json @@ -2,7 +2,7 @@ "config": { "error": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{username}", diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index 984a87e34ef..b47610f27e3 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/panasonic_viera/translations/ja.json b/homeassistant/components/panasonic_viera/translations/ja.json index 381978f5cbd..562e42217f8 100644 --- a/homeassistant/components/panasonic_viera/translations/ja.json +++ b/homeassistant/components/panasonic_viera/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_pin_code": "\u5165\u529b\u3057\u305fPIN\u30b3\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3057\u305f" }, "step": { diff --git a/homeassistant/components/philips_js/translations/ja.json b/homeassistant/components/philips_js/translations/ja.json index 3e7976af153..70c0f2d6acf 100644 --- a/homeassistant/components/philips_js/translations/ja.json +++ b/homeassistant/components/philips_js/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_pin": "\u7121\u52b9\u306aPIN", "pairing_failure": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f: {error_id}", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index aab44165526..313790dfcfc 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "api_key": { diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index 0a133c91e31..9233c2f6aea 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 6972961adeb..75a3ec07146 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -19,6 +19,7 @@ "token": "\u3053\u3053\u306b\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059", "use_webhook": "webhook\u3092\u4f7f\u7528" }, + "description": "API\u306b\u554f\u3044\u5408\u308f\u305b\u308b\u306b\u306f\u3001`auth_token` \u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u308c\u306f\u3001[\u6b21\u306e\u624b\u9806\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9: **{device_type}** \n\n\u7d44\u307f\u8fbc\u307f\u306eWebhook\u30e1\u30bd\u30c3\u30c9(Airlock\u306e\u307f)\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u3001\u30c1\u30a7\u30c3\u30af\u30dc\u30c3\u30af\u30b9\u3092\u30aa\u30f3\u306b\u3057\u3066\u3001\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3(Auth Token)\u3092\u7a7a\u767d\u306b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "API\u65b9\u5f0f\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 81bb84822a6..f6a98b1d8a2 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -22,7 +22,8 @@ "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "token": "\u30c8\u30fc\u30af\u30f3(\u30aa\u30d7\u30b7\u30e7\u30f3)", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" - } + }, + "title": "\u624b\u52d5\u306b\u3088\u308bPlex\u306e\u8a2d\u5b9a" }, "select_server": { "data": { @@ -37,7 +38,8 @@ "user_advanced": { "data": { "setup_method": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u65b9\u6cd5" - } + }, + "title": "Plex Media Server" } } }, @@ -45,6 +47,7 @@ "step": { "plex_mp_settings": { "data": { + "ignore_plex_web_clients": "Plex Web clients\u3092\u7121\u8996\u3059\u308b", "use_episode_art": "\u30a8\u30d4\u30bd\u30fc\u30c9\u30a2\u30fc\u30c8\u3092\u4f7f\u7528" }, "description": "Plex Media Player\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index f7e89c00f6a..cc3810edcd3 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/plum_lightpad/translations/ja.json b/homeassistant/components/plum_lightpad/translations/ja.json index 8fba23eb117..ff548917c18 100644 --- a/homeassistant/components/plum_lightpad/translations/ja.json +++ b/homeassistant/components/plum_lightpad/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index 53ebb4e0d75..e66e1ef19b6 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "wrong_version": "PowerWall\u306b\u3001\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306e\u30bd\u30d5\u30c8\u30a6\u30a7\u30a2\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3092\u691c\u8a0e\u3059\u308b\u304b\u3001\u3053\u306e\u554f\u984c\u3092\u5831\u544a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/progettihwsw/translations/ja.json b/homeassistant/components/progettihwsw/translations/ja.json index b11538f447b..39038a24b1a 100644 --- a/homeassistant/components/progettihwsw/translations/ja.json +++ b/homeassistant/components/progettihwsw/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/prosegur/translations/ja.json b/homeassistant/components/prosegur/translations/ja.json index 76dfa59ac95..7a10da31188 100644 --- a/homeassistant/components/prosegur/translations/ja.json +++ b/homeassistant/components/prosegur/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 85defbb7e9c..041ba2e121e 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -8,7 +8,7 @@ "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "credential_timeout": "\u8cc7\u683c\u60c5\u5831\u30b5\u30fc\u30d3\u30b9\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002\u9001\u4fe1(submit)\u3092\u62bc\u3057\u3066\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_ipaddress": "\u8a2d\u5b9a\u3057\u305f\u3044PlayStation4\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/rachio/translations/ja.json b/homeassistant/components/rachio/translations/ja.json index 82aab7313b0..81dd28070d0 100644 --- a/homeassistant/components/rachio/translations/ja.json +++ b/homeassistant/components/rachio/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/rainforest_eagle/translations/ja.json b/homeassistant/components/rainforest_eagle/translations/ja.json index e3ed5f9cff5..d215480e052 100644 --- a/homeassistant/components/rainforest_eagle/translations/ja.json +++ b/homeassistant/components/rainforest_eagle/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/rdw/translations/ja.json b/homeassistant/components/rdw/translations/ja.json index 9909b76e8b7..3813eaf04e0 100644 --- a/homeassistant/components/rdw/translations/ja.json +++ b/homeassistant/components/rdw/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown_license_plate": "\u4e0d\u660e\u306a\u30e9\u30a4\u30bb\u30f3\u30b9\u30d7\u30ec\u30fc\u30c8" }, "step": { diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index 6e3e7a8a615..bfbf14b31fb 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "setup_network": { diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index e2e6e6447af..76017fa2d22 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/rituals_perfume_genie/translations/ja.json b/homeassistant/components/rituals_perfume_genie/translations/ja.json index ff8d819d5f0..b341e8a40c0 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/ja.json +++ b/homeassistant/components/rituals_perfume_genie/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index 94fa710a91a..bb308c071c2 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index d0278cbe9eb..d2ae42b977d 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_irobot_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306fiRobot\u793e\u306e\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "short_blid": "BLID\u304c\u5207\u308a\u6368\u3066\u3089\u308c\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/ruckus_unleashed/translations/ja.json b/homeassistant/components/ruckus_unleashed/translations/ja.json index 5bd6c037226..a9d2ddfd3ac 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ja.json +++ b/homeassistant/components/ruckus_unleashed/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index f57daa22df8..21e72de5eac 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "id_missing": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index 7c91dd6b31c..557b36d10a6 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 280ac0cb10f..60fe4c88e20 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index adff0b482a2..b55de538fd3 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -6,7 +6,7 @@ "is_carbon_monoxide": "obecny poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", "is_energy": "obecna energia {entity_name}", - "is_frequency": "obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", + "is_frequency": "Obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", "is_gas": "obecny poziom gazu {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", @@ -33,7 +33,7 @@ "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", - "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", + "frequency": "zmiany cz\u0119stotliwo\u015b\u0107 {entity_name}", "gas": "{entity_name} wykryje zmian\u0119 poziomu gazu", "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", diff --git a/homeassistant/components/sharkiq/translations/ja.json b/homeassistant/components/sharkiq/translations/ja.json index a5f16e7104a..1f59ff9ae41 100644 --- a/homeassistant/components/sharkiq/translations/ja.json +++ b/homeassistant/components/sharkiq/translations/ja.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 0703dbc3c7c..9d3053f92bd 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -5,7 +5,7 @@ "unsupported_firmware": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306e\u30d5\u30a1\u30fc\u30e0\u30a6\u30a7\u30a2\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/sma/translations/ja.json b/homeassistant/components/sma/translations/ja.json index 2e9b67ef068..ff26a6caaf5 100644 --- a/homeassistant/components/sma/translations/ja.json +++ b/homeassistant/components/sma/translations/ja.json @@ -5,7 +5,7 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "cannot_retrieve_device_info": "\u63a5\u7d9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index c481989b6d5..068d62fa0d8 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, diff --git a/homeassistant/components/smart_meter_texas/translations/ja.json b/homeassistant/components/smart_meter_texas/translations/ja.json index 2f714747433..323de60808b 100644 --- a/homeassistant/components/smart_meter_texas/translations/ja.json +++ b/homeassistant/components/smart_meter_texas/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json index 475ffb4fcf4..304a537c9e0 100644 --- a/homeassistant/components/smarthab/translations/ja.json +++ b/homeassistant/components/smarthab/translations/ja.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "service": "SmartHab\u306b\u5230\u9054\u3057\u3088\u3046\u3068\u3057\u3066\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30b5\u30fc\u30d3\u30b9\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json index 2e214c0e6ad..248427ad9bf 100644 --- a/homeassistant/components/sms/translations/ja.json +++ b/homeassistant/components/sms/translations/ja.json @@ -5,7 +5,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/solarlog/translations/ja.json b/homeassistant/components/solarlog/translations/ja.json index 8275fdf956b..f5d4f9d206a 100644 --- a/homeassistant/components/solarlog/translations/ja.json +++ b/homeassistant/components/solarlog/translations/ja.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index bb6bdaa1226..32b57dd97b9 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index a5a69b2482c..e0dafa5a64a 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -22,7 +22,7 @@ }, "options": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "entity_config": { diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 3784edce53a..64785eed5ec 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "flow_title": "{name}", diff --git a/homeassistant/components/songpal/translations/ja.json b/homeassistant/components/songpal/translations/ja.json index 5a0ebc5b3f8..bdc521fbb0d 100644 --- a/homeassistant/components/songpal/translations/ja.json +++ b/homeassistant/components/songpal/translations/ja.json @@ -5,7 +5,7 @@ "not_songpal_device": "Songpal\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json index 7ddc6d40eca..142429027a6 100644 --- a/homeassistant/components/squeezebox/translations/ja.json +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -5,7 +5,7 @@ "no_server_found": "LMS\u30b5\u30fc\u30d0\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 4c0530303dd..69fc945db58 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID\u306f9\u6841\u306e\u6570\u5b57\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index 5e90ff79ec7..c557f3419bc 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "bad_pin_format": "PIN\u306f4\u6841\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "incorrect_pin": "PIN\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, diff --git a/homeassistant/components/surepetcare/translations/ja.json b/homeassistant/components/surepetcare/translations/ja.json index 580a8bb3a3d..b4c39a6b251 100644 --- a/homeassistant/components/surepetcare/translations/ja.json +++ b/homeassistant/components/surepetcare/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index a2a7a100286..41fb320428f 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index ff05e237fca..482087ed730 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/syncthru/translations/ja.json b/homeassistant/components/syncthru/translations/ja.json index 9cf9c6fe4c3..076a2a99a70 100644 --- a/homeassistant/components/syncthru/translations/ja.json +++ b/homeassistant/components/syncthru/translations/ja.json @@ -5,7 +5,8 @@ }, "error": { "invalid_url": "\u7121\u52b9\u306aURL", - "syncthru_not_supported": "\u30c7\u30d0\u30a4\u30b9\u306fSyncThru\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093" + "syncthru_not_supported": "\u30c7\u30d0\u30a4\u30b9\u306fSyncThru\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093", + "unknown_state": "\u30d7\u30ea\u30f3\u30bf\u306e\u72b6\u614b\u304c\u4e0d\u660e\u3067\u3059\u3002URL\u3068\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 6c975425efa..848991ffaf2 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -6,7 +6,7 @@ "reconfigure_successful": "\u518d\u8a2d\u5b9a\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -59,6 +59,7 @@ "step": { "init": { "data": { + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u306e\u9593\u306e\u5206\u6570", "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u79d2)" } } diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index 99274e33a4e..48ee7fc2378 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -6,7 +6,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/tado/translations/ja.json b/homeassistant/components/tado/translations/ja.json index db7de0a783a..5061a4e1e6b 100644 --- a/homeassistant/components/tado/translations/ja.json +++ b/homeassistant/components/tado/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_homes": "\u3053\u306etado\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ea\u30f3\u30af\u3055\u308c\u3066\u3044\u308b\u5bb6\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/tibber/translations/ja.json b/homeassistant/components/tibber/translations/ja.json index f2fd8d2ac83..ed3c2f71357 100644 --- a/homeassistant/components/tibber/translations/ja.json +++ b/homeassistant/components/tibber/translations/ja.json @@ -4,14 +4,16 @@ "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", - "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "timeout": "Tibber\u3078\u306e\u63a5\u7d9a\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "step": { "user": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, + "description": "https://developer.tibber.com/settings/accesstoken \u304b\u3089\u306e\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u307e\u3059", "title": "Tibber" } } diff --git a/homeassistant/components/tolo/translations/ja.json b/homeassistant/components/tolo/translations/ja.json index ce963469912..f8d4a1646ae 100644 --- a/homeassistant/components/tolo/translations/ja.json +++ b/homeassistant/components/tolo/translations/ja.json @@ -5,7 +5,7 @@ "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tolo/translations/pl.json b/homeassistant/components/tolo/translations/pl.json new file mode 100644 index 00000000000..4809e00e29f --- /dev/null +++ b/homeassistant/components/tolo/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.et.json b/homeassistant/components/tolo/translations/select.et.json new file mode 100644 index 00000000000..17014b9b867 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.et.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automaatne", + "manual": "k\u00e4sitsi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.ja.json b/homeassistant/components/tolo/translations/select.ja.json new file mode 100644 index 00000000000..dda0d8e99b3 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "\u81ea\u52d5", + "manual": "\u624b\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.no.json b/homeassistant/components/tolo/translations/select.no.json new file mode 100644 index 00000000000..880a8dc6f70 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatisk", + "manual": "manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.pl.json b/homeassistant/components/tolo/translations/select.pl.json new file mode 100644 index 00000000000..58a0192a7b8 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatyczny", + "manual": "r\u0119czny" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.ru.json b/homeassistant/components/tolo/translations/select.ru.json new file mode 100644 index 00000000000..d736bd8680c --- /dev/null +++ b/homeassistant/components/tolo/translations/select.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439", + "manual": "\u0440\u0443\u0447\u043d\u043e\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.zh-Hant.json b/homeassistant/components/tolo/translations/select.zh-Hant.json new file mode 100644 index 00000000000..dda0d8e99b3 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "\u81ea\u52d5", + "manual": "\u624b\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ja.json b/homeassistant/components/toon/translations/ja.json index ff8ff3b5c4f..9d956d5808c 100644 --- a/homeassistant/components/toon/translations/ja.json +++ b/homeassistant/components/toon/translations/ja.json @@ -6,6 +6,11 @@ "no_agreements": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u306f\u3001Toon displays\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u3059\u308b\u30c6\u30ca\u30f3\u30c8\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 615c0047a4b..f78e4adec9c 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -6,7 +6,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} {model} ({host})", "step": { diff --git a/homeassistant/components/tradfri/translations/ja.json b/homeassistant/components/tradfri/translations/ja.json index 987acc64a53..e8a2682af3b 100644 --- a/homeassistant/components/tradfri/translations/ja.json +++ b/homeassistant/components/tradfri/translations/ja.json @@ -6,7 +6,7 @@ }, "error": { "cannot_authenticate": "\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306f\u3001Homekit\u306a\u3069\u306e\u4ed6\u306e\u30b5\u30fc\u30d0\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_key": "\u63d0\u4f9b\u3055\u308c\u305f\u30ad\u30fc\u3067\u306e\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u304c\u5f15\u304d\u7d9a\u304d\u767a\u751f\u3059\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3067\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002" }, diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 39fdf77a407..14f3145978f 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" }, diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 025f1a41b18..1d2624615c6 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, @@ -42,7 +42,7 @@ }, "options": { "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "dev_multi_type": "\u69cb\u6210\u3059\u308b\u8907\u6570\u306e\u9078\u629e\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u540c\u3058\u30bf\u30a4\u30d7\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json index 053e5c2b007..0311d5ae5e3 100644 --- a/homeassistant/components/twentemilieu/translations/ja.json +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_address": "Twente Milieu\u30b5\u30fc\u30d3\u30b9\u30a8\u30ea\u30a2\u306b\u4f4f\u6240\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" }, "step": { diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index 9e407f5dba8..fcc91e956d1 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -4,7 +4,7 @@ "device_exists": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 508e908b14a..5d08c8b816d 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller site is already configured", + "already_configured": "UniFi Network site is already configured", "configuration_updated": "Configuration updated.", "reauth_successful": "Re-authentication was successful" }, @@ -21,7 +21,7 @@ "username": "Username", "verify_ssl": "Verify SSL certificate" }, - "title": "Set up UniFi Controller" + "title": "Set up UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Allow POE control of clients" }, "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", - "title": "UniFi options 2/3" + "title": "UniFi Network options 2/3" }, "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", - "ignore_wired_bug": "Disable UniFi wired bug logic", + "ignore_wired_bug": "Disable UniFi Network wired bug logic", "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" }, "description": "Configure device tracking", - "title": "UniFi options 1/3" + "title": "UniFi Network options 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)" }, - "description": "Configure UniFi integration" + "description": "Configure UniFi Network integration" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "Uptime sensors for network clients" }, "description": "Configure statistics sensors", - "title": "UniFi options 3/3" + "title": "UniFi Network options 3/3" } } } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 6214c863e55..fbaf31c067b 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -7,7 +7,7 @@ }, "error": { "faulty_credentials": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "service_unavailable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown_client_mac": "\u305d\u306eMAC\u30a2\u30c9\u30ec\u30b9\u3067\u4f7f\u7528\u53ef\u80fd\u306a\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093" }, "flow_title": "{site} ({host})", diff --git a/homeassistant/components/upb/translations/ja.json b/homeassistant/components/upb/translations/ja.json index ad3b7d003fc..97b50b62221 100644 --- a/homeassistant/components/upb/translations/ja.json +++ b/homeassistant/components/upb/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_upb_file": "UPB UPStart\u306e\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u306a\u3044\u304b\u7121\u52b9\u3067\u3059\u3002\u30d5\u30a1\u30a4\u30eb\u540d\u3068\u30d1\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/upcloud/translations/ja.json b/homeassistant/components/upcloud/translations/ja.json index 906dfcfb2cd..8883e40953f 100644 --- a/homeassistant/components/upcloud/translations/ja.json +++ b/homeassistant/components/upcloud/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index 06b788b86d5..deb806e1493 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -7,6 +7,9 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "description": "\u3053\u306e\u3001UPnP/IGD\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "unique_id": "\u30c7\u30d0\u30a4\u30b9", diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 11d79efa137..d890780eab9 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -7,7 +7,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/velbus/translations/ja.json b/homeassistant/components/velbus/translations/ja.json index 5534e240732..3c052c44d65 100644 --- a/homeassistant/components/velbus/translations/ja.json +++ b/homeassistant/components/velbus/translations/ja.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/venstar/translations/ja.json b/homeassistant/components/venstar/translations/ja.json index b699fcb4c3a..c86755bec34 100644 --- a/homeassistant/components/venstar/translations/ja.json +++ b/homeassistant/components/venstar/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json index ff17008eb0f..6f98d264f24 100644 --- a/homeassistant/components/vilfo/translations/ja.json +++ b/homeassistant/components/vilfo/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 5c4c3ccdc14..36220756a4d 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { diff --git a/homeassistant/components/vlc_telnet/translations/ja.json b/homeassistant/components/vlc_telnet/translations/ja.json index 233a0dede69..f351eac4dda 100644 --- a/homeassistant/components/vlc_telnet/translations/ja.json +++ b/homeassistant/components/vlc_telnet/translations/ja.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/volumio/translations/ja.json b/homeassistant/components/volumio/translations/ja.json index 8066a84a4cd..90c3e1598c3 100644 --- a/homeassistant/components/volumio/translations/ja.json +++ b/homeassistant/components/volumio/translations/ja.json @@ -5,7 +5,7 @@ "cannot_connect": "\u691c\u51fa\u3055\u308c\u305fVolumio\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index 504b319f5b4..8924bf891b9 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -5,7 +5,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "reauth_invalid": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u30aa\u30ea\u30b8\u30ca\u30eb\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/water_heater/translations/ja.json b/homeassistant/components/water_heater/translations/ja.json index 5401cf7248f..dc1775101a9 100644 --- a/homeassistant/components/water_heater/translations/ja.json +++ b/homeassistant/components/water_heater/translations/ja.json @@ -11,6 +11,7 @@ "electric": "\u30a8\u30ec\u30af\u30c8\u30ea\u30c3\u30af", "gas": "\u30ac\u30b9", "heat_pump": "\u30d2\u30fc\u30c8\u30dd\u30f3\u30d7", + "high_demand": "\u9ad8\u9700\u8981(High Demand)", "off": "\u30aa\u30d5", "performance": "\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9" } diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index e86fc7f18cd..4b61f0ea8f8 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json index 1988d82cf8c..1defa16a2fa 100644 --- a/homeassistant/components/whirlpool/translations/ja.json +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index e8380fae0b6..eb557810a0c 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -20,6 +20,7 @@ "data": { "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d" }, + "description": "\u3053\u306e\u30c7\u30fc\u30bf\u306b\u56fa\u6709\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u901a\u5e38\u3001\u3053\u308c\u306f\u524d\u306e\u624b\u9806\u3067\u9078\u629e\u3057\u305f\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u540d\u524d\u3067\u3059\u3002", "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" }, "reauth": { diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index a26ee4f8b66..68235900ee9 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/wled/translations/select.pl.json b/homeassistant/components/wled/translations/select.pl.json new file mode 100644 index 00000000000..381f2306d26 --- /dev/null +++ b/homeassistant/components/wled/translations/select.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "wy\u0142.", + "1": "w\u0142.", + "2": "Do czasu ponownego uruchomienia urz\u0105dzenia" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/ja.json b/homeassistant/components/wolflink/translations/ja.json index 9d87a7bf27a..fca5ea8f5e0 100644 --- a/homeassistant/components/wolflink/translations/ja.json +++ b/homeassistant/components/wolflink/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 36d59ff0836..ebb35d7143a 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -56,6 +56,7 @@ "warmwasser": "DHW", "warmwasser_schnellstart": "DHW\u30af\u30a4\u30c3\u30af\u30b9\u30bf\u30fc\u30c8", "warmwasserbetrieb": "DHW\u30e2\u30fc\u30c9", + "warmwassernachlauf": "DHW run-on", "warmwasservorrang": "DHW\u306e\u512a\u5148\u9806\u4f4d", "zunden": "\u30a4\u30b0\u30cb\u30c3\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index c8d1d3abc9f..e9b6c7a6cc9 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -8,7 +8,7 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_login_error": "Xiaomi Miio Cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a8d\u8a3c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_no_devices": "\u3053\u306eXiaomi Miio cloud\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index 1b901189bfc..e032979dcee 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -5,7 +5,7 @@ "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{model} {id} ({host})", "step": { diff --git a/homeassistant/components/youless/translations/ja.json b/homeassistant/components/youless/translations/ja.json index 2557c6ee9d6..c09398e1f70 100644 --- a/homeassistant/components/youless/translations/ja.json +++ b/homeassistant/components/youless/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index b8a76775aba..d7fc0d4d7b2 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -6,7 +6,7 @@ "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zoneminder/translations/ja.json b/homeassistant/components/zoneminder/translations/ja.json index b52e9701a23..0eab2b65c6a 100644 --- a/homeassistant/components/zoneminder/translations/ja.json +++ b/homeassistant/components/zoneminder/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, @@ -11,7 +11,7 @@ }, "error": { "auth_fail": "\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_error": "ZoneMinder\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 1b79614b078..6fd456998c3 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -8,13 +8,13 @@ "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002", "not_zwave_device": "\u767a\u898b\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Z-Wave\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { "addon_start_failed": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -95,11 +95,11 @@ "addon_set_config_failed": "Z-Wave JS\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "addon_start_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "different_device": "\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308bUSB\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3053\u306e\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3067\u4ee5\u524d\u306b\u69cb\u6210\u3057\u305f\u3082\u306e\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002\u4ee3\u308f\u308a\u306b\u3001\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u7528\u306b\u65b0\u3057\u3044\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u305f", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_ws_url": "\u7121\u52b9\u306aWebSocket URL", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, From d8c73e8685478df0cc1f9c617103393f8c9671e3 Mon Sep 17 00:00:00 2001 From: Alan Murray Date: Sat, 27 Nov 2021 18:35:23 +1100 Subject: [PATCH 0917/1452] Bump acmeda integration aiopulse dependency version to 0.4.3 (#60434) * Bump acmeda integration aiopulse dependency version to 0.4.3 to implement battery health monitoring. * Updated acmeda requirements --- homeassistant/components/acmeda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index ae72df5a323..6313b177f47 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -3,7 +3,7 @@ "name": "Rollease Acmeda Automate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/acmeda", - "requirements": ["aiopulse==0.4.2"], + "requirements": ["aiopulse==0.4.3"], "codeowners": ["@atmurray"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index bfbbbabbc88..14dd69bb0c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -228,7 +228,7 @@ aionotify==0.2.0 aionotion==3.0.2 # homeassistant.components.acmeda -aiopulse==0.4.2 +aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c48900dd0a4..a00a0454ef7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aionanoleaf==0.0.4 aionotion==3.0.2 # homeassistant.components.acmeda -aiopulse==0.4.2 +aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 From 729394547c86bdcf7e9c347989010a522566ac67 Mon Sep 17 00:00:00 2001 From: Ricardo Steijn <61013287+RicArch97@users.noreply.github.com> Date: Sat, 27 Nov 2021 08:37:07 +0100 Subject: [PATCH 0918/1452] Bump crownstone-sse to 2.0.3 (#60428) --- homeassistant/components/crownstone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index 585a44cfd14..758721d5f71 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/crownstone", "requirements": [ "crownstone-cloud==1.4.9", - "crownstone-sse==2.0.2", + "crownstone-sse==2.0.3", "crownstone-uart==2.1.0", "pyserial==3.5" ], diff --git a/requirements_all.txt b/requirements_all.txt index 14dd69bb0c0..f1cb4d7b98e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -505,7 +505,7 @@ croniter==1.0.6 crownstone-cloud==1.4.9 # homeassistant.components.crownstone -crownstone-sse==2.0.2 +crownstone-sse==2.0.3 # homeassistant.components.crownstone crownstone-uart==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a00a0454ef7..79d9e9852fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -319,7 +319,7 @@ croniter==1.0.6 crownstone-cloud==1.4.9 # homeassistant.components.crownstone -crownstone-sse==2.0.2 +crownstone-sse==2.0.3 # homeassistant.components.crownstone crownstone-uart==2.1.0 From d63e2d1db0a41e84afbd1e15e66e7110fd81a02a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 27 Nov 2021 09:25:27 +0100 Subject: [PATCH 0919/1452] fix blocking startup when NAS is busy (#60360) --- homeassistant/components/synology_dsm/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 1351f403306..8212bd67eda 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -298,6 +298,7 @@ class SynoApi: else: self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + self.initialized = False # DSM APIs self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None @@ -347,6 +348,7 @@ class SynoApi: await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() + self.initialized = True @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -506,6 +508,9 @@ class SynoApi: self.dsm.update, self._with_information ) except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + if not self.initialized: + raise err + _LOGGER.warning( "Connection error during update, fallback by reloading the entry" ) From bb99d07d82ad426c4008512ed9258866a7899939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 27 Nov 2021 11:18:58 +0100 Subject: [PATCH 0920/1452] Remove unused constant in Tibber (#60439) --- homeassistant/components/tibber/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 9a19e5a7602..e6a305b6f61 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -47,7 +47,6 @@ ICON = "mdi:currency-usd" SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PARALLEL_UPDATES = 0 -SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" RT_SENSORS: tuple[SensorEntityDescription, ...] = ( @@ -301,7 +300,7 @@ class TibberSensorElPrice(TibberSensor): } self._attr_icon = ICON self._attr_name = f"Electricity price {self._home_name}" - self._attr_unique_id = f"{self._tibber_home.home_id}" + self._attr_unique_id = self._tibber_home.home_id self._model = "Price Sensor" self._device_name = self._attr_name From 3cd80b95db7de6980d10b47b45248420bed3bf60 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 27 Nov 2021 16:56:53 +0100 Subject: [PATCH 0921/1452] Logging issue workaround for fritzconnection library (#60448) * Logging issue workaround * Better approach --- homeassistant/components/fritz/__init__.py | 8 ++++++++ homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/__init__.py | 8 ++++++++ .../components/fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 6d0030685b2..193e11f49f3 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -2,6 +2,7 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from fritzconnection.core.logger import fritzlogger from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -20,6 +21,13 @@ from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) +level = _LOGGER.getEffectiveLevel() +_LOGGER.info( + "Setting logging level of fritzconnection: %s", logging.getLevelName(level) +) +fritzlogger.set_level(level) +fritzlogger.enable() + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up fritzboxtools from config entry.""" diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e6ae95e3eb4..219e8f946ae 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.7.0", + "fritzconnection==1.7.2", "xmltodict==0.12.0" ], "dependencies": ["network"], diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 4c36ee3ddfb..edf463a84eb 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -2,6 +2,7 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from fritzconnection.core.logger import fritzlogger from requests.exceptions import ConnectionError as RequestsConnectionError from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -19,6 +20,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +level = _LOGGER.getEffectiveLevel() +_LOGGER.info( + "Setting logging level of fritzconnection: %s", logging.getLevelName(level) +) +fritzlogger.set_level(level) +fritzlogger.enable() + async def async_setup_entry(hass, config_entry): """Set up the fritzbox_callmonitor platforms.""" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 3d58f27e950..91bd73a6efd 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.7.0"], + "requirements": ["fritzconnection==1.7.2"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f1cb4d7b98e..79560d8cb60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -680,7 +680,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.7.0 +fritzconnection==1.7.2 # homeassistant.components.google_translate gTTS==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79d9e9852fa..f506655f8d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -415,7 +415,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.7.0 +fritzconnection==1.7.2 # homeassistant.components.google_translate gTTS==2.2.3 From fa2399030a1fbbd0a847c7a13411725088ddec13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Nov 2021 07:24:42 -1000 Subject: [PATCH 0922/1452] Bump flux_led to 0.25.0 (#60460) * Bump flux_led to 0.25.0 * Refactor color_temp_to_white_levels and improve code coverage by @bdraco in https://github.com/Danielhiversen/flux_led/pull/185 * Adjust protocol to handle newer models that send 0xB0 responses by @bdraco in https://github.com/Danielhiversen/flux_led/pull/186 * Fix effects with floor lamps by @bdraco in https://github.com/Danielhiversen/flux_led/pull/188 * Add support for CCT protocol aka 0x1C models by @bdraco in https://github.com/Danielhiversen/flux_led/pull/187 - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.24.38...0.25.0 * handle change in color_temp_to_white_levels --- homeassistant/components/flux_led/light.py | 4 +++- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0bccc2eafb3..cf43a90bf22 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -346,7 +346,9 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): brightness = kwargs.get( ATTR_BRIGHTNESS, self._device.getWhiteTemperature()[1] ) - cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness) + channels = color_temp_to_white_levels(color_temp_kelvin, brightness) + warm = channels.warm_white + cold = channels.cool_white await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold) return # Handle switch to RGB Color Mode diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 5145dd8ba16..6261eed28b4 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.38"], + "requirements": ["flux_led==0.25.0"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 79560d8cb60..60961f918c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.38 +flux_led==0.25.0 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f506655f8d0..31692a304e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.38 +flux_led==0.25.0 # homeassistant.components.homekit fnvhash==0.1.0 From adeeb99579232648e18671f85558286eb9c2d82a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Nov 2021 08:23:12 -1000 Subject: [PATCH 0923/1452] Bump flux_led to 0.25.1 (#60463) * Bump flux_led to 0.25.1 - Fixes for older firmwares - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.25.0...0.25.1 * empty , pypi behind --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 6261eed28b4..5740644c93e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.0"], + "requirements": ["flux_led==0.25.1"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 60961f918c4..d6bbf56bfb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.0 +flux_led==0.25.1 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31692a304e6..ea7d4d59c70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.0 +flux_led==0.25.1 # homeassistant.components.homekit fnvhash==0.1.0 From 4526d2569724c21d890d4a29168058ec6fce2218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jes=C3=BAs=20Garc=C3=ADa=20de=20Soria?= Date: Sat, 27 Nov 2021 18:35:07 +0000 Subject: [PATCH 0924/1452] Update PyTurboJPEG to v1.6.3 (#60400) --- homeassistant/components/camera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a8a834b60b3..6f1b8fdcb35 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,7 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.1"], + "requirements": ["PyTurboJPEG==1.6.3"], "after_dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index d6bbf56bfb8..c5dbd4d90f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.camera -PyTurboJPEG==1.6.1 +PyTurboJPEG==1.6.3 # homeassistant.components.vicare PyViCare==2.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea7d4d59c70..0b70292307e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,7 +30,7 @@ PyRMVtransport==0.3.3 PyTransportNSW==0.1.1 # homeassistant.components.camera -PyTurboJPEG==1.6.1 +PyTurboJPEG==1.6.3 # homeassistant.components.vicare PyViCare==2.13.1 From 2a0c1fa074ac26e28ea49e645f64cce74b5cb943 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 27 Nov 2021 19:56:00 +0100 Subject: [PATCH 0925/1452] don't issue requests for non-existing devices (#60416) --- homeassistant/components/fronius/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index f605e57bac2..2b863fbb505 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -198,8 +198,10 @@ class FroniusSolarNet: try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: + # ConfigEntryNotReady raised form FroniusError / KeyError in + # DataUpdateCoordinator if request not supported by the Fronius device + return None + # if no device for the request is installed an empty dict is returned + if not coordinator.data: return None - # keep coordinator only if devices are found - # else ConfigEntryNotReady raised form KeyError - # in FroniusMeterUpdateCoordinator._get_fronius_device_data return coordinator From 3af54d96c7dcc3f7087d1196e6ab0db029301ee7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 28 Nov 2021 00:14:19 +0000 Subject: [PATCH 0926/1452] [ci skip] Translation update --- .../alarm_control_panel/translations/ja.json | 1 + .../components/august/translations/ja.json | 1 + .../components/awair/translations/ja.json | 3 +- .../components/balboa/translations/bg.json | 18 ++++++++++++ .../components/balboa/translations/he.json | 18 ++++++++++++ .../components/balboa/translations/hu.json | 28 +++++++++++++++++++ .../components/balboa/translations/tr.json | 28 +++++++++++++++++++ .../binary_sensor/translations/ja.json | 11 ++++---- .../components/bond/translations/ja.json | 1 + .../components/brunt/translations/bg.json | 7 +++++ .../components/bsblan/translations/ja.json | 3 +- .../components/button/translations/ja.json | 2 +- .../components/button/translations/pl.json | 2 +- .../components/cast/translations/ja.json | 4 +-- .../components/climate/translations/ja.json | 1 + .../coolmaster/translations/ja.json | 8 +++++- .../coronavirus/translations/ja.json | 3 +- .../components/cover/translations/ja.json | 20 ++++++++++++- .../components/deconz/translations/ja.json | 10 ++++++- .../device_tracker/translations/ja.json | 4 +-- .../components/doorbird/translations/ja.json | 11 ++++++-- .../components/eafm/translations/ja.json | 1 + .../components/ecobee/translations/ja.json | 5 ++++ .../components/elgato/translations/ja.json | 2 +- .../components/elkm1/translations/ja.json | 3 +- .../flick_electric/translations/ja.json | 3 +- .../components/flume/translations/ja.json | 1 + .../flunearyou/translations/ja.json | 3 +- .../components/fritzbox/translations/ja.json | 3 +- .../components/fronius/translations/bg.json | 17 +++++++++++ .../components/fronius/translations/he.json | 18 ++++++++++++ .../components/fronius/translations/hu.json | 20 +++++++++++++ .../components/fronius/translations/tr.json | 20 +++++++++++++ .../components/gios/translations/ja.json | 10 +++++-- .../components/guardian/translations/ja.json | 6 +++- .../components/harmony/translations/ja.json | 4 +++ .../components/hive/translations/ja.json | 2 +- .../homekit_controller/translations/ja.json | 11 +++++++- .../huawei_lte/translations/ja.json | 1 + .../components/hue/translations/ja.json | 3 +- .../components/hue/translations/pl.json | 18 ++++++------ .../translations/ja.json | 6 ++-- .../hvv_departures/translations/ja.json | 3 +- .../components/hyperion/translations/ja.json | 2 +- .../components/icloud/translations/ja.json | 1 + .../components/insteon/translations/ja.json | 3 ++ .../components/ipp/translations/ja.json | 2 ++ .../components/knx/translations/bg.json | 3 ++ .../components/konnected/translations/bg.json | 3 ++ .../components/konnected/translations/he.json | 1 + .../components/konnected/translations/hu.json | 1 + .../components/konnected/translations/ja.json | 5 +++- .../components/konnected/translations/tr.json | 1 + .../components/life360/translations/ja.json | 1 + .../components/light/translations/ja.json | 1 + .../lutron_caseta/translations/ja.json | 2 +- .../media_player/translations/ja.json | 11 ++++++-- .../components/melcloud/translations/ja.json | 3 ++ .../meteo_france/translations/ja.json | 4 +++ .../mobile_app/translations/ja.json | 3 ++ .../components/mqtt/translations/ja.json | 2 +- .../components/netatmo/translations/ja.json | 8 +++--- .../nmap_tracker/translations/ja.json | 8 +++--- .../components/nuheat/translations/ja.json | 3 ++ .../components/nut/translations/ja.json | 3 +- .../components/owntracks/translations/ja.json | 2 +- .../components/plaato/translations/ja.json | 2 +- .../components/plex/translations/ja.json | 4 +++ .../components/powerwall/translations/ja.json | 3 +- .../pvpc_hourly_pricing/translations/ja.json | 1 + .../components/rachio/translations/ja.json | 3 +- .../components/rfxtrx/translations/pl.json | 2 +- .../components/samsungtv/translations/ja.json | 2 +- .../components/sensor/translations/ja.json | 16 +++++++++++ .../components/sensor/translations/pl.json | 2 +- .../simplisafe/translations/ja.json | 1 + .../components/smappee/translations/ja.json | 5 +++- .../components/smarthab/translations/ja.json | 1 + .../smartthings/translations/ja.json | 5 ++++ .../somfy_mylink/translations/ja.json | 2 +- .../components/spotify/translations/ja.json | 2 +- .../components/starline/translations/ja.json | 2 ++ .../synology_dsm/translations/ja.json | 4 ++- .../tellduslive/translations/ja.json | 2 +- .../components/tolo/translations/bg.json | 22 +++++++++++++++ .../components/tolo/translations/he.json | 22 +++++++++++++++ .../components/tolo/translations/hu.json | 23 +++++++++++++++ .../tolo/translations/select.bg.json | 8 ++++++ .../tolo/translations/select.he.json | 8 ++++++ .../tolo/translations/select.hu.json | 8 ++++++ .../tolo/translations/select.tr.json | 8 ++++++ .../components/tolo/translations/tr.json | 23 +++++++++++++++ .../totalconnect/translations/ja.json | 3 +- .../transmission/translations/ja.json | 6 ++-- .../tuya/translations/sensor.ja.json | 4 +-- .../twentemilieu/translations/ja.json | 1 + .../components/unifi/translations/ca.json | 14 +++++----- .../components/unifi/translations/de.json | 14 +++++----- .../components/unifi/translations/et.json | 14 +++++----- .../components/unifi/translations/id.json | 2 +- .../components/unifi/translations/ja.json | 9 ++++-- .../components/unifi/translations/ru.json | 12 ++++---- .../components/unifi/translations/tr.json | 2 +- .../components/vacuum/translations/ja.json | 9 ++++++ .../components/velbus/translations/ja.json | 4 ++- .../components/vera/translations/ja.json | 10 ++++++- .../components/vizio/translations/ja.json | 1 + .../waze_travel_time/translations/ja.json | 2 +- .../components/withings/translations/ja.json | 2 ++ .../components/wled/translations/ja.json | 1 + .../wolflink/translations/sensor.ja.json | 8 +++++- .../components/zha/translations/ja.json | 7 +++++ 112 files changed, 612 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/balboa/translations/bg.json create mode 100644 homeassistant/components/balboa/translations/he.json create mode 100644 homeassistant/components/balboa/translations/hu.json create mode 100644 homeassistant/components/balboa/translations/tr.json create mode 100644 homeassistant/components/brunt/translations/bg.json create mode 100644 homeassistant/components/fronius/translations/bg.json create mode 100644 homeassistant/components/fronius/translations/he.json create mode 100644 homeassistant/components/fronius/translations/hu.json create mode 100644 homeassistant/components/fronius/translations/tr.json create mode 100644 homeassistant/components/tolo/translations/bg.json create mode 100644 homeassistant/components/tolo/translations/he.json create mode 100644 homeassistant/components/tolo/translations/hu.json create mode 100644 homeassistant/components/tolo/translations/select.bg.json create mode 100644 homeassistant/components/tolo/translations/select.he.json create mode 100644 homeassistant/components/tolo/translations/select.hu.json create mode 100644 homeassistant/components/tolo/translations/select.tr.json create mode 100644 homeassistant/components/tolo/translations/tr.json diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index c9cfd274bb5..76f829502e9 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -7,6 +7,7 @@ "state": { "_": { "armed": "\u8b66\u6212", + "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u89e3\u9664", "disarming": "\u89e3\u9664", "pending": "\u4fdd\u7559\u4e2d", diff --git a/homeassistant/components/august/translations/ja.json b/homeassistant/components/august/translations/ja.json index d1b28dfd52d..f9d62163a8b 100644 --- a/homeassistant/components/august/translations/ja.json +++ b/homeassistant/components/august/translations/ja.json @@ -30,6 +30,7 @@ "data": { "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" }, + "description": "{login_method} ({username}) \u3092\u78ba\u8a8d\u3057\u3066\u3001\u4ee5\u4e0b\u306b\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "2\u8981\u7d20\u8a8d\u8a3c" } } diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index ee87ceaba8c..ad0d2904258 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -14,7 +14,8 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" - } + }, + "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/balboa/translations/bg.json b/homeassistant/components/balboa/translations/bg.json new file mode 100644 index 00000000000..cbf1e2ae7c9 --- /dev/null +++ b/homeassistant/components/balboa/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/he.json b/homeassistant/components/balboa/translations/he.json new file mode 100644 index 00000000000..1699e0f8e19 --- /dev/null +++ b/homeassistant/components/balboa/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/hu.json b/homeassistant/components/balboa/translations/hu.json new file mode 100644 index 00000000000..d9ae0e9c403 --- /dev/null +++ b/homeassistant/components/balboa/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + }, + "title": "Csatlakoz\u00e1s a Balboa Wi-Fi eszk\u00f6zh\u00f6z" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Balboa Spa kliens \u00f3r\u00e1j\u00e1nak szinkroniz\u00e1l\u00e1sa Home Assistanthoz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/tr.json b/homeassistant/components/balboa/translations/tr.json new file mode 100644 index 00000000000..f83ec51d377 --- /dev/null +++ b/homeassistant/components/balboa/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "title": "Balboa Wi-Fi cihaz\u0131na ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Balboa Spa \u0130stemcisi'nin zaman\u0131n\u0131 Home Assistant ile senkronize tutun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 0218f242979..ee695425407 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -4,13 +4,13 @@ "is_bat_low": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u304c\u5c11\u306a\u304f\u306a\u3063\u3066\u3044\u307e\u3059", "is_cold": "{entity_name} \u51b7\u3048\u3066\u3044\u308b", "is_connected": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", - "is_gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_gas": "{entity_name} \u304c\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", "is_hot": "{entity_name} \u71b1\u3044", "is_light": "{entity_name} \u304c\u5149\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "is_moist": "{entity_name} \u306f\u6e7f\u3063\u3066\u3044\u307e\u3059", "is_motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", - "is_moving": "{entity_name} \u304c\u3001\u79fb\u52d5\u4e2d\u3067\u3059", + "is_moving": "{entity_name} \u304c\u79fb\u52d5\u4e2d\u3067\u3059", "is_no_gas": "{entity_name} \u306f\u3001\u30ac\u30b9\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", "is_no_light": "{entity_name} \u306f\u3001\u5149\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", "is_no_motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u305b\u3093", @@ -54,7 +54,7 @@ "bat_low": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u304c\u5c11\u306a\u304f\u306a\u3063\u3066\u3044\u307e\u3059", "cold": "{entity_name} \u51b7\u3048\u3066\u3044\u307e\u3059", "connected": "{entity_name} \u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", - "gas": "{entity_name} \u304c\u3001\u30ac\u30b9\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "gas": "{entity_name} \u304c\u30ac\u30b9\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", "is_not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", @@ -78,6 +78,7 @@ "not_moist": "{entity_name} \u306f\u4e7e\u3044\u3066\u3044\u307e\u305b\u3093", "not_moving": "{entity_name} \u304c\u52d5\u304d\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "not_occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f", + "not_opened": "{entity_name} \u9589\u3058\u307e\u3057\u305f", "not_plugged_in": "{entity_name} \u306e\u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u307e\u3057\u305f", "not_powered": "{entity_name} \u306f\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u305b\u3093", "not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", @@ -90,8 +91,8 @@ "present": "{entity_name} \u304c\u5b58\u5728", "problem": "{entity_name} \u304c\u554f\u984c\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "running": "{entity_name} \u306e\u5b9f\u884c\u3092\u958b\u59cb", - "smoke": "{entity_name} \u304c\u3001\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", - "sound": "{entity_name} \u304c\u3001\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "smoke": "{entity_name} \u304c\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "sound": "{entity_name} \u304c\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", "unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u306a\u304f\u306a\u308a\u307e\u3057\u305f", diff --git a/homeassistant/components/bond/translations/ja.json b/homeassistant/components/bond/translations/ja.json index 0a39acc8cb2..c5bf98f6740 100644 --- a/homeassistant/components/bond/translations/ja.json +++ b/homeassistant/components/bond/translations/ja.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "old_firmware": "Bond device\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u53e4\u3044\u30d5\u30a1\u30fc\u30e0\u30a6\u30a7\u30a2 - \u7d9a\u884c\u3059\u308b\u524d\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/brunt/translations/bg.json b/homeassistant/components/brunt/translations/bg.json new file mode 100644 index 00000000000..e9a9c468402 --- /dev/null +++ b/homeassistant/components/brunt/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ja.json b/homeassistant/components/bsblan/translations/ja.json index 4dda07de639..3aa85a17cf8 100644 --- a/homeassistant/components/bsblan/translations/ja.json +++ b/homeassistant/components/bsblan/translations/ja.json @@ -16,7 +16,8 @@ "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "BSB-Lan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" + "description": "BSB-Lan\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002", + "title": "BSB-Lan device\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/button/translations/ja.json b/homeassistant/components/button/translations/ja.json index 2c54ca49380..9d2f1615dc1 100644 --- a/homeassistant/components/button/translations/ja.json +++ b/homeassistant/components/button/translations/ja.json @@ -4,7 +4,7 @@ "press": "{entity_name} \u30dc\u30bf\u30f3\u3092\u62bc\u3059" }, "trigger_type": { - "pressed": "{entity_name} \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" + "pressed": "{entity_name} \u304c\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "title": "\u30dc\u30bf\u30f3" diff --git a/homeassistant/components/button/translations/pl.json b/homeassistant/components/button/translations/pl.json index 81dcd88a091..e5af8b8c29b 100644 --- a/homeassistant/components/button/translations/pl.json +++ b/homeassistant/components/button/translations/pl.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "press": "Naci\u015bnij przycisk {entity_name}" + "press": "naci\u015bnij przycisk {entity_name}" }, "trigger_type": { "pressed": "{entity_name} zosta\u0142 naci\u015bni\u0119ty" diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index d16d72dbcbf..626ef56cba1 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" + "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30b3\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" }, "step": { "config": { @@ -21,7 +21,7 @@ }, "options": { "error": { - "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" + "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30b3\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" }, "step": { "advanced_options": { diff --git a/homeassistant/components/climate/translations/ja.json b/homeassistant/components/climate/translations/ja.json index 53a68714c30..c0b172839e3 100644 --- a/homeassistant/components/climate/translations/ja.json +++ b/homeassistant/components/climate/translations/ja.json @@ -12,6 +12,7 @@ "dry": "\u30c9\u30e9\u30a4", "fan_only": "\u30d5\u30a1\u30f3\u306e\u307f", "heat": "\u6696\u623f", + "heat_cool": "\u6696/\u51b7", "off": "\u30aa\u30d5" } }, diff --git a/homeassistant/components/coolmaster/translations/ja.json b/homeassistant/components/coolmaster/translations/ja.json index 06a6ca34b95..fd9b5952b9a 100644 --- a/homeassistant/components/coolmaster/translations/ja.json +++ b/homeassistant/components/coolmaster/translations/ja.json @@ -7,9 +7,15 @@ "step": { "user": { "data": { + "cool": "\u30af\u30fc\u30eb\u30e2\u30fc\u30c9\u3092\u30b5\u30dd\u30fc\u30c8", + "dry": "\u30c9\u30e9\u30a4\u30e2\u30fc\u30c9\u3092\u30b5\u30dd\u30fc\u30c8", + "fan_only": "\u30d5\u30a1\u30f3\u306e\u307f\u306e\u30e2\u30fc\u30c9\u3092\u30b5\u30dd\u30fc\u30c8", + "heat": "\u30d2\u30fc\u30c8\u30e2\u30fc\u30c9\u3092\u30b5\u30dd\u30fc\u30c8", + "heat_cool": "\u81ea\u52d5\u6696\u623f(\u52a0\u71b1)/\u30af\u30fc\u30eb\u30e2\u30fc\u30c9\u5bfe\u5fdc", "host": "\u30db\u30b9\u30c8", "off": "\u30aa\u30d5\u306b\u3067\u304d\u307e\u3059" - } + }, + "title": "CoolMasterNet\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/coronavirus/translations/ja.json b/homeassistant/components/coronavirus/translations/ja.json index 113db73a700..ef8059fc1b5 100644 --- a/homeassistant/components/coronavirus/translations/ja.json +++ b/homeassistant/components/coronavirus/translations/ja.json @@ -8,7 +8,8 @@ "user": { "data": { "country": "\u56fd" - } + }, + "title": "\u76e3\u8996\u3059\u308b\u56fd\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index 95343f36356..acd06502cff 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -2,13 +2,31 @@ "device_automation": { "action_type": { "stop": "\u505c\u6b62 {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} \u306f\u9589\u3058\u3066\u3044\u307e\u3059", + "is_closing": "{entity_name} \u304c\u7d42\u4e86\u3057\u3066\u3044\u307e\u3059", + "is_open": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059", + "is_opening": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059", + "is_position": "\u73fe\u5728\u306e {entity_name} \u4f4d\u7f6e", + "is_tilt_position": "\u73fe\u5728\u306e {entity_name} \u50be\u659c\u4f4d\u7f6e" + }, + "trigger_type": { + "closed": "{entity_name} \u9589\u3058\u307e\u3057\u305f", + "closing": "{entity_name} \u304c\u7d42\u4e86", + "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", + "opening": "{entity_name} \u304c\u958b\u304f", + "position": "{entity_name} \u4f4d\u7f6e\u306e\u5909\u66f4", + "tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u5909\u66f4" } }, "state": { "_": { "closed": "\u9589\u9396", + "closing": "\u9589\u3058\u3066\u3044\u307e\u3059", "open": "\u958b\u653e", - "opening": "\u6249" + "opening": "\u6249", + "stopped": "\u505c\u6b62" } }, "title": "\u30ab\u30d0\u30fc" diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index b8d54820183..cb1f95bccca 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -5,7 +5,8 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_bridges": "deCONZ\u30d6\u30ea\u30c3\u30b8\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "no_hardware_available": "deCONZ\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u7121\u7dda\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u3042\u308a\u307e\u305b\u3093", - "not_deconz_bridge": "deCONZ bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + "not_deconz_bridge": "deCONZ bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "updated_instance": "\u65b0\u3057\u3044\u30db\u30b9\u30c8\u30a2\u30c9\u30ec\u30b9\u3067deCONZ\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f" }, "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" @@ -52,6 +53,7 @@ "side_4": "\u30b5\u30a4\u30c94", "side_5": "\u30b5\u30a4\u30c95", "side_6": "\u30b5\u30a4\u30c96", + "top_buttons": "\u30c8\u30c3\u30d7\u30dc\u30bf\u30f3", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, @@ -60,7 +62,13 @@ "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3059\u308b \"{subtype}\"", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_double_tap": "\u30c7\u30d0\u30a4\u30b9 \"{subtype}\" \u304c\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f", + "remote_double_tap_any_side": "\u30c7\u30d0\u30a4\u30b9\u306e\u3044\u305a\u308c\u304b\u306e\u9762\u3092\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3057\u305f", + "remote_falling": "\u81ea\u7531\u843d\u4e0b\u6642\u306e\u30c7\u30d0\u30a4\u30b9(Device in free fall)", + "remote_flip_180_degrees": "\u30c7\u30d0\u30a4\u30b9\u304c180\u5ea6\u53cd\u8ee2", + "remote_flip_90_degrees": "\u30c7\u30d0\u30a4\u30b9\u304c90\u5ea6\u53cd\u8ee2", "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", + "remote_moved_any_side": "\u30c7\u30d0\u30a4\u30b9\u304c\u4efb\u610f\u306e\u9762\u3092\u4e0a\u306b\u3057\u3066\u79fb\u52d5\u3057\u305f", "remote_turned_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u6642\u8a08\u56de\u308a\u306b", "remote_turned_counter_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u6642\u8a08\u56de\u308a\u306b" } diff --git a/homeassistant/components/device_tracker/translations/ja.json b/homeassistant/components/device_tracker/translations/ja.json index 4609f6f9f3e..872448abf44 100644 --- a/homeassistant/components/device_tracker/translations/ja.json +++ b/homeassistant/components/device_tracker/translations/ja.json @@ -5,8 +5,8 @@ "is_not_home": "{entity_name} \u306f\u3001\u4e0d\u5728\u3067\u3059" }, "trigger_type": { - "enters": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u306b\u5165\u308b", - "leaves": "{entity_name} \u304c\u3001\u30be\u30fc\u30f3\u304b\u3089\u96e2\u308c\u308b" + "enters": "{entity_name} \u304c\u30be\u30fc\u30f3\u306b\u5165\u308b", + "leaves": "{entity_name} \u304c\u30be\u30fc\u30f3\u304b\u3089\u96e2\u308c\u308b" } }, "state": { diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 059ae70c39c..179edc8943c 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093" + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", + "not_doorbird_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001DoorBird\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -17,14 +18,18 @@ "name": "\u30c7\u30d0\u30a4\u30b9\u540d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "DoorBird\u306b\u63a5\u7d9a" } } }, "options": { "step": { "init": { - "description": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u3001\u30b3\u30f3\u30de\u533a\u5207\u308a\u3067\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u3057\u305f\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u307e\u3059\u3002 https://www.home-assistant.io/integrations/doorbird/#events. \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4f8b: somebody_pressed_the_button, motion" + "data": { + "events": "\u30a4\u30d9\u30f3\u30c8\u306e\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8\u3002" + }, + "description": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u3001\u30b3\u30f3\u30de\u533a\u5207\u308a\u3067\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u3057\u305f\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u307e\u3059\u3002https://www.home-assistant.io/integrations/doorbird/#events. \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4f8b: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/eafm/translations/ja.json b/homeassistant/components/eafm/translations/ja.json index fb891d2d872..aff9730fcf9 100644 --- a/homeassistant/components/eafm/translations/ja.json +++ b/homeassistant/components/eafm/translations/ja.json @@ -9,6 +9,7 @@ "data": { "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" }, + "description": "\u76e3\u8996\u3057\u305f\u3044\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u6d2a\u6c34\u76e3\u8996(Track a flood monitoring)\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8ffd\u8de1" } } diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 7aa67559664..73ac4cd1611 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -3,6 +3,10 @@ "abort": { "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, + "error": { + "pin_request_failed": "ecobee\u304b\u3089\u306ePIN\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f; API\u30ad\u30fc\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "token_request_failed": "ecobee\u304b\u3089\u306e\u30c8\u30fc\u30af\u30f3\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "authorize": { "description": "\u3053\u306e\u30a2\u30d7\u30ea\u3092 https://www.ecobee.com/consumerportal/index.html \u3067PIN\u30b3\u30fc\u30c9\u3067\u8a8d\u8a3c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n {pin}\n\n\u6b21\u306b\u3001\u9001\u4fe1(submit) \u3092\u62bc\u3057\u307e\u3059\u3002", @@ -12,6 +16,7 @@ "data": { "api_key": "API\u30ad\u30fc" }, + "description": "ecobee.com \u304b\u3089\u53d6\u5f97\u3057\u305fAPI\u30ad\u30fc\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "ecobee API\u30ad\u30fc" } } diff --git a/homeassistant/components/elgato/translations/ja.json b/homeassistant/components/elgato/translations/ja.json index 8280fd46ecc..d7686d574fc 100644 --- a/homeassistant/components/elgato/translations/ja.json +++ b/homeassistant/components/elgato/translations/ja.json @@ -17,7 +17,7 @@ "description": "Elgato Key Light\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" }, "zeroconf_confirm": { - "description": "\u30b7\u30ea\u30a2\u30eb\u756a\u53f7 '{serial_number}' \u306e\u3001Elgato Light\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "description": "\u30b7\u30ea\u30a2\u30eb\u756a\u53f7 `{serial_number}` \u306e\u3001Elgato Light\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "Elgato Light device\u3092\u767a\u898b" } } diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 0131bbb9fef..690d2b29969 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -14,7 +14,8 @@ "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Elk-M1 Control\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/flick_electric/translations/ja.json b/homeassistant/components/flick_electric/translations/ja.json index 7a595dcd56a..6091cfde5c6 100644 --- a/homeassistant/components/flick_electric/translations/ja.json +++ b/homeassistant/components/flick_electric/translations/ja.json @@ -15,7 +15,8 @@ "client_secret": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b7\u30fc\u30af\u30ec\u30c3\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "Flick\u306e\u30ed\u30b0\u30a4\u30f3\u8a8d\u8a3c\u60c5\u5831" } } } diff --git a/homeassistant/components/flume/translations/ja.json b/homeassistant/components/flume/translations/ja.json index 9e279f94596..f476c0fd35f 100644 --- a/homeassistant/components/flume/translations/ja.json +++ b/homeassistant/components/flume/translations/ja.json @@ -24,6 +24,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "Flume Personal API\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u306b\u306f\u3001https://portal.flumetech.com/settings#token \u3067\u3001'Client ID' \u3068 'Client Secret' \u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "Flume\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 2217ea645b4..23df88d984b 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -12,7 +12,8 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6" }, - "description": "\u30e6\u30fc\u30b6\u30fc\u30d9\u30fc\u30b9\u306e\u30ec\u30dd\u30fc\u30c8\u3068CDC\u306e\u30ec\u30dd\u30fc\u30c8\u3092\u30da\u30a2\u306b\u3057\u3066\u5ea7\u6a19\u3067\u30e2\u30cb\u30bf\u30fc\u3057\u307e\u3059\u3002" + "description": "\u30e6\u30fc\u30b6\u30fc\u30d9\u30fc\u30b9\u306e\u30ec\u30dd\u30fc\u30c8\u3068CDC\u306e\u30ec\u30dd\u30fc\u30c8\u3092\u30da\u30a2\u306b\u3057\u3066\u5ea7\u6a19\u3067\u30e2\u30cb\u30bf\u30fc\u3057\u307e\u3059\u3002", + "title": "\u8fd1\u304f\u306eFlu\u3092\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index 6b93e39252a..6631622d284 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -30,7 +30,8 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "AVM FRITZ!Box\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/fronius/translations/bg.json b/homeassistant/components/fronius/translations/bg.json new file mode 100644 index 00000000000..5d235f77133 --- /dev/null +++ b/homeassistant/components/fronius/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/he.json b/homeassistant/components/fronius/translations/he.json new file mode 100644 index 00000000000..1699e0f8e19 --- /dev/null +++ b/homeassistant/components/fronius/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/hu.json b/homeassistant/components/fronius/translations/hu.json new file mode 100644 index 00000000000..fc461b66121 --- /dev/null +++ b/homeassistant/components/fronius/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Adja meg a Fronius eszk\u00f6z\u00e9nek helyi c\u00edm\u00e9t (IP vagy hosztn\u00e9v).", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/tr.json b/homeassistant/components/fronius/translations/tr.json new file mode 100644 index 00000000000..4f45dd9fa1e --- /dev/null +++ b/homeassistant/components/fronius/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "description": "Fronius cihaz\u0131n\u0131z\u0131n IP adresini veya yerel ana bilgisayar ad\u0131n\u0131 yap\u0131land\u0131r\u0131n.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index faaf4644f10..2bcc20414bb 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -4,14 +4,18 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_sensors_data": "\u3053\u306e\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30f3\u30b5\u30fc\u306e\u30c7\u30fc\u30bf\u304c\u7121\u52b9\u3067\u3059\u3002", + "wrong_station_id": "\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306eID\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { "user": { "data": { - "name": "\u540d\u524d" + "name": "\u540d\u524d", + "station_id": "\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306eID" }, - "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } }, diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 57378efab44..0c282a324bd 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -13,7 +13,11 @@ "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" - } + }, + "description": "\u30ed\u30fc\u30ab\u30eb\u306eElexa Guardian\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + }, + "zeroconf_confirm": { + "description": "\u3053\u306eGuardian\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index 2730ab3ba1a..b34b504e586 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -24,6 +24,10 @@ "options": { "step": { "init": { + "data": { + "activity": "\u4f55\u3082\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u5b9f\u884c\u3055\u308c\u308b\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u3002", + "delay_secs": "\u30b3\u30de\u30f3\u30c9\u3092\u9001\u4fe1\u3059\u308b\u969b\u306e\u9045\u5ef6\u6642\u9593\u3002" + }, "description": "Harmony Hub\u306e\u8abf\u6574" } } diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 277b941fc0f..55b18b13427 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -18,7 +18,7 @@ "2fa": "2\u8981\u7d20\u30b3\u30fc\u30c9" }, "description": "Hive\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n\u5225\u306e\u30b3\u30fc\u30c9\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001\u30b3\u30fc\u30c9 0000 \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30cf\u30a4\u30d6(Hive)2\u8981\u7d20\u8a8d\u8a3c\u3002" + "title": "Hive 2\u8981\u7d20\u8a8d\u8a3c\u3002" }, "reauth": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index a6b675cb131..ca975c58723 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u3089\u306a\u3044\u305f\u3081\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002", "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", @@ -12,11 +13,19 @@ "error": { "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u6027\u8cea\u306a\u305f\u3081\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", + "max_peers_error": "\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u7121\u6599\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b9\u30c8\u30ec\u30fc\u30b8\u304c\u306a\u3044\u305f\u3081\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u8ffd\u52a0\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", + "pairing_failed": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u4e2d\u306b\u3001\u672a\u51e6\u7406\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3053\u308c\u306f\u4e00\u6642\u7684\u306a\u969c\u5bb3\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002", "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown_error": "\u30c7\u30d0\u30a4\u30b9\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3092\u5831\u544a\u3057\u307e\u3057\u305f\u3002\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" }, "flow_title": "{name}", "step": { + "busy_error": { + "title": "\u65e2\u306b\u4ed6\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "max_tries_error": { + "title": "\u8a8d\u8a3c\u306e\u6700\u5927\u8a66\u884c\u56de\u6570\u3092\u8d85\u3048\u307e\u3057\u305f" + }, "pair": { "data": { "allow_insecure_setup_codes": "\u5b89\u5168\u3067\u306a\u3044\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8a31\u53ef\u3059\u308b\u3002", @@ -49,7 +58,7 @@ "trigger_type": { "double_press": "\"{subtype}\" \u30922\u56de\u62bc\u3059", "long_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u305f\u307e\u307e", - "single_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" + "single_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "title": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc" diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 35a8de978c0..37169a86d4a 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -31,6 +31,7 @@ "step": { "init": { "data": { + "name": "\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u540d(\u5909\u66f4\u306b\u306f\u518d\u8d77\u52d5\u304c\u5fc5\u8981)", "recipient": "SMS\u901a\u77e5\u306e\u53d7\u4fe1\u8005", "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1", "track_wired_clients": "\u6709\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index ed2161e9fbc..a015f0d1806 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -43,10 +43,11 @@ "double_buttons_2_4": "2\u756a\u76ee\u30684\u756a\u76ee\u306e\u30dc\u30bf\u30f3" }, "trigger_type": { - "double_short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3059", + "double_short_release": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index c1109444bdb..1b8d3d4ef2d 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -35,10 +35,10 @@ }, "device_automation": { "trigger_subtype": { - "1": "Pierwszy przycisk", - "2": "Drugi przycisk", - "3": "Trzeci przycisk", - "4": "Czwarty przycisk", + "1": "pierwszy", + "2": "drugi", + "3": "trzeci", + "4": "czwarty", "button_1": "pierwszy", "button_2": "drugi", "button_3": "trzeci", @@ -51,16 +51,16 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { - "double_short_release": "Przycisk \"{subtype}\" zwolniony", - "initial_press": "Przycisk \"{subtype}\" wci\u015bni\u0119ty pocz\u0105tkowo", - "long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim przyci\u015bni\u0119ciu", + "double_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "initial_press": "przycisk \"{subtype}\" zostanie lekko naci\u015bni\u0119ty", + "long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione", - "repeat": "Przycisk \"{subtype}\" przytrzymany", - "short_release": "Przycisk \"{subtype}\" zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu" + "repeat": "przycisk \"{subtype}\" zostanie przytrzymany", + "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu" } }, "options": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ja.json b/homeassistant/components/hunterdouglas_powerview/translations/ja.json index 8df2288c362..a4c7674472a 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ja.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ja.json @@ -10,12 +10,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?", + "title": "PowerView Hub\u306b\u63a5\u7d9a" }, "user": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9" - } + }, + "title": "PowerView Hub\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/hvv_departures/translations/ja.json b/homeassistant/components/hvv_departures/translations/ja.json index 1dd658cedbf..89656d2669c 100644 --- a/homeassistant/components/hvv_departures/translations/ja.json +++ b/homeassistant/components/hvv_departures/translations/ja.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_results": "\u7d50\u679c\u306a\u3057\u3002\u5225\u306e\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3/\u30a2\u30c9\u30ec\u30b9\u3067\u8a66\u3057\u3066\u304f\u3060\u3055\u3044" }, "step": { "station": { diff --git a/homeassistant/components/hyperion/translations/ja.json b/homeassistant/components/hyperion/translations/ja.json index 2d5b66adc3f..ccd32102cff 100644 --- a/homeassistant/components/hyperion/translations/ja.json +++ b/homeassistant/components/hyperion/translations/ja.json @@ -27,7 +27,7 @@ "title": "Hyperion Ambilight\u30b5\u30fc\u30d3\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d" }, "create_token": { - "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "\u4ee5\u4e0b\u306e\u3001\u9001\u4fe1(submit)\u3092\u9078\u629e\u3057\u3066\u3001\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u307e\u3059\u3002\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u627f\u8a8d\u3059\u308b\u305f\u3081\u306b\u3001Hyperion UI\u306b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3055\u308c\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305fID\u304c \"{auth_id}\" \u3067\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u65b0\u3057\u3044\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u52d5\u7684\u306b\u4f5c\u6210\u3057\u307e\u3059" }, "create_token_external": { diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index a749d2fb926..ab79f0b6611 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_device": "\"iPhone\u3092\u63a2\u3059\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u306f\u3042\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 22a9d0a2b2b..cf433818237 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -45,6 +45,7 @@ "options": { "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "input_error": "\u30a8\u30f3\u30c8\u30ea\u304c\u7121\u52b9\u3067\u3059\u3002\u5024\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { @@ -75,8 +76,10 @@ }, "init": { "data": { + "add_override": "\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002", "add_x10": "X10 \u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002", "change_hub_config": "Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059\u3002", + "remove_override": "\u30c7\u30d0\u30a4\u30b9\u3092\u524a\u9664\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002", "remove_x10": "X10\u30c7\u30d0\u30a4\u30b9\u524a\u9664\u3092\u524a\u9664\u3057\u307e\u3059\u3002" }, "description": "\u8a2d\u5b9a\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/ipp/translations/ja.json b/homeassistant/components/ipp/translations/ja.json index 3e9159ce3a2..65226f78cbc 100644 --- a/homeassistant/components/ipp/translations/ja.json +++ b/homeassistant/components/ipp/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_upgrade": "\u63a5\u7d9a\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9(connection upgrade)\u304c\u5fc5\u8981\u306a\u305f\u3081\u3001\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "ipp_error": "IPP\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", "ipp_version_error": "IPP\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30d7\u30ea\u30f3\u30bf\u30fc\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "parse_error": "\u30d7\u30ea\u30f3\u30bf\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u306e\u89e3\u6790\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "unique_id_required": "\u691c\u51fa\u306b\u5fc5\u8981\u306a\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3042\u308a\u307e\u305b\u3093\u3002" @@ -16,6 +17,7 @@ "step": { "user": { "data": { + "base_path": "\u30d7\u30ea\u30f3\u30bf\u30fc\u3078\u306e\u76f8\u5bfe\u30d1\u30b9", "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 6fe37eac691..42ce4fc3608 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "manual_tunnel": { "data": { diff --git a/homeassistant/components/konnected/translations/bg.json b/homeassistant/components/konnected/translations/bg.json index 6b2ccdd56ec..1c804131ae8 100644 --- a/homeassistant/components/konnected/translations/bg.json +++ b/homeassistant/components/konnected/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index 5bfc5453409..f07caab4ee1 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 65a1c88b8d5..9b65090505b 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 162d7e25d1e..5e50b806208 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -87,14 +87,17 @@ "blink": "\u72b6\u614b\u5909\u66f4\u3092\u9001\u4fe1\u3059\u308b\u3068\u304d\u306b\u3001\u30d1\u30cd\u30eb\u306eLED\u3092\u70b9\u6ec5\u3055\u305b\u308b", "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" }, + "description": "\u30d1\u30cd\u30eb\u306b\u5fc5\u8981\u306a\u52d5\u4f5c\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u305d\u306e\u4ed6\u306e\u8a2d\u5b9a" }, "options_switch": { "data": { "activation": "\u30aa\u30f3\u306e\u3068\u304d\u306b\u51fa\u529b", + "momentary": "\u30d1\u30eb\u30b9\u6301\u7d9a\u6642\u9593(ms)(\u30aa\u30d7\u30b7\u30e7\u30f3)", "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "{zone}\u30aa\u30d7\u30b7\u30e7\u30f3 : \u72b6\u614b{state}" + "description": "{zone}\u30aa\u30d7\u30b7\u30e7\u30f3 : \u72b6\u614b{state}", + "title": "\u5207\u308a\u66ff\u3048\u53ef\u80fd\u306a\u51fa\u529b\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index be52b4ac92b..3c23b26ab0d 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", "not_konn_panel": "Bilinen bir Konnected.io cihaz\u0131 de\u011fil", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 725e1cfaae3..772b44b31d8 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -19,6 +19,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "\u8a73\u7d30\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001[Life360\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3059\u308b\u524d\u306b\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } } diff --git a/homeassistant/components/light/translations/ja.json b/homeassistant/components/light/translations/ja.json index c8e4a666c25..16d0fd40523 100644 --- a/homeassistant/components/light/translations/ja.json +++ b/homeassistant/components/light/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "flash": "\u30d5\u30e9\u30c3\u30b7\u30e5 {entity_name}", "toggle": "\u30c8\u30b0\u30eb {entity_name}", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 64d42df1c1d..4f7339efcd8 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -65,7 +65,7 @@ "stop_all": "\u3059\u3079\u3066\u505c\u6b62" }, "trigger_type": { - "press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f", + "press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f", "release": "\"{subtype}\" \u96e2\u3059" } } diff --git a/homeassistant/components/media_player/translations/ja.json b/homeassistant/components/media_player/translations/ja.json index e27e8dfbf20..b60a48d3279 100644 --- a/homeassistant/components/media_player/translations/ja.json +++ b/homeassistant/components/media_player/translations/ja.json @@ -1,9 +1,16 @@ { "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u306f\u3001\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u3067\u3059", + "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", + "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059", + "is_paused": "{entity_name} \u306f\u3001\u4e00\u6642\u505c\u6b62\u3057\u3066\u3044\u307e\u3059", + "is_playing": "{entity_name} \u304c\u518d\u751f\u3055\u308c\u3066\u3044\u307e\u3059" + }, "trigger_type": { - "idle": "{entity_name} \u304c\u3001\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u306b\u306a\u308a\u307e\u3059", + "idle": "{entity_name} \u304c\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u306b\u306a\u308a\u307e\u3059", "paused": "{entity_name} \u306f\u3001\u4e00\u6642\u505c\u6b62\u3057\u3066\u3044\u307e\u3059", - "playing": "{entity_name} \u304c\u3001\u518d\u751f\u3092\u958b\u59cb\u3057\u307e\u3059", + "playing": "{entity_name} \u304c\u518d\u751f\u3092\u958b\u59cb\u3057\u307e\u3059", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index 17675d40bfc..b8f2d14e5cd 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/meteo_france/translations/ja.json b/homeassistant/components/meteo_france/translations/ja.json index ca736d1c31d..2fa1f60225e 100644 --- a/homeassistant/components/meteo_france/translations/ja.json +++ b/homeassistant/components/meteo_france/translations/ja.json @@ -4,11 +4,15 @@ "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "error": { + "empty": "\u90fd\u5e02\u691c\u7d22\u306e\u7d50\u679c\u306f\u3042\u308a\u307e\u305b\u3093: \u90fd\u5e02\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, "step": { "cities": { "data": { "city": "\u90fd\u5e02" }, + "description": "\u30ea\u30b9\u30c8\u304b\u3089\u3042\u306a\u305f\u306e\u90fd\u5e02\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", "title": "M\u00e9t\u00e9o-France" }, "user": { diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index da9774e8350..6a5de20ec8d 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url}) \u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" + }, "step": { "confirm": { "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 78b002e492c..48165f64111 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -40,7 +40,7 @@ }, "trigger_type": { "button_long_press": "\"{subtype}\" \u304c\u3001\u7d99\u7d9a\u7684\u306b\u62bc\u3055\u308c\u305f", - "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" + "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "options": { diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 97b706fa99d..ac3c4b258d2 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -29,12 +29,12 @@ "trigger_type": { "alarm_started": "{entity_name} \u304c\u30a2\u30e9\u30fc\u30e0\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", "animal": "{entity_name} \u304c\u52d5\u7269\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", - "cancel_set_point": "{entity_name} \u304c\u3001\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb\u3092\u518d\u958b\u3057\u307e\u3057\u305f\u3002", - "human": "{entity_name} \u304c\u3001\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "cancel_set_point": "{entity_name} \u304c\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb\u3092\u518d\u958b\u3057\u307e\u3057\u305f\u3002", + "human": "{entity_name} \u304c\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", "movement": "{entity_name} \u304c\u52d5\u304d\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", "outdoor": "{entity_name} \u304c\u5c4b\u5916\u30a4\u30d9\u30f3\u30c8\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", - "person": "{entity_name} \u304c\u3001\u500b\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", - "person_away": "{entity_name} \u304c\u3001\u4eba\u304c\u53bb\u3063\u305f\u3053\u3068\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "person": "{entity_name} \u304c\u500b\u4eba\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", + "person_away": "{entity_name} \u304c\u4eba\u304c\u53bb\u3063\u305f\u3053\u3068\u3092\u691c\u51fa\u3057\u307e\u3057\u305f", "set_point": "\u76ee\u6a19\u6e29\u5ea6 {entity_name} \u3092\u624b\u52d5\u3067\u8a2d\u5b9a", "therm_mode": "{entity_name} \u306f \"{subtype}\" \u306b\u5207\u308a\u66ff\u308f\u308a\u307e\u3057\u305f\u3002", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index b8e45689a20..fe75eca98c5 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" @@ -26,9 +26,9 @@ "init": { "data": { "consider_home": "\u898b\u3048\u306a\u304f\u306a\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u898b\u306a\u3057\u3066\u3001\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u6a5f\u3059\u308b\u307e\u3067\u306e\u79d2\u6570\u3002", - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30ab\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3", "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1" diff --git a/homeassistant/components/nuheat/translations/ja.json b/homeassistant/components/nuheat/translations/ja.json index 8f2eb2cc09a..c8a9b3fc105 100644 --- a/homeassistant/components/nuheat/translations/ja.json +++ b/homeassistant/components/nuheat/translations/ja.json @@ -6,14 +6,17 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_thermostat": "\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u7121\u52b9\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "serial_number": "\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u3002", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "https://MyNuHeat.com \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3001\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u3092\u9078\u629e\u3057\u3066\u3001\u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u307e\u305f\u306fID\u3092\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "NuHeat\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 707a8bd8faa..4c52cc74ed2 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -27,7 +27,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "NUT server\u306b\u63a5\u7d9a" } } }, diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index 1db987a2734..faec1e6977b 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "create_entry": { - "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `'1A'`\n - Device ID: `'1B'`\n\niOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `'1A'`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\nOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 75a3ec07146..8b3f030b72f 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Plaato {device_type} \u540d\u524d **{device_name}** \u304c\u6b63\u5e38\u306b\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f\u3002" + "default": "Plaato {device_type} \u540d\u524d **{device_name}** \u304c\u6b63\u5e38\u306b\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "invalid_webhook_device": "Webhook\u3078\u306e\u30c7\u30fc\u30bf\u9001\u4fe1\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002Airlock\u3067\u306e\u307f\u5229\u7528\u53ef\u80fd\u3067\u3059", diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index f6a98b1d8a2..5fda1370e65 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -5,6 +5,7 @@ "already_configured": "\u3053\u306ePlex server\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "token_request_timeout": "\u30c8\u30fc\u30af\u30f3\u306e\u53d6\u5f97\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -33,6 +34,7 @@ "title": "Plex\u30b5\u30fc\u30d0\u30fc\u3092\u9078\u629e" }, "user": { + "description": "[plex.tv](https://plex.tv) \u306b\u9032\u307f\u3001Plex server\u3092\u30ea\u30f3\u30af\u3057\u307e\u3059\u3002", "title": "Plex Media Server" }, "user_advanced": { @@ -47,7 +49,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "\u65b0\u3057\u3044\u7ba1\u7406\u5bfe\u8c61/\u5171\u6709\u30e6\u30fc\u30b6\u30fc\u3092\u7121\u8996\u3059\u308b", "ignore_plex_web_clients": "Plex Web clients\u3092\u7121\u8996\u3059\u308b", + "monitored_users": "\u76e3\u8996\u5bfe\u8c61\u306e\u30e6\u30fc\u30b6\u30fc", "use_episode_art": "\u30a8\u30d4\u30bd\u30fc\u30c9\u30a2\u30fc\u30c8\u3092\u4f7f\u7528" }, "description": "Plex Media Player\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index e66e1ef19b6..27c9cfded6a 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -16,7 +16,8 @@ "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "title": "Powerwall\u306b\u63a5\u7d9a" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 245ad03a7e6..1021f47a38a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -11,6 +11,7 @@ "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" }, + "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c((hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/rachio/translations/ja.json b/homeassistant/components/rachio/translations/ja.json index 81dd28070d0..37b8fa99800 100644 --- a/homeassistant/components/rachio/translations/ja.json +++ b/homeassistant/components/rachio/translations/ja.json @@ -13,7 +13,8 @@ "data": { "api_key": "API\u30ad\u30fc" }, - "description": "https://app.rach.io/ \u304b\u3089\u306eAPI Key\u304c\u5fc5\u8981\u3067\u3059\u3002Settings(\u8a2d\u5b9a)\u3092\u958b\u304d\u3001'GET API KEY'\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + "description": "https://app.rach.io/ \u304b\u3089\u306eAPI Key\u304c\u5fc5\u8981\u3067\u3059\u3002Settings(\u8a2d\u5b9a)\u3092\u958b\u304d\u3001'GET API KEY'\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", + "title": "Rachio device\u306b\u63a5\u7d9a" } } }, diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index 0ac33ed40fc..be3e39fca70 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -49,7 +49,7 @@ "send_status": "wy\u015blij aktualizacj\u0119 statusu: {subtype}" }, "trigger_type": { - "command": "Otrzymane polecenie: {subtype}", + "command": "otrzymane polecenie {subtype}", "status": "otrzymany status: {subtype}" } }, diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 21e72de5eac..b10b92906f4 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -17,7 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u30c7\u30d0\u30a4\u30b9}\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", + "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", "title": "Samsung TV" }, "reauth_confirm": { diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 1cf40e43e8b..ef9de383daa 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,12 +1,15 @@ { "device_automation": { "condition_type": { + "is_battery_level": "\u73fe\u5728\u306e {entity_name} \u96fb\u6c60\u6b8b\u91cf", "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_current": "\u73fe\u5728\u306e {entity_name} \u96fb\u6d41", "is_energy": "\u73fe\u5728\u306e {entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc", "is_frequency": "\u73fe\u5728\u306e {entity_name} \u983b\u5ea6(frequency)", "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", + "is_humidity": "\u73fe\u5728\u306e {entity_name} \u6e7f\u5ea6", + "is_illuminance": "\u73fe\u5728\u306e {entity_name} \u7167\u5ea6", "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrogen_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrous_oxide": "\u73fe\u5728\u306e {entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -14,18 +17,26 @@ "is_pm1": "\u73fe\u5728\u306e {entity_name} PM1\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_pm10": "\u73fe\u5728\u306e {entity_name} PM10\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_pm25": "\u73fe\u5728\u306e {entity_name} PM2.5\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_power": "\u73fe\u5728\u306e {entity_name} \u96fb\u6e90", "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", + "is_pressure": "\u73fe\u5728\u306e {entity_name} \u5727\u529b", + "is_signal_strength": "\u73fe\u5728\u306e {entity_name} \u4fe1\u53f7\u5f37\u5ea6", "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", + "is_temperature": "\u73fe\u5728\u306e {entity_name} \u6e29\u5ea6", + "is_value": "\u73fe\u5728\u306e {entity_name} \u5024", "is_volatile_organic_compounds": "\u73fe\u5728\u306e {entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u306e\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_voltage": "\u73fe\u5728\u306e {entity_name} \u96fb\u5727" }, "trigger_type": { + "battery_level": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u306e\u5909\u5316", "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "current": "{entity_name} \u73fe\u5728\u306e\u5909\u5316", "energy": "{entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc\u306e\u5909\u5316", "frequency": "{entity_name} \u983b\u5ea6(frequency)\u306e\u5909\u66f4", "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", + "humidity": "{entity_name} \u6e7f\u5ea6\u306e\u5909\u5316", + "illuminance": "{entity_name} \u7167\u5ea6\u306e\u5909\u5316", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrogen_monoxide": "{entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrous_oxide": "{entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", @@ -33,8 +44,13 @@ "pm1": "{entity_name} PM1\u6fc3\u5ea6\u306e\u5909\u5316", "pm10": "{entity_name} PM10\u6fc3\u5ea6\u306e\u5909\u5316", "pm25": "{entity_name} PM2.5\u6fc3\u5ea6\u306e\u5909\u5316", + "power": "{entity_name} \u96fb\u6e90(power)\u306e\u5909\u5316", "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4", + "pressure": "{entity_name} \u5727\u529b\u306e\u5909\u5316", + "signal_strength": "{entity_name} \u4fe1\u53f7\u5f37\u5ea6\u306e\u5909\u5316", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", + "temperature": "{entity_name} \u6e29\u5ea6\u5909\u5316", + "value": "{entity_name} \u5024\u306e\u5909\u5316", "volatile_organic_compounds": "{entity_name} \u63ee\u767a\u6027\u6709\u6a5f\u5316\u5408\u7269\u6fc3\u5ea6\u306e\u5909\u5316", "voltage": "{entity_name} \u96fb\u5727\u306e\u5909\u5316" } diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index b55de538fd3..c844f9d3221 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -33,7 +33,7 @@ "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", - "frequency": "zmiany cz\u0119stotliwo\u015b\u0107 {entity_name}", + "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", "gas": "{entity_name} wykryje zmian\u0119 poziomu gazu", "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index ea151bfa868..39ff690804f 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 068d62fa0d8..1b1751d63f0 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -4,6 +4,7 @@ "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_mdns": "Smappee\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, @@ -18,12 +19,14 @@ "local": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u3057\u307e\u3059" }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "zeroconf_confirm": { + "description": "`{serialnumber}` \u306eSmappee device\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "Smappee device\u3092\u767a\u898b" } } diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json index 304a537c9e0..826692925d7 100644 --- a/homeassistant/components/smarthab/translations/ja.json +++ b/homeassistant/components/smarthab/translations/ja.json @@ -11,6 +11,7 @@ "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, + "description": "\u6280\u8853\u7684\u306a\u7406\u7531\u304b\u3089\u3001Home Assistant\u306e\u8a2d\u5b9a\u306b\u56fa\u6709\u306e\u30bb\u30ab\u30f3\u30c0\u30ea\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002SmartHab\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304b\u3089\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002", "title": "SmartHab\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index bab4e1838ef..1174e136535 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "SmartThings\u304b\u3089\u66f4\u65b0\u3092\u53d7\u4fe1\u3059\u308b\u3088\u3046\u306bHome Assistant\u304c\u6b63\u3057\u304f\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 Webhook\u306eURL\u304c\u7121\u52b9\u3067\u3059:\n> {webhook_url}\n\n[\u8aac\u660e\u66f8]({component_url}) \u306b\u5f93\u3063\u3066\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_available_locations": "Home Assistant\u306b\u8a2d\u5b9a\u3067\u304d\u308bSmartThings\u306e\u5834\u6240\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + }, "error": { "app_setup_error": "SmartApp\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "token_forbidden": "\u30c8\u30fc\u30af\u30f3\u306b\u5fc5\u8981\u306aOAuth\u30b9\u30b3\u30fc\u30d7(OAuth scopes)\u304c\u3042\u308a\u307e\u305b\u3093\u3002", @@ -21,6 +25,7 @@ "data": { "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" }, + "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index e0dafa5a64a..4482dd37db2 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -29,7 +29,7 @@ "data": { "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" }, - "description": "{{entity_id}} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a", + "description": "{entity_id}} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a", "title": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u8a2d\u5b9a" }, "init": { diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index d5304c1db0d..2b057f2ced2 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -12,7 +12,7 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json index 8b95713541d..dc64e26ac64 100644 --- a/homeassistant/components/starline/translations/ja.json +++ b/homeassistant/components/starline/translations/ja.json @@ -11,6 +11,7 @@ "app_id": "App ID", "app_secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" }, + "description": "[StarLine\u958b\u767a\u8005\u30a2\u30ab\u30a6\u30f3\u30c8](https://my.starline.ru/developer)\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID\u3068\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u30b3\u30fc\u30c9", "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831" }, "auth_captcha": { @@ -24,6 +25,7 @@ "data": { "mfa_code": "SMS\u30b3\u30fc\u30c9" }, + "description": "\u96fb\u8a71 {phone_number} \u306b\u9001\u4fe1\u3055\u308c\u305f\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "2\u8981\u7d20\u8a8d\u8a3c" }, "auth_user": { diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 848991ffaf2..90ea6970c61 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -15,7 +15,8 @@ "2sa": { "data": { "otp_code": "\u30b3\u30fc\u30c9" - } + }, + "title": "Synology DSM: 2\u8981\u7d20\u8a8d\u8a3c" }, "link": { "data": { @@ -25,6 +26,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?", "title": "Synology DSM" }, "reauth": { diff --git a/homeassistant/components/tellduslive/translations/ja.json b/homeassistant/components/tellduslive/translations/ja.json index 41803a6f62b..cbeefb97eb2 100644 --- a/homeassistant/components/tellduslive/translations/ja.json +++ b/homeassistant/components/tellduslive/translations/ja.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f:\n1. \u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n2. Telldus Live\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\n3. **{app_name}** \u3092\u627f\u8a8d\u3057\u307e\u3059(**Yes(\u306f\u3044)**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059)\n4. \u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u3001**\u9001\u4fe1(submit)** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n [TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30ea\u30f3\u30af]({auth_url})", + "description": "TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f:\n1. \u4ee5\u4e0b\u306e\u30ea\u30f3\u30af\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n2. Telldus Live\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\n3. **{app_name}** \u3092\u627f\u8a8d\u3057\u307e\u3059(**Yes(\u306f\u3044)**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059)\n4. \u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u3001**\u9001\u4fe1(submit)** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n\n[TelldusLive\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30ea\u30f3\u30af]({auth_url})", "title": "TelldusLive\u306b\u5bfe\u3057\u3066\u8a8d\u8a3c\u3059\u308b" }, "user": { diff --git a/homeassistant/components/tolo/translations/bg.json b/homeassistant/components/tolo/translations/bg.json new file mode 100644 index 00000000000..f1c33573305 --- /dev/null +++ b/homeassistant/components/tolo/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/he.json b/homeassistant/components/tolo/translations/he.json new file mode 100644 index 00000000000..9da8a69a4fe --- /dev/null +++ b/homeassistant/components/tolo/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/hu.json b/homeassistant/components/tolo/translations/hu.json new file mode 100644 index 00000000000..55239599c16 --- /dev/null +++ b/homeassistant/components/tolo/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Adja meg a TOLO Sauna eszk\u00f6z\u00e9nek hostnev\u00e9t vagy IP-c\u00edm\u00e9t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.bg.json b/homeassistant/components/tolo/translations/select.bg.json new file mode 100644 index 00000000000..e66365b2339 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "manual": "\u0440\u044a\u0447\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.he.json b/homeassistant/components/tolo/translations/select.he.json new file mode 100644 index 00000000000..f02588f3be5 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", + "manual": "\u05d9\u05d3\u05e0\u05d9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.hu.json b/homeassistant/components/tolo/translations/select.hu.json new file mode 100644 index 00000000000..1768eeaa0a8 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatikus", + "manual": "k\u00e9zi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.tr.json b/homeassistant/components/tolo/translations/select.tr.json new file mode 100644 index 00000000000..fda5a726da0 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.tr.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "otomatik", + "manual": "manuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json new file mode 100644 index 00000000000..2d5c8117cd6 --- /dev/null +++ b/homeassistant/components/tolo/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "description": "TOLO Sauna cihaz\u0131n\u0131z\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index aeb6b3af691..d6bffce6dae 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -26,7 +26,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "\u30c8\u30fc\u30bf\u30eb\u30b3\u30cd\u30af\u30c8" } } } diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 14f3145978f..5bd5b98a8b3 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -26,8 +26,10 @@ "init": { "data": { "limit": "\u30ea\u30df\u30c3\u30c8", - "order": "\u30aa\u30fc\u30c0\u30fc" - } + "order": "\u30aa\u30fc\u30c0\u30fc", + "scan_interval": "\u66f4\u65b0\u983b\u5ea6" + }, + "title": "Transmission\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/tuya/translations/sensor.ja.json b/homeassistant/components/tuya/translations/sensor.ja.json index 6c08faf52de..aecb556cf17 100644 --- a/homeassistant/components/tuya/translations/sensor.ja.json +++ b/homeassistant/components/tuya/translations/sensor.ja.json @@ -3,8 +3,8 @@ "tuya__status": { "boiling_temp": "\u6cb8\u70b9", "cooling": "\u51b7\u5374", - "heating": "\u6696\u623f", - "heating_temp": "\u52a0\u71b1(\u6696\u623f)\u6e29\u5ea6", + "heating": "\u6696\u623f(\u52a0\u71b1)", + "heating_temp": "\u6696\u623f(\u52a0\u71b1)\u6e29\u5ea6", "reserve_1": "Reserve 1", "reserve_2": "Reserve 2", "reserve_3": "Reserve 3", diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json index 0311d5ae5e3..8ec65b24fb5 100644 --- a/homeassistant/components/twentemilieu/translations/ja.json +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "house_letter": "\u30cf\u30a6\u30b9\u30ec\u30bf\u30fc/\u8ffd\u52a0", "house_number": "\u5bb6\u5c4b\u756a\u53f7", "post_code": "\u90f5\u4fbf\u756a\u53f7" }, diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index 1c6e95bd962..4bb01d82ef6 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El lloc del controlador ja est\u00e0 configurat", + "already_configured": "El lloc web d'UniFi Network ja est\u00e0 configurat", "configuration_updated": "S'ha actualitzat la configuraci\u00f3.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, @@ -21,7 +21,7 @@ "username": "[%key::common::config_flow::data::username%]", "verify_ssl": "Verifica el certificat SSL" }, - "title": "Configuraci\u00f3 del controlador UniFi" + "title": "Configuraci\u00f3 d'UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Permet control POE dels clients" }, "description": "Configura els controls del client \n\nConfigura interruptors per als n\u00fameros de s\u00e8rie als quals vulguis controlar l'acc\u00e9s a la xarxa.", - "title": "Opcions d'UniFi 2/3" + "title": "Opcions d'UniFi Network 2/3" }, "device_tracker": { "data": { "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", - "ignore_wired_bug": "Desactiva la l\u00f2gica d'errors amb UniFi", + "ignore_wired_bug": "Desactiva la l\u00f2gica d'errors d'UniFi Network", "ssid_filter": "Selecciona els SSID's on fer-hi el seguiment de clients", "track_clients": "Segueix clients de la xarxa", "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", "track_wired_clients": "Inclou clients de xarxa per cable" }, "description": "Configuraci\u00f3 de seguiment de dispositius", - "title": "Opcions d'UniFi 1/3" + "title": "Opcions d'UniFi Network 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "[%key::component::unifi::options::step::device_tracker::data::track_clients%]", "track_devices": "[%key::component::unifi::options::step::device_tracker::data::track_devices%]" }, - "description": "Configura la integraci\u00f3 d'UniFi" + "description": "Configura la integraci\u00f3 UniFi Network" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "Sensors de temps d'activitat per a clients de xarxa" }, "description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques", - "title": "Opcions d'UniFi 3/3" + "title": "Opcions d'UniFi Network 3/3" } } } diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 04fc14845c9..ce4047ced42 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller-Site ist bereits konfiguriert", + "already_configured": "UniFi-Netzwerkstandort ist bereits konfiguriert", "configuration_updated": "Konfiguration aktualisiert.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, @@ -21,7 +21,7 @@ "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "title": "UniFi-Controller einrichten" + "title": "UniFi-Netzwerk einrichten" } } }, @@ -34,19 +34,19 @@ "poe_clients": "POE-Kontrolle von Clients zulassen" }, "description": "Konfiguriere Client-Steuerelemente \n\nErstelle Switches f\u00fcr Seriennummern, f\u00fcr die du den Netzwerkzugriff steuern m\u00f6chtest.", - "title": "UniFi-Optionen 2/3" + "title": "UniFi Netzwerk Optionen 2/3" }, "device_tracker": { "data": { "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", - "ignore_wired_bug": "Deaktivieren der kabelgebundenen UniFi-Fehlerlogik", + "ignore_wired_bug": "Deaktiviere die kabelgebundene Fehlerlogik des UniFi-Netzwerks", "ssid_filter": "W\u00e4hle SSIDs zur Verfolgung von drahtlosen Clients aus", "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" }, "description": "Konfiguriere die Ger\u00e4teverfolgung", - "title": "UniFi-Optionen 1/3" + "title": "UniFi Netzwerk Optionen 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)" }, - "description": "Konfiguriere die UniFi-Integration" + "description": "Konfigurieren der UniFi-Netzwerkintegration" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "Uptime-Sensoren f\u00fcr Netzwerk-Clients" }, "description": "Konfiguriere die Statistiksensoren", - "title": "UniFi-Optionen 3/3" + "title": "UniFi Netzwerk Optionen 3/3" } } } diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json index 443d036bd96..76d93c95d97 100644 --- a/homeassistant/components/unifi/translations/et.json +++ b/homeassistant/components/unifi/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Kontroller on juba seadistatud", + "already_configured": "UniFi Network on juba seadistatud", "configuration_updated": "Seaded on v\u00e4rskendatud.", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, @@ -21,7 +21,7 @@ "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati" }, - "title": "Seadista UniFi kontroller" + "title": "Seadista UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Luba klientide POE kontroll" }, "description": "Seadista kliendi juhtelemendid \n\n Loo seerianumbrite. mille v\u00f5rgule juurdep\u00e4\u00e4su soovite kontrollida, jaoks l\u00fclitid.", - "title": "UniFi valikud 2/3" + "title": "UniFi Network valikud 2/3" }, "device_tracker": { "data": { "detection_time": "Aeg sekundites alates viimasest ilmnemisest kuni olekuni '\u00e4ra'", - "ignore_wired_bug": "UniFi kaabel\u00fchenduse vealoogika keelamine", + "ignore_wired_bug": "UniFi Network kaabel\u00fchenduse vealoogika keelamine", "ssid_filter": "Vali WiFi klientide j\u00e4lgimiseks SSID", "track_clients": "J\u00e4lgi v\u00f5rgu kliente", "track_devices": "V\u00f5rguseadmete j\u00e4lgimine (Ubiquiti seadmed)", "track_wired_clients": "Kaasa juhtmega v\u00f5rgukliendid" }, "description": "Seadista seadme j\u00e4lgimine", - "title": "UniFi valikud 1/3" + "title": "UniFi Network valikud 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "J\u00e4lgi v\u00f5rgu kliente", "track_devices": "J\u00e4lgi v\u00f5rgu seadmeid (Ubiquiti seadmed)" }, - "description": "Seadista UniFi sidumine" + "description": "Seadista UniFi Network sidumine" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "V\u00f5rguklientide t\u00f6\u00f6soleku andurid" }, "description": "Seadista statistikaandurid", - "title": "UniFi valikud 3/3" + "title": "UniFi Network valikud 3/3" } } } diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json index ec023fa7363..e22b5a024a7 100644 --- a/homeassistant/components/unifi/translations/id.json +++ b/homeassistant/components/unifi/translations/id.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Situs Controller sudah dikonfigurasi", + "already_configured": "Situs Jaringan UniFi sudah dikonfigurasi", "configuration_updated": "Konfigurasi diperbarui.", "reauth_successful": "Autentikasi ulang berhasil" }, diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index fbaf31c067b..9517b707531 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -30,7 +30,8 @@ "client_control": { "data": { "block_client": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u30af\u30e9\u30a4\u30a2\u30f3\u30c8", - "dpi_restrictions": "DPI\u5236\u9650\u30b0\u30eb\u30fc\u30d7\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b" + "dpi_restrictions": "DPI\u5236\u9650\u30b0\u30eb\u30fc\u30d7\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b", + "poe_clients": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306ePOE\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b" }, "description": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a2d\u5b9a\n\n\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u5236\u5fa1\u3057\u305f\u3044\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc\u306e\u30b9\u30a4\u30c3\u30c1\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", "title": "UniFi\u30aa\u30d7\u30b7\u30e7\u30f32/3" @@ -38,10 +39,14 @@ "device_tracker": { "data": { "detection_time": "\u6700\u5f8c\u306b\u898b\u305f\u3082\u306e\u304b\u3089\u96e2\u308c\u3066\u3044\u308b\u3068\u898b\u306a\u3055\u308c\u308b\u307e\u3067\u306e\u6642\u9593(\u79d2)", + "ignore_wired_bug": "UniFi\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306e\u6709\u7dda\u30d0\u30b0 \u30ed\u30b8\u30c3\u30af\u3092\u7121\u52b9\u306b\u3059\u308b", + "ssid_filter": "\u30ef\u30a4\u30e4\u30ec\u30b9\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308bSSID\u3092\u9078\u629e", "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)", "track_wired_clients": "\u6709\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u542b\u3081\u308b" - } + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u306e\u8a2d\u5b9a", + "title": "UniFi\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30aa\u30d7\u30b7\u30e7\u30f3 1/3" }, "simple_options": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index a2c9bfe8061..4d9726ab751 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -21,7 +21,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, - "title": "UniFi Controller" + "title": "UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c POE \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f.\n\n\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0438 \u0434\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0442\u0438.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 2" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi Network. \u0428\u0430\u0433 2" }, "device_tracker": { "data": { "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0434\u043e\u043c\u0430", - "ignore_wired_bug": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043a\u0438 \u0434\u043b\u044f \u043d\u0435 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 UniFi", + "ignore_wired_bug": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043a\u0438 \u0434\u043b\u044f \u043d\u0435 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 UniFi Network", "ssid_filter": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 SSID \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432", "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 1" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi Network. \u0428\u0430\u0433 1" }, "init": { "data": { @@ -62,7 +62,7 @@ "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 UniFi." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 UniFi Network." }, "statistics_sensors": { "data": { @@ -70,7 +70,7 @@ "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 3" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi Network. \u0428\u0430\u0433 3" } } } diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index b165bee4187..6baa8108098 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Denetleyici sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured": "UniFi Network sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, diff --git a/homeassistant/components/vacuum/translations/ja.json b/homeassistant/components/vacuum/translations/ja.json index 74fb5660762..649cc868971 100644 --- a/homeassistant/components/vacuum/translations/ja.json +++ b/homeassistant/components/vacuum/translations/ja.json @@ -1,6 +1,15 @@ { "device_automation": { + "action_type": { + "clean": "{entity_name} \u30af\u30ea\u30fc\u30f3\u30a2\u30c3\u30d7\u3057\u307e\u3057\u3087\u3046", + "dock": "{entity_name} \u3092\u30c9\u30c3\u30af\u306b\u623b\u3057\u3066\u307f\u307e\u3057\u3087\u3046" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u306f\u30af\u30ea\u30fc\u30cb\u30f3\u30b0\u4e2d\u3067\u3059", + "is_docked": "{entity_name} \u304c\u30c9\u30c3\u30ad\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059" + }, "trigger_type": { + "cleaning": "{entity_name} \u304c\u30af\u30ea\u30fc\u30cb\u30f3\u30b0\u3092\u958b\u59cb", "docked": "{entity_name} \u30c9\u30c3\u30ad\u30f3\u30b0\u6e08\u307f" } }, diff --git a/homeassistant/components/velbus/translations/ja.json b/homeassistant/components/velbus/translations/ja.json index 3c052c44d65..8a1b2055945 100644 --- a/homeassistant/components/velbus/translations/ja.json +++ b/homeassistant/components/velbus/translations/ja.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { + "name": "\u3053\u306e\u3001velbus connection\u306e\u540d\u524d", "port": "\u63a5\u7d9a\u6587\u5b57\u5217" - } + }, + "title": "velbus connection\u30bf\u30a4\u30d7\u306e\u5b9a\u7fa9" } } } diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json index bef1a45ba32..2f800ee7b83 100644 --- a/homeassistant/components/vera/translations/ja.json +++ b/homeassistant/components/vera/translations/ja.json @@ -6,15 +6,23 @@ "step": { "user": { "data": { + "exclude": "Home Assistant\u304b\u3089\u9664\u5916\u3059\u308bVera\u30c7\u30d0\u30a4\u30b9\u306eID\u3002", + "lights": "Vera\u306f\u3001Home Assistant\u3067\u30e9\u30a4\u30c8\u3068\u3057\u3066\u6271\u3046\u30c7\u30d0\u30a4\u30b9ID\u3092\u5207\u308a\u66ff\u3048\u307e\u3059\u3002", "vera_controller_url": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL" }, - "description": "Vera\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL\u3092\u4ee5\u4e0b\u306b\u793a\u3057\u307e\u3059: http://192.168.1.161:3480 \u306e\u3088\u3046\u306b\u306a\u3063\u3066\u3044\u308b\u306f\u305a\u3067\u3059\u3002" + "description": "Vera\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL\u3092\u4ee5\u4e0b\u306b\u793a\u3057\u307e\u3059: http://192.168.1.161:3480 \u306e\u3088\u3046\u306b\u306a\u3063\u3066\u3044\u308b\u306f\u305a\u3067\u3059\u3002", + "title": "Vera controller\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, "options": { "step": { "init": { + "data": { + "exclude": "Home Assistant\u304b\u3089\u9664\u5916\u3059\u308bVera\u30c7\u30d0\u30a4\u30b9\u306eID\u3002", + "lights": "Vera\u306f\u3001Home Assistant\u3067\u30e9\u30a4\u30c8\u3068\u3057\u3066\u6271\u3046\u30c7\u30d0\u30a4\u30b9ID\u3092\u5207\u308a\u66ff\u3048\u307e\u3059\u3002" + }, + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001vera\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044: https://www.home-assistant.io/integrations/vera/ \u6ce8: \u3053\u3053\u3067\u5909\u66f4\u3092\u884c\u3063\u305f\u5834\u5408\u306f\u3001Home Assistant\u30b5\u30fc\u30d0\u30fc\u3092\u518d\u8d77\u52d5\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u5024\u3092\u30af\u30ea\u30a2\u3059\u308b\u306b\u306f\u3001\u30b9\u30da\u30fc\u30b9\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002", "title": "Vera controller\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 36220756a4d..2496d351314 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -16,6 +16,7 @@ "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u306e\u5b8c\u4e86" }, "pairing_complete": { + "description": "\u3042\u306a\u305f\u306e\u3001VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9\u304cHome Assistant\u306b\u63a5\u7d9a\u3055\u308c\u307e\u3057\u305f\u3002", "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" }, "pairing_complete_import": { diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 4b61f0ea8f8..5ed0741a67e 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -14,7 +14,7 @@ "origin": "\u30aa\u30ea\u30b8\u30f3", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30ab\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" + "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30b3\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index eb557810a0c..c409579fbc8 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" @@ -24,6 +25,7 @@ "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" }, "reauth": { + "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 68235900ee9..0efbe56e3e0 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -16,6 +16,7 @@ "description": "WLED\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" }, "zeroconf_confirm": { + "description": "Home Assistant\u306b\u3001`{name}` \u3068\u3044\u3046\u540d\u524d\u306eWLED\u3092\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", "title": "\u767a\u898b\u3055\u308c\u305fWLED device" } } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index ebb35d7143a..d37a81baaf0 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -17,17 +17,23 @@ "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", + "frost_heizkreis": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u306e\u971c", "frostschutz": "\u971c\u9632\u6b62", "gasdruck": "\u30ac\u30b9\u5727", "glt_betrieb": "BMS\u30e2\u30fc\u30c9", "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", - "heizung": "\u6696\u623f", + "heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u30e2\u30fc\u30c9", + "heizung": "\u6696\u623f(\u52a0\u71b1)", "initialisierung": "\u521d\u671f\u5316", "kalibration": "\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "kalibration_heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u306e\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "kalibration_kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9 \u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "nachlauf_heizkreispumpe": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u30dd\u30f3\u30d7\u306e\u4f5c\u52d5", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", "parallelbetrieb": "\u30d1\u30e9\u30ec\u30eb\u30e2\u30fc\u30c9", "partymodus": "\u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9", + "perm_cooling": "PermCooling", "permanent": "\u6c38\u7d9a", "permanentbetrieb": "\u30d1\u30fc\u30de\u30cd\u30f3\u30c8\u30e2\u30fc\u30c9", "reduzierter_betrieb": "\u5236\u9650\u4ed8\u304d\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index d7fc0d4d7b2..89521148137 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -51,6 +51,9 @@ } }, "device_automation": { + "action_type": { + "squawk": "\u30b9\u30b3\u30fc\u30af(Squawk)" + }, "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", @@ -69,8 +72,12 @@ "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { + "device_flipped": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u8ee2\u3057\u307e\u3057\u305f \"{subtype}\"", + "device_knocked": "\u30c7\u30d0\u30a4\u30b9\u304c\u30ce\u30c3\u30af\u3055\u308c\u307e\u3057\u305f \"{subtype}\"", "device_offline": "\u30c7\u30d0\u30a4\u30b9\u304c\u30aa\u30d5\u30e9\u30a4\u30f3", + "device_rotated": "\u30c7\u30d0\u30a4\u30b9\u304c\u56de\u8ee2\u3057\u307e\u3057\u305f \"{subtype}\"", "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", + "device_slid": "\u30c7\u30d0\u30a4\u30b9 \u30b9\u30e9\u30a4\u30c9 \"{subtype}\"", "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b", "remote_button_alt_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_short_press": "\"{subtype}\" \u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", From 9b493be2b2f2ec139d07574469cd5e63b3d41b27 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 27 Nov 2021 22:49:59 -0500 Subject: [PATCH 0927/1452] Bump up ZHA dependencies (#60474) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1fa15259821..a1c7a1697ef 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.28.0", + "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.5", "zha-quirks==0.0.63", - "zigpy-deconz==0.13.0", + "zigpy-deconz==0.14.0", "zigpy==0.41.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", diff --git a/requirements_all.txt b/requirements_all.txt index c5dbd4d90f9..dea12e89841 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ beautifulsoup4==4.10.0 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.28.0 +bellows==0.29.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.8.2 @@ -2492,7 +2492,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.13.0 +zigpy-deconz==0.14.0 # homeassistant.components.zha zigpy-xbee==0.14.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b70292307e..bdd362d314b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ azure-eventhub==5.5.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.28.0 +bellows==0.29.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.8.2 @@ -1469,7 +1469,7 @@ zeroconf==0.37.0 zha-quirks==0.0.63 # homeassistant.components.zha -zigpy-deconz==0.13.0 +zigpy-deconz==0.14.0 # homeassistant.components.zha zigpy-xbee==0.14.0 From a3e34c74dbdd7771a288e65db03ed5070884bdee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Nov 2021 21:36:38 -1000 Subject: [PATCH 0928/1452] Fix flux_led discovery missing responses (#60464) --- homeassistant/components/flux_led/__init__.py | 28 +++++++++++-------- homeassistant/components/flux_led/const.py | 1 + 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index b82fbe97913..b37a3640db3 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -1,6 +1,7 @@ """The Flux LED/MagicLight integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import Any, Final, cast @@ -26,6 +27,7 @@ from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, FLUX_LED_DISCOVERY, + FLUX_LED_DISCOVERY_LOCK, FLUX_LED_EXCEPTIONS, SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, @@ -76,16 +78,20 @@ async def async_discover_devices( hass: HomeAssistant, timeout: int, address: str | None = None ) -> list[dict[str, str]]: """Discover flux led devices.""" - scanner = AIOBulbScanner() - try: - discovered: list[dict[str, str]] = await scanner.async_scan( - timeout=timeout, address=address - ) - except OSError as ex: - _LOGGER.debug("Scanning failed with error: %s", ex) - return [] - else: - return discovered + domain_data = hass.data.setdefault(DOMAIN, {}) + if FLUX_LED_DISCOVERY_LOCK not in domain_data: + domain_data[FLUX_LED_DISCOVERY_LOCK] = asyncio.Lock() + async with domain_data[FLUX_LED_DISCOVERY_LOCK]: + scanner = AIOBulbScanner() + try: + discovered: list[dict[str, str]] = await scanner.async_scan( + timeout=timeout, address=address + ) + except OSError as ex: + _LOGGER.debug("Scanning failed with error: %s", ex) + return [] + else: + return discovered async def async_discover_device( @@ -118,7 +124,7 @@ def async_trigger_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the flux_led component.""" - domain_data = hass.data[DOMAIN] = {} + domain_data = hass.data.setdefault(DOMAIN, {}) domain_data[FLUX_LED_DISCOVERY] = await async_discover_devices( hass, STARTUP_SCAN_TIMEOUT ) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 639bac7165e..88c50402a05 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -39,6 +39,7 @@ DEFAULT_SCAN_INTERVAL: Final = 5 DEFAULT_EFFECT_SPEED: Final = 50 FLUX_LED_DISCOVERY: Final = "flux_led_discovery" +FLUX_LED_DISCOVERY_LOCK: Final = "flux_led_discovery_lock" FLUX_LED_EXCEPTIONS: Final = ( asyncio.TimeoutError, From 6d08bee3df972c8a5eff186f89550593e6786884 Mon Sep 17 00:00:00 2001 From: MJJ Date: Sun, 28 Nov 2021 10:46:32 +0100 Subject: [PATCH 0929/1452] Update buienradar library to 1.0.5 (#60473) * Update buienradar to 1.0.5 should fix https://github.com/home-assistant/core/issues/60200 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/buienradar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index d7759aa9b8d..f88bfb83ddf 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -3,7 +3,7 @@ "name": "Buienradar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/buienradar", - "requirements": ["buienradar==1.0.4"], + "requirements": ["buienradar==1.0.5"], "codeowners": ["@mjj4791", "@ties", "@Robbie1221"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index dea12e89841..3be7b8ebcdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,7 +455,7 @@ bthomehub5-devicelist==0.1.1 btsmarthub_devicelist==0.2.0 # homeassistant.components.buienradar -buienradar==1.0.4 +buienradar==1.0.5 # homeassistant.components.caldav caldav==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdd362d314b..b3c6f93287e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -287,7 +287,7 @@ brunt==1.0.0 bsblan==0.4.0 # homeassistant.components.buienradar -buienradar==1.0.4 +buienradar==1.0.5 # homeassistant.components.caldav caldav==0.7.1 From 1aadda4b0ffa649a9484df10f7d810765dee9590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kiss?= <70820303+g-kiss@users.noreply.github.com> Date: Sun, 28 Nov 2021 11:07:18 +0100 Subject: [PATCH 0930/1452] Fix Shelly dual mode bulb mode switch (#60471) * fix_shelly_dual_mode_bulb_mode_switch * Update __init__.py * Update light.py --- homeassistant/components/shelly/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 60810c9df4e..3b96cd61f32 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -366,7 +366,11 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): self.wrapper.model, ) - if set_mode and self.wrapper.model in DUAL_MODE_LIGHT_MODELS: + if ( + set_mode + and set_mode != self.mode + and self.wrapper.model in DUAL_MODE_LIGHT_MODELS + ): params["mode"] = set_mode self.control_result = await self.set_state(**params) From c6ec84d0cfe71d73629f0b030bc833b450e9b086 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 28 Nov 2021 05:14:52 -0800 Subject: [PATCH 0931/1452] Remove store user as auth result (#60468) --- homeassistant/components/auth/__init__.py | 36 +++++++---------------- tests/components/auth/test_init.py | 36 ++++++++++++++++++----- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index bcdcf4de747..359f67ed0a5 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -124,11 +124,7 @@ from aiohttp import web import voluptuous as vol from homeassistant.auth import InvalidAuthError -from homeassistant.auth.models import ( - TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, - Credentials, - User, -) +from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials from homeassistant.components import websocket_api from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth @@ -179,15 +175,12 @@ SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( ) RESULT_TYPE_CREDENTIALS = "credentials" -RESULT_TYPE_USER = "user" @bind_hass -def create_auth_code( - hass, client_id: str, credential_or_user: Credentials | User -) -> str: +def create_auth_code(hass, client_id: str, credential: Credentials) -> str: """Create an authorization code to fetch tokens.""" - return hass.data[DOMAIN](client_id, credential_or_user) + return hass.data[DOMAIN](client_id, credential) async def async_setup(hass, config): @@ -296,7 +289,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.BAD_REQUEST, ) - credential = self._retrieve_auth(client_id, RESULT_TYPE_CREDENTIALS, code) + credential = self._retrieve_auth(client_id, code) if credential is None or not isinstance(credential, Credentials): return self.json( @@ -399,9 +392,7 @@ class LinkUserView(HomeAssistantView): hass = request.app["hass"] user = request["hass_user"] - credentials = self._retrieve_credentials( - data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"] - ) + credentials = self._retrieve_credentials(data["client_id"], data["code"]) if credentials is None: return self.json_message("Invalid code", status_code=HTTPStatus.BAD_REQUEST) @@ -426,30 +417,25 @@ def _create_auth_code_store(): @callback def store_result(client_id, result): """Store flow result and return a code to retrieve it.""" - if isinstance(result, User): - result_type = RESULT_TYPE_USER - elif isinstance(result, Credentials): - result_type = RESULT_TYPE_CREDENTIALS - else: - raise ValueError("result has to be either User or Credentials") + if not isinstance(result, Credentials): + raise ValueError("result has to be a Credentials instance") code = uuid.uuid4().hex - temp_results[(client_id, result_type, code)] = ( + temp_results[(client_id, code)] = ( dt_util.utcnow(), - result_type, result, ) return code @callback - def retrieve_result(client_id, result_type, code): + def retrieve_result(client_id, code): """Retrieve flow result.""" - key = (client_id, result_type, code) + key = (client_id, code) if key not in temp_results: return None - created, _, result = temp_results.pop(key) + created, result = temp_results.pop(key) # OAuth 4.2.1 # The authorization code MUST expire shortly after it is issued to diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 2c96f545b41..53cc291a5db 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -3,10 +3,11 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.auth import InvalidAuthError from homeassistant.auth.models import Credentials from homeassistant.components import auth -from homeassistant.components.auth import RESULT_TYPE_USER from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -15,6 +16,18 @@ from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser +@pytest.fixture +def mock_credential(): + """Return a mock credential.""" + return Credentials( + id="mock-credential-id", + auth_provider_type="insecure_example", + auth_provider_id=None, + data={"username": "test-user"}, + is_new=False, + ) + + async def async_setup_user_refresh_token(hass): """Create a testing user with a connected credential.""" user = await hass.auth.async_create_user("Test User") @@ -96,29 +109,38 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == HTTPStatus.OK -def test_auth_code_store_expiration(): +def test_auth_code_store_expiration(mock_credential): """Test that the auth code store will not return expired tokens.""" store, retrieve = auth._create_auth_code_store() client_id = "bla" - user = MockUser(id="mock_user") now = utcnow() with patch("homeassistant.util.dt.utcnow", return_value=now): - code = store(client_id, user) + code = store(client_id, mock_credential) with patch( "homeassistant.util.dt.utcnow", return_value=now + timedelta(minutes=10) ): - assert retrieve(client_id, RESULT_TYPE_USER, code) is None + assert retrieve(client_id, code) is None with patch("homeassistant.util.dt.utcnow", return_value=now): - code = store(client_id, user) + code = store(client_id, mock_credential) with patch( "homeassistant.util.dt.utcnow", return_value=now + timedelta(minutes=9, seconds=59), ): - assert retrieve(client_id, RESULT_TYPE_USER, code) == user + assert retrieve(client_id, code) == mock_credential + + +def test_auth_code_store_requires_credentials(mock_credential): + """Test we require credentials.""" + store, _retrieve = auth._create_auth_code_store() + + with pytest.raises(ValueError): + store(None, MockUser()) + + store(None, mock_credential) async def test_ws_current_user(hass, hass_ws_client, hass_access_token): From 76b047dd123c4ae379e88de9ff967542e71e611c Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Sun, 28 Nov 2021 15:21:44 +0200 Subject: [PATCH 0932/1452] Update pymelcloud to 2.5.5 (#60484) --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 5c8f7d7ca2c..f875984453d 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.4"], + "requirements": ["pymelcloud==2.5.5"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 3be7b8ebcdc..2535e69fb88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ pymazda==0.2.2 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.4 +pymelcloud==2.5.5 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3c6f93287e..6d242698d6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ pymata-express==1.19 pymazda==0.2.2 # homeassistant.components.melcloud -pymelcloud==2.5.4 +pymelcloud==2.5.5 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From caf5ee2fabdce28502c427ffb81bd2933e28d98a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 28 Nov 2021 14:54:07 +0100 Subject: [PATCH 0933/1452] Remove optional validation when creating conditions (#60481) --- .../alarm_control_panel/device_condition.py | 6 +- .../components/automation/__init__.py | 2 +- .../binary_sensor/device_condition.py | 7 +- .../components/climate/device_condition.py | 7 +- .../components/cover/device_condition.py | 7 +- .../device_automation/toggle_entity.py | 1 + .../device_tracker/device_condition.py | 7 +- .../components/fan/device_condition.py | 6 +- .../components/humidifier/device_condition.py | 7 +- .../components/light/device_condition.py | 6 +- .../components/lock/device_condition.py | 6 +- .../media_player/device_condition.py | 6 +- .../components/remote/device_condition.py | 6 +- .../components/select/device_condition.py | 6 +- .../components/sensor/device_condition.py | 7 +- .../components/switch/device_condition.py | 6 +- .../components/vacuum/device_condition.py | 6 +- .../components/websocket_api/commands.py | 6 +- .../components/zwave_js/device_condition.py | 7 +- homeassistant/helpers/condition.py | 77 +- homeassistant/helpers/script.py | 2 +- .../integration/device_condition.py | 6 +- .../zwave_js/test_device_condition.py | 2 +- tests/helpers/test_condition.py | 867 +++++++++--------- 24 files changed, 486 insertions(+), 580 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 9367ef8f811..6eb0144a4b4 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -104,12 +104,8 @@ async def async_get_conditions( return conditions -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == CONDITION_TRIGGERED: state = STATE_ALARM_TRIGGERED elif config[CONF_TYPE] == CONDITION_DISARMED: diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 699fdc8745b..64c6b335fbd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -714,7 +714,7 @@ async def _async_process_if(hass, name, config, p_config): checks = [] for if_config in if_configs: try: - checks.append(await condition.async_from_config(hass, if_config, False)) + checks.append(await condition.async_from_config(hass, if_config)) except HomeAssistantError as ex: LOGGER.warning("Invalid condition: %s", ex) return None diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 8351234182d..e6202ef66a4 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -264,12 +264,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - if config_validation: - config = CONDITION_SCHEMA(config) condition_type = config[CONF_TYPE] if condition_type in IS_ON: stat = "on" @@ -282,6 +278,7 @@ def async_condition_from_config( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] + state_config = cv.STATE_CONDITION_SCHEMA(state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 97bb4515f14..03e456965fe 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -70,13 +70,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_hvac_mode": attribute = const.ATTR_HVAC_MODE else: diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 28a057cb17f..d719c4835e2 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -119,13 +119,8 @@ async def async_get_condition_capabilities( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] in STATE_CONDITION_TYPES: if config[CONF_TYPE] == "is_open": state = STATE_OPEN diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 99473777658..a1ee84da2fb 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -141,6 +141,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] + state_config = cv.STATE_CONDITION_SCHEMA(state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index afa899444f6..78cedd6e900 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -55,13 +55,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) - reverse = config[CONF_TYPE] == "is_not_home" @callback diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 56d9208b2d2..4e3aed026dc 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -55,12 +55,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_on": state = STATE_ON else: diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index f2bf032b195..28d74d1efff 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -65,13 +65,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_mode": attribute = ATTR_MODE else: diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index e5ff8a83ba3..908af72ecfd 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -19,12 +19,8 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: """Evaluate state based on configuration.""" - if config_validation: - config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config) diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 74b55a1a89c..b3fe4ffcfe1 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -67,12 +67,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_jammed": state = STATE_JAMMED elif config[CONF_TYPE] == "is_locking": diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index e392c274f33..41034bbe870 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -59,12 +59,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_playing": state = STATE_PLAYING elif config[CONF_TYPE] == "is_idle": diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py index 02e6ea6bd23..ffbfc1c1061 100644 --- a/homeassistant/components/remote/device_condition.py +++ b/homeassistant/components/remote/device_condition.py @@ -19,12 +19,8 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: """Evaluate state based on configuration.""" - if config_validation: - config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config) diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index 4f650ddadda..eadfebbc711 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -52,12 +52,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) @callback def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 224ca26d28d..6444f58a1de 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -186,12 +186,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - if config_validation: - config = CONDITION_SCHEMA(config) numeric_state_config = { condition.CONF_CONDITION: "numeric_state", condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], @@ -201,6 +197,7 @@ def async_condition_from_config( if CONF_BELOW in config: numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] + numeric_state_config = cv.NUMERIC_STATE_CONDITION_SCHEMA(numeric_state_config) return condition.async_numeric_state_from_config(numeric_state_config) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index b59e533375c..94469275ab3 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -19,12 +19,8 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: """Evaluate state based on configuration.""" - if config_validation: - config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config) diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index a66df1323f7..1c7b0c93332 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -53,12 +53,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_docked": test_states = [STATE_DOCKED] else: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4216eae5a09..a4abbd30dff 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -495,7 +495,11 @@ async def handle_test_condition( # pylint: disable=import-outside-toplevel from homeassistant.helpers import condition - check_condition = await condition.async_from_config(hass, msg["condition"]) + # Do static + dynamic validation of the condition + config = cv.CONDITION_SCHEMA(msg["condition"]) + config = await condition.async_validate_condition_config(hass, config) + # Test the condition + check_condition = await condition.async_from_config(hass, config) connection.send_result( msg["id"], {"result": check_condition(hass, msg.get("variables"))} ) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 6694d88a135..4258a0b0892 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -147,13 +147,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) - condition_type = config[CONF_TYPE] device_id = config[CONF_DEVICE_ID] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 5608e61a07f..a4c62709778 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -152,7 +152,6 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke async def async_from_config( hass: HomeAssistant, config: ConfigType | Template, - config_validation: bool = True, ) -> ConditionCheckerType: """Turn a condition configuration into a method. @@ -181,21 +180,15 @@ async def async_from_config( check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): - return cast( - ConditionCheckerType, await factory(hass, config, config_validation) - ) - return cast(ConditionCheckerType, factory(config, config_validation)) + return cast(ConditionCheckerType, await factory(hass, config)) + return cast(ConditionCheckerType, factory(config)) async def async_and_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Create multi condition matcher using 'AND'.""" - if config_validation: - config = cv.AND_CONDITION_SCHEMA(config) - checks = [ - await async_from_config(hass, entry, False) for entry in config["conditions"] - ] + checks = [await async_from_config(hass, entry) for entry in config["conditions"]] @trace_condition_function def if_and_condition( @@ -223,14 +216,10 @@ async def async_and_from_config( async def async_or_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Create multi condition matcher using 'OR'.""" - if config_validation: - config = cv.OR_CONDITION_SCHEMA(config) - checks = [ - await async_from_config(hass, entry, False) for entry in config["conditions"] - ] + checks = [await async_from_config(hass, entry) for entry in config["conditions"]] @trace_condition_function def if_or_condition( @@ -258,14 +247,10 @@ async def async_or_from_config( async def async_not_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Create multi condition matcher using 'NOT'.""" - if config_validation: - config = cv.NOT_CONDITION_SCHEMA(config) - checks = [ - await async_from_config(hass, entry, False) for entry in config["conditions"] - ] + checks = [await async_from_config(hass, entry) for entry in config["conditions"]] @trace_condition_function def if_not_condition( @@ -433,12 +418,8 @@ def async_numeric_state( # noqa: C901 return True -def async_numeric_state_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def async_numeric_state_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with state based condition.""" - if config_validation: - config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) entity_ids = config.get(CONF_ENTITY_ID, []) attribute = config.get(CONF_ATTRIBUTE) below = config.get(CONF_BELOW) @@ -548,12 +529,8 @@ def state( return duration_ok -def state_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def state_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with state based condition.""" - if config_validation: - config = cv.STATE_CONDITION_SCHEMA(config) entity_ids = config.get(CONF_ENTITY_ID, []) req_states: str | list[str] = config.get(CONF_STATE, []) for_period = config.get("for") @@ -656,12 +633,8 @@ def sun( return True -def sun_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def sun_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with sun based condition.""" - if config_validation: - config = cv.SUN_CONDITION_SCHEMA(config) before = config.get("before") after = config.get("after") before_offset = config.get("before_offset") @@ -703,12 +676,8 @@ def async_template( return result -def async_template_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def async_template_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with state based condition.""" - if config_validation: - config = cv.TEMPLATE_CONDITION_SCHEMA(config) value_template = cast(Template, config.get(CONF_VALUE_TEMPLATE)) @trace_condition_function @@ -808,12 +777,8 @@ def time( return True -def time_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def time_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with time based condition.""" - if config_validation: - config = cv.TIME_CONDITION_SCHEMA(config) before = config.get(CONF_BEFORE) after = config.get(CONF_AFTER) weekday = config.get(CONF_WEEKDAY) @@ -873,12 +838,8 @@ def zone( ) -def zone_from_config( - config: ConfigType, config_validation: bool = True -) -> ConditionCheckerType: +def zone_from_config(config: ConfigType) -> ConditionCheckerType: """Wrap action method with zone based condition.""" - if config_validation: - config = cv.ZONE_CONDITION_SCHEMA(config) entity_ids = config.get(CONF_ENTITY_ID, []) zone_entity_ids = config.get(CONF_ZONE, []) @@ -915,28 +876,24 @@ def zone_from_config( async def async_device_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Test a device condition.""" - if config_validation: - config = cv.DEVICE_CONDITION_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "condition" ) return trace_condition_function( cast( ConditionCheckerType, - platform.async_condition_from_config(config, config_validation), # type: ignore + platform.async_condition_from_config(config), # type: ignore ) ) async def async_trigger_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Test a trigger condition.""" - if config_validation: - config = cv.TRIGGER_CONDITION_SCHEMA(config) trigger_id = config[CONF_ID] @trace_condition_function diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 293a8a9455f..933b44d9ec9 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1275,7 +1275,7 @@ class Script: else: config_cache_key = frozenset((k, str(v)) for k, v in config.items()) if not (cond := self._config_cache.get(config_cache_key)): - cond = await condition.async_from_config(self._hass, config, False) + cond = await condition.async_from_config(self._hass, config) self._config_cache[config_cache_key] = cond return cond diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 4180f81b3ff..ca0eda163b8 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -58,12 +58,8 @@ async def async_get_conditions( @callback -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config_validation: - config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_on": state = STATE_ON else: diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index dfdbb16c8e8..1f6a9d9ca90 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -552,7 +552,7 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): with pytest.raises(HomeAssistantError): await device_condition.async_condition_from_config( - {"type": "failed.test", "device_id": device.id}, False + {"type": "failed.test", "device_id": device.id} ) with patch( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index cd355129770..92bad8777bd 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -16,7 +16,7 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.exceptions import ConditionError, HomeAssistantError -from homeassistant.helpers import condition, trace +from homeassistant.helpers import condition, config_validation as cv, trace from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -106,25 +106,25 @@ async def test_invalid_condition(hass): async def test_and_condition(hass): """Test the 'and' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "And Condition", - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - }, - ) + config = { + "alias": "And Condition", + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError): test(hass) @@ -179,25 +179,25 @@ async def test_and_condition(hass): async def test_and_condition_raises(hass): """Test the 'and' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "And Condition", - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature2", - "above": 110, - }, - ], - }, - ) + config = { + "alias": "And Condition", + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature2", + "above": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) # All subconditions raise, the AND-condition should raise with pytest.raises(ConditionError): @@ -252,24 +252,24 @@ async def test_and_condition_raises(hass): async def test_and_condition_with_template(hass): """Test the 'and' condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "alias": "Template Condition", - "condition": "template", - "value_template": '{{ states.sensor.temperature.state == "100" }}', - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "alias": "Template Condition", + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -291,25 +291,25 @@ async def test_and_condition_with_template(hass): async def test_or_condition(hass): """Test the 'or' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "Or Condition", - "condition": "or", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - }, - ) + config = { + "alias": "Or Condition", + "condition": "or", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError): test(hass) @@ -374,25 +374,25 @@ async def test_or_condition(hass): async def test_or_condition_raises(hass): """Test the 'or' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "Or Condition", - "condition": "or", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature2", - "above": 110, - }, - ], - }, - ) + config = { + "alias": "Or Condition", + "condition": "or", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature2", + "above": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) # All subconditions raise, the OR-condition should raise with pytest.raises(ConditionError): @@ -447,20 +447,20 @@ async def test_or_condition_raises(hass): async def test_or_condition_with_template(hass): """Test the 'or' condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "or", - "conditions": [ - {'{{ states.sensor.temperature.state == "100" }}'}, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - }, - ) + config = { + "condition": "or", + "conditions": [ + {'{{ states.sensor.temperature.state == "100" }}'}, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -474,25 +474,25 @@ async def test_or_condition_with_template(hass): async def test_not_condition(hass): """Test the 'not' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "Not Condition", - "condition": "not", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 50, - }, - ], - }, - ) + config = { + "alias": "Not Condition", + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError): test(hass) @@ -573,25 +573,25 @@ async def test_not_condition(hass): async def test_not_condition_raises(hass): """Test the 'and' condition.""" - test = await condition.async_from_config( - hass, - { - "alias": "Not Condition", - "condition": "not", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature2", - "below": 50, - }, - ], - }, - ) + config = { + "alias": "Not Condition", + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature2", + "below": 50, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) # All subconditions raise, the NOT-condition should raise with pytest.raises(ConditionError): @@ -640,23 +640,23 @@ async def test_not_condition_raises(hass): async def test_not_condition_with_template(hass): """Test the 'or' condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "not", - "conditions": [ - { - "condition": "template", - "value_template": '{{ states.sensor.temperature.state == "100" }}', - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 50, - }, - ], - }, - ) + config = { + "condition": "not", + "conditions": [ + { + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 101) assert test(hass) @@ -676,14 +676,24 @@ async def test_time_window(hass): sixam = "06:00:00" sixpm = "18:00:00" - test1 = await condition.async_from_config( - hass, - {"alias": "Time Cond", "condition": "time", "after": sixam, "before": sixpm}, - ) - test2 = await condition.async_from_config( - hass, - {"alias": "Time Cond", "condition": "time", "after": sixpm, "before": sixam}, - ) + config1 = { + "alias": "Time Cond", + "condition": "time", + "after": sixam, + "before": sixpm, + } + config1 = cv.CONDITION_SCHEMA(config1) + config1 = await condition.async_validate_condition_config(hass, config1) + config2 = { + "alias": "Time Cond", + "condition": "time", + "after": sixpm, + "before": sixam, + } + config2 = cv.CONDITION_SCHEMA(config2) + config2 = await condition.async_validate_condition_config(hass, config2) + test1 = await condition.async_from_config(hass, config1) + test2 = await condition.async_from_config(hass, config2) with patch( "homeassistant.helpers.condition.dt_util.now", @@ -925,14 +935,14 @@ async def test_state_raises(hass): condition.state(hass, entity=None, req_state="missing") # Unknown entities - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": ["sensor.door_unknown", "sensor.window_unknown"], - "state": "open", - }, - ) + config = { + "condition": "state", + "entity_id": ["sensor.door_unknown", "sensor.window_unknown"], + "state": "open", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError, match="unknown entity.*door"): test(hass) with pytest.raises(ConditionError, match="unknown entity.*window"): @@ -940,14 +950,14 @@ async def test_state_raises(hass): # Unknown state entity with pytest.raises(ConditionError, match="input_text.missing"): - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": "sensor.door", - "state": "input_text.missing", - }, - ) + config = { + "condition": "state", + "entity_id": "sensor.door", + "state": "input_text.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.door", "open") test(hass) @@ -956,15 +966,15 @@ async def test_state_raises(hass): async def test_state_unknown_attribute(hass): """Test that state returns False on unknown attribute.""" # Unknown attribute - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": "sensor.door", - "attribute": "model", - "state": "acme", - }, - ) + config = { + "condition": "state", + "entity_id": "sensor.door", + "attribute": "model", + "state": "acme", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.door", "open") assert not test(hass) @@ -985,19 +995,19 @@ async def test_state_unknown_attribute(hass): async def test_state_multiple_entities(hass): """Test with multiple entities in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], - "state": "100", - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], + "state": "100", + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature_1", 100) hass.states.async_set("sensor.temperature_2", 100) @@ -1014,20 +1024,20 @@ async def test_state_multiple_entities(hass): async def test_multiple_states(hass): """Test with multiple states in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "alias": "State Condition", - "condition": "state", - "entity_id": "sensor.temperature", - "state": ["100", "200"], - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "alias": "State Condition", + "condition": "state", + "entity_id": "sensor.temperature", + "state": ["100", "200"], + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 100) assert test(hass) @@ -1041,20 +1051,20 @@ async def test_multiple_states(hass): async def test_state_attribute(hass): """Test with state attribute in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "attribute": "attribute1", - "state": 200, - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "attribute": "attribute1", + "state": 200, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 200}) assert not test(hass) @@ -1074,15 +1084,15 @@ async def test_state_attribute(hass): async def test_state_attribute_boolean(hass): """Test with boolean state attribute in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": "sensor.temperature", - "attribute": "happening", - "state": False, - }, - ) + config = { + "condition": "state", + "entity_id": "sensor.temperature", + "attribute": "happening", + "state": False, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 100, {"happening": 200}) assert not test(hass) @@ -1119,23 +1129,23 @@ async def test_state_using_input_entities(hass): }, ) - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.salut", - "state": [ - "input_text.hello", - "input_select.hello", - "salut", - ], - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.salut", + "state": [ + "input_text.hello", + "input_select.hello", + "salut", + ], + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.salut", "goodbye") assert test(hass) @@ -1181,14 +1191,14 @@ async def test_state_using_input_entities(hass): async def test_numeric_state_known_non_matching(hass): """Test that numeric_state doesn't match on known non-matching states.""" hass.states.async_set("sensor.temperature", "unavailable") - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "above": 0, - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) # Unavailable state assert not test(hass) @@ -1229,14 +1239,14 @@ async def test_numeric_state_known_non_matching(hass): async def test_numeric_state_raises(hass): """Test that numeric_state raises ConditionError on errors.""" # Unknown entities - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": ["sensor.temperature_unknown", "sensor.humidity_unknown"], - "above": 0, - }, - ) + config = { + "condition": "numeric_state", + "entity_id": ["sensor.temperature_unknown", "sensor.humidity_unknown"], + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError, match="unknown entity.*temperature"): test(hass) with pytest.raises(ConditionError, match="unknown entity.*humidity"): @@ -1244,43 +1254,43 @@ async def test_numeric_state_raises(hass): # Template error with pytest.raises(ConditionError, match="ZeroDivisionError"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "value_template": "{{ 1 / 0 }}", - "above": 0, - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "value_template": "{{ 1 / 0 }}", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 50) test(hass) # Bad number with pytest.raises(ConditionError, match="cannot be processed as a number"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "above": 0, - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", "fifty") test(hass) # Below entity missing with pytest.raises(ConditionError, match="'below' entity"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": "input_number.missing", - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": "input_number.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 50) test(hass) @@ -1295,14 +1305,14 @@ async def test_numeric_state_raises(hass): # Above entity missing with pytest.raises(ConditionError, match="'above' entity"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "above": "input_number.missing", - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": "input_number.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 50) test(hass) @@ -1319,15 +1329,15 @@ async def test_numeric_state_raises(hass): async def test_numeric_state_unknown_attribute(hass): """Test that numeric_state returns False on unknown attribute.""" # Unknown attribute - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "attribute": "temperature", - "above": 0, - }, - ) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "attribute": "temperature", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 50) assert not test(hass) @@ -1348,20 +1358,20 @@ async def test_numeric_state_unknown_attribute(hass): async def test_numeric_state_multiple_entities(hass): """Test with multiple entities in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "alias": "Numeric State Condition", - "condition": "numeric_state", - "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], - "below": 50, - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "alias": "Numeric State Condition", + "condition": "numeric_state", + "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], + "below": 50, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature_1", 49) hass.states.async_set("sensor.temperature_2", 49) @@ -1378,20 +1388,20 @@ async def test_numeric_state_multiple_entities(hass): async def test_numeric_state_attribute(hass): """Test with numeric state attribute in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "attribute": "attribute1", - "below": 50, - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "attribute": "attribute1", + "below": 50, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 10}) assert not test(hass) @@ -1422,20 +1432,20 @@ async def test_numeric_state_using_input_number(hass): }, ) - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": "input_number.high", - "above": "number.low", - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": "input_number.high", + "above": "number.low", + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set("sensor.temperature", 42) assert test(hass) @@ -1481,14 +1491,14 @@ async def test_numeric_state_using_input_number(hass): async def test_zone_raises(hass): """Test that zone raises ConditionError on errors.""" - test = await condition.async_from_config( - hass, - { - "condition": "zone", - "entity_id": "device_tracker.cat", - "zone": "zone.home", - }, - ) + config = { + "condition": "zone", + "entity_id": "device_tracker.cat", + "zone": "zone.home", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError, match="no zone"): condition.zone(hass, zone_ent=None, entity="sensor.any") @@ -1535,14 +1545,14 @@ async def test_zone_raises(hass): # All okay, now test multiple failed conditions assert test(hass) - test = await condition.async_from_config( - hass, - { - "condition": "zone", - "entity_id": ["device_tracker.cat", "device_tracker.dog"], - "zone": ["zone.home", "zone.work"], - }, - ) + config = { + "condition": "zone", + "entity_id": ["device_tracker.cat", "device_tracker.dog"], + "zone": ["zone.home", "zone.work"], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError, match="dog"): test(hass) @@ -1567,20 +1577,20 @@ async def test_zone_raises(hass): async def test_zone_multiple_entities(hass): """Test with multiple entities in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "alias": "Zone Condition", - "condition": "zone", - "entity_id": ["device_tracker.person_1", "device_tracker.person_2"], - "zone": "zone.home", - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "alias": "Zone Condition", + "condition": "zone", + "entity_id": ["device_tracker.person_1", "device_tracker.person_2"], + "zone": "zone.home", + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set( "zone.home", @@ -1627,19 +1637,19 @@ async def test_zone_multiple_entities(hass): async def test_multiple_zones(hass): """Test with multiple entities in condition.""" - test = await condition.async_from_config( - hass, - { - "condition": "and", - "conditions": [ - { - "condition": "zone", - "entity_id": "device_tracker.person", - "zone": ["zone.home", "zone.work"], - }, - ], - }, - ) + config = { + "condition": "and", + "conditions": [ + { + "condition": "zone", + "entity_id": "device_tracker.person", + "zone": ["zone.home", "zone.work"], + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) hass.states.async_set( "zone.home", @@ -1801,9 +1811,10 @@ async def test_extract_devices(): async def test_condition_template_error(hass): """Test invalid template.""" - test = await condition.async_from_config( - hass, {"condition": "template", "value_template": "{{ undefined.state }}"} - ) + config = {"condition": "template", "value_template": "{{ undefined.state }}"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) with pytest.raises(ConditionError, match="template"): test(hass) @@ -1811,24 +1822,28 @@ async def test_condition_template_error(hass): async def test_condition_template_invalid_results(hass): """Test template condition render false with invalid results.""" - test = await condition.async_from_config( - hass, {"condition": "template", "value_template": "{{ 'string' }}"} - ) + config = {"condition": "template", "value_template": "{{ 'string' }}"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) assert not test(hass) - test = await condition.async_from_config( - hass, {"condition": "template", "value_template": "{{ 10.1 }}"} - ) + config = {"condition": "template", "value_template": "{{ 10.1 }}"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) assert not test(hass) - test = await condition.async_from_config( - hass, {"condition": "template", "value_template": "{{ 42 }}"} - ) + config = {"condition": "template", "value_template": "{{ 42 }}"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) assert not test(hass) - test = await condition.async_from_config( - hass, {"condition": "template", "value_template": "{{ [1, 2, 3] }}"} - ) + config = {"condition": "template", "value_template": "{{ [1, 2, 3] }}"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) assert not test(hass) @@ -2894,10 +2909,10 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, hass_ws_client, c async def test_trigger(hass): """Test trigger condition.""" - test = await condition.async_from_config( - hass, - {"alias": "Trigger Cond", "condition": "trigger", "id": "123456"}, - ) + config = {"alias": "Trigger Cond", "condition": "trigger", "id": "123456"} + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) assert not test(hass) assert not test(hass, {}) From e1036f3c71a77904bff09e83fcd0275292aaa1a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 28 Nov 2021 16:34:15 +0100 Subject: [PATCH 0934/1452] Upgrade restrictedpython to 5.2 (#60493) --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index f3c25ad493b..8db94bb9817 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.2a1.dev0"], + "requirements": ["restrictedpython==5.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 2535e69fb88..76f28895763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2052,7 +2052,7 @@ regenmaschine==2021.10.0 renault-api==0.1.4 # homeassistant.components.python_script -restrictedpython==5.2a1.dev0 +restrictedpython==5.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d242698d6f..86012569af0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1220,7 +1220,7 @@ regenmaschine==2021.10.0 renault-api==0.1.4 # homeassistant.components.python_script -restrictedpython==5.2a1.dev0 +restrictedpython==5.2 # homeassistant.components.rflink rflink==0.0.58 From bae01ca7d8f81ee38786ac2597009e8b6200cf4c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 28 Nov 2021 10:44:43 -0500 Subject: [PATCH 0935/1452] Fix climacell hourly and nowcast forecasts (#60454) --- homeassistant/components/climacell/weather.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 6a1cab5e1bf..bf1c95b524a 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -377,12 +377,13 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY) temp = values.get(CC_ATTR_TEMPERATURE_HIGH) - temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) + temp_low = None wind_direction = values.get(CC_ATTR_WIND_DIRECTION) wind_speed = values.get(CC_ATTR_WIND_SPEED) if self.forecast_type == DAILY: use_datetime = False + temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) if precipitation: precipitation = precipitation * 24 elif self.forecast_type == NOWCAST: From 2d1d9e9e203f1f049cdee27dbcb1279230275b39 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Sun, 28 Nov 2021 16:50:57 +0100 Subject: [PATCH 0936/1452] Address late review of tolo integration (#60453) * improvements requested by @MartinHjelmare * addressed requested changes * more improvements --- homeassistant/components/tolo/__init__.py | 4 ++-- homeassistant/components/tolo/config_flow.py | 2 +- homeassistant/components/tolo/strings.json | 1 - homeassistant/components/tolo/translations/en.json | 3 +-- tests/components/tolo/test_config_flow.py | 4 +--- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 3776c4bd61c..666e86c951b 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -64,7 +64,7 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): hass=hass, logger=_LOGGER, name=f"{entry.title} ({entry.data[CONF_HOST]}) Data Update Coordinator", - update_interval=timedelta(seconds=3), + update_interval=timedelta(seconds=5), ) async def _async_update_data(self) -> ToloSaunaData: @@ -78,9 +78,9 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): settings = self.client.get_settings_info( resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT ) - return ToloSaunaData(status, settings) except ResponseTimedOutError as error: raise UpdateFailed("communication timeout") from error + return ToloSaunaData(status, settings) class ToloSaunaCoordinatorEntity(CoordinatorEntity): diff --git a/homeassistant/components/tolo/config_flow.py b/homeassistant/components/tolo/config_flow.py index 4503fd511ba..24708580d20 100644 --- a/homeassistant/components/tolo/config_flow.py +++ b/homeassistant/components/tolo/config_flow.py @@ -35,9 +35,9 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): result = client.get_status_info( resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT ) - return result is not None except ResponseTimedOutError: return False + return result is not None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json index f9316f4d72b..0a81dad73f6 100644 --- a/homeassistant/components/tolo/strings.json +++ b/homeassistant/components/tolo/strings.json @@ -16,7 +16,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } diff --git a/homeassistant/components/tolo/translations/en.json b/homeassistant/components/tolo/translations/en.json index 488c2f7ae69..dea5a3b30df 100644 --- a/homeassistant/components/tolo/translations/en.json +++ b/homeassistant/components/tolo/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "no_devices_found": "No devices found on the network" + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 6634d444bce..df9134b4dbd 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -27,9 +27,7 @@ def toloclient_fixture() -> Mock: async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock): """Test a user initiated config flow with provided host which times out.""" - toloclient().get_status_info.side_effect = lambda *args, **kwargs: ( - _ for _ in () - ).throw(ResponseTimedOutError()) + toloclient().get_status_info.side_effect = ResponseTimedOutError() result = await hass.config_entries.flow.async_init( DOMAIN, From efebb76a7e666b84a963cda8df32e6ec1990de43 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Sun, 28 Nov 2021 18:26:57 +0200 Subject: [PATCH 0937/1452] Bump wirelesstagpy to 0.8.1 (#60472) * Bumped version of wirelessttagpy to 0.8.1 * Removed dependency on wirelesstagpy for tests as no tests yet present --- homeassistant/components/wirelesstag/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index 2772b117e07..6074b64d664 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -2,7 +2,7 @@ "domain": "wirelesstag", "name": "Wireless Sensor Tags", "documentation": "https://www.home-assistant.io/integrations/wirelesstag", - "requirements": ["wirelesstagpy==0.8.0"], + "requirements": ["wirelesstagpy==0.8.1"], "codeowners": ["@sergeymaysak"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 76f28895763..05efb307062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2424,7 +2424,7 @@ whirlpool-sixth-sense==0.15.1 wiffi==1.0.1 # homeassistant.components.wirelesstag -wirelesstagpy==0.8.0 +wirelesstagpy==0.8.1 # homeassistant.components.withings withings-api==2.3.2 From 4d345e0665f9a03edfb3c17177cbee90cae508e0 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Sun, 28 Nov 2021 17:41:01 +0000 Subject: [PATCH 0938/1452] Add Tesla Wall Connector integration (#60000) --- CODEOWNERS | 1 + .../tesla_wall_connector/__init__.py | 173 +++++++++++++++ .../tesla_wall_connector/binary_sensor.py | 77 +++++++ .../tesla_wall_connector/config_flow.py | 160 ++++++++++++++ .../components/tesla_wall_connector/const.py | 11 + .../tesla_wall_connector/manifest.json | 25 +++ .../tesla_wall_connector/strings.json | 30 +++ .../tesla_wall_connector/translations/en.json | 30 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 15 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../tesla_wall_connector/__init__.py | 1 + .../tesla_wall_connector/conftest.py | 72 ++++++ .../tesla_wall_connector/test_config_flow.py | 207 ++++++++++++++++++ .../tesla_wall_connector/test_init.py | 35 +++ 16 files changed, 844 insertions(+) create mode 100644 homeassistant/components/tesla_wall_connector/__init__.py create mode 100644 homeassistant/components/tesla_wall_connector/binary_sensor.py create mode 100644 homeassistant/components/tesla_wall_connector/config_flow.py create mode 100644 homeassistant/components/tesla_wall_connector/const.py create mode 100644 homeassistant/components/tesla_wall_connector/manifest.json create mode 100644 homeassistant/components/tesla_wall_connector/strings.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/en.json create mode 100644 tests/components/tesla_wall_connector/__init__.py create mode 100644 tests/components/tesla_wall_connector/conftest.py create mode 100644 tests/components/tesla_wall_connector/test_config_flow.py create mode 100644 tests/components/tesla_wall_connector/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index c3e623ca482..bf9b88d5eec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -534,6 +534,7 @@ homeassistant/components/tasmota/* @emontnemery homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core +homeassistant/components/tesla_wall_connector/* @einarhauks homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/threshold/* @fabaff diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py new file mode 100644 index 00000000000..0796f4660d4 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -0,0 +1,173 @@ +"""The Tesla Wall Connector integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any + +from tesla_wall_connector import WallConnector +from tesla_wall_connector.exceptions import ( + WallConnectorConnectionError, + WallConnectorConnectionTimeoutError, + WallConnectorError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, + WALLCONNECTOR_DATA_LIFETIME, + WALLCONNECTOR_DATA_VITALS, + WALLCONNECTOR_DEVICE_NAME, +) + +PLATFORMS: list[str] = ["binary_sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Tesla Wall Connector from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hostname = entry.data[CONF_HOST] + + wall_connector = WallConnector(host=hostname, session=async_get_clientsession(hass)) + + try: + version_data = await wall_connector.async_get_version() + except WallConnectorError as ex: + raise ConfigEntryNotReady from ex + + async def async_update_data(): + """Fetch new data from the Wall Connector.""" + try: + vitals = await wall_connector.async_get_vitals() + lifetime = await wall_connector.async_get_lifetime() + except WallConnectorConnectionTimeoutError as ex: + raise UpdateFailed( + f"Could not fetch data from Tesla WallConnector at {hostname}: Timeout" + ) from ex + except WallConnectorConnectionError as ex: + raise UpdateFailed( + f"Could not fetch data from Tesla WallConnector at {hostname}: Cannot connect" + ) from ex + except WallConnectorError as ex: + raise UpdateFailed( + f"Could not fetch data from Tesla WallConnector at {hostname}: {ex}" + ) from ex + + return { + WALLCONNECTOR_DATA_VITALS: vitals, + WALLCONNECTOR_DATA_LIFETIME: lifetime, + } + + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="tesla-wallconnector", + update_interval=get_poll_interval(entry), + update_method=async_update_data, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = WallConnectorData( + wall_connector_client=wall_connector, + hostname=hostname, + part_number=version_data.part_number, + firmware_version=version_data.firmware_version, + serial_number=version_data.serial_number, + update_coordinator=coordinator, + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +def get_poll_interval(entry: ConfigEntry) -> timedelta: + """Get the poll interval from config.""" + return timedelta( + seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) + + +async def update_listener(hass, entry): + """Handle options update.""" + wall_connector_data: WallConnectorData = hass.data[DOMAIN][entry.entry_id] + wall_connector_data.update_coordinator.update_interval = get_poll_interval(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +def prefix_entity_name(name: str) -> str: + """Prefixes entity name.""" + return f"{WALLCONNECTOR_DEVICE_NAME} {name}" + + +def get_unique_id(serial_number: str, key: str) -> str: + """Get a unique entity name.""" + return f"{serial_number}-{key}" + + +class WallConnectorEntity(CoordinatorEntity): + """Base class for Wall Connector entities.""" + + def __init__(self, wall_connector_data: WallConnectorData) -> None: + """Initialize WallConnector Entity.""" + self.wall_connector_data = wall_connector_data + self._attr_unique_id = get_unique_id( + wall_connector_data.serial_number, self.entity_description.key + ) + super().__init__(wall_connector_data.update_coordinator) + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return DeviceInfo( + identifiers={(DOMAIN, self.wall_connector_data.serial_number)}, + default_name=WALLCONNECTOR_DEVICE_NAME, + model=self.wall_connector_data.part_number, + sw_version=self.wall_connector_data.firmware_version, + default_manufacturer="Tesla", + ) + + +@dataclass() +class WallConnectorLambdaValueGetterMixin: + """Mixin with a function pointer for getting sensor value.""" + + value_fn: Callable[[dict], Any] + + +@dataclass +class WallConnectorData: + """Data for the Tesla Wall Connector integration.""" + + wall_connector_client: WallConnector + update_coordinator: DataUpdateCoordinator + hostname: str + part_number: str + firmware_version: str + serial_number: str diff --git a/homeassistant/components/tesla_wall_connector/binary_sensor.py b/homeassistant/components/tesla_wall_connector/binary_sensor.py new file mode 100644 index 00000000000..8aef4f86478 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/binary_sensor.py @@ -0,0 +1,77 @@ +"""Binary Sensors for Tesla Wall Connector.""" +from dataclasses import dataclass +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC + +from . import ( + WallConnectorData, + WallConnectorEntity, + WallConnectorLambdaValueGetterMixin, + prefix_entity_name, +) +from .const import DOMAIN, WALLCONNECTOR_DATA_VITALS + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class WallConnectorBinarySensorDescription( + BinarySensorEntityDescription, WallConnectorLambdaValueGetterMixin +): + """Binary Sensor entity description.""" + + +WALL_CONNECTOR_SENSORS = [ + WallConnectorBinarySensorDescription( + key="vehicle_connected", + name=prefix_entity_name("Vehicle connected"), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].vehicle_connected, + device_class=DEVICE_CLASS_PLUG, + ), + WallConnectorBinarySensorDescription( + key="contactor_closed", + name=prefix_entity_name("Contactor closed"), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].contactor_closed, + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ), +] + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Create the Wall Connector sensor devices.""" + wall_connector_data = hass.data[DOMAIN][config_entry.entry_id] + + all_entities = [ + WallConnectorBinarySensorEntity(wall_connector_data, description) + for description in WALL_CONNECTOR_SENSORS + ] + + async_add_devices(all_entities) + + +class WallConnectorBinarySensorEntity(WallConnectorEntity, BinarySensorEntity): + """Wall Connector Sensor Entity.""" + + def __init__( + self, + wall_connectord_data: WallConnectorData, + description: WallConnectorBinarySensorDescription, + ) -> None: + """Initialize WallConnectorBinarySensorEntity.""" + self.entity_description = description + super().__init__(wall_connectord_data) + + @property + def is_on(self): + """Return the state of the sensor.""" + + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py new file mode 100644 index 00000000000..8b4dc423fbb --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -0,0 +1,160 @@ +"""Config flow for Tesla Wall Connector integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from tesla_wall_connector import WallConnector +from tesla_wall_connector.exceptions import WallConnectorError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, + WALLCONNECTOR_DEVICE_NAME, + WALLCONNECTOR_SERIAL_NUMBER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + wall_connector = WallConnector( + host=data[CONF_HOST], session=async_get_clientsession(hass) + ) + + version = await wall_connector.async_get_version() + + return { + "title": WALLCONNECTOR_DEVICE_NAME, + WALLCONNECTOR_SERIAL_NUMBER: version.serial_number, + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tesla Wall Connector.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + super().__init__() + self.ip_address = None + self.serial_number = None + + async def async_step_dhcp(self, discovery_info) -> FlowResult: + """Handle dhcp discovery.""" + self.ip_address = discovery_info[IP_ADDRESS] + _LOGGER.debug("Discovered Tesla Wall Connector at [%s]", self.ip_address) + + self._async_abort_entries_match({CONF_HOST: self.ip_address}) + + try: + wall_connector = WallConnector( + host=self.ip_address, session=async_get_clientsession(self.hass) + ) + version = await wall_connector.async_get_version() + except WallConnectorError as ex: + _LOGGER.debug( + "Could not read serial number from Tesla WallConnector at [%s]: [%s]", + self.ip_address, + ex, + ) + return self.async_abort(reason="cannot_connect") + + self.serial_number = version.serial_number + + await self.async_set_unique_id(self.serial_number) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) + + _LOGGER.debug( + "No entry found for wall connector with IP %s. Serial nr: %s", + self.ip_address, + self.serial_number, + ) + + placeholders = { + CONF_HOST: self.ip_address, + WALLCONNECTOR_SERIAL_NUMBER: self.serial_number, + } + + self.context["title_placeholders"] = placeholders + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + data_schema = vol.Schema( + {vol.Required(CONF_HOST, default=self.ip_address): str} + ) + if user_input is None: + return self.async_show_form(step_id="user", data_schema=data_schema) + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except WallConnectorError: + errors["base"] = "cannot_connect" + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + errors["base"] = "unknown" + + if not errors: + existing_entry = await self.async_set_unique_id( + info[WALLCONNECTOR_SERIAL_NUMBER] + ) + 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="already_configured") + + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Tesla Wall Connector.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): vol.All(vol.Coerce(int), vol.Clamp(min=1)) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/tesla_wall_connector/const.py b/homeassistant/components/tesla_wall_connector/const.py new file mode 100644 index 00000000000..2a660ee1aae --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/const.py @@ -0,0 +1,11 @@ +"""Constants for the Tesla Wall Connector integration.""" + +DOMAIN = "tesla_wall_connector" +DEFAULT_SCAN_INTERVAL = 30 + +WALLCONNECTOR_SERIAL_NUMBER = "serial_number" + +WALLCONNECTOR_DATA_VITALS = "vitals" +WALLCONNECTOR_DATA_LIFETIME = "lifetime" + +WALLCONNECTOR_DEVICE_NAME = "Tesla Wall Connector" diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json new file mode 100644 index 00000000000..bf03f51b13f --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -0,0 +1,25 @@ +{ + "domain": "tesla_wall_connector", + "name": "Tesla Wall Connector", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", + "requirements": ["tesla-wall-connector==0.2.0"], + "dhcp": [ + { + "hostname": "teslawallconnector_*", + "macaddress": "DC44271*" + }, + { + "hostname": "teslawallconnector_*", + "macaddress": "98ED5C*" + }, + { + "hostname": "teslawallconnector_*", + "macaddress": "4CFCAA*" + } + ], + "codeowners": [ + "@einarhauks" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/strings.json b/homeassistant/components/tesla_wall_connector/strings.json new file mode 100644 index 00000000000..78d3556fbd1 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "title": "Configure Tesla Wall Connector", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure options for Tesla Wall Connector", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/en.json b/homeassistant/components/tesla_wall_connector/translations/en.json new file mode 100644 index 00000000000..79a3005299f --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Configure Tesla Wall Connector" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure options for Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0c0ea7964e0..b8ba8c4aade 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -297,6 +297,7 @@ FLOWS = [ "tado", "tasmota", "tellduslive", + "tesla_wall_connector", "tibber", "tile", "tolo", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 59f346e0be0..17189705056 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -361,6 +361,21 @@ DHCP = [ "domain": "tado", "hostname": "tado*" }, + { + "domain": "tesla_wall_connector", + "hostname": "teslawallconnector_*", + "macaddress": "DC44271*" + }, + { + "domain": "tesla_wall_connector", + "hostname": "teslawallconnector_*", + "macaddress": "98ED5C*" + }, + { + "domain": "tesla_wall_connector", + "hostname": "teslawallconnector_*", + "macaddress": "4CFCAA*" + }, { "domain": "tolo", "hostname": "usr-tcp232-ed2" diff --git a/requirements_all.txt b/requirements_all.txt index 05efb307062..3d47a00fe2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2301,6 +2301,9 @@ temperusb==1.5.3 # homeassistant.components.powerwall tesla-powerwall==0.3.12 +# homeassistant.components.tesla_wall_connector +tesla-wall-connector==0.2.0 + # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86012569af0..46e7f97a7f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1353,6 +1353,9 @@ tellduslive==0.10.11 # homeassistant.components.powerwall tesla-powerwall==0.3.12 +# homeassistant.components.tesla_wall_connector +tesla-wall-connector==0.2.0 + # homeassistant.components.tolo tololib==0.1.0b3 diff --git a/tests/components/tesla_wall_connector/__init__.py b/tests/components/tesla_wall_connector/__init__.py new file mode 100644 index 00000000000..cd5c6308425 --- /dev/null +++ b/tests/components/tesla_wall_connector/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tesla Wall Connector integration.""" diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py new file mode 100644 index 00000000000..a22061e197e --- /dev/null +++ b/tests/components/tesla_wall_connector/conftest.py @@ -0,0 +1,72 @@ +"""Common fixutres with default mocks as well as common test helper methods.""" +from unittest.mock import patch + +import pytest +import tesla_wall_connector + +from homeassistant.components.tesla_wall_connector.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_wall_connector_version(): + """Fixture to mock get_version calls to the wall connector API.""" + + with patch( + "tesla_wall_connector.WallConnector.async_get_version", + return_value=get_default_version_data(), + ): + yield + + +def get_default_version_data(): + """Return default version data object for a wall connector.""" + return tesla_wall_connector.wall_connector.Version( + { + "serial_number": "abc123", + "part_number": "part_123", + "firmware_version": "1.2.3", + } + ) + + +async def create_wall_connector_entry( + hass: HomeAssistant, side_effect=None +) -> MockConfigEntry: + """Create a wall connector entry in hass.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + options={CONF_SCAN_INTERVAL: 30}, + ) + + entry.add_to_hass(hass) + + # We need to return vitals with a contactor_closed attribute + # Since that is used to determine the update scan interval + fake_vitals = tesla_wall_connector.wall_connector.Vitals( + { + "contactor_closed": "false", + } + ) + + with patch( + "tesla_wall_connector.WallConnector.async_get_version", + return_value=get_default_version_data(), + side_effect=side_effect, + ), patch( + "tesla_wall_connector.WallConnector.async_get_vitals", + return_value=fake_vitals, + side_effect=side_effect, + ), patch( + "tesla_wall_connector.WallConnector.async_get_lifetime", + return_value=None, + side_effect=side_effect, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py new file mode 100644 index 00000000000..e28f0749b5a --- /dev/null +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -0,0 +1,207 @@ +"""Test the Tesla Wall Connector config flow.""" +from unittest.mock import patch + +from tesla_wall_connector.exceptions import WallConnectorConnectionError + +from homeassistant import config_entries, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.tesla_wall_connector.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> 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"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.tesla_wall_connector.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Tesla Wall Connector" + assert result2["data"] == {CONF_HOST: "1.1.1.1"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "tesla_wall_connector.WallConnector.async_get_version", + side_effect=WallConnectorConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1"}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_other_error( + mock_wall_connector_version, hass: HomeAssistant +) -> None: + """Test we handle any other error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "tesla_wall_connector.WallConnector.async_get_version", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1"}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(mock_wall_connector_version, hass): + """Test we get already configured.""" + + entry = MockConfigEntry( + domain=DOMAIN, unique_id="abc123", data={CONF_HOST: "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tesla_wall_connector.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data[CONF_HOST] == "1.1.1.1" + + +async def test_dhcp_can_finish(mock_wall_connector_version, hass): + """Test DHCP discovery flow can finish right away.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + HOSTNAME: "teslawallconnector_abc", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "DC:44:27:12:12", + }, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_HOST: "1.2.3.4"} + + +async def test_dhcp_already_exists(mock_wall_connector_version, hass): + """Test DHCP discovery flow when device already exists.""" + + entry = MockConfigEntry( + domain=DOMAIN, unique_id="abc123", data={CONF_HOST: "1.2.3.4"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + HOSTNAME: "teslawallconnector_aabbcc", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_dhcp_error_from_wall_connector(mock_wall_connector_version, hass): + """Test DHCP discovery flow when we cannot communicate with the device.""" + + with patch( + "tesla_wall_connector.WallConnector.async_get_version", + side_effect=WallConnectorConnectionError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + HOSTNAME: "teslawallconnector_aabbcc", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_option_flow(hass): + """Test option flow.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id="abc123", data={CONF_HOST: "1.2.3.4"} + ) + entry.add_to_hass(hass) + + assert not entry.options + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + entry.entry_id, + data=None, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 30}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_SCAN_INTERVAL: 30} diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py new file mode 100644 index 00000000000..b86a363dc3c --- /dev/null +++ b/tests/components/tesla_wall_connector/test_init.py @@ -0,0 +1,35 @@ +"""Test the Tesla Wall Connector config flow.""" +from tesla_wall_connector.exceptions import WallConnectorConnectionError + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant + +from .conftest import create_wall_connector_entry + + +async def test_init_success(hass: HomeAssistant) -> None: + """Test setup and that we get the device info, including firmware version.""" + + entry = await create_wall_connector_entry(hass) + + assert entry.state == config_entries.ConfigEntryState.LOADED + + +async def test_init_while_offline(hass: HomeAssistant) -> None: + """Test init with the wall connector offline.""" + entry = await create_wall_connector_entry( + hass, side_effect=WallConnectorConnectionError + ) + + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + + +async def test_load_unload(hass): + """Config entry can be unloaded.""" + + entry = await create_wall_connector_entry(hass) + + assert entry.state is config_entries.ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From b4730f4ffed94720716e1eb4b1ea88f04f27312e Mon Sep 17 00:00:00 2001 From: micha91 Date: Sun, 28 Nov 2021 18:52:46 +0100 Subject: [PATCH 0939/1452] Add Yamaha MusicCast number entities (#60093) --- .coveragerc | 1 + .../components/yamaha_musiccast/__init__.py | 46 +++++++++++++- .../components/yamaha_musiccast/const.py | 14 +++++ .../components/yamaha_musiccast/manifest.json | 2 +- .../components/yamaha_musiccast/number.py | 62 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/yamaha_musiccast/number.py diff --git a/.coveragerc b/.coveragerc index b0f3baf33cc..e7863427d63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1288,6 +1288,7 @@ omit = homeassistant/components/yale_smart_alarm/coordinator.py homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yamaha_musiccast/number.py homeassistant/components/yandex_transport/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 2245dd952a9..06a242c6070 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging from aiomusiccast import MusicCastConnectionException +from aiomusiccast.capabilities import Capability from aiomusiccast.musiccast_device import MusicCastData, MusicCastDevice from homeassistant.components import ssdp @@ -20,9 +21,16 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import BRAND, CONF_SERIAL, CONF_UPNP_DESC, DEFAULT_ZONE, DOMAIN +from .const import ( + BRAND, + CONF_SERIAL, + CONF_UPNP_DESC, + DEFAULT_ZONE, + DOMAIN, + ENTITY_CATEGORY_MAPPING, +) -PLATFORMS = ["media_player"] +PLATFORMS = ["media_player", "number"] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) @@ -66,6 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator = MusicCastDataUpdateCoordinator(hass, client=client) await coordinator.async_config_entry_first_refresh() + coordinator.musiccast.build_capabilities() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator @@ -190,3 +199,36 @@ class MusicCastDeviceEntity(MusicCastEntity): device_info["via_device"] = (DOMAIN, self.coordinator.data.device_id) return device_info + + +class MusicCastCapabilityEntity(MusicCastDeviceEntity): + """Base Entity type for all capabilities.""" + + def __init__( + self, + coordinator: MusicCastDataUpdateCoordinator, + capability: Capability, + zone_id: str = None, + ) -> None: + """Initialize a capability based entity.""" + if zone_id is not None: + self._zone_id = zone_id + self.capability = capability + super().__init__(name=capability.name, icon="", coordinator=coordinator) + self._attr_entity_category = ENTITY_CATEGORY_MAPPING.get(capability.entity_type) + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + await super().async_added_to_hass() + # All capability based entities should register callbacks to update HA when their state changes + self.coordinator.musiccast.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + await super().async_added_to_hass() + self.coordinator.musiccast.remove_callback(self.async_write_ha_state) + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.device_id}_{self.capability.id}" diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 55ce3920fa1..5384cc56694 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -1,5 +1,7 @@ """Constants for the MusicCast integration.""" +from aiomusiccast.capabilities import EntityType + from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_TRACK, @@ -7,6 +9,11 @@ from homeassistant.components.media_player.const import ( REPEAT_MODE_OFF, REPEAT_MODE_ONE, ) +from homeassistant.const import ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_SYSTEM, +) DOMAIN = "yamaha_musiccast" @@ -42,3 +49,10 @@ MEDIA_CLASS_MAPPING = { "directory": MEDIA_CLASS_DIRECTORY, "categories": MEDIA_CLASS_DIRECTORY, } + +ENTITY_CATEGORY_MAPPING = { + EntityType.CONFIG: ENTITY_CATEGORY_CONFIG, + EntityType.REGULAR: None, + EntityType.DIAGNOSTIC: ENTITY_CATEGORY_DIAGNOSTIC, + EntityType.SYSTEM: ENTITY_CATEGORY_SYSTEM, +} diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 8a7e9cf2c79..329fa2354d5 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.13.1" + "aiomusiccast==0.14.2" ], "ssdp": [ { diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py new file mode 100644 index 00000000000..daef8bacd12 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -0,0 +1,62 @@ +"""Number entities for musiccast.""" + +from aiomusiccast.capabilities import NumberSetter + +from homeassistant.components.number import NumberEntity +from homeassistant.components.yamaha_musiccast import ( + DOMAIN, + MusicCastCapabilityEntity, + MusicCastDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MusicCast number entities based on a config entry.""" + coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + number_entities = [] + + for capability in coordinator.data.capabilities: + if isinstance(capability, NumberSetter): + number_entities.append(NumberCapability(coordinator, capability)) + + for zone, data in coordinator.data.zones.items(): + for capability in data.capabilities: + if isinstance(capability, NumberSetter): + number_entities.append(NumberCapability(coordinator, capability, zone)) + + async_add_entities(number_entities) + + +class NumberCapability(MusicCastCapabilityEntity, NumberEntity): + """Representation of a MusicCast Number entity.""" + + capability: NumberSetter + + def __init__( + self, + coordinator: MusicCastDataUpdateCoordinator, + capability: NumberSetter, + zone_id: str = None, + ) -> None: + """Initialize the number entity.""" + super().__init__(coordinator, capability, zone_id) + self._attr_min_value = capability.value_range.minimum + self._attr_max_value = capability.value_range.maximum + self._attr_step = capability.value_range.step + + @property + def value(self): + """Return the current value.""" + return self.capability.current + + async def async_set_value(self, value: float): + """Set a new value.""" + await self.capability.set(value) diff --git a/requirements_all.txt b/requirements_all.txt index 3d47a00fe2e..5e6ab6c13b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -216,7 +216,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.13.1 +aiomusiccast==0.14.2 # homeassistant.components.nanoleaf aionanoleaf==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46e7f97a7f2..d8e91756a36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.13.1 +aiomusiccast==0.14.2 # homeassistant.components.nanoleaf aionanoleaf==0.0.4 From 313d6a81d046aacffd1fe75f688ccbc67c277b25 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 28 Nov 2021 19:25:52 +0100 Subject: [PATCH 0940/1452] Fix docker prefix for meta image (#60495) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index aa1c2b2a3e1..56da4acc490 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -291,7 +291,7 @@ jobs: function validate_image() { local image=${1} - if ! cas authenticate --signerID notary@home-assistant.io "${image}"; then + if ! cas authenticate --signerID notary@home-assistant.io "docker://${image}"; then echo "Invalid signature!" exit 1 fi From 2918e2d7d0a8efeb04d59436be2506eb7d0d028c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 29 Nov 2021 00:13:08 +0000 Subject: [PATCH 0941/1452] [ci skip] Translation update --- .../components/awair/translations/ja.json | 3 +- .../components/blebox/translations/ja.json | 3 +- .../components/climacell/translations/no.json | 4 +-- .../components/climate/translations/ja.json | 5 ++++ .../cloudflare/translations/ja.json | 1 + .../components/cover/translations/ja.json | 6 ++-- .../components/deconz/translations/ja.json | 9 ++++-- .../components/demo/translations/ja.json | 1 + .../components/denonavr/translations/ja.json | 4 ++- .../components/elkm1/translations/ja.json | 4 +++ .../forked_daapd/translations/ja.json | 4 ++- .../components/fritzbox/translations/ja.json | 1 + .../components/harmony/translations/ja.json | 1 + .../components/homekit/translations/ja.json | 1 + .../components/homekit/translations/no.json | 4 +-- .../homekit_controller/translations/ja.json | 8 +++++ .../huawei_lte/translations/ja.json | 1 + .../components/hue/translations/ja.json | 11 ++++++- .../humidifier/translations/ja.json | 2 +- .../components/icloud/translations/ja.json | 5 +++- .../components/insteon/translations/ja.json | 6 ++++ .../components/isy994/translations/ja.json | 4 ++- .../components/juicenet/translations/ja.json | 1 + .../components/konnected/translations/ja.json | 9 ++++-- .../components/light/translations/ja.json | 2 ++ .../logi_circle/translations/ja.json | 1 + .../lutron_caseta/translations/ja.json | 4 +++ .../motion_blinds/translations/ja.json | 2 +- .../components/mysensors/translations/ja.json | 2 +- .../components/nws/translations/ja.json | 4 ++- .../components/onvif/translations/ja.json | 2 +- .../opentherm_gw/translations/ja.json | 5 ++-- .../components/plex/translations/ja.json | 1 + .../components/risco/translations/ja.json | 3 ++ .../components/roku/translations/ja.json | 3 +- .../components/samsungtv/translations/ja.json | 3 +- .../screenlogic/translations/ja.json | 2 +- .../components/select/translations/ja.json | 2 +- .../components/sensor/translations/ja.json | 6 ++-- .../simplisafe/translations/ja.json | 2 ++ .../components/smappee/translations/ja.json | 1 + .../smartthings/translations/ja.json | 2 +- .../components/solarlog/translations/ja.json | 6 ++-- .../components/soma/translations/ja.json | 2 ++ .../somfy_mylink/translations/no.json | 4 +-- .../components/spotify/translations/ja.json | 4 ++- .../squeezebox/translations/ja.json | 4 ++- .../synology_dsm/translations/ja.json | 2 ++ .../components/tado/translations/ja.json | 1 + .../tesla_wall_connector/translations/ca.json | 24 +++++++++++++++ .../tesla_wall_connector/translations/de.json | 30 +++++++++++++++++++ .../tesla_wall_connector/translations/et.json | 30 +++++++++++++++++++ .../tesla_wall_connector/translations/no.json | 19 ++++++++++++ .../tesla_wall_connector/translations/ru.json | 30 +++++++++++++++++++ .../components/tolo/translations/en.json | 3 +- .../components/tuya/translations/ja.json | 5 +++- .../components/unifi/translations/ja.json | 2 +- .../components/upb/translations/ja.json | 1 + .../components/upnp/translations/ja.json | 1 + .../components/vizio/translations/ja.json | 15 ++++++---- .../waze_travel_time/translations/no.json | 2 +- .../components/wilight/translations/ja.json | 1 + .../wolflink/translations/sensor.ja.json | 9 ++++++ .../xiaomi_aqara/translations/ja.json | 2 +- .../xiaomi_miio/translations/fr.json | 3 +- .../components/zha/translations/ja.json | 2 +- .../components/zwave_js/translations/ja.json | 2 +- 67 files changed, 298 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/tesla_wall_connector/translations/ca.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/de.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/et.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/no.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/ru.json diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index ad0d2904258..83121c9fe42 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -21,7 +21,8 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" - } + }, + "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/blebox/translations/ja.json b/homeassistant/components/blebox/translations/ja.json index 866204dc3da..87e840f043e 100644 --- a/homeassistant/components/blebox/translations/ja.json +++ b/homeassistant/components/blebox/translations/ja.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unsupported_version": "BleBox device\u306e\u30d5\u30a1\u30fc\u30e0\u30a6\u30a7\u30a2\u304c\u53e4\u304f\u306a\u3063\u3066\u3044\u307e\u3059\u3002\u6700\u521d\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index 7a821bcccbb..b9994be7a86 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -15,7 +15,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "Hvis Breddegrad og Lengdegrad ikke er oppgitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en enhet for hver prognosetype, men bare de du velger blir aktivert som standard." + "description": "Hvis Breddegrad og Lengdegrad ikke er oppgitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en entitet for hver prognosetype, men bare de du velger blir aktivert som standard." } } }, @@ -25,7 +25,7 @@ "data": { "timestep": "Min. mellom NowCast prognoser" }, - "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselenheten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", + "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", "title": "Oppdater ClimaCell Alternativer" } } diff --git a/homeassistant/components/climate/translations/ja.json b/homeassistant/components/climate/translations/ja.json index c0b172839e3..146d928a483 100644 --- a/homeassistant/components/climate/translations/ja.json +++ b/homeassistant/components/climate/translations/ja.json @@ -3,6 +3,11 @@ "action_type": { "set_hvac_mode": "{entity_name} \u306eHVAC\u30e2\u30fc\u30c9\u3092\u5909\u66f4", "set_preset_mode": "{entity_name} \u306e\u30d7\u30ea\u30bb\u30c3\u30c8\u3092\u5909\u66f4" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u6e2c\u5b9a\u6e7f\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", + "current_temperature_changed": "{entity_name} \u6e2c\u5b9a\u6e29\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", + "hvac_mode_changed": "{entity_name} HVAC\u30e2\u30fc\u30c9\u304c\u5909\u5316\u3057\u307e\u3057\u305f" } }, "state": { diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 87e08b4ad88..81fdf638148 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -28,6 +28,7 @@ "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" }, + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", "title": "Cloudflare\u306b\u63a5\u7d9a" }, "zone": { diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index acd06502cff..c8ec1f5bbed 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "set_position": "{entity_name} \u4f4d\u7f6e\u306e\u8a2d\u5b9a", + "set_tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u8a2d\u5b9a", "stop": "\u505c\u6b62 {entity_name}" }, "condition_type": { @@ -16,8 +18,8 @@ "closing": "{entity_name} \u304c\u7d42\u4e86", "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", "opening": "{entity_name} \u304c\u958b\u304f", - "position": "{entity_name} \u4f4d\u7f6e\u306e\u5909\u66f4", - "tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u5909\u66f4" + "position": "{entity_name} \u4f4d\u7f6e\u306e\u5909\u5316", + "tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u5909\u5316" } }, "state": { diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index cb1f95bccca..71c3caa6ad0 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -18,7 +18,7 @@ "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306e\u3001deCONZ Zigbee gateway" }, "link": { - "description": "deCONZ\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3057\u3066\u3001Home Assistant\u306b\u767b\u9332\u3057\u307e\u3059\u3002 \n\n1. deCONZ\u8a2d\u5b9a -> \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 -> \u8a73\u7d30\u306b\u79fb\u52d5\n2. \"\u30a2\u30d7\u30ea\u306e\u8a8d\u8a3c(Authenticate app)\" \u30dc\u30bf\u30f3\u3092\u62bc\u3059", + "description": "deCONZ gateway\u306e\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3057\u3066\u3001Home Assistant\u306b\u767b\u9332\u3057\u307e\u3059\u3002 \n\n1. deCONZ\u8a2d\u5b9a -> \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 -> \u8a73\u7d30\u306b\u79fb\u52d5\n2. \"\u30a2\u30d7\u30ea\u306e\u8a8d\u8a3c(Authenticate app)\" \u30dc\u30bf\u30f3\u3092\u62bc\u3059", "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" }, "manual_input": { @@ -26,6 +26,11 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" } + }, + "user": { + "data": { + "host": "\u691c\u51fa\u3055\u308c\u305fdeCONZ gateway\u3092\u9078\u629e\u3057\u307e\u3059" + } } } }, @@ -43,7 +48,7 @@ "button_8": "8\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "close": "\u9589\u3058\u308b", "dim_down": "\u8584\u6697\u304f\u3059\u308b", - "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", + "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", "open": "\u30aa\u30fc\u30d7\u30f3", "right": "\u53f3", diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 3f95f9134ca..d987ee472e2 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -3,6 +3,7 @@ "step": { "options_1": { "data": { + "bool": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u771f\u507d\u5024(booleans)", "constant": "\u5b9a\u6570", "int": "\u6570\u5024\u5165\u529b" } diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index f0f8831234b..4300e1f515b 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -19,7 +19,9 @@ "select": { "data": { "select_host": "\u53d7\u4fe1\u6a5f\u306eIP\u30a2\u30c9\u30ec\u30b9" - } + }, + "description": "\u8ffd\u52a0\u306e\u53d7\u4fe1\u6a5f\u3092\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u63a5\u7d9a\u3057\u305f\u3044\u53d7\u4fe1\u6a5f\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 690d2b29969..4dc86431964 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "\u3053\u306e\u30a2\u30c9\u30ec\u30b9\u306eElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured": "\u3053\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u6301\u3064ElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 41f1167d063..692b7ca8346 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -33,7 +33,9 @@ "max_playlists": "\u30bd\u30fc\u30b9\u3068\u3057\u3066\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u6700\u5927\u6570", "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570", "tts_volume": "TTS\u30dc\u30ea\u30e5\u30fc\u30e0(\u7bc4\u56f2\u306f\u3001[0,1]\u306e\u5c0f\u6570\u70b9)" - } + }, + "description": "forked-daapd\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "title": "forked-daapd\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index 6631622d284..c246ea5fb0d 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "AVM FRITZ!Box\u306b\u63a5\u7d9a\u3057\u307e\u3057\u305f\u304c\u3001Smart Home devices\u3092\u5236\u5fa1\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/harmony/translations/ja.json b/homeassistant/components/harmony/translations/ja.json index b34b504e586..21af2f3bb2a 100644 --- a/homeassistant/components/harmony/translations/ja.json +++ b/homeassistant/components/harmony/translations/ja.json @@ -10,6 +10,7 @@ "flow_title": "{name}", "step": { "link": { + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?", "title": "Logitech Harmony Hub\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "user": { diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index e46c4d1999a..06a7d6bc1d3 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -32,6 +32,7 @@ "camera_audio": "\u97f3\u58f0\u306b\u5bfe\u5fdc\u3057\u305f\u30ab\u30e1\u30e9", "camera_copy": "H.264\u306e\u30cd\u30a4\u30c6\u30a3\u30d6\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308b\u30ab\u30e1\u30e9" }, + "description": "\u3059\u3079\u3066\u306e\u30ab\u30e1\u30e9\u304c\u3001\u30cd\u30a4\u30c6\u30a3\u30d6\u3067H.264\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002\u30ab\u30e1\u30e9\u304c\u3001H.264\u30b9\u30c8\u30ea\u30fc\u30e0\u51fa\u529b\u306b\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u3001\u30b7\u30b9\u30c6\u30e0\u306f\u3001HomeKit\u306eH.264\u306b\u30d3\u30c7\u30aa\u3092\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c9\u3057\u307e\u3059\u3002\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u306b\u306f\u9ad8\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u306aCPU\u304c\u5fc5\u8981\u306a\u306e\u3067\u3001\u30b7\u30f3\u30b0\u30eb\u30dc\u30fc\u30c9\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u3067\u306f\u52d5\u4f5c\u3057\u306a\u3044\u3068\u601d\u308f\u308c\u307e\u3059\u3002", "title": "\u30ab\u30e1\u30e9\u306e\u8a2d\u5b9a" }, "include_exclude": { diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 86e5c8d95cb..868b3ff03fe 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -40,8 +40,8 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", - "title": "Velg enheter som skal inkluderes" + "description": "Velg entitetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt entitet inkludert. I bridge-inkluderingsmodus vil alle entiteter i domenet bli inkludert, med mindre spesifikke entiteter er valgt. I bridge-ekskluderingsmodus vil alle entiteter i domenet bli inkludert, bortsett fra de ekskluderte entitetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", + "title": "Velg entiteter som skal inkluderes" }, "init": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index ca975c58723..d02809b291a 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -21,9 +21,11 @@ "flow_title": "{name}", "step": { "busy_error": { + "description": "\u3059\u3079\u3066\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3067\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u4e2d\u6b62\u3059\u308b\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u518d\u958b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u65e2\u306b\u4ed6\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059" }, "max_tries_error": { + "description": "\u30c7\u30d0\u30a4\u30b9\u306f\u3001100\u56de\u3092\u8d85\u3048\u308b\u8a8d\u8a3c\u8a66\u884c\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u518d\u958b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u8a8d\u8a3c\u306e\u6700\u5927\u8a66\u884c\u56de\u6570\u3092\u8d85\u3048\u307e\u3057\u305f" }, "pair": { @@ -31,12 +33,18 @@ "allow_insecure_setup_codes": "\u5b89\u5168\u3067\u306a\u3044\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8a31\u53ef\u3059\u308b\u3002", "pairing_code": "\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9" }, + "description": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306f\u3001\u5225\u306eHomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3084iCloud\u3092\u4f7f\u7528\u305b\u305a\u306b\u3001\u30bb\u30ad\u30e5\u30a2\u306a\u6697\u53f7\u5316\u63a5\u7d9a\u3092\u4f7f\u7528\u3057\u3066\u30ed\u30fc\u30ab\u30eb\u30a8\u30ea\u30a2\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067 {name} \u3068\u901a\u4fe1\u3057\u307e\u3059\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001HomeKit \u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9(XXX-XX-XXX \u306e\u5f62\u5f0f)\u5165\u529b\u3057\u307e\u3059\u3002\u3053\u306e\u30b3\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30c7\u30d0\u30a4\u30b9\u672c\u4f53\u307e\u305f\u306f\u30d1\u30c3\u30b1\u30fc\u30b8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "title": "HomeKit Accessory Protocol\u3092\u4ecb\u3057\u3066\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0" }, + "protocol_error": { + "description": "\u30c7\u30d0\u30a4\u30b9\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u7269\u7406\u307e\u305f\u306f\u4eee\u60f3\u7684\u306a\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u518d\u958b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3068\u306e\u901a\u4fe1\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + }, "user": { "data": { "device": "\u30c7\u30d0\u30a4\u30b9" }, + "description": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306f\u3001\u5225\u306eHomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3084iCloud\u3092\u4f7f\u7528\u305b\u305a\u306b\u3001\u30bb\u30ad\u30e5\u30a2\u306a\u6697\u53f7\u5316\u63a5\u7d9a\u3092\u4f7f\u7528\u3057\u3066\u30ed\u30fc\u30ab\u30eb\u30a8\u30ea\u30a2\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u7d4c\u7531\u3067\u901a\u4fe1\u3057\u307e\u3059\u3002\u30da\u30a2\u30ea\u30f3\u30b0\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044:", "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" } } diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 37169a86d4a..6c74f5a7918 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -11,6 +11,7 @@ "incorrect_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_url": "\u7121\u52b9\u306aURL", + "login_attempts_exceeded": "\u30ed\u30b0\u30a4\u30f3\u8a66\u884c\u56de\u6570\u304c\u6700\u5927\u5024\u3092\u8d85\u3048\u307e\u3057\u305f\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", "response_error": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u4e0d\u660e\u306a\u30a8\u30e9\u30fc", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index a015f0d1806..68fff69f518 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -39,14 +39,23 @@ "2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "dim_down": "\u8584\u6697\u304f\u3059\u308b", + "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "double_buttons_1_3": "1\u756a\u76ee\u30683\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "double_buttons_2_4": "2\u756a\u76ee\u30684\u756a\u76ee\u306e\u30dc\u30bf\u30f3" + "double_buttons_2_4": "2\u756a\u76ee\u30684\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "turn_off": "\u30aa\u30d5\u306b\u3059\u308b", + "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { "double_short_release": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" diff --git a/homeassistant/components/humidifier/translations/ja.json b/homeassistant/components/humidifier/translations/ja.json index 2c17d1e3f61..9b272bfb55f 100644 --- a/homeassistant/components/humidifier/translations/ja.json +++ b/homeassistant/components/humidifier/translations/ja.json @@ -13,7 +13,7 @@ "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059" }, "trigger_type": { - "target_humidity_changed": "{entity_name} \u30bf\u30fc\u30b2\u30c3\u30c8\u6e7f\u5ea6\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", + "target_humidity_changed": "{entity_name} \u30bf\u30fc\u30b2\u30c3\u30c8\u6e7f\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index ab79f0b6611..d60b1e1a335 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -7,6 +7,7 @@ }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "send_verification_code": "\u78ba\u8a8d\u30b3\u30fc\u30c9\u306e\u9001\u4fe1\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "validate_verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u78ba\u8a8d\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u518d\u5ea6\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "step": { @@ -27,7 +28,8 @@ "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "E\u30e1\u30fc\u30eb" + "username": "E\u30e1\u30fc\u30eb", + "with_family": "\u5bb6\u65cf\u3068\u5171\u6709" }, "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" @@ -36,6 +38,7 @@ "data": { "verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" }, + "description": "iCloud\u304b\u3089\u53d7\u3051\u53d6\u3063\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "iCloud \u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9" } } diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index cf433818237..d4ddf083f1b 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -31,6 +31,7 @@ "data": { "device": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, + "description": "Insteon PowerLink Modem (PLM)\u306e\u8a2d\u5b9a\u3092\u884c\u3044\u307e\u3059\u3002", "title": "Insteon PLM" }, "user": { @@ -55,11 +56,14 @@ "cat": "\u30c7\u30d0\u30a4\u30b9\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x10)", "subcat": "\u30c7\u30d0\u30a4\u30b9 \u30b5\u30d6\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x0a)" }, + "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002", "title": "Insteon" }, "add_x10": { "data": { + "housecode": "\u30cf\u30a6\u30b9\u30b3\u30fc\u30c9(a\uff5ep)", "platform": "\u30d7\u30e9\u30c3\u30c8\u30db\u30fc\u30e0", + "steps": "\u8abf\u5149\u30b9\u30c6\u30c3\u30d7(\u30e9\u30a4\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u307f\u3001\u30c7\u30d5\u30a9\u30eb\u30c822)", "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" }, "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002", @@ -72,6 +76,7 @@ "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "Insteon Hub\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5909\u66f4\u3057\u307e\u3059\u3002\u3053\u306e\u5909\u66f4\u3092\u884c\u3063\u305f\u5f8c\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u3053\u308c\u306f\u3001Hub\u81ea\u4f53\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u3082\u306e\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u306b\u306f\u3001Hub\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", "title": "Insteon" }, "init": { @@ -89,6 +94,7 @@ "data": { "address": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e" }, + "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u524a\u9664\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059", "title": "Insteon" }, "remove_x10": { diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 20ed55f9fb2..438eadfcd83 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -29,8 +29,10 @@ "data": { "ignore_string": "\u7121\u8996\u3059\u308b\u6587\u5b57\u5217", "restore_light_state": "\u30e9\u30a4\u30c8\u306e\u660e\u308b\u3055\u3092\u5fa9\u5143\u3059\u308b", - "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217" + "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217", + "variable_sensor_string": "\u53ef\u5909\u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217(Variable Sensor String)" }, + "description": "ISY\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/juicenet/translations/ja.json b/homeassistant/components/juicenet/translations/ja.json index 245fe0cdadc..bc06bd44c9b 100644 --- a/homeassistant/components/juicenet/translations/ja.json +++ b/homeassistant/components/juicenet/translations/ja.json @@ -13,6 +13,7 @@ "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" }, + "description": "https://home.juice.net/Manage \u304b\u3089API\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002", "title": "JuiceNet\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/konnected/translations/ja.json b/homeassistant/components/konnected/translations/ja.json index 5e50b806208..5572841b3f2 100644 --- a/homeassistant/components/konnected/translations/ja.json +++ b/homeassistant/components/konnected/translations/ja.json @@ -84,8 +84,10 @@ }, "options_misc": { "data": { + "api_host": "API\u30db\u30b9\u30c8\u306eURL\u3092\u4e0a\u66f8\u304d\u3059\u308b(\u30aa\u30d7\u30b7\u30e7\u30f3)", "blink": "\u72b6\u614b\u5909\u66f4\u3092\u9001\u4fe1\u3059\u308b\u3068\u304d\u306b\u3001\u30d1\u30cd\u30eb\u306eLED\u3092\u70b9\u6ec5\u3055\u305b\u308b", - "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b" + "discovery": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u691c\u51fa(discovery)\u8981\u6c42\u306b\u5fdc\u7b54\u3059\u308b", + "override_api_host": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eHome Assistant API\u30db\u30b9\u30c8\u30d1\u30cd\u30eb\u306eURL\u3092\u4e0a\u66f8\u304d\u3059\u308b" }, "description": "\u30d1\u30cd\u30eb\u306b\u5fc5\u8981\u306a\u52d5\u4f5c\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u305d\u306e\u4ed6\u306e\u8a2d\u5b9a" @@ -94,7 +96,10 @@ "data": { "activation": "\u30aa\u30f3\u306e\u3068\u304d\u306b\u51fa\u529b", "momentary": "\u30d1\u30eb\u30b9\u6301\u7d9a\u6642\u9593(ms)(\u30aa\u30d7\u30b7\u30e7\u30f3)", - "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)" + "more_states": "\u3053\u306e\u30be\u30fc\u30f3\u306e\u8ffd\u52a0\u72b6\u614b\u306e\u8a2d\u5b9a", + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "pause": "\u30d1\u30eb\u30b9\u9593\u306e\u4e00\u6642\u505c\u6b62(ms)(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "repeat": "\u7e70\u308a\u8fd4\u3059\u6642\u9593(-1 =\u7121\u9650)(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, "description": "{zone}\u30aa\u30d7\u30b7\u30e7\u30f3 : \u72b6\u614b{state}", "title": "\u5207\u308a\u66ff\u3048\u53ef\u80fd\u306a\u51fa\u529b\u306e\u8a2d\u5b9a" diff --git a/homeassistant/components/light/translations/ja.json b/homeassistant/components/light/translations/ja.json index 16d0fd40523..c7d3c968bcb 100644 --- a/homeassistant/components/light/translations/ja.json +++ b/homeassistant/components/light/translations/ja.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "{entity_name} \u660e\u308b\u3055\u3092\u4e0b\u3052\u308b", + "brightness_increase": "{entity_name} \u660e\u308b\u3055\u3092\u4e0a\u3052\u308b", "flash": "\u30d5\u30e9\u30c3\u30b7\u30e5 {entity_name}", "toggle": "\u30c8\u30b0\u30eb {entity_name}", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", diff --git a/homeassistant/components/logi_circle/translations/ja.json b/homeassistant/components/logi_circle/translations/ja.json index e972b95daee..8f611814597 100644 --- a/homeassistant/components/logi_circle/translations/ja.json +++ b/homeassistant/components/logi_circle/translations/ja.json @@ -20,6 +20,7 @@ "data": { "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" }, + "description": "Logi Circle\u3067\u8a8d\u8a3c\u3059\u308b\u305f\u3081\u306e\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" } } diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 4f7339efcd8..6815b5e33dc 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -10,6 +10,10 @@ }, "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "configuration.yaml\u304b\u3089\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u305fbridge (host: {host})\u3001\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "title": "Cas\u00e9ta bridge\u69cb\u6210\u306e\u30a4\u30f3\u30dd\u30fc\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002" + }, "link": { "description": "{name} ({host}) \u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3059\u308b\u306b\u306f\u3001\u3053\u306e\u30d5\u30a9\u30fc\u30e0\u3092\u9001\u4fe1(submit)\u3057\u305f\u5f8c\u3001\u30d6\u30ea\u30c3\u30b8\u306e\u80cc\u9762\u306b\u3042\u308b\u9ed2\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u307e\u3059\u3002", "title": "\u30d6\u30ea\u30c3\u30b8\u3068\u30da\u30a2" diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index ffc66c45cb7..b81cfa365c3 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -23,7 +23,7 @@ "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "Motion Gateway\u3092\u8ffd\u52a0\u3057\u3066\u63a5\u7d9a\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "\u8ffd\u52a0\u306eMotion Gateway\u3092\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u63a5\u7d9a\u3057\u305f\u3044Motion Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "user": { diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index e62dc657dbb..9842d241ad1 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -48,7 +48,7 @@ "topic_out_prefix": "\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8 \u30c8\u30d4\u30c3\u30af\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(topic_out_prefix)", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, - "description": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "MQTT gateway\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "gw_serial": { "data": { diff --git a/homeassistant/components/nws/translations/ja.json b/homeassistant/components/nws/translations/ja.json index 16b3b37c6e0..39f7b5512dc 100644 --- a/homeassistant/components/nws/translations/ja.json +++ b/homeassistant/components/nws/translations/ja.json @@ -12,8 +12,10 @@ "data": { "api_key": "API\u30ad\u30fc", "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6" + "longitude": "\u7d4c\u5ea6", + "station": "METAR station code" }, + "description": "METAR station code\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u3092\u4f7f\u7528\u3057\u3066\u6700\u3082\u8fd1\u3044\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u691c\u7d22\u3055\u308c\u307e\u3059\u3002\u4eca\u306e\u3068\u3053\u308d\u3001API\u30ad\u30fc\u306f\u4f55\u3067\u3082\u304b\u307e\u3044\u307e\u305b\u3093\u3002\u6709\u52b9\u306a\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3092\u4f7f\u7528\u3059\u308b\u3053\u3068\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002", "title": "\u30a2\u30e1\u30ea\u30ab\u56fd\u7acb\u6c17\u8c61\u5c40\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index 388332d6e03..db36b9fee5a 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "no_h264": "\u5229\u7528\u53ef\u80fd\u306aH264\u30b9\u30c8\u30ea\u30fc\u30e0\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u69cb\u6210\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_h264": "\u5229\u7528\u53ef\u80fd\u306aH264\u30b9\u30c8\u30ea\u30fc\u30e0\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_mac": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "onvif_error": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 0a7a15093ea..fa31eced5ab 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -12,7 +12,7 @@ "id": "ID", "name": "\u540d\u524d" }, - "title": "OpenTherm\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4" + "title": "OpenTherm Gateway" } } }, @@ -24,7 +24,8 @@ "read_precision": "\u7cbe\u5ea6\u3092\u8aad\u307f\u8fbc\u3080", "set_precision": "\u7cbe\u5ea6\u3092\u8a2d\u5b9a\u3059\u308b", "temporary_override_mode": "\u4e00\u6642\u7684\u306a\u30bb\u30c3\u30c8\u30dd\u30a4\u30f3\u30c8\u306e\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u30e2\u30fc\u30c9" - } + }, + "description": "OpenTherm Gateway\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 5fda1370e65..4b48bdfe695 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -10,6 +10,7 @@ }, "error": { "faulty_credentials": "\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "host_or_token": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u30db\u30b9\u30c8\u307e\u305f\u306f\u30c8\u30fc\u30af\u30f3\u3092\u63d0\u4f9b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "no_servers": "Plex\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ea\u30f3\u30af\u3055\u308c\u3066\u3044\u308b\u30b5\u30fc\u30d0\u30fc\u306f\u3042\u308a\u307e\u305b\u3093", "not_found": "Plex server\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "ssl_error": "SSL\u8a3c\u660e\u66f8\u306e\u554f\u984c" diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index 76017fa2d22..017faaadd1c 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -21,6 +21,9 @@ "options": { "step": { "init": { + "data": { + "scan_interval": "Risco\u3092\u30dd\u30fc\u30ea\u30f3\u30b0\u3059\u308b\u983b\u5ea6(\u79d2\u5358\u4f4d)" + }, "title": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" }, "risco_to_ha": { diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index bb308c071c2..65f2ac2272a 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -21,7 +21,8 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "description": "Roku\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index b10b92906f4..0cfad60efcb 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -27,7 +27,8 @@ "data": { "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" - } + }, + "description": "Samsung TV\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u3053\u308c\u307e\u3067\u306bHome Assistant\u306b\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/screenlogic/translations/ja.json b/homeassistant/components/screenlogic/translations/ja.json index 557b36d10a6..91ded1b3200 100644 --- a/homeassistant/components/screenlogic/translations/ja.json +++ b/homeassistant/components/screenlogic/translations/ja.json @@ -13,7 +13,7 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "port": "\u30dd\u30fc\u30c8" }, - "description": "ScreenLogic\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "ScreenLogic gateway\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "ScreenLogic" }, "gateway_select": { diff --git a/homeassistant/components/select/translations/ja.json b/homeassistant/components/select/translations/ja.json index 808684aeb6f..7900a1c8806 100644 --- a/homeassistant/components/select/translations/ja.json +++ b/homeassistant/components/select/translations/ja.json @@ -7,7 +7,7 @@ "selected_option": "\u73fe\u5728\u9078\u629e\u3055\u308c\u3066\u3044\u308b {entity_name} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "trigger_type": { - "current_option_changed": "{entity_name} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" + "current_option_changed": "{entity_name} \u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u5909\u5316\u3057\u307e\u3057\u305f" } }, "title": "\u9078\u629e" diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index ef9de383daa..fccbedad2b2 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -33,8 +33,8 @@ "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "current": "{entity_name} \u73fe\u5728\u306e\u5909\u5316", "energy": "{entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc\u306e\u5909\u5316", - "frequency": "{entity_name} \u983b\u5ea6(frequency)\u306e\u5909\u66f4", - "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u66f4", + "frequency": "{entity_name} \u983b\u5ea6(frequency)\u304c\u5909\u5316", + "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u5316", "humidity": "{entity_name} \u6e7f\u5ea6\u306e\u5909\u5316", "illuminance": "{entity_name} \u7167\u5ea6\u306e\u5909\u5316", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", @@ -45,7 +45,7 @@ "pm10": "{entity_name} PM10\u6fc3\u5ea6\u306e\u5909\u5316", "pm25": "{entity_name} PM2.5\u6fc3\u5ea6\u306e\u5909\u5316", "power": "{entity_name} \u96fb\u6e90(power)\u306e\u5909\u5316", - "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u66f4", + "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u5316", "pressure": "{entity_name} \u5727\u529b\u306e\u5909\u5316", "signal_strength": "{entity_name} \u4fe1\u53f7\u5f37\u5ea6\u306e\u5909\u5316", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 39ff690804f..2dc0b2609ac 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -19,6 +19,7 @@ "title": "\u627f\u8a8d\u7d42\u4e86" }, "mfa": { + "description": "SimpliSafe\u304b\u3089\u306e\u30ea\u30f3\u30af\u306b\u3064\u3044\u3066\u306f\u30e1\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30ea\u30f3\u30af\u3092\u78ba\u8a8d\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002", "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" }, "reauth_confirm": { @@ -30,6 +31,7 @@ }, "user": { "data": { + "code": "\u30b3\u30fc\u30c9(Home Assistant UI\u3067\u4f7f\u7528)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 1b1751d63f0..9dff009e3dd 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured_local_device": "\u30ed\u30fc\u30ab\u30eb\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30af\u30e9\u30a6\u30c9\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u524d\u306b\u3001\u307e\u305a\u305d\u308c\u3089\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_mdns": "Smappee\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 1174e136535..4d1b8a15cfc 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "SmartThings\u304b\u3089\u66f4\u65b0\u3092\u53d7\u4fe1\u3059\u308b\u3088\u3046\u306bHome Assistant\u304c\u6b63\u3057\u304f\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 Webhook\u306eURL\u304c\u7121\u52b9\u3067\u3059:\n> {webhook_url}\n\n[\u8aac\u660e\u66f8]({component_url}) \u306b\u5f93\u3063\u3066\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_webhook_url": "SmartThings\u304b\u3089\u66f4\u65b0\u3092\u53d7\u4fe1\u3059\u308b\u3088\u3046\u306bHome Assistant\u304c\u6b63\u3057\u304f\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 Webhook\u306eURL\u304c\u7121\u52b9\u3067\u3059:\n> {webhook_url}\n\n[\u8aac\u660e\u66f8]({component_url}) \u306b\u5f93\u3063\u3066\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_available_locations": "Home Assistant\u306b\u8a2d\u5b9a\u3067\u304d\u308bSmartThings\u306e\u5834\u6240\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/solarlog/translations/ja.json b/homeassistant/components/solarlog/translations/ja.json index f5d4f9d206a..c682ca60b48 100644 --- a/homeassistant/components/solarlog/translations/ja.json +++ b/homeassistant/components/solarlog/translations/ja.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8" - } + "host": "\u30db\u30b9\u30c8", + "name": "Solar-Log sensors\u306b\u4f7f\u7528\u3055\u308c\u308b\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9" + }, + "title": "Solar-Log connection\u306e\u5b9a\u7fa9" } } } diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 32b57dd97b9..026499458d8 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -4,6 +4,7 @@ "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "missing_configuration": "SOMA Connect\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "result_error": "SOMAConnect\u306f\u30a8\u30e9\u30fc\u30b9\u30c6\u30fc\u30bf\u30b9\u3067\u5fdc\u7b54\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { @@ -15,6 +16,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, + "description": "SOMA Connect\u306e\u63a5\u7d9a\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "SOMA Connect" } } diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json index 2b629015f36..65c89866f65 100644 --- a/homeassistant/components/somfy_mylink/translations/no.json +++ b/homeassistant/components/somfy_mylink/translations/no.json @@ -30,12 +30,12 @@ "reverse": "Rullegardinet reverseres" }, "description": "Konfigurer alternativer for \"{entity_id}\"", - "title": "Konfigurer enhet" + "title": "Konfigurer entitet" }, "init": { "data": { "default_reverse": "Standard tilbakef\u00f8ringsstatus for ukonfigurerte rullegardiner", - "entity_id": "Konfigurer en bestemt enhet.", + "entity_id": "Konfigurer en bestemt entitet.", "target_id": "Konfigurer alternativer for et rullgardin" }, "title": "Konfigurere MyLink-alternativer" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 2b057f2ced2..d21e6919bf7 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + "missing_configuration": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "create_entry": { "default": "Spotify\u306e\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002" diff --git a/homeassistant/components/squeezebox/translations/ja.json b/homeassistant/components/squeezebox/translations/ja.json index 142429027a6..1855d7aef56 100644 --- a/homeassistant/components/squeezebox/translations/ja.json +++ b/homeassistant/components/squeezebox/translations/ja.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_server_found": "\u30b5\u30fc\u30d0\u30fc\u3092\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{host}", @@ -17,7 +18,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "\u63a5\u7d9a\u60c5\u5831\u306e\u7de8\u96c6" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 90ea6970c61..c526869805d 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -8,6 +8,8 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "missing_data": "\u30c7\u30fc\u30bf\u6b20\u843d: \u5f8c\u3067\u518d\u8a66\u884c\u3059\u308b\u304b\u3001\u5225\u306e\u8a2d\u5b9a\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "otp_failed": "2\u6bb5\u968e\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u65b0\u3057\u3044\u30d1\u30b9\u30b3\u30fc\u30c9\u3067\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/tado/translations/ja.json b/homeassistant/components/tado/translations/ja.json index 5061a4e1e6b..99ef3691ba6 100644 --- a/homeassistant/components/tado/translations/ja.json +++ b/homeassistant/components/tado/translations/ja.json @@ -25,6 +25,7 @@ "data": { "fallback": "\u30d5\u30a9\u30fc\u30eb\u30d0\u30c3\u30af\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u3002" }, + "description": "\u30d5\u30a9\u30fc\u30eb\u30d0\u30c3\u30af\u30e2\u30fc\u30c9\u306f\u3001\u624b\u52d5\u3067\u30be\u30fc\u30f3\u3092\u8abf\u6574\u3057\u305f\u5f8c\u3001\u6b21\u306e\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb\u5207\u308a\u66ff\u3048\u6642\u306bSmart Schedule\u306b\u5207\u308a\u66ff\u308f\u308a\u307e\u3059\u3002", "title": "Tado\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } diff --git a/homeassistant/components/tesla_wall_connector/translations/ca.json b/homeassistant/components/tesla_wall_connector/translations/ca.json new file mode 100644 index 00000000000..33facc1426c --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/de.json b/homeassistant/components/tesla_wall_connector/translations/de.json new file mode 100644 index 00000000000..3500fc1aa7b --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Tesla Wall Connector konfigurieren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Optionen f\u00fcr Tesla Wall Connector konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/et.json b/homeassistant/components/tesla_wall_connector/translations/et.json new file mode 100644 index 00000000000..a13b447d542 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Tesla Wall Connector'i seadistamine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "V\u00e4rskendussagedus" + }, + "title": "Tesla Wall Connector'i seadistamise valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/no.json b/homeassistant/components/tesla_wall_connector/translations/no.json new file mode 100644 index 00000000000..df06035b1d8 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/ru.json b/homeassistant/components/tesla_wall_connector/translations/ru.json new file mode 100644 index 00000000000..0fc1ef88790 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/ru.json @@ -0,0 +1,30 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tesla Wall Connector" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/en.json b/homeassistant/components/tolo/translations/en.json index dea5a3b30df..488c2f7ae69 100644 --- a/homeassistant/components/tolo/translations/en.json +++ b/homeassistant/components/tolo/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network" }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 1d2624615c6..96b31ac0781 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -9,7 +9,7 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "login_error": "\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc ({code}): {msg}" }, - "flow_title": "Tuya\u306e\u69cb\u6210", + "flow_title": "Tuya\u306e\u8a2d\u5b9a", "step": { "login": { "data": { @@ -54,6 +54,9 @@ "data": { "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", "curr_temp_divider": "\u73fe\u5728\u306e\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", + "max_kelvin": "\u30b1\u30eb\u30d3\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6700\u5927\u8272\u6e29\u5ea6", + "max_temp": "\u6700\u5927\u76ee\u6a19\u6e29\u5ea6(\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6700\u5c0f\u304a\u3088\u3073\u6700\u5927 = 0\u3092\u4f7f\u7528)", + "min_kelvin": "\u30b1\u30eb\u30d3\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6700\u5c0f\u8272\u6e29\u5ea6", "min_temp": "\u6700\u5c0f\u76ee\u6a19\u6e29\u5ea6(\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6700\u5c0f\u304a\u3088\u3073\u6700\u5927 = 0\u3092\u4f7f\u7528)", "set_temp_divided": "\u8a2d\u5b9a\u6e29\u5ea6\u30b3\u30de\u30f3\u30c9\u306b\u533a\u5207\u3089\u308c\u305f\u6e29\u5ea6\u5024\u3092\u4f7f\u7528", "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8", diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 9517b707531..1f77f15d519 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u30b5\u30a4\u30c8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u30b5\u30a4\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "configuration_updated": "\u8a2d\u5b9a\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, diff --git a/homeassistant/components/upb/translations/ja.json b/homeassistant/components/upb/translations/ja.json index 97b50b62221..d73bfe2b08a 100644 --- a/homeassistant/components/upb/translations/ja.json +++ b/homeassistant/components/upb/translations/ja.json @@ -15,6 +15,7 @@ "file_path": "UPStart UPB\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u30d1\u30b9\u3068\u540d\u524d\u3002", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb" }, + "description": "Universal Powerline Bus Powerline Interface Module (UPB PIM)\u3092\u63a5\u7d9a\u3057\u307e\u3059\u3002\u30a2\u30c9\u30ec\u30b9\u6587\u5b57\u5217\u306f\u3001'tcp'\u306e\u5834\u5408\u3001'address[:port]'\u306e\u5f62\u5f0f\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002\u30dd\u30fc\u30c8\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f2101\u3067\u3059\u3002\u4f8b: '192.168.1.42'\u3002\u30b7\u30ea\u30a2\u30eb\u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u5834\u5408\u3001\u30a2\u30c9\u30ec\u30b9\u306f\u3001'/dev/ttyS1'\u306e\u5f62\u5f0f\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002baud\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f4800\u3067\u3059\u3002\u4f8b: 'tty[:baud]'\u3002", "title": "UPB PIM\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index deb806e1493..2148b0d8a16 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -12,6 +12,7 @@ }, "user": { "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694(\u79d2\u3001\u6700\u5c0f30)", "unique_id": "\u30c7\u30d0\u30a4\u30b9", "usn": "\u30c7\u30d0\u30a4\u30b9" } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index 2496d351314..b40d9ef8680 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -2,24 +2,28 @@ "config": { "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "updated_entry": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u8a2d\u5b9a\u3067\u5b9a\u7fa9\u3055\u308c\u305f\u540d\u524d\u3001\u30a2\u30d7\u30ea\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u4ee5\u524d\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u305f\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u306f\u305d\u308c\u306b\u5fdc\u3058\u3066\u66f4\u65b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "complete_pairing_failed": "\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5b8c\u4e86\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u518d\u9001\u4fe1\u3059\u308b\u524d\u306b\u3001\u5165\u529b\u3057\u305fPIN\u304c\u6b63\u3057\u304f\u3001\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u3066\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "existing_config_entry_found": "\u540c\u3058\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u3092\u6301\u3064\u65e2\u5b58\u306e\u3001VIZIO SmartCast Device\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u304c\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u69cb\u6210\u3059\u308b\u306b\u306f\u3001\u65e2\u5b58\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u524a\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "step": { "pair_tv": { "data": { "pin": "PIN\u30b3\u30fc\u30c9" }, + "description": "\u30c6\u30ec\u30d3\u306b\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u306f\u305a\u3067\u3059\u3002\u305d\u306e\u30b3\u30fc\u30c9\u3092\u30d5\u30a9\u30fc\u30e0\u306b\u5165\u529b\u3057\u3001\u6b21\u306e\u30b9\u30c6\u30c3\u30d7\u306b\u9032\u3080\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u5b8c\u4e86\u3057\u307e\u3059\u3002", "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u306e\u5b8c\u4e86" }, "pairing_complete": { - "description": "\u3042\u306a\u305f\u306e\u3001VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9\u304cHome Assistant\u306b\u63a5\u7d9a\u3055\u308c\u307e\u3057\u305f\u3002", + "description": "\u3042\u306a\u305f\u306e\u3001VIZIO SmartCast Device\u304cHome Assistant\u306b\u63a5\u7d9a\u3055\u308c\u307e\u3057\u305f\u3002", "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" }, "pairing_complete_import": { + "description": "VIZIO SmartCast Device\u304c\u3001Home Assistant\u306b\u63a5\u7d9a\u3055\u308c\u307e\u3057\u305f\u3002\n\nVIZIO SmartCast Device\u306f\u3001'**{access_token}**' '\u3067\u3059\u3002", "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u5b8c\u4e86" }, "user": { @@ -29,7 +33,8 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "title": "VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9" + "description": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u306f\u30c6\u30ec\u30d3\u306e\u5834\u5408\u306e\u307f\u5fc5\u8981\u3067\u3059\u3002\u30c6\u30ec\u30d3\u3092\u8a2d\u5b9a\u3057\u3066\u3044\u3066\u3001\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u307e\u3060\u6301\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u3092\u5b9f\u884c\u3059\u308b\u305f\u3081\u306b\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002", + "title": "VIZIO SmartCast Device" } } }, @@ -42,7 +47,7 @@ "volume_step": "\u30dc\u30ea\u30e5\u30fc\u30e0 \u30b9\u30c6\u30c3\u30d7\u30b5\u30a4\u30ba" }, "description": "\u30b9\u30de\u30fc\u30c8\u30c6\u30ec\u30d3\u3092\u304a\u6301\u3061\u306e\u5834\u5408\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a2\u30d7\u30ea\u3092\u9078\u629e\u3057\u3066\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3067\u304d\u307e\u3059\u3002", - "title": "VIZIO SmartCast\u30c7\u30d0\u30a4\u30b9\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0" + "title": "VIZIO SmartCast Device\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0" } } } diff --git a/homeassistant/components/waze_travel_time/translations/no.json b/homeassistant/components/waze_travel_time/translations/no.json index c9baef06743..15e30100519 100644 --- a/homeassistant/components/waze_travel_time/translations/no.json +++ b/homeassistant/components/waze_travel_time/translations/no.json @@ -14,7 +14,7 @@ "origin": "Opprinnelse", "region": "Region" }, - "description": "For opprinnelse og destinasjon, skriv inn adressen eller GPS-koordinatene til stedet (GPS-koordinatene m\u00e5 v\u00e6re atskilt med komma). Du kan ogs\u00e5 angi en enhets-ID som gir denne informasjonen i sin tilstand, en enhets-id med breddegrad og lengdegrad eller attributt navn." + "description": "For opprinnelse og destinasjon, skriv inn adressen eller GPS-koordinatene til stedet (GPS-koordinatene m\u00e5 v\u00e6re atskilt med komma). Du kan ogs\u00e5 angi en entitets-ID som gir denne informasjonen i sin tilstand, en entitets-id med breddegrad og lengdegrad eller attributt navn." } } }, diff --git a/homeassistant/components/wilight/translations/ja.json b/homeassistant/components/wilight/translations/ja.json index 48023a1ca57..0a01f69d43a 100644 --- a/homeassistant/components/wilight/translations/ja.json +++ b/homeassistant/components/wilight/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "not_supported_device": "\u3053\u306eWiLight\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "not_wilight_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001WiLight\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index d37a81baaf0..8af9c32c079 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -2,6 +2,7 @@ "state": { "wolflink__state": { "1_x_warmwasser": "1 x DHW", + "abgasklappe": "\u6392\u7159\u30ac\u30b9\u30c0\u30f3\u30d1\u30fc(Flue gas damper)", "absenkbetrieb": "\u30bb\u30c3\u30c8\u30d0\u30c3\u30af\u30e2\u30fc\u30c9", "absenkstop": "\u30bb\u30c3\u30c8\u30d0\u30c3\u30af \u30b9\u30c8\u30c3\u30d7", "aktiviert": "\u6709\u52b9\u5316", @@ -12,23 +13,31 @@ "auto": "\u30aa\u30fc\u30c8", "automatik_aus": "\u81ea\u52d5\u30aa\u30d5", "automatik_ein": "\u81ea\u52d5\u30aa\u30f3", + "bereit_keine_ladung": "\u6e96\u5099\u5b8c\u4e86\u3001\u8aad\u307f\u8fbc\u307f\u4e2d\u3067\u306f\u306a\u3044", + "betrieb_ohne_brenner": "\u30d0\u30fc\u30ca\u30fc\u306a\u3057\u3067\u306e\u4f5c\u696d", + "cooling": "\u51b7\u5374", "deaktiviert": "\u975e\u6d3b\u6027", "dhw_prior": "DHWPrior", "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", + "externe_deaktivierung": "\u5916\u90e8\u306e\u975e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", "frost_heizkreis": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u306e\u971c", + "frost_warmwasser": "DHW\u30d5\u30ed\u30b9\u30c8", "frostschutz": "\u971c\u9632\u6b62", "gasdruck": "\u30ac\u30b9\u5727", "glt_betrieb": "BMS\u30e2\u30fc\u30c9", "gradienten_uberwachung": "\u50be\u659c\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0", "heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u30e2\u30fc\u30c9", + "heizgerat_mit_speicher": "\u30b7\u30ea\u30f3\u30c0\u30fc\u4ed8\u304d\u30dc\u30a4\u30e9\u30fc", "heizung": "\u6696\u623f(\u52a0\u71b1)", "initialisierung": "\u521d\u671f\u5316", "kalibration": "\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u306e\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9 \u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9", + "kombigerat": "\u30b3\u30f3\u30d3 \u30dc\u30a4\u30e9\u30fc", "nachlauf_heizkreispumpe": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u30dd\u30f3\u30d7\u306e\u4f5c\u52d5", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", "parallelbetrieb": "\u30d1\u30e9\u30ec\u30eb\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index f459b030e9b..87e757a2c5d 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "\u8ffd\u52a0\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u63a5\u7d9a\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "\u8ffd\u52a0\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u63a5\u7d9a\u3057\u305f\u3044Xiaomi Aqara Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "settings": { diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 06a0d29c608..65e7f90d253 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -13,7 +13,8 @@ "cloud_login_error": "Impossible de se connecter \u00e0 Xioami Miio Cloud, v\u00e9rifiez les informations d'identification.", "cloud_no_devices": "Aucun appareil trouv\u00e9 dans ce compte cloud Xiaomi Miio.", "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.", - "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration." + "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration.", + "wrong_token": "Erreur de somme de contr\u00f4le, jeton incorrect" }, "flow_title": "Xiaomi Miio: {name}", "step": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 89521148137..3fd080778ab 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -64,7 +64,7 @@ "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "close": "\u9589\u3058\u308b", "dim_down": "\u8584\u6697\u304f\u3059\u308b", - "dim_up": "\u8584\u660e\u308b\u304f\u3059\u308b", + "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", "open": "\u30aa\u30fc\u30d7\u30f3", "right": "\u53f3", diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 6fd456998c3..248c8d001e6 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -82,7 +82,7 @@ "event.value_notification.basic": "{subtype} \u306a\u30d9\u30fc\u30b7\u30c3\u30af CC \u30a4\u30d9\u30f3\u30c8", "event.value_notification.central_scene": "{subtype} \u306e\u30bb\u30f3\u30c8\u30e9\u30eb \u30b7\u30fc\u30f3 \u30a2\u30af\u30b7\u30e7\u30f3", "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", - "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", + "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u5316\u3057\u307e\u3057\u305f", "zwave_js.value_updated.config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u306e\u5909\u66f4", "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u306e\u5909\u66f4" } From a29cc29304686d8ed3d68b0e6dedc958628783bc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Nov 2021 02:06:11 +0100 Subject: [PATCH 0942/1452] Fix DeprecationWarning for asyncio.wait() in mysensors (#60512) --- homeassistant/components/mysensors/gateway.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index fa7bf1ca88d..b167c8c58de 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -246,15 +246,8 @@ async def finish_setup( hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ) -> None: """Load any persistent devices and platforms and start gateway.""" - discover_tasks = [] - start_tasks = [] - discover_tasks.append(_discover_persistent_devices(hass, entry, gateway)) - start_tasks.append(_gw_start(hass, entry, gateway)) - if discover_tasks: - # Make sure all devices and platforms are loaded before gateway start. - await asyncio.wait(discover_tasks) - if start_tasks: - await asyncio.wait(start_tasks) + await _discover_persistent_devices(hass, entry, gateway) + await _gw_start(hass, entry, gateway) async def _discover_persistent_devices( From 110fd261ee196daabf05b4ebaca3156619f1d804 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 29 Nov 2021 05:18:15 +0100 Subject: [PATCH 0943/1452] Fix & update for latest Shelly Valve firmware (#60458) --- homeassistant/components/shelly/__init__.py | 3 +-- homeassistant/components/shelly/climate.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4109130ab80..27f25211a96 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,13 +68,12 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ "binary_sensor", "button", - "climate", "cover", "light", "sensor", "switch", ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 8574ac17f46..f2db157ecf2 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -49,6 +49,9 @@ async def async_setup_entry( if get_device_entry_gen(config_entry) == 2: return + device_block: Block | None = None + sensor_block: Block | None = None + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] for block in wrapper.device.blocks: if block.type == "device": @@ -176,7 +179,10 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): return preset_index = self._attr_preset_modes.index(preset_mode) - await self.set_state_full_path( - schedule=(0 if preset_index == 0 else 1), - schedule_profile=f"{preset_index}", - ) + + if preset_index == 0: + await self.set_state_full_path(schedule=0) + else: + await self.set_state_full_path( + schedule=1, schedule_profile=f"{preset_index}" + ) From 325861addf58a8796e10fa77b9637021a3c7c31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 29 Nov 2021 06:18:39 +0100 Subject: [PATCH 0944/1452] Set internal quality_scale for the safe_mode integration (#60526) --- homeassistant/components/safe_mode/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json index 78a656511bd..5ce7c3abf7b 100644 --- a/homeassistant/components/safe_mode/manifest.json +++ b/homeassistant/components/safe_mode/manifest.json @@ -4,5 +4,6 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/safe_mode", "dependencies": ["frontend", "persistent_notification", "cloud"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } From c535f785a8920eb556a8a2251c13be5225a6e86d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Nov 2021 19:21:33 -1000 Subject: [PATCH 0945/1452] Bump flux_led to 0.25.2 (#60519) - Fixes warm/cold values on RGBCW bulbs with newer firmware - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.25.1...0.25.2 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 5740644c93e..fcd66521153 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.1"], + "requirements": ["flux_led==0.25.2"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5e6ab6c13b4..63be0cabace 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.1 +flux_led==0.25.2 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8e91756a36..8388016e44d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.1 +flux_led==0.25.2 # homeassistant.components.homekit fnvhash==0.1.0 From 70b8decfb51cf90bf87d9c77489ce393cb60a1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 29 Nov 2021 08:22:48 +0100 Subject: [PATCH 0946/1452] Replace Tibber STATE_CLASS_TOTAL_INCREASING with STATE_CLASS_TOTAL for not strictly increasing sensors (#60501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, fix #60490 not strictly increasing Signed-off-by: Daniel Hjelseth Høyer * Tibber, fix #60490 not strictly increasing Signed-off-by: Daniel Hjelseth Høyer * Tibber, fix #60490 not strictly increasing Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index e6a305b6f61..df79f287cf7 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, @@ -87,7 +88,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=STATE_CLASS_TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", @@ -101,7 +102,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( name="accumulated production", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=STATE_CLASS_TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", From 15bf4dae9bb76cec1d7636ef96a3bf8b94c5557b Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Sun, 28 Nov 2021 23:27:32 -0800 Subject: [PATCH 0947/1452] Add zwave_js speed configurations for GE/Jasco 12730 and 14287 fans (#60517) --- .../components/zwave_js/discovery.py | 29 ++++- .../zwave_js/discovery_data_template.py | 45 ++++++++ tests/components/zwave_js/conftest.py | 12 +- ...trol_state.json => fan_generic_state.json} | 18 +-- tests/components/zwave_js/test_fan.py | 109 ++++++++++++++---- 5 files changed, 176 insertions(+), 37 deletions(-) rename tests/components/zwave_js/fixtures/{in_wall_smart_fan_control_state.json => fan_generic_state.json} (96%) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 5620afb9714..6f2d83f99c3 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -47,6 +47,7 @@ from .discovery_data_template import ( ConfigurableFanSpeedDataTemplate, CoverTiltDataTemplate, DynamicCurrentTempClimateDataTemplate, + FixedFanSpeedDataTemplate, NumericSensorDataTemplate, ZwaveValueID, ) @@ -230,11 +231,35 @@ DISCOVERY_SCHEMAS = [ product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), - # GE/Jasco fan controllers using switch multilevel CC + # GE/Jasco - In-Wall Smart Fan Control - 12730 / ZW4002 + ZWaveDiscoverySchema( + platform="fan", + hint="configured_fan_speed", + manufacturer_id={0x0063}, + product_id={0x3034}, + product_type={0x4944}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + data_template=FixedFanSpeedDataTemplate( + speeds=[33, 67, 99], + ), + ), + # GE/Jasco - In-Wall Smart Fan Control - 14287 / ZW4002 + ZWaveDiscoverySchema( + platform="fan", + hint="configured_fan_speed", + manufacturer_id={0x0063}, + product_id={0x3131}, + product_type={0x4944}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + data_template=FixedFanSpeedDataTemplate( + speeds=[32, 66, 99], + ), + ), + # GE/Jasco - In-Wall Smart Fan Control - 14314 / ZW4002 ZWaveDiscoverySchema( platform="fan", manufacturer_id={0x0063}, - product_id={0x3034, 0x3131, 0x3138}, + product_id={0x3138}, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 3c5ef220ae9..3e7db7cdcd9 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -523,3 +523,48 @@ class ConfigurableFanSpeedDataTemplate( return None return speed_config + + +@dataclass +class FixedFanSpeedValueMix: + """Mixin data class for defining supported fan speeds.""" + + speeds: list[int] + + def __post_init__(self) -> None: + """ + Validate inputs. + + These inputs are hardcoded in `discovery.py`, so these checks should + only fail due to developer error. + """ + assert len(self.speeds) > 0 + assert sorted(self.speeds) == self.speeds + + +@dataclass +class FixedFanSpeedDataTemplate( + BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, FixedFanSpeedValueMix +): + """ + Specifies a fixed set of fan speeds. + + Example: + ZWaveDiscoverySchema( + platform="fan", + hint="configured_fan_speed", + ... + data_template=FixedFanSpeedDataTemplate( + speeds=[32,65,99] + ), + ), + + `speeds` indicates the maximum setting on the underlying fan controller + for each actual speed. + """ + + def get_speed_config( + self, resolved_data: dict[str, ZwaveConfigurationValue] + ) -> list[int]: + """Get the fan speed configuration for this device.""" + return self.speeds diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 6d75f899e4c..e2db9fe7a6b 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -326,10 +326,10 @@ def window_cover_state_fixture(): return json.loads(load_fixture("zwave_js/chain_actuator_zws12_state.json")) -@pytest.fixture(name="in_wall_smart_fan_control_state", scope="session") -def in_wall_smart_fan_control_state_fixture(): +@pytest.fixture(name="fan_generic_state", scope="session") +def fan_generic_state_fixture(): """Load the fan node state fixture data.""" - return json.loads(load_fixture("zwave_js/in_wall_smart_fan_control_state.json")) + return json.loads(load_fixture("zwave_js/fan_generic_state.json")) @pytest.fixture(name="hs_fc200_state", scope="session") @@ -695,10 +695,10 @@ def window_cover_fixture(client, chain_actuator_zws12_state): return node -@pytest.fixture(name="in_wall_smart_fan_control") -def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): +@pytest.fixture(name="fan_generic") +def fan_generic_fixture(client, fan_generic_state): """Mock a fan node.""" - node = Node(client, copy.deepcopy(in_wall_smart_fan_control_state)) + node = Node(client, copy.deepcopy(fan_generic_state)) client.driver.controller.nodes[node.node_id] = node return node diff --git a/tests/components/zwave_js/fixtures/in_wall_smart_fan_control_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json similarity index 96% rename from tests/components/zwave_js/fixtures/in_wall_smart_fan_control_state.json rename to tests/components/zwave_js/fixtures/fan_generic_state.json index 74467664955..1f4f55dd220 100644 --- a/tests/components/zwave_js/fixtures/in_wall_smart_fan_control_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -19,22 +19,22 @@ "isSecure": false, "version": 4, "isBeaming": true, - "manufacturerId": 99, - "productId": 12593, - "productType": 18756, + "manufacturerId": 4919, + "productId": 4919, + "productType": 4919, "firmwareVersion": "5.22", "zwavePlusVersion": 1, "nodeType": 0, "roleType": 5, "deviceConfig": { - "manufacturerId": 99, - "manufacturer": "GE/Jasco", + "manufacturerId": 4919, + "manufacturer": "Unknown", "label": "ZW4002", - "description": "In-Wall Smart Fan Control", + "description": "Generic Fan Controller", "devices": [ { - "productType": "0x4944", - "productId": "0x3131" + "productType": "0x1337", + "productId": "0x1337" } ], "firmwareVersion": { @@ -349,4 +349,4 @@ } } ] - } \ No newline at end of file + } diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index f5d8a0b89e3..74ad642127f 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -11,14 +11,12 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, ) -STANDARD_FAN_ENTITY = "fan.in_wall_smart_fan_control" -HS_FAN_ENTITY = "fan.scene_capable_fan_control_switch" - -async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration): - """Test the fan entity.""" - node = in_wall_smart_fan_control - state = hass.states.get(STANDARD_FAN_ENTITY) +async def test_generic_fan(hass, client, fan_generic, integration): + """Test the fan entity for a generic fan that lacks specific speed configuration.""" + node = fan_generic + entity_id = "fan.generic_fan_controller" + state = hass.states.get(entity_id) assert state assert state.state == "off" @@ -27,7 +25,7 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration await hass.services.async_call( "fan", "turn_on", - {"entity_id": STANDARD_FAN_ENTITY, "speed": SPEED_MEDIUM}, + {"entity_id": entity_id, "speed": SPEED_MEDIUM}, blocking=True, ) @@ -60,7 +58,7 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration await hass.services.async_call( "fan", "set_speed", - {"entity_id": STANDARD_FAN_ENTITY, "speed": 99}, + {"entity_id": entity_id, "speed": 99}, blocking=True, ) @@ -70,7 +68,7 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration await hass.services.async_call( "fan", "turn_on", - {"entity_id": STANDARD_FAN_ENTITY}, + {"entity_id": entity_id}, blocking=True, ) @@ -102,7 +100,7 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration await hass.services.async_call( "fan", "turn_off", - {"entity_id": STANDARD_FAN_ENTITY}, + {"entity_id": entity_id}, blocking=True, ) @@ -150,7 +148,7 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration ) node.receive_event(event) - state = hass.states.get(STANDARD_FAN_ENTITY) + state = hass.states.get(entity_id) assert state.state == "on" assert state.attributes[ATTR_SPEED] == "high" @@ -175,13 +173,16 @@ async def test_standard_fan(hass, client, in_wall_smart_fan_control, integration ) node.receive_event(event) - state = hass.states.get(STANDARD_FAN_ENTITY) + state = hass.states.get(entity_id) assert state.state == "off" assert state.attributes[ATTR_SPEED] == "off" -async def test_hs_fan(hass, client, hs_fc200, integration): +async def test_configurable_speeds_fan(hass, client, hs_fc200, integration): """Test a fan entity with configurable speeds.""" + node = hs_fc200 + node_id = 39 + entity_id = "fan.scene_capable_fan_control_switch" async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" @@ -189,14 +190,14 @@ async def test_hs_fan(hass, client, hs_fc200, integration): await hass.services.async_call( "fan", "turn_on", - {"entity_id": HS_FAN_ENTITY, "percentage": percentage}, + {"entity_id": entity_id, "percentage": percentage}, 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"] == 39 + assert args["nodeId"] == node_id return args["value"] async def get_percentage_from_zwave_speed(zwave_speed): @@ -206,7 +207,7 @@ async def test_hs_fan(hass, client, hs_fc200, integration): data={ "source": "node", "event": "value updated", - "nodeId": 39, + "nodeId": node_id, "args": { "commandClassName": "Multilevel Switch", "commandClass": 38, @@ -218,10 +219,12 @@ async def test_hs_fan(hass, client, hs_fc200, integration): }, }, ) - hs_fc200.receive_event(event) - state = hass.states.get(HS_FAN_ENTITY) + node.receive_event(event) + state = hass.states.get(entity_id) return state.attributes[ATTR_PERCENTAGE] + # In 3-speed mode, the speeds are: + # low = 1-33, med=34-66, high=67-99 percentages_to_zwave_speeds = [ [[0], [0]], [range(1, 34), range(1, 34)], @@ -237,5 +240,71 @@ async def test_hs_fan(hass, client, hs_fc200, integration): actual_percentage = await get_percentage_from_zwave_speed(zwave_speed) assert actual_percentage in percentages - state = hass.states.get(HS_FAN_ENTITY) + state = hass.states.get(entity_id) + assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + + +async def test_fixed_speeds_fan(hass, client, ge_12730, integration): + """Test a fan entity with fixed speeds.""" + node = ge_12730 + node_id = 24 + entity_id = "fan.in_wall_smart_fan_control" + + async def get_zwave_speed_from_percentage(percentage): + """Set the fan to a particular percentage and get the resulting Zwave speed.""" + client.async_send_command.reset_mock() + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": entity_id, "percentage": percentage}, + 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_id + return args["value"] + + async def get_percentage_from_zwave_speed(zwave_speed): + """Set the underlying device speed and get the resulting percentage.""" + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": zwave_speed, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(entity_id) + return state.attributes[ATTR_PERCENTAGE] + + # This device has the speeds: + # low = 1-33, med = 34-67, high = 68-99 + percentages_to_zwave_speeds = [ + [[0], [0]], + [range(1, 34), range(1, 34)], + [range(34, 68), range(34, 68)], + [range(68, 101), range(68, 100)], + ] + + for percentages, zwave_speeds in percentages_to_zwave_speeds: + for percentage in percentages: + actual_zwave_speed = await get_zwave_speed_from_percentage(percentage) + assert actual_zwave_speed in zwave_speeds + for zwave_speed in zwave_speeds: + actual_percentage = await get_percentage_from_zwave_speed(zwave_speed) + assert actual_percentage in percentages + + state = hass.states.get(entity_id) assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) From ef8cf9e59714fffc96b799ac952610dbe0f77969 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Nov 2021 21:44:11 -1000 Subject: [PATCH 0948/1452] Add configuration_url to bond (#60523) --- homeassistant/components/bond/__init__.py | 3 ++- homeassistant/components/bond/config_flow.py | 2 +- homeassistant/components/bond/entity.py | 1 + homeassistant/components/bond/utils.py | 3 ++- tests/components/bond/test_fan.py | 6 +++++- tests/components/bond/test_init.py | 1 + 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index bf40d4c6066..501ddfb6888 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: timeout=ClientTimeout(total=_API_TIMEOUT), session=async_get_clientsession(hass), ) - hub = BondHub(bond) + hub = BondHub(bond, host) try: await hub.setup() except ClientResponseError as ex: @@ -78,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=hub.target, sw_version=hub.fw_ver, suggested_area=hub.location, + configuration_url=f"http://{host}", ) _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index d9398edf2c9..cca8930532d 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -47,7 +47,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass) ) try: - hub = BondHub(bond) + hub = BondHub(bond, data[CONF_HOST]) await hub.setup(max_devices=1) except ClientConnectionError as error: raise InputValidationError("cannot_connect") from error diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 5f37de4fa19..342e407ff48 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -65,6 +65,7 @@ class BondEntity(Entity): manufacturer=self._hub.make, # type ignore: tuple items should not be Optional identifiers={(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type] + configuration_url=f"http://{self._hub.host}", ) if self.name is not None: device_info[ATTR_NAME] = self.name diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index abe3cc98002..b9dbe07ea50 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -124,9 +124,10 @@ class BondDevice: class BondHub: """Hub device representing Bond Bridge.""" - def __init__(self, bond: Bond) -> None: + def __init__(self, bond: Bond, host: str) -> None: """Initialize Bond Hub.""" self.bond: Bond = bond + self.host = host self._bridge: dict[str, Any] = {} self._version: dict[str, Any] = {} self._devices: list[BondDevice] = [] diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index d24128617d2..0c58596bb7f 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -25,7 +25,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util import utcnow @@ -84,6 +84,10 @@ async def test_entity_registry(hass: core.HomeAssistant): entity = registry.entities["fan.name_1"] assert entity.unique_id == "test-hub-id_test-device-id" + device_registry = dr.async_get(hass) + device = device_registry.async_get(entity.device_id) + assert device.configuration_url == "http://some host" + async def test_non_standard_speed_list(hass: core.HomeAssistant): """Tests that the device is registered with custom speed list if number of supported speeds differs form 3.""" diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 42eca44dfa7..db54ffdf716 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -107,6 +107,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss assert hub.manufacturer == "Olibra" assert hub.model == "test-model" assert hub.sw_version == "test-version" + assert hub.configuration_url == "http://some host" # verify supported domains are setup assert len(mock_cover_async_setup_entry.mock_calls) == 1 From 823c37f4c77c94828a66e4eef11ded63c2d7da2f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 29 Nov 2021 09:15:10 +0100 Subject: [PATCH 0949/1452] 100% test coverage for Fronius integration (#60515) * support for multiple fixture sets * add test data from Fronius Gen24 device * test Gen24 with Storage * test Gen24 inverter without storage * 100% coverage * copy&paste fail --- tests/components/fronius/__init__.py | 34 +-- .../fronius/fixtures/gen24/GetAPIVersion.json | 5 + .../fixtures/gen24/GetInverterInfo.json | 25 +++ .../GetInverterRealtimeData_Device_1.json | 80 +++++++ .../fronius/fixtures/gen24/GetLoggerInfo.json | 14 ++ .../fixtures/gen24/GetMeterRealtimeData.json | 61 +++++ .../gen24/GetOhmPilotRealtimeData.json | 16 ++ .../gen24/GetPowerFlowRealtimeData.json | 45 ++++ .../gen24/GetStorageRealtimeData.json | 16 ++ .../fixtures/gen24_storage/GetAPIVersion.json | 5 + .../gen24_storage/GetInverterInfo.json | 25 +++ .../GetInverterRealtimeData_Device_1.json | 80 +++++++ .../fixtures/gen24_storage/GetLoggerInfo.json | 14 ++ .../gen24_storage/GetMeterRealtimeData.json | 61 +++++ .../GetOhmPilotRealtimeData.json | 30 +++ .../GetPowerFlowRealtimeData.json | 51 +++++ .../gen24_storage/GetStorageRealtimeData.json | 36 +++ ... => GetInverterRealtimeData_Device_1.json} | 0 ...tInverterRealtimeData_Device_1_night.json} | 0 ..._System.json => GetMeterRealtimeData.json} | 0 .../symo/GetMeterRealtimeData_Device_0.json | 60 ----- .../symo/GetOhmPilotRealtimeData.json | 17 ++ ...day.json => GetPowerFlowRealtimeData.json} | 0 ...ystem.json => GetStorageRealtimeData.json} | 0 tests/components/fronius/test_init.py | 23 ++ tests/components/fronius/test_sensor.py | 210 +++++++++++++++++- 26 files changed, 831 insertions(+), 77 deletions(-) create mode 100644 tests/components/fronius/fixtures/gen24/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/gen24/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/gen24/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json rename tests/components/fronius/fixtures/symo/{GetInverterRealtimeDate_Device_1_day.json => GetInverterRealtimeData_Device_1.json} (100%) rename tests/components/fronius/fixtures/symo/{GetInverterRealtimeDate_Device_1_night.json => GetInverterRealtimeData_Device_1_night.json} (100%) rename tests/components/fronius/fixtures/symo/{GetMeterRealtimeData_System.json => GetMeterRealtimeData.json} (100%) delete mode 100644 tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json create mode 100644 tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json rename tests/components/fronius/fixtures/symo/{GetPowerFlowRealtimeData_day.json => GetPowerFlowRealtimeData.json} (100%) rename tests/components/fronius/fixtures/symo/{GetStorageRealtimeData_System.json => GetStorageRealtimeData.json} (100%) diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 37c9481e32e..683575a11c8 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,6 +1,8 @@ """Tests for the Fronius integration.""" from homeassistant.components.fronius.const import DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt @@ -11,14 +13,16 @@ MOCK_HOST = "http://fronius" MOCK_UID = "123.4567890" # has to match mocked logger unique_id -async def setup_fronius_integration(hass): +async def setup_fronius_integration( + hass: HomeAssistant, is_logger: bool = True +) -> ConfigEntry: """Create the Fronius integration.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_UID, data={ CONF_HOST: MOCK_HOST, - "is_logger": True, + "is_logger": is_logger, }, ) entry.add_to_hass(hass) @@ -30,48 +34,50 @@ async def setup_fronius_integration(hass): def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, + fixture_set: str = "symo", night: bool = False, ) -> None: """Mock responses for Fronius Symo inverter with meter.""" aioclient_mock.clear_requests() - _day_or_night = "night" if night else "day" + _night = "_night" if night else "" aioclient_mock.get( f"{host}/solar_api/GetAPIVersion.cgi", - text=load_fixture("symo/GetAPIVersion.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" "DeviceId=1&DataCollection=CommonInverterData", text=load_fixture( - f"symo/GetInverterRealtimeDate_Device_1_{_day_or_night}.json", "fronius" + f"{fixture_set}/GetInverterRealtimeData_Device_1{_night}.json", + "fronius", ), ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", - text=load_fixture("symo/GetInverterInfo.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), ) aioclient_mock.get( f"{host}/solar_api/v1/GetLoggerInfo.cgi", - text=load_fixture("symo/GetLoggerInfo.json", "fronius"), - ) - aioclient_mock.get( - f"{host}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0", - text=load_fixture("symo/GetMeterRealtimeData_Device_0.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetLoggerInfo.json", "fronius"), ) aioclient_mock.get( f"{host}/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System", - text=load_fixture("symo/GetMeterRealtimeData_System.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetMeterRealtimeData.json", "fronius"), ) aioclient_mock.get( f"{host}/solar_api/v1/GetPowerFlowRealtimeData.fcgi", text=load_fixture( - f"symo/GetPowerFlowRealtimeData_{_day_or_night}.json", "fronius" + f"{fixture_set}/GetPowerFlowRealtimeData{_night}.json", "fronius" ), ) aioclient_mock.get( f"{host}/solar_api/v1/GetStorageRealtimeData.cgi?Scope=System", - text=load_fixture("symo/GetStorageRealtimeData_System.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetStorageRealtimeData.json", "fronius"), + ) + aioclient_mock.get( + f"{host}/solar_api/v1/GetOhmPilotRealtimeData.cgi?Scope=System", + text=load_fixture(f"{fixture_set}/GetOhmPilotRealtimeData.json", "fronius"), ) diff --git a/tests/components/fronius/fixtures/gen24/GetAPIVersion.json b/tests/components/fronius/fixtures/gen24/GetAPIVersion.json new file mode 100644 index 00000000000..a76e4813e5f --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.7-3" +} diff --git a/tests/components/fronius/fixtures/gen24/GetInverterInfo.json b/tests/components/fronius/fixtures/gen24/GetInverterInfo.json new file mode 100644 index 00000000000..8a20c6c806b --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetInverterInfo.json @@ -0,0 +1,25 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Inverter name", + "DT" : 1, + "ErrorCode" : 0, + "InverterState" : "Running", + "PVPower" : 9360, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "12345678" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:53+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..35e7b84f452 --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,80 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : null + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "InverterState" : "Running", + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 49.99169921875 + }, + "IAC" : { + "Unit" : "A", + "Value" : 0.15894582867622375 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.078323781490325928 + }, + "IDC_2" : { + "Unit" : "A", + "Value" : 0.075399093329906464 + }, + "IDC_3" : { + "Unit" : "A", + "Value" : null + }, + "PAC" : { + "Unit" : "W", + "Value" : 37.320449829101562 + }, + "SAC" : { + "Unit" : "VA", + "Value" : 37.40447998046875 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 1530193.4199999999 + }, + "UAC" : { + "Unit" : "V", + "Value" : 234.91676330566406 + }, + "UDC" : { + "Unit" : "V", + "Value" : 411.38107299804688 + }, + "UDC_2" : { + "Unit" : "V", + "Value" : 403.43124389648438 + }, + "UDC_3" : { + "Unit" : "V", + "Value" : null + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : null + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:53+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json b/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json new file mode 100644 index 00000000000..103da09d9ba --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 11, + "Reason" : "v1/GetLoggerInfo.cgi request is not supported.", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:53+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json new file mode 100644 index 00000000000..2fcc208468a --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json @@ -0,0 +1,61 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Current_AC_Phase_1" : 1.145, + "Current_AC_Phase_2" : 2.3300000000000001, + "Current_AC_Phase_3" : 1.825, + "Current_AC_Sum" : 5.2999999999999998, + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "Smart Meter TS 65A-3", + "Serial" : "1234567890" + }, + "Enable" : 1, + "EnergyReactive_VArAC_Sum_Consumed" : 88221.0, + "EnergyReactive_VArAC_Sum_Produced" : 1989125.0, + "EnergyReal_WAC_Minus_Absolute" : 3863340.0, + "EnergyReal_WAC_Plus_Absolute" : 2013105.0, + "EnergyReal_WAC_Sum_Consumed" : 2013105.0, + "EnergyReal_WAC_Sum_Produced" : 3863340.0, + "Frequency_Phase_Average" : 49.899999999999999, + "Meter_Location_Current" : 0.0, + "PowerApparent_S_Phase_1" : 243.30000000000001, + "PowerApparent_S_Phase_2" : 323.39999999999998, + "PowerApparent_S_Phase_3" : 301.19999999999999, + "PowerApparent_S_Sum" : 868.0, + "PowerFactor_Phase_1" : 0.441, + "PowerFactor_Phase_2" : 0.93400000000000005, + "PowerFactor_Phase_3" : 0.83199999999999996, + "PowerFactor_Sum" : 0.82799999999999996, + "PowerReactive_Q_Phase_1" : -218.59999999999999, + "PowerReactive_Q_Phase_2" : -132.80000000000001, + "PowerReactive_Q_Phase_3" : -166.0, + "PowerReactive_Q_Sum" : -517.39999999999998, + "PowerReal_P_Phase_1" : 106.8, + "PowerReal_P_Phase_2" : 294.89999999999998, + "PowerReal_P_Phase_3" : 251.30000000000001, + "PowerReal_P_Sum" : 653.10000000000002, + "TimeStamp" : 1637924872.0, + "Visible" : 1.0, + "Voltage_AC_PhaseToPhase_12" : 408.69999999999999, + "Voltage_AC_PhaseToPhase_23" : 409.60000000000002, + "Voltage_AC_PhaseToPhase_31" : 409.39999999999998, + "Voltage_AC_Phase_1" : 235.90000000000001, + "Voltage_AC_Phase_2" : 236.09999999999999, + "Voltage_AC_Phase_3" : 236.90000000000001 + } + } + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:52+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..87d53b13d8b --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json @@ -0,0 +1,16 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:53+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..86fd9f12aff --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json @@ -0,0 +1,45 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "Battery_Mode" : "disabled", + "DT" : 1, + "E_Day" : null, + "E_Total" : 1530193.4199999999, + "E_Year" : null, + "P" : 37.320449829101562, + "SOC" : 0.0 + } + }, + "Site" : { + "BackupMode" : false, + "BatteryStandby" : false, + "E_Day" : null, + "E_Total" : 1530193.4199999999, + "E_Year" : null, + "Meter_Location" : "grid", + "Mode" : "meter", + "P_Akku" : null, + "P_Grid" : 658.39999999999998, + "P_Load" : -695.68274917602537, + "P_PV" : 62.948148727416992, + "rel_Autonomy" : 5.3591596485874495, + "rel_SelfConsumption" : 100.0 + }, + "Smartloads" : { + "Ohmpilots" : {} + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:53+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json new file mode 100644 index 00000000000..573a97b7a61 --- /dev/null +++ b/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json @@ -0,0 +1,16 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-26T11:07:52+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json b/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json new file mode 100644 index 00000000000..a76e4813e5f --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.7-3" +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json b/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json new file mode 100644 index 00000000000..f96f13ae0e8 --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json @@ -0,0 +1,25 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Gen24 Storage", + "DT" : 1, + "ErrorCode" : 0, + "InverterState" : "Running", + "PVPower" : 13930, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "12345678" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:12:59+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..0b7c01f56eb --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,80 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : null + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "InverterState" : "Running", + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 49.981552124023438 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.1086627244949341 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.39519637823104858 + }, + "IDC_2" : { + "Unit" : "A", + "Value" : 0.35640031099319458 + }, + "IDC_3" : { + "Unit" : "A", + "Value" : null + }, + "PAC" : { + "Unit" : "W", + "Value" : 250.90925598144531 + }, + "SAC" : { + "Unit" : "VA", + "Value" : 250.9410400390625 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 7512794.0116666667 + }, + "UAC" : { + "Unit" : "V", + "Value" : 227.35398864746094 + }, + "UDC" : { + "Unit" : "V", + "Value" : 419.10092163085938 + }, + "UDC_2" : { + "Unit" : "V", + "Value" : 318.81027221679688 + }, + "UDC_3" : { + "Unit" : "V", + "Value" : null + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : null + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:51:45+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json b/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json new file mode 100644 index 00000000000..55ad726cd83 --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 11, + "Reason" : "v1/GetLoggerInfo.cgi request is not supported.", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:17:43+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json new file mode 100644 index 00000000000..f32ba740eaa --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json @@ -0,0 +1,61 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Current_AC_Phase_1" : 1.7010000000000001, + "Current_AC_Phase_2" : 1.8320000000000001, + "Current_AC_Phase_3" : 0.64500000000000002, + "Current_AC_Sum" : 4.1780000000000008, + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "Smart Meter TS 65A-3", + "Serial" : "1234567890" + }, + "Enable" : 1, + "EnergyReactive_VArAC_Sum_Consumed" : 5482.0, + "EnergyReactive_VArAC_Sum_Produced" : 3266105.0, + "EnergyReal_WAC_Minus_Absolute" : 1705128.0, + "EnergyReal_WAC_Plus_Absolute" : 1247204.0, + "EnergyReal_WAC_Sum_Consumed" : 1247204.0, + "EnergyReal_WAC_Sum_Produced" : 1705128.0, + "Frequency_Phase_Average" : 49.899999999999999, + "Meter_Location_Current" : 0.0, + "PowerApparent_S_Phase_1" : 319.5, + "PowerApparent_S_Phase_2" : 383.89999999999998, + "PowerApparent_S_Phase_3" : 118.40000000000001, + "PowerApparent_S_Sum" : 821.89999999999998, + "PowerFactor_Phase_1" : 0.995, + "PowerFactor_Phase_2" : 0.38900000000000001, + "PowerFactor_Phase_3" : 0.16300000000000001, + "PowerFactor_Sum" : 0.69799999999999995, + "PowerReactive_Q_Phase_1" : -31.300000000000001, + "PowerReactive_Q_Phase_2" : -353.39999999999998, + "PowerReactive_Q_Phase_3" : -116.7, + "PowerReactive_Q_Sum" : -501.5, + "PowerReal_P_Phase_1" : 317.89999999999998, + "PowerReal_P_Phase_2" : 150.0, + "PowerReal_P_Phase_3" : 19.600000000000001, + "PowerReal_P_Sum" : 487.69999999999999, + "TimeStamp" : 1638104813.0, + "Visible" : 1.0, + "Voltage_AC_PhaseToPhase_12" : 396.0, + "Voltage_AC_PhaseToPhase_23" : 393.0, + "Voltage_AC_PhaseToPhase_31" : 394.30000000000001, + "Voltage_AC_Phase_1" : 229.40000000000001, + "Voltage_AC_Phase_2" : 225.59999999999999, + "Voltage_AC_Phase_3" : 228.30000000000001 + } + } + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:06:54+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..0215201ad6e --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json @@ -0,0 +1,30 @@ +{ + "Body" : { + "Data" : { + "0" : { + "CodeOfState" : 0.0, + "Details" : { + "Hardware" : "6", + "Manufacturer" : "Fronius", + "Model" : "Ohmpilot", + "Serial" : "23456789", + "Software" : "1.0.25-3" + }, + "EnergyReal_WAC_Sum_Consumed" : 1233295.0, + "PowerReal_PAC_Sum" : 0.0, + "Temperature_Channel_1" : 38.899999999999999 + } + } + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:11:42+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..fcd82a9bf1d --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json @@ -0,0 +1,51 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "Battery_Mode" : "suspended", + "DT" : 1, + "E_Day" : null, + "E_Total" : 7512664.4041666668, + "E_Year" : null, + "P" : 186.54914855957031, + "SOC" : 4.5999999999999996 + } + }, + "Site" : { + "BackupMode" : true, + "BatteryStandby" : false, + "E_Day" : null, + "E_Total" : 7512664.4041666668, + "E_Year" : null, + "Meter_Location" : "grid", + "Mode" : "bidirectional", + "P_Akku" : 0.15907810628414154, + "P_Grid" : 2274.9000000000001, + "P_Load" : -2459.3092254638673, + "P_PV" : 216.43276786804199, + "rel_Autonomy" : 7.4984155532163506, + "rel_SelfConsumption" : 100.0 + }, + "Smartloads" : { + "Ohmpilots" : { + "0" : { + "P_AC_Total" : 0.0, + "State" : "normal", + "Temperature" : 38.799999999999997 + } + } + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:12:20+00:00" + } +} diff --git a/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json new file mode 100644 index 00000000000..d810962c66c --- /dev/null +++ b/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json @@ -0,0 +1,36 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Controller" : { + "Capacity_Maximum" : 16588, + "Current_DC" : 0.0, + "DesignedCapacity" : 16588, + "Details" : { + "Manufacturer" : "BYD", + "Model" : "BYD Battery-Box Premium HV", + "Serial" : "P030T020Z2001234567 " + }, + "Enable" : 1, + "StateOfCharge_Relative" : 4.5999999999999996, + "Status_BatteryCell" : 0.0, + "Temperature_Cell" : 21.5, + "TimeStamp" : 1638105056.0, + "Voltage_DC" : 0.0 + }, + "Modules" : [] + } + } + }, + "Head" : { + "RequestArguments" : { + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T13:10:57+00:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_day.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json similarity index 100% rename from tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_day.json rename to tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json diff --git a/tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_night.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json similarity index 100% rename from tests/components/fronius/fixtures/symo/GetInverterRealtimeDate_Device_1_night.json rename to tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json diff --git a/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_System.json b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json similarity index 100% rename from tests/components/fronius/fixtures/symo/GetMeterRealtimeData_System.json rename to tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json diff --git a/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json deleted file mode 100644 index 762ec968dbc..00000000000 --- a/tests/components/fronius/fixtures/symo/GetMeterRealtimeData_Device_0.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "Body": { - "Data": { - "Current_AC_Phase_1": 7.7549999999999999, - "Current_AC_Phase_2": 6.6799999999999997, - "Current_AC_Phase_3": 10.102, - "Details": { - "Manufacturer": "Fronius", - "Model": "Smart Meter 63A", - "Serial": "12345678" - }, - "Enable": 1, - "EnergyReactive_VArAC_Sum_Consumed": 59960790, - "EnergyReactive_VArAC_Sum_Produced": 723160, - "EnergyReal_WAC_Minus_Absolute": 35623065, - "EnergyReal_WAC_Plus_Absolute": 15303334, - "EnergyReal_WAC_Sum_Consumed": 15303334, - "EnergyReal_WAC_Sum_Produced": 35623065, - "Frequency_Phase_Average": 50, - "Meter_Location_Current": 0, - "PowerApparent_S_Phase_1": 1772.7929999999999, - "PowerApparent_S_Phase_2": 1527.048, - "PowerApparent_S_Phase_3": 2333.5619999999999, - "PowerApparent_S_Sum": 5592.5699999999997, - "PowerFactor_Phase_1": -0.98999999999999999, - "PowerFactor_Phase_2": -0.98999999999999999, - "PowerFactor_Phase_3": 0.98999999999999999, - "PowerFactor_Sum": 1, - "PowerReactive_Q_Phase_1": 51.479999999999997, - "PowerReactive_Q_Phase_2": 115.63, - "PowerReactive_Q_Phase_3": -164.24000000000001, - "PowerReactive_Q_Sum": 2.8700000000000001, - "PowerReal_P_Phase_1": 1765.55, - "PowerReal_P_Phase_2": 1515.8, - "PowerReal_P_Phase_3": 2311.2199999999998, - "PowerReal_P_Sum": 5592.5699999999997, - "TimeStamp": 1633977078, - "Visible": 1, - "Voltage_AC_PhaseToPhase_12": 395.89999999999998, - "Voltage_AC_PhaseToPhase_23": 398, - "Voltage_AC_PhaseToPhase_31": 398, - "Voltage_AC_Phase_1": 228.59999999999999, - "Voltage_AC_Phase_2": 228.59999999999999, - "Voltage_AC_Phase_3": 231 - } - }, - "Head": { - "RequestArguments": { - "DeviceClass": "Meter", - "DeviceId": "0", - "Scope": "Device" - }, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-11T20:31:18+02:00" - } -} \ No newline at end of file diff --git a/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..38cbde318ab --- /dev/null +++ b/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "OhmPilot", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-11-28T22:36:04+01:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_day.json b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json similarity index 100% rename from tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_day.json rename to tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json diff --git a/tests/components/fronius/fixtures/symo/GetStorageRealtimeData_System.json b/tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json similarity index 100% rename from tests/components/fronius/fixtures/symo/GetStorageRealtimeData_System.json rename to tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json diff --git a/tests/components/fronius/test_init.py b/tests/components/fronius/test_init.py index 6ceeb273cba..bfbd631ff37 100644 --- a/tests/components/fronius/test_init.py +++ b/tests/components/fronius/test_init.py @@ -1,4 +1,8 @@ """Test the Fronius integration.""" +from unittest.mock import patch + +from pyfronius import FroniusError + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -21,3 +25,22 @@ async def test_unload_config_entry(hass, aioclient_mock): assert test_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_logger_error(hass, aioclient_mock): + """Test setup when logger reports an error.""" + # gen24 dataset will raise FroniusError when logger is called + mock_responses(aioclient_mock, fixture_set="gen24") + config_entry = await setup_fronius_integration(hass, is_logger=True) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_inverter_error(hass, aioclient_mock): + """Test setup when inverter_info reports an error.""" + mock_responses(aioclient_mock) + with patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ): + config_entry = await setup_fronius_integration(hass) + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index b4e56a06d62..82ec7afd8ea 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -57,6 +57,17 @@ async def test_symo_inverter(hass, aioclient_mock): assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + # Third test at nighttime - additional AC entities aren't changed + mock_responses(aioclient_mock, night=True) + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + async def test_symo_logger(hass, aioclient_mock): """Test Fronius Symo logger entities.""" @@ -75,7 +86,7 @@ async def test_symo_logger(hass, aioclient_mock): # software_version, time_zone, time_zone_location # time_stamp, unique_identifier, utc_offset # - # states are rounded to 2 decimals + # states are rounded to 4 decimals assert_state( "sensor.cash_factor_fronius_logger_info_0_http_fronius", 0.078, @@ -109,7 +120,7 @@ async def test_symo_meter(hass, aioclient_mock): # ignored entities: # manufacturer, model, serial, enable, timestamp, visible, meter_location # - # states are rounded to 2 decimals + # states are rounded to 4 decimals assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.102) @@ -173,7 +184,7 @@ async def test_symo_power_flow(hass, aioclient_mock): assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 # ignored: location, mode, timestamp # - # states are rounded to 2 decimals + # states are rounded to 4 decimals assert_state( "sensor.energy_day_fronius_power_flow_0_http_fronius", 10828, @@ -255,3 +266,196 @@ async def test_symo_power_flow(hass, aioclient_mock): "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100, ) + + +async def test_gen24(hass, aioclient_mock): + """Test Fronius Gen24 inverter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="gen24") + config_entry = await setup_fronius_integration(hass, is_logger=False) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 57 + # inverter 1 + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", STATE_UNKNOWN) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 0.1589) + assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.0754) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", STATE_UNKNOWN) + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.0783) + assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 403.4312) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 37.3204) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 411.3811) + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 1530193.42) + assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 234.9168) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9917) + # meter + assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 3863340.0) + assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 2013105.0) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", 653.1) + assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) + assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.828) + assert_state( + "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 88221.0 + ) + assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 3863340.0) + assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 2.33) + assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 235.9) + assert_state( + "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 408.7 + ) + assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 294.9) + assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 2013105.0) + assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 236.1) + assert_state( + "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 1989125.0 + ) + assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 236.9) + assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.441) + assert_state( + "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 409.6 + ) + assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 1.825) + assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.832) + assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 243.3) + assert_state( + "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 409.4 + ) + assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 323.4) + assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 301.2) + assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 106.8) + assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.934) + assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 251.3) + assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -218.6) + assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -132.8) + assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -166.0) + assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 868.0) + assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -517.4) + assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.145) + # power_flow + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 658.4) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 + ) + assert_state( + "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 62.9481 + ) + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -695.6827) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "meter") + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 5.3592) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN + ) + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 1530193.42) + + +async def test_gen24_storage(hass, aioclient_mock): + """Test Fronius Gen24 inverter with BYD battery and Ohmpilot entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="gen24_storage") + config_entry = await setup_fronius_integration(hass, is_logger=False) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 31 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 63 + # inverter 1 + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) + assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) + assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.3564) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", STATE_UNKNOWN) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 1.1087) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 250.9093) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", STATE_UNKNOWN) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 7512794.0117) + assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 419.1009) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.354) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9816) + # meter + assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 1705128.0) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", 487.7) + assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.698) + assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 1247204.0) + assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) + assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -501.5) + assert_state( + "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 3266105.0 + ) + assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 19.6) + assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 0.645) + assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 1705128.0) + assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 383.9) + assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.701) + assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 1.832) + assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 319.5) + assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 229.4) + assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 150.0) + assert_state( + "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 394.3 + ) + assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 225.6) + assert_state( + "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 5482.0 + ) + assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 1247204.0) + assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.995) + assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.163) + assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.389) + assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -31.3) + assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -116.7) + assert_state( + "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 396.0 + ) + assert_state( + "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 393.0 + ) + assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -353.4) + assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 317.9) + assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) + assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) + assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + # power_flow + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) + assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591) + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2459.3092) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 + ) + assert_state( + "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 216.4328 + ) + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 7.4984) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "bidirectional") + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 7512664.4042) + # storage + assert_state("sensor.current_dc_fronius_storage_0_http_fronius", 0.0) + assert_state("sensor.state_of_charge_fronius_storage_0_http_fronius", 4.6) + assert_state("sensor.capacity_maximum_fronius_storage_0_http_fronius", 16588) + assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) + assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) + assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) From e5718ccac437d791274f8bb84fe8bfc9bba16b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 29 Nov 2021 10:47:14 +0200 Subject: [PATCH 0950/1452] Add removal versions and backcompat constants for device registry enum changes (#60421) --- homeassistant/helpers/device_registry.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 45a3d2bc795..7a78bf69ba7 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -57,6 +57,12 @@ class DeviceEntryDisabler(StrEnum): USER = "user" +# DISABLED_* are deprecated, to be removed in 2022.3 +DISABLED_CONFIG_ENTRY = DeviceEntryDisabler.CONFIG_ENTRY.value +DISABLED_INTEGRATION = DeviceEntryDisabler.INTEGRATION.value +DISABLED_USER = DeviceEntryDisabler.USER.value + + class DeviceEntryType(StrEnum): """Device entry type.""" @@ -348,8 +354,9 @@ class DeviceRegistry: if isinstance(entry_type, str) and not isinstance(entry_type, DeviceEntryType): report( # type: ignore[unreachable] - "uses str for device registry entry_type. This is deprecated, " - "it should be updated to use DeviceEntryType instead", + "uses str for device registry entry_type. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "DeviceEntryType instead", error_if_core=False, ) entry_type = DeviceEntryType(entry_type) @@ -445,8 +452,9 @@ class DeviceRegistry: disabled_by, DeviceEntryDisabler ): report( # type: ignore[unreachable] - "uses str for device registry disabled_by. This is deprecated, " - "it should be updated to use DeviceEntryDisabler instead", + "uses str for device registry disabled_by. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "DeviceEntryDisabler instead", error_if_core=False, ) disabled_by = DeviceEntryDisabler(disabled_by) From 622d9606a57d262ed83f9718579fd7b0d79b2b7d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Nov 2021 21:57:37 +1300 Subject: [PATCH 0951/1452] Esphome/button (#60522) --- .coveragerc | 1 + homeassistant/components/esphome/button.py | 44 +++++++++++++++++++ .../components/esphome/entry_data.py | 2 + .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/button.py diff --git a/.coveragerc b/.coveragerc index e7863427d63..519fa1b3937 100644 --- a/.coveragerc +++ b/.coveragerc @@ -286,6 +286,7 @@ omit = homeassistant/components/eq3btsmart/climate.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py + homeassistant/components/esphome/button.py homeassistant/components/esphome/camera.py homeassistant/components/esphome/climate.py homeassistant/components/esphome/cover.py diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py new file mode 100644 index 00000000000..b2b780ccaff --- /dev/null +++ b/homeassistant/components/esphome/button.py @@ -0,0 +1,44 @@ +"""Support for ESPHome buttons.""" +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import ButtonInfo +from aioesphomeapi.model import EntityState + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ESPHome buttons based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="button", + info_type=ButtonInfo, + entity_type=EsphomeButton, + state_type=EntityState, + ) + + +class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): + """A button implementation for ESPHome.""" + + @callback + def _on_device_update(self) -> None: + """Update the entity state when device info has changed.""" + # This override the EsphomeEntity method as the button entity + # never gets a state update. + self._on_state_update() + + async def async_press(self, **kwargs: Any) -> None: + """Press the button.""" + await self._client.button_command(self._static_info.key) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 847997731d4..e7bbc27141c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -26,6 +26,7 @@ from aioesphomeapi import ( TextSensorInfo, UserService, ) +from aioesphomeapi.model import ButtonInfo from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -37,6 +38,7 @@ SAVE_DELAY = 120 # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: "binary_sensor", + ButtonInfo: "button", CameraInfo: "camera", ClimateInfo: "climate", CoverInfo: "cover", diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 247c78abb92..3b4eea3395c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.2.0"], + "requirements": ["aioesphomeapi==10.3.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 63be0cabace..39eb3e19908 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.2.0 +aioesphomeapi==10.3.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8388016e44d..63865061d31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.2.0 +aioesphomeapi==10.3.0 # homeassistant.components.flo aioflo==0.4.1 From 5e86c78c46a81f8570b86cb6806cb9f463aba058 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 00:59:01 -0800 Subject: [PATCH 0952/1452] Redact ?auth= url parameters in stream sources when logged (#60438) --- homeassistant/components/stream/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 77e946770e6..731a610abf9 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -55,12 +55,17 @@ from .hls import HlsStreamOutput, async_setup_hls _LOGGER = logging.getLogger(__name__) -STREAM_SOURCE_RE = re.compile("//.*:.*@") +STREAM_SOURCE_REDACT_PATTERN = [ + (re.compile(r"//.*:.*@"), "//****:****@"), + (re.compile(r"\?auth=.*"), "?auth=****"), +] def redact_credentials(data: str) -> str: """Redact credentials from string data.""" - return STREAM_SOURCE_RE.sub("//****:****@", data) + for (pattern, repl) in STREAM_SOURCE_REDACT_PATTERN: + data = pattern.sub(repl, data) + return data def create_stream( From dd001cacfd7343c71d8e6f7ff17c2178a6eacf7d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Nov 2021 22:06:56 +1300 Subject: [PATCH 0953/1452] Fix missing name for ESPHome reauth dialog (#60508) --- homeassistant/components/esphome/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index fab4b045d0c..94c1f80a5e4 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -71,6 +71,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._port = entry.data[CONF_PORT] self._password = entry.data[CONF_PASSWORD] self._noise_psk = entry.data.get(CONF_NOISE_PSK) + self._name = entry.title return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( From 4cff04cbd5a2a71e58de5a91253ed56c96788611 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Nov 2021 11:32:30 +0100 Subject: [PATCH 0954/1452] Remove obsolete light attributes from WLED (#60535) --- homeassistant/components/wled/light.py | 11 ----------- tests/components/wled/test_light.py | 10 ---------- 2 files changed, 21 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 2081208e398..d2c92989100 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -216,17 +216,6 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): return super().available - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the entity.""" - segment = self.coordinator.data.state.segments[self._segment] - return { - ATTR_INTENSITY: segment.intensity, - ATTR_PALETTE: segment.palette.name, - ATTR_REVERSE: segment.reverse, - ATTR_SPEED: segment.speed, - } - @property def rgb_color(self) -> tuple[int, int, int] | None: """Return the color value.""" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 7826fe5521b..6036176d233 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -55,11 +55,6 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_EFFECT) == "Solid" assert state.attributes.get(ATTR_HS_COLOR) == (37.412, 100.0) assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" - assert state.attributes.get(ATTR_INTENSITY) == 128 - assert state.attributes.get(ATTR_PALETTE) == "Default" - assert state.attributes.get(ATTR_PRESET) is None - assert state.attributes.get(ATTR_REVERSE) is False - assert state.attributes.get(ATTR_SPEED) == 32 assert state.state == STATE_ON entry = entity_registry.async_get("light.wled_rgb_light") @@ -73,11 +68,6 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_EFFECT) == "Blink" assert state.attributes.get(ATTR_HS_COLOR) == (148.941, 100.0) assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" - assert state.attributes.get(ATTR_INTENSITY) == 64 - assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" - assert state.attributes.get(ATTR_PRESET) is None - assert state.attributes.get(ATTR_REVERSE) is True - assert state.attributes.get(ATTR_SPEED) == 16 assert state.state == STATE_ON entry = entity_registry.async_get("light.wled_rgb_light_segment_1") From 909784bff863c3306e571e065f76906f6ed50887 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Nov 2021 11:51:58 +0100 Subject: [PATCH 0955/1452] Upgrade black to 21.11b1 (#60532) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4382543675e..91216c9efa9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.11b0 + rev: 21.11b1 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 5e2ae42ca4a..66786035e98 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.11b0 +black==21.11b1 codespell==2.0.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 From 1f2a5ae98dccee962195236bae197415c7efb851 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Nov 2021 11:52:13 +0100 Subject: [PATCH 0956/1452] Upgrade coverage to 6.2.0 (#60530) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 4e08d5b320f..6184e2dc224 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.1.2 +coverage==6.2.0 freezegun==1.1.0 jsonpickle==1.4.1 mock-open==1.4.0 From 8600b5597a22b4784bf1aa9091678628c0b7d0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Mon, 29 Nov 2021 08:41:52 -0500 Subject: [PATCH 0957/1452] Add 'trigger' support for MQTT Alarm Control Panel (#60525) When configuring an Alarm Control Panel through MQTT discovery, it was not possible to use the trigger service. This fixes that by making it available the same way as ARM and DISARM services are. --- homeassistant/components/mqtt/abbreviations.py | 2 ++ .../components/mqtt/alarm_control_panel.py | 18 ++++++++++++++++++ .../mqtt/test_alarm_control_panel.py | 9 ++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 1daa6f837c7..9ad5ca4ce1c 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -39,6 +39,7 @@ ABBREVIATIONS = { "cmd_tpl": "command_template", "cod_arm_req": "code_arm_required", "cod_dis_req": "code_disarm_required", + "cod_trig_req": "code_trigger_required", "curr_temp_t": "current_temperature_topic", "curr_temp_tpl": "current_temperature_template", "dev": "device", @@ -147,6 +148,7 @@ ABBREVIATIONS = { "pl_ret": "payload_return_to_base", "pl_toff": "payload_turn_off", "pl_ton": "payload_turn_on", + "pl_trig": "payload_trigger", "pl_unlk": "payload_unlock", "pos_clsd": "position_closed", "pos_open": "position_open", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index ffea92f14c3..3c324c0789b 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -12,6 +12,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, SUPPORT_ALARM_ARM_VACATION, + SUPPORT_ALARM_TRIGGER, ) from homeassistant.const import ( CONF_CODE, @@ -43,12 +44,14 @@ _LOGGER = logging.getLogger(__name__) CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" +CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" CONF_PAYLOAD_DISARM = "payload_disarm" CONF_PAYLOAD_ARM_HOME = "payload_arm_home" CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation" CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" +CONF_PAYLOAD_TRIGGER = "payload_trigger" CONF_COMMAND_TEMPLATE = "command_template" MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset( @@ -66,6 +69,7 @@ DEFAULT_ARM_AWAY = "ARM_AWAY" DEFAULT_ARM_HOME = "ARM_HOME" DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" DEFAULT_DISARM = "DISARM" +DEFAULT_TRIGGER = "TRIGGER" DEFAULT_NAME = "MQTT Alarm" REMOTE_CODE = "REMOTE_CODE" @@ -76,6 +80,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, + vol.Optional(CONF_CODE_TRIGGER_REQUIRED, default=True): cv.boolean, vol.Optional( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, @@ -91,6 +96,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS ): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, + vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -203,6 +209,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): | SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_VACATION | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + | SUPPORT_ALARM_TRIGGER ) @property @@ -286,6 +293,17 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] await self._publish(code, action) + async def async_alarm_trigger(self, code=None): + """Send trigger command. + + This method is a coroutine. + """ + code_required = self._config[CONF_CODE_TRIGGER_REQUIRED] + if code_required and not self._validate_code(code, "triggering"): + return + action = self._config[CONF_PAYLOAD_TRIGGER] + await self._publish(code, action) + async def _publish(self, code, action): """Publish via mqtt.""" command_template = self._config[CONF_COMMAND_TEMPLATE] diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 1351ae59496..2a74a75c241 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, @@ -192,6 +193,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), (SERVICE_ALARM_DISARM, "DISARM"), + (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): @@ -222,6 +224,7 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), (SERVICE_ALARM_DISARM, "DISARM"), + (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): @@ -271,6 +274,7 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), (SERVICE_ALARM_DISARM, "DISARM"), + (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): @@ -311,6 +315,7 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), (SERVICE_ALARM_DISARM, "DISARM"), + (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payload): @@ -351,6 +356,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION", "code_arm_required"), (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS", "code_arm_required"), (SERVICE_ALARM_DISARM, "DISARM", "code_disarm_required"), + (SERVICE_ALARM_TRIGGER, "TRIGGER", "code_trigger_required"), ], ) async def test_publish_mqtt_with_code_required_false( @@ -358,7 +364,8 @@ async def test_publish_mqtt_with_code_required_false( ): """Test publishing of MQTT messages when code is configured. - code_arm_required = False / code_disarm_required = false + code_arm_required = False / code_disarm_required = False / + code_trigger_required = False """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN][disable_code] = False From 5a97db668590e880ed800303d959afec7973b108 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Nov 2021 14:43:02 +0100 Subject: [PATCH 0958/1452] Upgrade wled to 0.10.1 (#60542) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 8c87880ad38..a99278a80c6 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.10.0"], + "requirements": ["wled==0.10.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 39eb3e19908..a91244636cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2433,7 +2433,7 @@ wirelesstagpy==0.8.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.10.0 +wled==0.10.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63865061d31..150eb82c072 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.10.0 +wled==0.10.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 09af85c6a4e96fd371bbd08146003947fda38743 Mon Sep 17 00:00:00 2001 From: rianadon Date: Mon, 29 Nov 2021 05:44:44 -0800 Subject: [PATCH 0959/1452] Add native unit types for weather entities (#59533) * Add native unit types for weather entities * Update weatherentity and change precision in climacell test * Move weather test to demo tests * Add weather test for temperature conversion * Add more unit conversion tests * Remove extra native_ methods * Remove extra properties and save precision change for another PR * Remove visibility_unit from metoffice component The vibility values given by metoffice are formatted into strings, which means they can't automatically be converted. * Improve docstrings and convert pressures in forecast * Add precipitation and wind speed units * Clean up tests * Round converted weather values * Round weather values to 2 decimal places * Move number of rounding decimal places to constant * Docstring and styles --- homeassistant/components/metoffice/weather.py | 7 +- homeassistant/components/weather/__init__.py | 90 ++++++++-- .../{weather => demo}/test_weather.py | 2 +- tests/components/weather/test_init.py | 170 ++++++++++++++++++ .../custom_components/test/weather.py | 126 +++++++++++++ 5 files changed, 378 insertions(+), 17 deletions(-) rename tests/components/{weather => demo}/test_weather.py (98%) create mode 100644 tests/components/weather/test_init.py create mode 100644 tests/testing_config/custom_components/test/weather.py diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 56d94409db5..6ce1a49e6f1 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -8,7 +8,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) -from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -117,11 +117,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): _visibility = f"{visibility_class} - {visibility_distance}" return _visibility - @property - def visibility_unit(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - @property def pressure(self): """Return the mean sea-level pressure.""" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 81d245c19bb..b9fa7e2ae39 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -61,6 +61,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=30) +ROUNDING_PRECISION = 2 + class Forecast(TypedDict, total=False): """Typed weather forecast dict.""" @@ -112,38 +114,52 @@ class WeatherEntity(Entity): _attr_ozone: float | None = None _attr_precision: float _attr_pressure: float | None = None + _attr_pressure_unit: str | None = None _attr_state: None = None _attr_temperature_unit: str _attr_temperature: float | None _attr_visibility: float | None = None + _attr_visibility_unit: str | None = None + _attr_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None _attr_wind_speed: float | None = None + _attr_wind_speed_unit: str | None = None @property def temperature(self) -> float | None: - """Return the platform temperature.""" + """Return the platform temperature in native units (i.e. not converted).""" return self._attr_temperature @property def temperature_unit(self) -> str: - """Return the unit of measurement.""" + """Return the native unit of measurement for temperature.""" return self._attr_temperature_unit @property def pressure(self) -> float | None: - """Return the pressure.""" + """Return the pressure in native units.""" return self._attr_pressure + @property + def pressure_unit(self) -> str | None: + """Return the native unit of measurement for pressure.""" + return self._attr_pressure_unit + @property def humidity(self) -> float | None: - """Return the humidity.""" + """Return the humidity in native units.""" return self._attr_humidity @property def wind_speed(self) -> float | None: - """Return the wind speed.""" + """Return the wind speed in native units.""" return self._attr_wind_speed + @property + def wind_speed_unit(self) -> str | None: + """Return the native unit of measurement for wind speed.""" + return self._attr_wind_speed_unit + @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" @@ -156,17 +172,27 @@ class WeatherEntity(Entity): @property def visibility(self) -> float | None: - """Return the visibility.""" + """Return the visibility in native units.""" return self._attr_visibility + @property + def visibility_unit(self) -> str | None: + """Return the native unit of measurement for visibility.""" + return self._attr_visibility_unit + @property def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" + """Return the forecast in native units.""" return self._attr_forecast + @property + def precipitation_unit(self) -> str | None: + """Return the native unit of measurement for accumulated precipitation.""" + return self._attr_precipitation_unit + @property def precision(self) -> float: - """Return the precision of the temperature value.""" + """Return the precision of the temperature value, after unit conversion.""" if hasattr(self, "_attr_precision"): return self._attr_precision return ( @@ -178,11 +204,14 @@ class WeatherEntity(Entity): @final @property def state_attributes(self): - """Return the state attributes.""" + """Return the state attributes, converted from native units to user-configured units.""" data = {} if self.temperature is not None: data[ATTR_WEATHER_TEMPERATURE] = show_temp( - self.hass, self.temperature, self.temperature_unit, self.precision + self.hass, + self.temperature, + self.temperature_unit, + self.precision, ) if (humidity := self.humidity) is not None: @@ -192,15 +221,28 @@ class WeatherEntity(Entity): data[ATTR_WEATHER_OZONE] = ozone if (pressure := self.pressure) is not None: + if (unit := self.pressure_unit) is not None: + pressure = round( + self.hass.config.units.pressure(pressure, unit), ROUNDING_PRECISION + ) data[ATTR_WEATHER_PRESSURE] = pressure if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing if (wind_speed := self.wind_speed) is not None: + if (unit := self.wind_speed_unit) is not None: + wind_speed = round( + self.hass.config.units.wind_speed(wind_speed, unit), + ROUNDING_PRECISION, + ) data[ATTR_WEATHER_WIND_SPEED] = wind_speed if (visibility := self.visibility) is not None: + if (unit := self.visibility_unit) is not None: + visibility = round( + self.hass.config.units.length(visibility, unit), ROUNDING_PRECISION + ) data[ATTR_WEATHER_VISIBILITY] = visibility if self.forecast is not None: @@ -220,6 +262,34 @@ class WeatherEntity(Entity): self.temperature_unit, self.precision, ) + if ATTR_FORECAST_PRESSURE in forecast_entry: + if (unit := self.pressure_unit) is not None: + pressure = round( + self.hass.config.units.pressure( + forecast_entry[ATTR_FORECAST_PRESSURE], unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_PRESSURE] = pressure + if ATTR_FORECAST_WIND_SPEED in forecast_entry: + if (unit := self.wind_speed_unit) is not None: + wind_speed = round( + self.hass.config.units.wind_speed( + forecast_entry[ATTR_FORECAST_WIND_SPEED], unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed + if ATTR_FORECAST_PRECIPITATION in forecast_entry: + if (unit := self.precipitation_unit) is not None: + precipitation = round( + self.hass.config.units.accumulated_precipitation( + forecast_entry[ATTR_FORECAST_PRECIPITATION], unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation + forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast diff --git a/tests/components/weather/test_weather.py b/tests/components/demo/test_weather.py similarity index 98% rename from tests/components/weather/test_weather.py rename to tests/components/demo/test_weather.py index 3057532668a..c4ae8fcd79c 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -1,4 +1,4 @@ -"""The tests for the Weather component.""" +"""The tests for the demo weather component.""" from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_FORECAST, diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py new file mode 100644 index 00000000000..4125e94749a --- /dev/null +++ b/tests/components/weather/test_init.py @@ -0,0 +1,170 @@ +"""The test for weather entity.""" +import pytest +from pytest import approx + +from homeassistant.components.weather import ( + ATTR_CONDITION_SUNNY, + ATTR_FORECAST, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.const import ( + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + TEMP_FAHRENHEIT, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.speed import convert as convert_speed +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + + +async def create_entity(hass, **kwargs): + """Create the weather entity to run tests on.""" + kwargs = {"temperature": None, "temperature_unit": None, **kwargs} + platform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", condition=ATTR_CONDITION_SUNNY, **kwargs + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + return entity0 + + +@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) +async def test_temperature_conversion( + hass, + enable_custom_integrations, + unit_system, +): + """Test temperature conversion.""" + hass.config.units = unit_system + native_value = 38 + native_unit = TEMP_FAHRENHEIT + + entity0 = await create_entity( + hass, temperature=native_value, temperature_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = convert_temperature( + native_value, native_unit, unit_system.temperature_unit + ) + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected, rel=0.1 + ) + assert float(forecast[ATTR_FORECAST_TEMP]) == approx(expected, rel=0.1) + assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) + + +@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) +async def test_pressure_conversion( + hass, + enable_custom_integrations, + unit_system, +): + """Test pressure conversion.""" + hass.config.units = unit_system + native_value = 30 + native_unit = PRESSURE_INHG + + entity0 = await create_entity( + hass, pressure=native_value, pressure_unit=native_unit + ) + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = convert_pressure(native_value, native_unit, unit_system.pressure_unit) + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) + assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) +async def test_wind_speed_conversion( + hass, + enable_custom_integrations, + unit_system, +): + """Test wind speed conversion.""" + hass.config.units = unit_system + native_value = 10 + native_unit = SPEED_METERS_PER_SECOND + + entity0 = await create_entity( + hass, wind_speed=native_value, wind_speed_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = convert_speed(native_value, native_unit, unit_system.wind_speed_unit) + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected, rel=1e-2 + ) + assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) +async def test_visibility_conversion( + hass, + enable_custom_integrations, + unit_system, +): + """Test visibility conversion.""" + hass.config.units = unit_system + native_value = 10 + native_unit = LENGTH_MILES + + entity0 = await create_entity( + hass, visibility=native_value, visibility_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + expected = convert_distance(native_value, native_unit, unit_system.length_unit) + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) +async def test_precipitation_conversion( + hass, + enable_custom_integrations, + unit_system, +): + """Test precipitation conversion.""" + hass.config.units = unit_system + native_value = 30 + native_unit = LENGTH_MILLIMETERS + + entity0 = await create_entity( + hass, precipitation=native_value, precipitation_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = convert_distance( + native_value, native_unit, unit_system.accumulated_precipitation_unit + ) + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py new file mode 100644 index 00000000000..224d6495548 --- /dev/null +++ b/tests/testing_config/custom_components/test/weather.py @@ -0,0 +1,126 @@ +""" +Provide a mock weather platform. + +Call init before using it in your tests to ensure clean test data. +""" +from __future__ import annotations + +from homeassistant.components.weather import ( + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + Forecast, + WeatherEntity, +) + +from tests.common import MockEntity + +ENTITIES = [] + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + ENTITIES = [] if empty else [MockWeather()] + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) + + +class MockWeather(MockEntity, WeatherEntity): + """Mock weather class.""" + + @property + def temperature(self) -> float | None: + """Return the platform temperature.""" + return self._handle("temperature") + + @property + def temperature_unit(self) -> str | None: + """Return the unit of measurement for temperature.""" + return self._handle("temperature_unit") + + @property + def pressure(self) -> float | None: + """Return the pressure.""" + return self._handle("pressure") + + @property + def pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + return self._handle("pressure_unit") + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + return self._handle("humidity") + + @property + def wind_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("wind_speed") + + @property + def wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + return self._handle("wind_speed_unit") + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + return self._handle("wind_bearing") + + @property + def ozone(self) -> float | None: + """Return the ozone level.""" + return self._handle("ozone") + + @property + def visibility(self) -> float | None: + """Return the visibility.""" + return self._handle("visibility") + + @property + def visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + return self._handle("visibility_unit") + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self._handle("forecast") + + @property + def precipitation_unit(self) -> str | None: + """Return the native unit of measurement for accumulated precipitation.""" + return self._handle("precipitation_unit") + + @property + def condition(self) -> str | None: + """Return the current condition.""" + return self._handle("condition") + + +class MockWeatherMockForecast(MockWeather): + """Mock weather class with mocked forecast.""" + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return [ + { + ATTR_FORECAST_TEMP: self.temperature, + ATTR_FORECAST_TEMP_LOW: self.temperature, + ATTR_FORECAST_PRESSURE: self.pressure, + ATTR_FORECAST_WIND_SPEED: self.wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"), + } + ] From 3aa35e15c2324107df498fa182ced040017dd54f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Nov 2021 15:15:50 +0100 Subject: [PATCH 0960/1452] Remove custom WLED services (#60537) --- homeassistant/components/wled/const.py | 7 - homeassistant/components/wled/light.py | 100 +---------- tests/components/wled/test_light.py | 236 +------------------------ 3 files changed, 2 insertions(+), 341 deletions(-) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 25c1fea8f9a..e323b5ab87b 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -19,18 +19,11 @@ ATTR_DURATION = "duration" ATTR_FADE = "fade" ATTR_INTENSITY = "intensity" ATTR_ON = "on" -ATTR_PALETTE = "palette" -ATTR_PRESET = "preset" -ATTR_REVERSE = "reverse" ATTR_SEGMENT_ID = "segment_id" ATTR_SOFTWARE_VERSION = "sw_version" ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" -# Services -SERVICE_EFFECT = "effect" -SERVICE_PRESET = "preset" - # Device classes DEVICE_CLASS_WLED_LIVE_OVERRIDE: Final = "wled__live_override" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index d2c92989100..b42c7b0a8b4 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -4,8 +4,6 @@ from __future__ import annotations from functools import partial from typing import Any, Tuple, cast -import voluptuous as vol - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -21,23 +19,9 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_COLOR_PRIMARY, - ATTR_INTENSITY, - ATTR_ON, - ATTR_PALETTE, - ATTR_PRESET, - ATTR_REVERSE, - ATTR_SEGMENT_ID, - ATTR_SPEED, - DOMAIN, - LOGGER, - SERVICE_EFFECT, - SERVICE_PRESET, -) +from .const import ATTR_COLOR_PRIMARY, ATTR_ON, ATTR_SEGMENT_ID, DOMAIN from .coordinator import WLEDDataUpdateCoordinator from .helpers import wled_exception_handler from .models import WLEDEntity @@ -52,35 +36,6 @@ async def async_setup_entry( ) -> None: """Set up WLED light based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_EFFECT, - { - vol.Optional(ATTR_EFFECT): vol.Any(cv.positive_int, cv.string), - vol.Optional(ATTR_INTENSITY): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) - ), - vol.Optional(ATTR_PALETTE): vol.Any(cv.positive_int, cv.string), - vol.Optional(ATTR_REVERSE): cv.boolean, - vol.Optional(ATTR_SPEED): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) - ), - }, - "async_effect", - ) - - platform.async_register_entity_service( - SERVICE_PRESET, - { - vol.Required(ATTR_PRESET): vol.All( - vol.Coerce(int), vol.Range(min=-1, max=65535) - ), - }, - "async_preset", - ) - if coordinator.keep_master_light: async_add_entities([WLEDMasterLight(coordinator=coordinator)]) @@ -146,32 +101,6 @@ class WLEDMasterLight(WLEDEntity, LightEntity): on=True, brightness=kwargs.get(ATTR_BRIGHTNESS), transition=transition ) - async def async_effect( - self, - effect: int | str | None = None, - intensity: int | None = None, - palette: int | str | None = None, - reverse: bool | None = None, - speed: int | None = None, - ) -> None: - """Set the effect of a WLED light.""" - # Master light does not have an effect setting. - - @wled_exception_handler - async def async_preset( - self, - preset: int, - ) -> None: - """Set a WLED light to a saved preset.""" - # The WLED preset service is replaced by a preset select entity - # and marked deprecated as of Home Assistant 2021.8 - LOGGER.warning( - "The 'wled.preset' service is deprecated and replaced by a " - "dedicated preset select entity; Please use that entity to " - "change presets instead" - ) - await self.coordinator.wled.preset(preset=preset) - class WLEDSegmentLight(WLEDEntity, LightEntity): """Defines a WLED light based on a segment.""" @@ -323,33 +252,6 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): await self.coordinator.wled.segment(**data) - @wled_exception_handler - async def async_effect( - self, - effect: int | str | None = None, - intensity: int | None = None, - palette: int | str | None = None, - reverse: bool | None = None, - speed: int | None = None, - ) -> None: - """Set the effect of a WLED light.""" - await self.coordinator.wled.segment( - segment_id=self._segment, - effect=effect, - intensity=intensity, - palette=palette, - reverse=reverse, - speed=speed, - ) - - @wled_exception_handler - async def async_preset( - self, - preset: int, - ) -> None: - """Set a WLED light to a saved preset.""" - await self.coordinator.wled.preset(preset=preset) - @callback def async_update_segments( diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 6036176d233..fb2efce404a 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -14,18 +14,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.wled.const import ( - ATTR_INTENSITY, - ATTR_PALETTE, - ATTR_PRESET, - ATTR_REVERSE, - ATTR_SPEED, - CONF_KEEP_MASTER_LIGHT, - DOMAIN, - SCAN_INTERVAL, - SERVICE_EFFECT, - SERVICE_PRESET, -) +from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, @@ -390,229 +379,6 @@ async def test_rgbw_light( ) -async def test_effect_service( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock -) -> None: - """Test the effect service of a WLED light.""" - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_INTENSITY: 200, - ATTR_PALETTE: "Tiamat", - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 1 - mock_wled.segment.assert_called_with( - effect="Rainbow", - intensity=200, - palette="Tiamat", - reverse=True, - segment_id=0, - speed=100, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 2 - mock_wled.segment.assert_called_with( - segment_id=0, - effect=9, - intensity=None, - palette=None, - reverse=None, - speed=None, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_INTENSITY: 200, - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 3 - mock_wled.segment.assert_called_with( - intensity=200, - reverse=True, - segment_id=0, - speed=100, - effect=None, - palette=None, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_PALETTE: "Tiamat", - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 4 - mock_wled.segment.assert_called_with( - effect="Rainbow", - palette="Tiamat", - reverse=True, - segment_id=0, - speed=100, - intensity=None, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_INTENSITY: 200, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 5 - mock_wled.segment.assert_called_with( - effect="Rainbow", - intensity=200, - segment_id=0, - speed=100, - palette=None, - reverse=None, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_INTENSITY: 200, - ATTR_REVERSE: True, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.segment.call_count == 6 - mock_wled.segment.assert_called_with( - effect="Rainbow", - intensity=200, - reverse=True, - segment_id=0, - palette=None, - speed=None, - ) - - -async def test_effect_service_error( - hass: HomeAssistant, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test error handling of the WLED effect service.""" - mock_wled.segment.side_effect = WLEDError - - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgb_light") - assert state - assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text - assert mock_wled.segment.call_count == 1 - mock_wled.segment.assert_called_with( - effect=9, segment_id=0, intensity=None, palette=None, reverse=None, speed=None - ) - - -async def test_preset_service( - hass: HomeAssistant, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the preset service of a WLED light.""" - await hass.services.async_call( - DOMAIN, - SERVICE_PRESET, - { - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_PRESET: 1, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.preset.call_count == 1 - mock_wled.preset.assert_called_with(preset=1) - - await hass.services.async_call( - DOMAIN, - SERVICE_PRESET, - { - ATTR_ENTITY_ID: "light.wled_rgb_light_master", - ATTR_PRESET: 2, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.preset.call_count == 2 - mock_wled.preset.assert_called_with(preset=2) - - assert "The 'wled.preset' service is deprecated" in caplog.text - - -async def test_preset_service_error( - hass: HomeAssistant, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test error handling of the WLED preset service.""" - mock_wled.preset.side_effect = WLEDError - - await hass.services.async_call( - DOMAIN, - SERVICE_PRESET, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_PRESET: 1}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgb_light") - assert state - assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text - assert mock_wled.preset.call_count == 1 - mock_wled.preset.assert_called_with(preset=1) - - @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) async def test_single_segment_with_keep_master_light( hass: HomeAssistant, From a88cc8b98c1ba466e849b41dd5c610d28ac51883 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 29 Nov 2021 09:00:37 -0600 Subject: [PATCH 0961/1452] Move Sonos bass & treble controls to number entities (#60498) --- homeassistant/components/sonos/const.py | 10 ++- .../components/sonos/media_player.py | 35 ---------- homeassistant/components/sonos/number.py | 64 +++++++++++++++++++ homeassistant/components/sonos/services.yaml | 24 ------- homeassistant/components/sonos/speaker.py | 19 +++--- tests/components/sonos/conftest.py | 2 + 6 files changed, 86 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/sonos/number.py diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 05abc662b48..4b636b3e0f6 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ) +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -27,7 +28,13 @@ UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager" -PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN} +PLATFORMS = { + BINARY_SENSOR_DOMAIN, + MP_DOMAIN, + NUMBER_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +} SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" @@ -139,6 +146,7 @@ SONOS_CHECK_ACTIVITY = "sonos_check_activity" SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" +SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f5389c7966a..664245a1c99 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -108,7 +108,6 @@ SERVICE_RESTORE = "restore" SERVICE_SET_TIMER = "set_sleep_timer" SERVICE_CLEAR_TIMER = "clear_sleep_timer" SERVICE_UPDATE_ALARM = "update_alarm" -SERVICE_SET_OPTION = "set_option" SERVICE_PLAY_QUEUE = "play_queue" SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue" @@ -120,8 +119,6 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_MASTER = "master" ATTR_WITH_GROUP = "with_group" ATTR_QUEUE_POSITION = "queue_position" -ATTR_EQ_BASS = "bass_level" -ATTR_EQ_TREBLE = "treble_level" async def async_setup_entry( @@ -225,19 +222,6 @@ async def async_setup_entry( "set_alarm", ) - platform.async_register_entity_service( # type: ignore - SERVICE_SET_OPTION, - { - vol.Optional(ATTR_EQ_BASS): vol.All( - vol.Coerce(int), vol.Range(min=-10, max=10) - ), - vol.Optional(ATTR_EQ_TREBLE): vol.All( - vol.Coerce(int), vol.Range(min=-10, max=10) - ), - }, - "set_option", - ) - platform.async_register_entity_service( # type: ignore SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, @@ -605,19 +589,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): alarm.include_linked_zones = include_linked_zones alarm.save() - @soco_error() - def set_option( - self, - bass_level: int | None = None, - treble_level: int | None = None, - ) -> None: - """Modify playback options.""" - if bass_level is not None: - self.soco.bass = bass_level - - if treble_level is not None: - self.soco.treble = treble_level - @soco_error() def play_queue(self, queue_position: int = 0) -> None: """Start playing the queue.""" @@ -635,12 +606,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ATTR_SONOS_GROUP: self.speaker.sonos_group_entities } - if self.speaker.bass_level is not None: - attributes[ATTR_EQ_BASS] = self.speaker.bass_level - - if self.speaker.treble_level is not None: - attributes[ATTR_EQ_TREBLE] = self.speaker.treble_level - if self.media.queue_position is not None: attributes[ATTR_QUEUE_POSITION] = self.media.queue_position diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py new file mode 100644 index 00000000000..372a89edda0 --- /dev/null +++ b/homeassistant/components/sonos/number.py @@ -0,0 +1,64 @@ +"""Entity representing a Sonos number control.""" +from __future__ import annotations + +from homeassistant.components.number import NumberEntity +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import SONOS_CREATE_LEVELS +from .entity import SonosEntity +from .helpers import soco_error +from .speaker import SonosSpeaker + +LEVEL_TYPES = ("bass", "treble") + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Sonos number platform from a config entry.""" + + async def _async_create_entities(speaker: SonosSpeaker) -> None: + entities = [] + for level_type in LEVEL_TYPES: + entities.append(SonosLevelEntity(speaker, level_type)) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_LEVELS, _async_create_entities) + ) + + +class SonosLevelEntity(SonosEntity, NumberEntity): + """Representation of a Sonos level entity.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_min_value = -10 + _attr_max_value = 10 + + def __init__(self, speaker: SonosSpeaker, level_type: str) -> None: + """Initialize the level entity.""" + super().__init__(speaker) + self.level_type = level_type + + @property + def unique_id(self) -> str: + """Return the unique ID.""" + return f"{self.soco.uid}-{self.level_type}" + + @property + def name(self) -> str: + """Return the name.""" + return f"{self.speaker.zone_name} {self.level_type.capitalize()}" + + async def _async_poll(self) -> None: + """Poll the value if subscriptions are not working.""" + # Handled by SonosSpeaker + + @soco_error() + def set_value(self, value: float) -> None: + """Set a new value.""" + setattr(self.soco, self.level_type, value) + + @property + def value(self) -> float: + """Return the current value.""" + return getattr(self.speaker, self.level_type) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index af664f0b367..4f04b2407ff 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -87,30 +87,6 @@ clear_sleep_timer: device: integration: sonos -set_option: - name: Set option - description: Set Sonos sound options. - target: - device: - integration: sonos - fields: - bass_level: - name: Bass Level - description: Bass level for EQ. - selector: - number: - min: -10 - max: 10 - mode: box - treble_level: - name: Treble Level - description: Treble level for EQ. - selector: - number: - min: -10 - max: 10 - mode: box - play_queue: name: Play queue description: Start playing the queue from the first item. diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 1acb814ee17..e8cd729bf6c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -46,6 +46,7 @@ from .const import ( SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, + SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_SWITCHES, SONOS_ENTITY_CREATED, @@ -192,8 +193,8 @@ class SonosSpeaker: self.night_mode: bool | None = None self.dialog_mode: bool | None = None self.cross_fade: bool | None = None - self.bass_level: int | None = None - self.treble_level: int | None = None + self.bass: int | None = None + self.treble: int | None = None # Misc features self.buttons_enabled: bool | None = None @@ -234,6 +235,8 @@ class SonosSpeaker: ) future.result(timeout=10) + dispatcher_send(self.hass, SONOS_CREATE_LEVELS, self) + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info # Battery events can be infrequent, polling is still necessary @@ -490,11 +493,11 @@ class SonosSpeaker: if "dialog_level" in variables: self.dialog_mode = variables["dialog_level"] == "1" - if "bass_level" in variables: - self.bass_level = variables["bass_level"] + if "bass" in variables: + self.bass = variables["bass"] - if "treble_level" in variables: - self.treble_level = variables["treble_level"] + if "treble" in variables: + self.treble = variables["treble"] self.async_write_entity_states() @@ -968,8 +971,8 @@ class SonosSpeaker: self.muted = self.soco.mute self.night_mode = self.soco.night_mode self.dialog_mode = self.soco.dialog_mode - self.bass_level = self.soco.bass - self.treble_level = self.soco.treble + self.bass = self.soco.bass + self.treble = self.soco.treble try: self.cross_fade = self.soco.cross_fade diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8a3a6571faa..cb31604081f 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -70,6 +70,8 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.night_mode = True mock_soco.dialog_mode = True mock_soco.volume = 19 + mock_soco.bass = 1 + mock_soco.treble = -1 mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = [mock_soco] yield mock_soco From 0f2e39adee496ce45b416c832611d75534b97cca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Nov 2021 16:11:36 +0100 Subject: [PATCH 0962/1452] Add get method to DhcpServiceInfo (#60527) Co-authored-by: epenet --- homeassistant/components/dhcp/__init__.py | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 65f1759b54e..47a202aa2dd 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -46,6 +46,8 @@ from homeassistant.loader import async_get_dhcp from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback +from .const import DOMAIN + FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" @@ -71,20 +73,38 @@ class DhcpServiceInfo(BaseServiceInfo): def __getitem__(self, name: str) -> Any: """ - Allow property access by name for compatibility reason. + Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ if not self._warning_logged: report( f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"dhcp"}, + exclude_integrations={DOMAIN}, error_if_core=False, level=logging.DEBUG, ) self._warning_logged = True return getattr(self, name) + def get(self, name: str, default: Any = None) -> Any: + """ + Enable method for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + if hasattr(self, name): + return getattr(self, name) + return default + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" From 07c09ab268c9fa630ca849c5b3850755c320b553 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 29 Nov 2021 16:16:49 +0100 Subject: [PATCH 0963/1452] Add new name handling for Shelly RPC devices (#60539) --- homeassistant/components/shelly/utils.py | 4 +--- tests/components/shelly/conftest.py | 6 +++--- tests/components/shelly/test_config_flow.py | 4 +++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d61140f56b9..d5fcf154e28 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -57,9 +57,7 @@ def get_block_device_name(device: BlockDevice) -> str: def get_rpc_device_name(device: RpcDevice) -> str: """Naming for device.""" - # Gen2 does not support setting device name - # AP SSID name is used as a nicely formatted device name - return cast(str, device.config["wifi"]["ap"]["ssid"] or device.hostname) + return cast(str, device.config["sys"]["device"]["name"] or device.hostname) def get_number_of_channels(device: BlockDevice, block: Block) -> int: diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 16dc8fdae9e..7fc8fba0e33 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -58,9 +58,9 @@ MOCK_BLOCKS = [ MOCK_CONFIG = { "input:0": {"id": 0, "type": "button"}, "switch:0": {"name": "test switch_0"}, - "sys": {"ui_data": {}}, - "wifi": { - "ap": {"ssid": "Test name"}, + "sys": { + "ui_data": {}, + "device": {"name": "Test name"}, }, } diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 2d9cb20342f..12690a35faa 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -26,7 +26,9 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( type="mock_type", ) MOCK_CONFIG = { - "wifi": {"ap": {"ssid": "Test name"}}, + "sys": { + "device": {"name": "Test name"}, + }, } From c407e24a1823affbd839ed619ff9d0e407a437eb Mon Sep 17 00:00:00 2001 From: Steffen Zimmermann Date: Mon, 29 Nov 2021 16:29:31 +0100 Subject: [PATCH 0964/1452] Add wiffi device configuration url support (#60367) --- homeassistant/components/wiffi/__init__.py | 1 + homeassistant/components/wiffi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 05b86c209a6..a7f6b8a7b22 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -148,6 +148,7 @@ class WiffiEntity(Entity): model=device.moduletype, name=f"{device.moduletype} {device.mac_address}", sw_version=device.sw_version, + configuration_url=device.configuration_url, ) self._name = metric.description self._expiration_date = None diff --git a/homeassistant/components/wiffi/manifest.json b/homeassistant/components/wiffi/manifest.json index 803c5f7e520..58d0f9778d7 100644 --- a/homeassistant/components/wiffi/manifest.json +++ b/homeassistant/components/wiffi/manifest.json @@ -3,7 +3,7 @@ "name": "Wiffi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wiffi", - "requirements": ["wiffi==1.0.1"], + "requirements": ["wiffi==1.1.0"], "codeowners": ["@mampfes"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index a91244636cc..668bb6be4e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2424,7 +2424,7 @@ webexteamssdk==1.1.1 whirlpool-sixth-sense==0.15.1 # homeassistant.components.wiffi -wiffi==1.0.1 +wiffi==1.1.0 # homeassistant.components.wirelesstag wirelesstagpy==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 150eb82c072..8869f8ee7b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ watchdog==2.1.6 whirlpool-sixth-sense==0.15.1 # homeassistant.components.wiffi -wiffi==1.0.1 +wiffi==1.1.0 # homeassistant.components.withings withings-api==2.3.2 From f18fe342ace7fe4fb687b17edfb8a4e116a2d286 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 29 Nov 2021 16:33:26 +0100 Subject: [PATCH 0965/1452] Remove configuration.yaml support for the velbus component (#60411) --- homeassistant/components/velbus/__init__.py | 30 ++----------------- .../components/velbus/config_flow.py | 10 ------- tests/components/velbus/test_config_flow.py | 22 ++------------ 3 files changed, 4 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index a63a533e487..330a4315a25 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -7,14 +7,13 @@ from velbusaio.channels import Channel as VelbusChannel from velbusaio.controller import Velbus import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_INTERFACE, @@ -27,34 +26,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA -) - PLATFORMS = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Velbus platform.""" - # Import from the configuration file if needed - if DOMAIN not in config: - return True - - _LOGGER.warning("Loading VELBUS via configuration.yaml is deprecated") - - port = config[DOMAIN].get(CONF_PORT) - data = {} - - if port: - data = {CONF_PORT: port, CONF_NAME: "Velbus import"} - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=data - ) - ) - return True - - async def velbus_connect_task( controller: Velbus, hass: HomeAssistant, entry_id: str ) -> None: diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 3facd8c6a33..3a057b482df 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -82,13 +82,3 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=self._errors, ) - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import a config entry.""" - user_input[CONF_NAME] = "Velbus Import" - prt = user_input[CONF_PORT] - if self._prt_in_configuration_exists(prt): - # if the velbus import is already in the config - # we should not proceed the import - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 6c10b3c84f4..01a40af1751 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -77,29 +77,11 @@ async def test_user_fail(hass: HomeAssistant): assert result["errors"] == {CONF_PORT: "cannot_connect"} -@pytest.mark.usefixtures("controller") -async def test_import(hass: HomeAssistant): - """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import({CONF_PORT: PORT_TCP}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "velbus_import" - - @pytest.mark.usefixtures("config_entry") async def test_abort_if_already_setup(hass: HomeAssistant): - """Test we abort if Daikin is already setup.""" + """Test we abort if Velbus is already setup.""" flow = init_config_flow(hass) - result = await flow.async_step_import( - {CONF_PORT: PORT_TCP, CONF_NAME: "velbus import test"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - result = await flow.async_step_user( - {CONF_PORT: PORT_TCP, CONF_NAME: "velbus import test"} - ) + result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"port": "already_configured"} From 9aa33a3cf8071e04050ba8bd1694bd3db2bc66ba Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Mon, 29 Nov 2021 07:45:00 -0800 Subject: [PATCH 0966/1452] Fix brightness support for Tuya dimmers that use the Light ("dj") category (#60385) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 581cd85647b..16eda1cc324 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -330,6 +330,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): _brightness_type: IntegerTypeData | None = None _color_data_dpcode: DPCode | None = None _color_data_type: ColorTypeData | None = None + _color_mode_dpcode: DPCode | None = None _color_temp_dpcode: DPCode | None = None _color_temp_type: IntegerTypeData | None = None @@ -361,6 +362,13 @@ class TuyaLightEntity(TuyaEntity, LightEntity): None, ) + # Determine color mode DPCode + if ( + description.color_mode is not None + and description.color_mode in device.function + ): + self._color_mode_dpcode = description.color_mode + # Determine DPCodes for color temperature if ( isinstance(description.color_temp, DPCode) @@ -451,10 +459,10 @@ class TuyaLightEntity(TuyaEntity, LightEntity): commands = [{"code": self.entity_description.key, "value": True}] if self._color_temp_type and ATTR_COLOR_TEMP in kwargs: - if color_mode_dpcode := self.entity_description.color_mode: + if self._color_mode_dpcode: commands += [ { - "code": color_mode_dpcode, + "code": self._color_mode_dpcode, "value": WorkMode.WHITE, }, ] @@ -476,10 +484,10 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ATTR_HS_COLOR in kwargs or (ATTR_BRIGHTNESS in kwargs and self.color_mode == COLOR_MODE_HS) ): - if color_mode_dpcode := self.entity_description.color_mode: + if self._color_mode_dpcode: commands += [ { - "code": color_mode_dpcode, + "code": self._color_mode_dpcode, "value": WorkMode.COLOUR, }, ] @@ -651,9 +659,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): # We consider it to be in HS color mode, when work mode is anything # else than "white". if ( - self.entity_description.color_mode - and self.device.status.get(self.entity_description.color_mode) - != WorkMode.WHITE + self._color_mode_dpcode + and self.device.status.get(self._color_mode_dpcode) != WorkMode.WHITE ): return COLOR_MODE_HS if self._color_temp_dpcode: From 2be7773f5e53bdeca6231383f0cc7c7f3c69d0f8 Mon Sep 17 00:00:00 2001 From: PlusPlus-ua Date: Mon, 29 Nov 2021 17:52:15 +0200 Subject: [PATCH 0967/1452] Add Tuya Fingerbot device support (#59880) --- homeassistant/components/tuya/const.py | 4 ++++ homeassistant/components/tuya/number.py | 21 +++++++++++++++++++ homeassistant/components/tuya/select.py | 10 +++++++++ homeassistant/components/tuya/sensor.py | 2 ++ .../components/tuya/strings.select.json | 4 ++++ homeassistant/components/tuya/switch.py | 8 +++++++ .../tuya/translations/select.en.json | 4 ++++ 7 files changed, 53 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index dc781ad8e42..e551c6a5229 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -85,6 +85,7 @@ DEVICE_CLASS_TUYA_MOTION_SENSITIVITY = "tuya__motion_sensitivity" DEVICE_CLASS_TUYA_RECORD_MODE = "tuya__record_mode" DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" DEVICE_CLASS_TUYA_STATUS = "tuya__status" +DEVICE_CLASS_TUYA_FINGERBOT_MODE = "tuya__fingerbot_mode" TUYA_DISCOVERY_NEW = "tuya_discovery_new" TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" @@ -138,6 +139,8 @@ class DPCode(str, Enum): ANGLE_HORIZONTAL = "angle_horizontal" ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit + ARM_DOWN_PERCENT = "arm_down_percent" + ARM_UP_PERCENT = "arm_up_percent" BASIC_ANTI_FLICKER = "basic_anti_flicker" BASIC_DEVICE_VOLUME = "basic_device_volume" BASIC_FLIP = "basic_flip" @@ -168,6 +171,7 @@ class DPCode(str, Enum): CH4_SENSOR_STATE = "ch4_sensor_state" CH4_SENSOR_VALUE = "ch4_sensor_value" CHILD_LOCK = "child_lock" # Child lock + CLICK_SUSTAIN_TIME = "click_sustain_time" CO_STATE = "co_state" CO_STATUS = "co_status" CO_VALUE = "co_value" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index c4063d22e22..a3ef644fa16 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -209,6 +209,27 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Fingerbot + "szjqr": ( + NumberEntityDescription( + key=DPCode.ARM_DOWN_PERCENT, + name="Move Down %", + icon="mdi:arrow-down-bold", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.ARM_UP_PERCENT, + name="Move Up %", + icon="mdi:arrow-up-bold", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.CLICK_SUSTAIN_TIME, + name="Down Delay", + icon="mdi:timer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 921dc21eaaa..4eee162f814 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -19,6 +19,7 @@ from .const import ( DEVICE_CLASS_TUYA_BASIC_ANTI_FLICKR, DEVICE_CLASS_TUYA_BASIC_NIGHTVISION, DEVICE_CLASS_TUYA_DECIBEL_SENSITIVITY, + DEVICE_CLASS_TUYA_FINGERBOT_MODE, DEVICE_CLASS_TUYA_IPC_WORK_MODE, DEVICE_CLASS_TUYA_LED_TYPE, DEVICE_CLASS_TUYA_LIGHT_MODE, @@ -209,6 +210,15 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Fingerbot + "szjqr": ( + SelectEntityDescription( + key=DPCode.MODE, + name="Mode", + device_class=DEVICE_CLASS_TUYA_FINGERBOT_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index e95ebc2982e..b09d0ca905f 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -496,6 +496,8 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": BATTERY_SENSORS, + # Fingerbot + "szjqr": BATTERY_SENSORS, } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json index ccc78704166..6bc005f8ea8 100644 --- a/homeassistant/components/tuya/strings.select.json +++ b/homeassistant/components/tuya/strings.select.json @@ -44,6 +44,10 @@ "on": "[%key:common::state::on%]", "power_off": "[%key:common::state::off%]", "power_on": "[%key:common::state::on%]" + }, + "tuya__fingerbot_mode": { + "click": "Push", + "switch": "Switch" } } } diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 5fb5a0f39b0..97361a5c2c7 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -449,6 +449,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Fingerbot + "szjqr": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + icon="mdi:cursor-pointer", + ), + ), } # Socket (duplicate of `pc`) diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index 7ac1f656d87..cd03cf8eeaf 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -44,6 +44,10 @@ "on": "On", "power_off": "Off", "power_on": "On" + }, + "tuya__fingerbot_mode": { + "click": "Push", + "switch": "Switch" } } } \ No newline at end of file From 6167e4178b9a04d6d63a6ceaadba101fd2cc8062 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 29 Nov 2021 16:54:03 +0100 Subject: [PATCH 0968/1452] Use find_coordinates in here_travel_time (#59938) --- .../components/here_travel_time/sensor.py | 60 ++----------------- .../here_travel_time/test_sensor.py | 5 +- 2 files changed, 9 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 1d47bbaf89f..3277d737a8e 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -10,8 +10,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, ATTR_MODE, CONF_API_KEY, CONF_MODE, @@ -22,10 +20,10 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, TIME_MINUTES, ) -from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers import location +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import dt @@ -313,63 +311,15 @@ class HERETravelTimeSensor(SensorEntity): """Update Sensor Information.""" # Convert device_trackers to HERE friendly location if self._origin_entity_id is not None: - self._here_data.origin = await self._get_location_from_entity( - self._origin_entity_id - ) + self._here_data.origin = find_coordinates(self.hass, self._origin_entity_id) if self._destination_entity_id is not None: - self._here_data.destination = await self._get_location_from_entity( - self._destination_entity_id + self._here_data.destination = find_coordinates( + self.hass, self._destination_entity_id ) await self.hass.async_add_executor_job(self._here_data.update) - async def _get_location_from_entity(self, entity_id: str) -> str | None: - """Get the location from the entity state or attributes.""" - if (entity := self.hass.states.get(entity_id)) is None: - _LOGGER.error("Unable to find entity %s", entity_id) - return None - - # Check if the entity has location attributes - if location.has_location(entity): - return self._get_location_from_attributes(entity) - - # Check if device is in a zone - zone_entity = self.hass.states.get(f"zone.{entity.state}") - if location.has_location(zone_entity): - _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_entity.entity_id - ) - return self._get_location_from_attributes(zone_entity) - - # Check if state is valid coordinate set - if self._entity_state_is_valid_coordinate_set(entity.state): - return entity.state - - _LOGGER.error( - "The state of %s is not a valid set of coordinates: %s", - entity_id, - entity.state, - ) - return None - - @staticmethod - def _entity_state_is_valid_coordinate_set(state: str) -> bool: - """Check that the given string is a valid set of coordinates.""" - schema = vol.Schema(cv.gps) - try: - coordinates = state.split(",") - schema(coordinates) - return True - except (vol.MultipleInvalid): - return False - - @staticmethod - def _get_location_from_attributes(entity: State) -> str: - """Get the lat/long string from an entities attributes.""" - attr = entity.attributes - return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" - class HERETravelTimeData: """HERETravelTime data object.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index ce0c2d9ca6d..b67e24dfc18 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1021,7 +1021,10 @@ async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): await hass.async_block_till_done() assert len(caplog.records) == 1 - assert "is not a valid set of coordinates" in caplog.text + assert ( + "Entity sensor.origin does not contain a location and does not point at an entity that does: invalid" + in caplog.text + ) async def test_pattern_entity_state_with_space(hass, requests_mock_truck_response): From 923cb0f4b72c36473ac885118f8b649ccd362e02 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Mon, 29 Nov 2021 16:57:32 +0100 Subject: [PATCH 0969/1452] Bump aiopvpc to 2.2.4 to fix price sensor attributes for pvpc_hourly_pricing (#60012) --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index a30ae1e2732..86696784638 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.2.1"], + "requirements": ["aiopvpc==2.2.4"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 668bb6be4e3..df0f31ea28a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aiopulse==0.4.3 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.1 +aiopvpc==2.2.4 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8869f8ee7b0..19df25b2390 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aiopulse==0.4.3 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.1 +aiopvpc==2.2.4 # homeassistant.components.webostv aiopylgtv==0.4.0 From 7ece86ee8da7b19ffbf113a6c04dd7878eaa1bdd Mon Sep 17 00:00:00 2001 From: einarhauks Date: Mon, 29 Nov 2021 16:05:14 +0000 Subject: [PATCH 0970/1452] Add sensors to Tesla Wall Connector Integration (#60507) --- .../tesla_wall_connector/__init__.py | 2 +- .../components/tesla_wall_connector/sensor.py | 141 ++++++++++++++++++ .../tesla_wall_connector/conftest.py | 98 +++++++++--- .../test_binary_sensor.py | 41 +++++ .../tesla_wall_connector/test_sensor.py | 71 +++++++++ 5 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/tesla_wall_connector/sensor.py create mode 100644 tests/components/tesla_wall_connector/test_binary_sensor.py create mode 100644 tests/components/tesla_wall_connector/test_sensor.py diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 0796f4660d4..78ae232d493 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -34,7 +34,7 @@ from .const import ( WALLCONNECTOR_DEVICE_NAME, ) -PLATFORMS: list[str] = ["binary_sensor"] +PLATFORMS: list[str] = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py new file mode 100644 index 00000000000..db4064c8c31 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -0,0 +1,141 @@ +"""Sensors for Tesla Wall Connector.""" +from dataclasses import dataclass +import logging + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, + FREQUENCY_HERTZ, + POWER_KILO_WATT, + TEMP_CELSIUS, +) + +from . import ( + WallConnectorData, + WallConnectorEntity, + WallConnectorLambdaValueGetterMixin, + prefix_entity_name, +) +from .const import DOMAIN, WALLCONNECTOR_DATA_LIFETIME, WALLCONNECTOR_DATA_VITALS + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class WallConnectorSensorDescription( + SensorEntityDescription, WallConnectorLambdaValueGetterMixin +): + """Sensor entity description with a function pointer for getting sensor value.""" + + +WALL_CONNECTOR_SENSORS = [ + WallConnectorSensorDescription( + key="evse_state", + name=prefix_entity_name("State"), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].evse_state, + ), + WallConnectorSensorDescription( + key="handle_temp_c", + name=prefix_entity_name("Handle Temperature"), + native_unit_of_measurement=TEMP_CELSIUS, + value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].handle_temp_c, 1), + device_class=DEVICE_CLASS_TEMPERATURE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, + ), + WallConnectorSensorDescription( + key="grid_v", + name=prefix_entity_name("Grid Voltage"), + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_v, 1), + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="grid_hz", + name=prefix_entity_name("Grid Frequency"), + native_unit_of_measurement=FREQUENCY_HERTZ, + value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_hz, 3), + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="power", + name=prefix_entity_name("Power"), + native_unit_of_measurement=POWER_KILO_WATT, + value_fn=lambda data: round( + ( + ( + data[WALLCONNECTOR_DATA_VITALS].currentA_a + * data[WALLCONNECTOR_DATA_VITALS].voltageA_v + ) + + ( + data[WALLCONNECTOR_DATA_VITALS].currentB_a + * data[WALLCONNECTOR_DATA_VITALS].voltageB_v + ) + + ( + data[WALLCONNECTOR_DATA_VITALS].currentC_a + * data[WALLCONNECTOR_DATA_VITALS].voltageC_v + ) + ) + / 1000.0, + 1, + ), + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + WallConnectorSensorDescription( + key="total_energy_kWh", + name=prefix_entity_name("Total Energy"), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh / 1000.0, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_ENERGY, + ), +] + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Create the Wall Connector sensor devices.""" + wall_connector_data = hass.data[DOMAIN][config_entry.entry_id] + + all_entities = [ + WallConnectorSensorEntity(wall_connector_data, description) + for description in WALL_CONNECTOR_SENSORS + ] + + async_add_devices(all_entities) + + +class WallConnectorSensorEntity(WallConnectorEntity, SensorEntity): + """Wall Connector Sensor Entity.""" + + entity_description: WallConnectorSensorDescription + + def __init__( + self, + wall_connector_data: WallConnectorData, + description: WallConnectorSensorDescription, + ) -> None: + """Initialize WallConnectorSensorEntity.""" + self.entity_description = description + super().__init__(wall_connector_data) + + @property + def native_value(self): + """Return the state of the sensor.""" + + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index a22061e197e..477926d4b66 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -1,14 +1,21 @@ """Common fixutres with default mocks as well as common test helper methods.""" -from unittest.mock import patch +from dataclasses import dataclass +from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock, patch import pytest -import tesla_wall_connector +from tesla_wall_connector.wall_connector import Lifetime, Version, Vitals -from homeassistant.components.tesla_wall_connector.const import DOMAIN +from homeassistant.components.tesla_wall_connector.const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -24,7 +31,7 @@ def mock_wall_connector_version(): def get_default_version_data(): """Return default version data object for a wall connector.""" - return tesla_wall_connector.wall_connector.Version( + return Version( { "serial_number": "abc123", "part_number": "part_123", @@ -34,39 +41,96 @@ def get_default_version_data(): async def create_wall_connector_entry( - hass: HomeAssistant, side_effect=None + hass: HomeAssistant, side_effect=None, vitals_data=None, lifetime_data=None ) -> MockConfigEntry: """Create a wall connector entry in hass.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, - options={CONF_SCAN_INTERVAL: 30}, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) entry.add_to_hass(hass) - # We need to return vitals with a contactor_closed attribute - # Since that is used to determine the update scan interval - fake_vitals = tesla_wall_connector.wall_connector.Vitals( - { - "contactor_closed": "false", - } - ) - with patch( "tesla_wall_connector.WallConnector.async_get_version", return_value=get_default_version_data(), side_effect=side_effect, ), patch( "tesla_wall_connector.WallConnector.async_get_vitals", - return_value=fake_vitals, + return_value=vitals_data, side_effect=side_effect, ), patch( "tesla_wall_connector.WallConnector.async_get_lifetime", - return_value=None, + return_value=lifetime_data, side_effect=side_effect, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry + + +def get_vitals_mock() -> Vitals: + """Get mocked vitals object.""" + vitals = MagicMock(auto_spec=Vitals) + return vitals + + +def get_lifetime_mock() -> Lifetime: + """Get mocked lifetime object.""" + lifetime = MagicMock(auto_spec=Lifetime) + return lifetime + + +@dataclass +class EntityAndExpectedValues: + """Class for keeping entity id along with expected value for first and second data updates.""" + + entity_id: str + first_value: Any + second_value: Any + + +async def _test_sensors( + hass: HomeAssistant, + entities_and_expected_values, + vitals_first_update: Vitals, + vitals_second_update: Vitals, + lifetime_first_update: Lifetime, + lifetime_second_update: Lifetime, +) -> None: + """Test update of sensor values.""" + + # First Update: Data is fetched when the integration is initialized + await create_wall_connector_entry( + hass, vitals_data=vitals_first_update, lifetime_data=lifetime_first_update + ) + + # Verify expected vs actual values of first update + for entity in entities_and_expected_values: + state = hass.states.get(entity.entity_id) + assert state, f"Unable to get state of {entity.entity_id}" + assert ( + state.state == entity.first_value + ), f"First update: {entity.entity_id} is expected to have state {entity.first_value} but has {state.state}" + + # Simulate second data update + with patch( + "tesla_wall_connector.WallConnector.async_get_vitals", + return_value=vitals_second_update, + ), patch( + "tesla_wall_connector.WallConnector.async_get_lifetime", + return_value=lifetime_second_update, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL) + ) + await hass.async_block_till_done() + + # Verify expected vs actual values of second update + for entity in entities_and_expected_values: + state = hass.states.get(entity.entity_id) + assert ( + state.state == entity.second_value + ), f"Second update: {entity.entity_id} is expected to have state {entity.second_value} but has {state.state}" diff --git a/tests/components/tesla_wall_connector/test_binary_sensor.py b/tests/components/tesla_wall_connector/test_binary_sensor.py new file mode 100644 index 00000000000..09283cb5352 --- /dev/null +++ b/tests/components/tesla_wall_connector/test_binary_sensor.py @@ -0,0 +1,41 @@ +"""Tests for binary sensors.""" +from homeassistant.core import HomeAssistant + +from .conftest import ( + EntityAndExpectedValues, + _test_sensors, + get_lifetime_mock, + get_vitals_mock, +) + + +async def test_sensors(hass: HomeAssistant) -> None: + """Test all binary sensors.""" + + entity_and_expected_values = [ + EntityAndExpectedValues( + "binary_sensor.tesla_wall_connector_contactor_closed", "off", "on" + ), + EntityAndExpectedValues( + "binary_sensor.tesla_wall_connector_vehicle_connected", "on", "off" + ), + ] + + mock_vitals_first_update = get_vitals_mock() + mock_vitals_first_update.contactor_closed = False + mock_vitals_first_update.vehicle_connected = True + + mock_vitals_second_update = get_vitals_mock() + mock_vitals_second_update.contactor_closed = True + mock_vitals_second_update.vehicle_connected = False + + lifetime_mock = get_lifetime_mock() + + await _test_sensors( + hass, + entities_and_expected_values=entity_and_expected_values, + vitals_first_update=mock_vitals_first_update, + vitals_second_update=mock_vitals_second_update, + lifetime_first_update=lifetime_mock, + lifetime_second_update=lifetime_mock, + ) diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py new file mode 100644 index 00000000000..37d6c7d9cd1 --- /dev/null +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -0,0 +1,71 @@ +"""Tests for sensors.""" +from homeassistant.core import HomeAssistant + +from .conftest import ( + EntityAndExpectedValues, + _test_sensors, + get_lifetime_mock, + get_vitals_mock, +) + + +async def test_sensors(hass: HomeAssistant) -> None: + """Test all sensors.""" + + entity_and_expected_values = [ + EntityAndExpectedValues("sensor.tesla_wall_connector_state", "1", "2"), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_handle_temperature", "25.5", "-1.4" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_grid_voltage", "230.2", "229.2" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" + ), + EntityAndExpectedValues("sensor.tesla_wall_connector_power", "7.6", "7.6"), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_total_energy", "988.022", "989.0" + ), + ] + + mock_vitals_first_update = get_vitals_mock() + mock_vitals_first_update.evse_state = 1 + mock_vitals_first_update.handle_temp_c = 25.51 + mock_vitals_first_update.grid_v = 230.15 + mock_vitals_first_update.grid_hz = 50.021 + # to calculate power, we calculate power of each phase and sum up + # (230.1*10) + (231.1*11) + (232.1*12) = 7628.3 W + mock_vitals_first_update.voltageA_v = 230.1 + mock_vitals_first_update.voltageB_v = 231.1 + mock_vitals_first_update.voltageC_v = 232.1 + mock_vitals_first_update.currentA_a = 10 + mock_vitals_first_update.currentB_a = 11 + mock_vitals_first_update.currentC_a = 12 + + mock_vitals_second_update = get_vitals_mock() + mock_vitals_second_update.evse_state = 2 + mock_vitals_second_update.handle_temp_c = -1.42 + mock_vitals_second_update.grid_v = 229.21 + mock_vitals_second_update.grid_hz = 49.981 + # (228.1*10) + (229.1*11) + (230.1*12) = 7562.3 W + mock_vitals_second_update.voltageB_v = 228.1 + mock_vitals_second_update.voltageC_v = 229.1 + mock_vitals_second_update.voltageA_v = 230.1 + mock_vitals_second_update.currentA_a = 10 + mock_vitals_second_update.currentB_a = 11 + mock_vitals_second_update.currentC_a = 12 + + lifetime_mock_first_update = get_lifetime_mock() + lifetime_mock_first_update.energy_wh = 988022 + lifetime_mock_second_update = get_lifetime_mock() + lifetime_mock_second_update.energy_wh = 989000 + + await _test_sensors( + hass, + entities_and_expected_values=entity_and_expected_values, + vitals_first_update=mock_vitals_first_update, + vitals_second_update=mock_vitals_second_update, + lifetime_first_update=lifetime_mock_first_update, + lifetime_second_update=lifetime_mock_second_update, + ) From ec1c52d9453e5456d8e874b9382f5b079453c283 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Nov 2021 17:10:07 +0100 Subject: [PATCH 0971/1452] Use dataclass for SsdpServiceInfo (#59931) Co-authored-by: epenet --- homeassistant/components/axis/config_flow.py | 4 +- .../components/directv/config_flow.py | 4 +- .../components/dlna_dmr/config_flow.py | 13 +- .../components/dlna_dmr/media_player.py | 4 +- homeassistant/components/fritz/config_flow.py | 4 +- .../components/fritzbox/config_flow.py | 4 +- .../components/huawei_lte/config_flow.py | 3 +- homeassistant/components/hue/config_flow.py | 6 +- .../components/hyperion/config_flow.py | 3 +- .../components/nanoleaf/config_flow.py | 5 +- homeassistant/components/roku/config_flow.py | 5 +- .../components/samsungtv/config_flow.py | 5 +- homeassistant/components/ssdp/__init__.py | 131 ++++----- .../components/synology_dsm/config_flow.py | 2 +- homeassistant/config_entries.py | 5 +- homeassistant/helpers/config_entry_flow.py | 11 +- tests/components/ssdp/test_init.py | 251 ++++++++++++------ .../helpers/test_config_entry_oauth2_flow.py | 8 +- tests/test_config_entries.py | 2 +- 19 files changed, 295 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index eb4fd9296fc..74dbeb0b2c7 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -6,7 +6,7 @@ from urllib.parse import urlsplit import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, @@ -163,7 +163,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): } ) - async def async_step_ssdp(self, discovery_info: dict): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo): """Prepare configuration for a SSDP discovered Axis device.""" url = urlsplit(discovery_info["presentationURL"]) return await self._process_discovered_device( diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index d2d1e2ec003..338af62364d 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -8,13 +8,13 @@ from urllib.parse import urlparse from directv import DIRECTV, DIRECTVError import voluptuous as vol +from homeassistant.components import ssdp from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_RECEIVER_ID, DOMAIN @@ -67,7 +67,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname receiver_id = None diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 2b38cf4e56d..5370b1395fd 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -25,7 +25,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_CALLBACK_URL_OVERRIDE, @@ -57,7 +56,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._discoveries: dict[str, Mapping[str, Any]] = {} + self._discoveries: dict[str, ssdp.SsdpServiceInfo] = {} self._location: str | None = None self._udn: str | None = None self._device_type: str | None = None @@ -205,7 +204,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._set_confirm_only() return self.async_show_form(step_id="import_turn_on", errors=errors) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) @@ -330,7 +329,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=data, options=self._options) async def _async_set_info_from_discovery( - self, discovery_info: Mapping[str, Any], abort_if_configured: bool = True + self, discovery_info: ssdp.SsdpServiceInfo, abort_if_configured: bool = True ) -> None: """Set information required for a config entry from the SSDP discovery.""" LOGGER.debug( @@ -361,12 +360,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or DEFAULT_NAME ) - async def _async_get_discoveries(self) -> list[Mapping[str, Any]]: + async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]: """Get list of unconfigured DLNA devices discovered by SSDP.""" LOGGER.debug("_get_discoveries") # Get all compatible devices from ssdp's cache - discoveries: list[Mapping[str, Any]] = [] + discoveries: list[ssdp.SsdpServiceInfo] = [] for udn_st in DmrDevice.DEVICE_TYPES: st_discoveries = await ssdp.async_get_discovery_info_by_st( self.hass, udn_st @@ -454,7 +453,7 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): ) -def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: +def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: """Return True if this device should be ignored for discovery. These devices are supported better by other integrations, so don't bug diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 68a41e1fa62..d7afe92d1c9 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping, Sequence +from collections.abc import Sequence import contextlib from datetime import datetime, timedelta import functools @@ -241,7 +241,7 @@ class DlnaDmrEntity(MediaPlayerEntity): await self._device_disconnect() async def async_ssdp_callback( - self, info: Mapping[str, Any], change: ssdp.SsdpChange + self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange ) -> None: """Handle notification from SSDP of device state change.""" _LOGGER.debug( diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 55c60cc41a8..37f3d766d5d 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -9,6 +9,7 @@ from urllib.parse import ParseResult, urlparse from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import voluptuous as vol +from homeassistant.components import ssdp from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -22,7 +23,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .common import FritzBoxTools from .const import ( @@ -115,7 +115,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION]) self._host = ssdp_location.hostname diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index bcf17a1a958..e51a1a5f528 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -8,6 +8,7 @@ from pyfritzhome import Fritzhome, LoginError from requests.exceptions import HTTPError import voluptuous as vol +from homeassistant.components import ssdp from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, @@ -16,7 +17,6 @@ from homeassistant.components.ssdp import ( from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN @@ -119,7 +119,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors ) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname assert isinstance(host, str) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index a3e7390802f..61f6727f64a 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -31,7 +31,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_TRACK_WIRED_CLIENTS, @@ -202,7 +201,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index b1bed30478c..8cb25e1acfe 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ALLOW_HUE_GROUPS, @@ -186,7 +186,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Hue bridge. This flow is triggered by the SSDP component. It will check if the @@ -213,7 +213,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_hue_bridge") host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) # type: ignore[arg-type] + bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) await self.async_set_unique_id(bridge.id) self._abort_if_unique_id_configured( diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 6c76f03e3de..f5beb185830 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -10,6 +10,7 @@ from urllib.parse import urlparse from hyperion import client, const import voluptuous as vol +from homeassistant.components import ssdp from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( SOURCE_REAUTH, @@ -151,7 +152,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index f55a0d86b08..e54d0646d21 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -9,11 +9,10 @@ from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.json import load_json, save_json from .const import DOMAIN @@ -114,7 +113,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID], ) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle Nanoleaf SSDP discovery.""" _LOGGER.debug("SSDP discovered: %s", discovery_info) return await self._async_discovery_handler( diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 63b1fb788a4..996d6ca295f 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -7,7 +7,7 @@ from urllib.parse import urlparse from rokuecp import Roku, RokuError import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, @@ -18,7 +18,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -115,7 +114,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 9c118ca1caa..45b2789d478 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -11,7 +11,7 @@ import getmac import voluptuous as vol from homeassistant import config_entries, data_entry_flow -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -28,7 +28,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.typing import DiscoveryInfoType from .bridge import ( SamsungTVBridge, @@ -266,7 +265,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) async def async_step_ssdp( - self, discovery_info: DiscoveryInfoType + self, discovery_info: ssdp.SsdpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index afcb2211ea6..c5ff03264a0 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -2,13 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Iterator +from collections.abc import Awaitable, Mapping from dataclasses import dataclass, field from datetime import timedelta from enum import Enum from ipaddress import IPv4Address, IPv6Address import logging -from typing import Any, Callable, Mapping +from typing import Any, Callable from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource @@ -63,28 +63,6 @@ ATTR_HA_MATCHING_DOMAINS = "x_homeassistant_matching_domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] -DISCOVERY_MAPPING = { - "usn": ATTR_SSDP_USN, - "ext": ATTR_SSDP_EXT, - "server": ATTR_SSDP_SERVER, - "st": ATTR_SSDP_ST, - "location": ATTR_SSDP_LOCATION, - "_udn": ATTR_SSDP_UDN, - "nt": ATTR_SSDP_NT, -} - -SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE") -SsdpCallback = Callable[[Mapping[str, Any], SsdpChange], Awaitable] - - -SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { - SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE, - SsdpSource.SEARCH_CHANGED: SsdpChange.ALIVE, - SsdpSource.ADVERTISEMENT_ALIVE: SsdpChange.ALIVE, - SsdpSource.ADVERTISEMENT_BYEBYE: SsdpChange.BYEBYE, - SsdpSource.ADVERTISEMENT_UPDATE: SsdpChange.UPDATE, -} - _LOGGER = logging.getLogger(__name__) @@ -106,13 +84,14 @@ class _SsdpServiceDescription: ssdp_udn: str | None = None ssdp_ext: str | None = None ssdp_server: str | None = None + ssdp_headers: Mapping[str, Any] = field(default_factory=dict) @dataclass class _UpnpServiceDescription: """UPnP info.""" - upnp: dict[str, Any] + upnp: Mapping[str, Any] @dataclass @@ -136,7 +115,7 @@ class SsdpServiceInfo( if not self._warning_logged: report( f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"ssdp"}, + exclude_integrations={DOMAIN}, error_if_core=False, level=logging.DEBUG, ) @@ -144,41 +123,58 @@ class SsdpServiceInfo( # Use a property if it is available, fallback to upnp data if hasattr(self, name): return getattr(self, name) + if name in self.ssdp_headers and name not in self.upnp: + return self.ssdp_headers.get(name) return self.upnp[name] def get(self, name: str, default: Any = None) -> Any: """ - Allow property access by name for compatibility reason. + Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ if not self._warning_logged: report( f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"ssdp"}, + exclude_integrations={DOMAIN}, error_if_core=False, level=logging.DEBUG, ) self._warning_logged = True if hasattr(self, name): return getattr(self, name) - return self.upnp.get(name, default) + return self.upnp.get(name, self.ssdp_headers.get(name, default)) - def __iter__(self) -> Iterator[str]: + def __contains__(self, name: str) -> bool: """ - Implement iter(self) on upnp data. + Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ if not self._warning_logged: report( - "accessed discovery_info.__iter__() instead of discovery_info.upnp.__iter__(); this will fail in version 2022.6", - exclude_integrations={"ssdp"}, + "accessed discovery_info.__contains__() instead of discovery_info.upnp.__contains__() " + "or discovery_info.ssdp_headers.__contains__(); this will fail in version 2022.6", + exclude_integrations={DOMAIN}, error_if_core=False, level=logging.DEBUG, ) self._warning_logged = True - return self.upnp.__iter__() + if hasattr(self, name): + return getattr(self, name) is not None + return name in self.upnp or name in self.ssdp_headers + + +SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE") +SsdpCallback = Callable[[SsdpServiceInfo, SsdpChange], Awaitable] + +SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { + SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE, + SsdpSource.SEARCH_CHANGED: SsdpChange.ALIVE, + SsdpSource.ADVERTISEMENT_ALIVE: SsdpChange.ALIVE, + SsdpSource.ADVERTISEMENT_BYEBYE: SsdpChange.BYEBYE, + SsdpSource.ADVERTISEMENT_UPDATE: SsdpChange.UPDATE, +} @bind_hass @@ -198,7 +194,7 @@ async def async_register_callback( @bind_hass async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name hass: HomeAssistant, udn: str, st: str -) -> dict[str, str] | None: +) -> SsdpServiceInfo | None: """Fetch the discovery info cache.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn_st(udn, st) @@ -207,7 +203,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_st( # pylint: disable=invalid-name hass: HomeAssistant, st: str -) -> list[dict[str, str]]: +) -> list[SsdpServiceInfo]: """Fetch all the entries matching the st.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_st(st) @@ -216,7 +212,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name @bind_hass async def async_get_discovery_info_by_udn( hass: HomeAssistant, udn: str -) -> list[dict[str, str]]: +) -> list[SsdpServiceInfo]: """Fetch all the entries matching the udn.""" scanner: Scanner = hass.data[DOMAIN] return await scanner.async_get_discovery_info_by_udn(udn) @@ -237,7 +233,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_process_callbacks( callbacks: list[SsdpCallback], - discovery_info: dict[str, str], + discovery_info: SsdpServiceInfo, ssdp_change: SsdpChange, ) -> None: for callback in callbacks: @@ -496,8 +492,10 @@ class Scanner: if not callbacks and not matching_domains: return - discovery_info = discovery_info_from_headers_and_description(info_with_desc) - discovery_info[ATTR_HA_MATCHING_DOMAINS] = matching_domains + discovery_info = discovery_info_from_headers_and_description( + combined_headers, info_desc + ) + discovery_info.x_homeassistant_matching_domains = matching_domains ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source] await _async_process_callbacks(callbacks, discovery_info, ssdp_change) @@ -505,6 +503,8 @@ class Scanner: if ssdp_change == SsdpChange.BYEBYE: return + _LOGGER.debug("Discovery info: %s", discovery_info) + for domain in matching_domains: _LOGGER.debug("Discovered %s at %s", domain, location) discovery_flow.async_create_flow( @@ -523,7 +523,7 @@ class Scanner: async def _async_headers_to_discovery_info( self, headers: Mapping[str, Any] - ) -> dict[str, Any]: + ) -> SsdpServiceInfo: """Combine the headers and description into discovery_info. Building this is a bit expensive so we only do it on demand. @@ -533,13 +533,11 @@ class Scanner: info_desc = ( await self._description_cache.async_get_description_dict(location) or {} ) - return discovery_info_from_headers_and_description( - CaseInsensitiveDict(headers, **info_desc) - ) + return discovery_info_from_headers_and_description(headers, info_desc) async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name self, udn: str, st: str - ) -> dict[str, Any] | None: + ) -> SsdpServiceInfo | None: """Return discovery_info for a udn and st.""" if headers := self._all_headers_from_ssdp_devices.get((udn, st)): return await self._async_headers_to_discovery_info(headers) @@ -547,7 +545,7 @@ class Scanner: async def async_get_discovery_info_by_st( # pylint: disable=invalid-name self, st: str - ) -> list[dict[str, Any]]: + ) -> list[SsdpServiceInfo]: """Return matching discovery_infos for a st.""" return [ await self._async_headers_to_discovery_info(headers) @@ -555,7 +553,7 @@ class Scanner: if udn_st[1] == st ] - async def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, Any]]: + async def async_get_discovery_info_by_udn(self, udn: str) -> list[SsdpServiceInfo]: """Return matching discovery_infos for a udn.""" return [ await self._async_headers_to_discovery_info(headers) @@ -565,23 +563,36 @@ class Scanner: def discovery_info_from_headers_and_description( - info_with_desc: CaseInsensitiveDict, -) -> dict[str, Any]: + combined_headers: Mapping[str, Any], + info_desc: Mapping[str, Any], +) -> SsdpServiceInfo: """Convert headers and description to discovery_info.""" - info = { - DISCOVERY_MAPPING.get(k.lower(), k): v - for k, v in info_with_desc.as_dict().items() - } + ssdp_usn = combined_headers["usn"] + ssdp_st = combined_headers.get("st") + upnp_info = {**info_desc} - if ATTR_UPNP_UDN not in info and ATTR_SSDP_USN in info: - if udn := _udn_from_usn(info[ATTR_SSDP_USN]): - info[ATTR_UPNP_UDN] = udn + # Increase compatibility: depending on the message type, + # either the ST (Search Target, from M-SEARCH messages) + # or NT (Notification Type, from NOTIFY messages) header is mandatory + if not ssdp_st: + ssdp_st = combined_headers["nt"] - # Increase compatibility. - if ATTR_SSDP_ST not in info and ATTR_SSDP_NT in info: - info[ATTR_SSDP_ST] = info[ATTR_SSDP_NT] + # Ensure UPnP "udn" is set + if ATTR_UPNP_UDN not in upnp_info: + if udn := _udn_from_usn(ssdp_usn): + upnp_info[ATTR_UPNP_UDN] = udn - return info + return SsdpServiceInfo( + ssdp_usn=ssdp_usn, + ssdp_st=ssdp_st, + ssdp_ext=combined_headers.get("ext"), + ssdp_server=combined_headers.get("server"), + ssdp_location=combined_headers.get("location"), + ssdp_udn=combined_headers.get("_udn"), + ssdp_nt=combined_headers.get("nt"), + ssdp_headers=combined_headers, + upnp=upnp_info, + ) def _udn_from_usn(usn: str | None) -> str | None: diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index a40dfd87744..985b18d68a9 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -237,7 +237,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_form(step) return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered synology_dsm.""" parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) friendly_name = ( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9f1731e40d6..3fa905daf48 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -35,6 +35,7 @@ import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.mqtt.discovery import MqttServiceInfo + from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -1370,10 +1371,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_ssdp( - self, discovery_info: DiscoveryInfoType + self, discovery_info: SsdpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by SSDP discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 939394a243a..66bb36b7dc6 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.mqtt import discovery as mqtt from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -123,7 +123,14 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() - async_step_ssdp = async_step_discovery + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a flow initialized by Ssdp discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: """Handle a flow initialized by import.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index c9098726ab4..9e4d7424eb9 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,4 +1,6 @@ """Test the SSDP integration.""" +# pylint: disable=protected-access + from datetime import datetime, timedelta from ipaddress import IPv4Address, IPv6Address from unittest.mock import ANY, AsyncMock, patch @@ -62,18 +64,28 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow assert mock_flow_init.mock_calls[0][2]["context"] == { "source": config_entries.SOURCE_SSDP } - assert mock_flow_init.mock_calls[0][2]["data"] == { - ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", - ssdp.ATTR_SSDP_SERVER: "mock-server", - ssdp.ATTR_SSDP_EXT: "", - ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", - ssdp.ATTR_SSDP_UDN: ANY, - "_timestamp": ANY, - ssdp.ATTR_HA_MATCHING_DOMAINS: {"mock-domain"}, - } + mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] + assert mock_call_data.ssdp_st == "mock-st" + assert mock_call_data.ssdp_location == "http://1.1.1.1" + assert mock_call_data.ssdp_usn == "uuid:mock-udn::mock-st" + assert mock_call_data.ssdp_server == "mock-server" + assert mock_call_data.ssdp_ext == "" + assert mock_call_data.ssdp_udn == ANY + assert mock_call_data.ssdp_headers["_timestamp"] == ANY + assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"} + assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"} assert "Failed to fetch ssdp data" not in caplog.text + # Compatibility with old dict access (to be removed after 2022.6) + assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st" + assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert mock_call_data[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st" + assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server" + assert mock_call_data[ssdp.ATTR_SSDP_EXT] == "" + assert mock_call_data[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn" + assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY + assert mock_call_data["_timestamp"] == ANY + assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == {"mock-domain"} + # End compatibility checks @pytest.mark.usefixtures("mock_get_source_ip") @@ -347,19 +359,31 @@ async def test_discovery_from_advertisement_sets_ssdp_st( await hass.async_block_till_done() discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn") - assert discovery_info == [ - { - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_NT: "mock-st", - ssdp.ATTR_SSDP_ST: "mock-st", # Set by ssdp component, not in original advertisement. - ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", - ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_SSDP_UDN: ANY, - "nts": "ssdp:alive", - "_timestamp": ANY, - } - ] + discovery_info = discovery_info[0] + assert discovery_info.ssdp_location == "http://1.1.1.1" + assert discovery_info.ssdp_nt == "mock-st" + # Set by ssdp component, not in original advertisement. + assert discovery_info.ssdp_st == "mock-st" + assert discovery_info.ssdp_usn == "uuid:mock-udn::mock-st" + assert discovery_info.ssdp_udn == ANY + assert discovery_info.ssdp_headers["nts"] == "ssdp:alive" + assert discovery_info.ssdp_headers["_timestamp"] == ANY + assert discovery_info.upnp == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", + } + # Compatibility with old dict access (to be removed after 2022.6) + assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info[ssdp.ATTR_SSDP_NT] == "mock-st" + # Set by ssdp component, not in original advertisement. + assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" + assert discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st" + assert discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn" + assert discovery_info[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" + assert discovery_info[ssdp.ATTR_SSDP_UDN] == ANY + assert discovery_info["nts"] == "ssdp:alive" + assert discovery_info["_timestamp"] == ANY + # End compatibility checks @patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip? @@ -452,29 +476,48 @@ async def test_scan_with_registered_callback( assert async_integration_match_all_not_present_callback1.call_count == 0 assert async_match_any_callback1.call_count == 1 assert async_not_matching_integration_callback1.call_count == 0 - assert async_integration_callback.call_args[0] == ( - { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_SSDP_EXT: "", - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_SERVER: "mock-server", - ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", - "x-rincon-bootseq": "55", - ssdp.ATTR_SSDP_UDN: ANY, - "_timestamp": ANY, - ssdp.ATTR_HA_MATCHING_DOMAINS: set(), - }, - ssdp.SsdpChange.ALIVE, + assert async_integration_callback.call_args[0][1] == ssdp.SsdpChange.ALIVE + mock_call_data: ssdp.SsdpServiceInfo = async_integration_callback.call_args[0][0] + assert mock_call_data.ssdp_ext == "" + assert mock_call_data.ssdp_location == "http://1.1.1.1" + assert mock_call_data.ssdp_server == "mock-server" + assert mock_call_data.ssdp_st == "mock-st" + assert ( + mock_call_data.ssdp_usn == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st" ) + assert mock_call_data.ssdp_headers["x-rincon-bootseq"] == "55" + assert mock_call_data.ssdp_udn == ANY + assert mock_call_data.ssdp_headers["_timestamp"] == ANY + assert mock_call_data.x_homeassistant_matching_domains == set() + assert mock_call_data.upnp == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + # Compatibility with old dict access (to be removed after 2022.6) + assert mock_call_data[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" + assert mock_call_data[ssdp.ATTR_SSDP_EXT] == "" + assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server" + assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + mock_call_data[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st" + ) + assert ( + mock_call_data[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert mock_call_data["x-rincon-bootseq"] == "55" + assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY + assert mock_call_data["_timestamp"] == ANY + assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == set() + # End of compatibility checks assert "Failed to callback info" in caplog.text async_integration_callback_from_cache = AsyncMock() await ssdp.async_register_callback( hass, async_integration_callback_from_cache, {"st": "mock-st"} ) - assert async_integration_callback_from_cache.call_count == 1 @@ -510,51 +553,109 @@ async def test_getting_existing_headers( await ssdp_listener._on_search(mock_ssdp_search_response) discovery_info_by_st = await ssdp.async_get_discovery_info_by_st(hass, "mock-st") - assert discovery_info_by_st == [ - { - ssdp.ATTR_SSDP_EXT: "", - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_SERVER: "mock-server", - ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_SSDP_UDN: ANY, - "_timestamp": ANY, - } - ] + discovery_info_by_st = discovery_info_by_st[0] + assert discovery_info_by_st.ssdp_ext == "" + assert discovery_info_by_st.ssdp_location == "http://1.1.1.1" + assert discovery_info_by_st.ssdp_server == "mock-server" + assert discovery_info_by_st.ssdp_st == "mock-st" + assert ( + discovery_info_by_st.ssdp_usn + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert discovery_info_by_st.ssdp_udn == ANY + assert discovery_info_by_st.ssdp_headers["_timestamp"] == ANY + assert discovery_info_by_st.upnp == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + # Compatibility with old dict access (to be removed after 2022.6) + assert discovery_info_by_st[ssdp.ATTR_SSDP_EXT] == "" + assert discovery_info_by_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info_by_st[ssdp.ATTR_SSDP_SERVER] == "mock-server" + assert discovery_info_by_st[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info_by_st[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert ( + discovery_info_by_st[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert discovery_info_by_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" + assert discovery_info_by_st[ssdp.ATTR_SSDP_UDN] == ANY + assert discovery_info_by_st["_timestamp"] == ANY + # End of compatibility checks discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" ) - assert discovery_info_by_udn == [ - { - ssdp.ATTR_SSDP_EXT: "", - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_SERVER: "mock-server", - ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_SSDP_UDN: ANY, - "_timestamp": ANY, - } - ] + discovery_info_by_udn = discovery_info_by_udn[0] + assert discovery_info_by_udn.ssdp_ext == "" + assert discovery_info_by_udn.ssdp_location == "http://1.1.1.1" + assert discovery_info_by_udn.ssdp_server == "mock-server" + assert discovery_info_by_udn.ssdp_st == "mock-st" + assert ( + discovery_info_by_udn.ssdp_usn + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert discovery_info_by_udn.ssdp_udn == ANY + assert discovery_info_by_udn.ssdp_headers["_timestamp"] == ANY + assert discovery_info_by_udn.upnp == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + # Compatibility with old dict access (to be removed after 2022.6) + assert discovery_info_by_udn[ssdp.ATTR_SSDP_EXT] == "" + assert discovery_info_by_udn[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info_by_udn[ssdp.ATTR_SSDP_SERVER] == "mock-server" + assert discovery_info_by_udn[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info_by_udn[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert ( + discovery_info_by_udn[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert discovery_info_by_udn[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" + assert discovery_info_by_udn[ssdp.ATTR_SSDP_UDN] == ANY + assert discovery_info_by_udn["_timestamp"] == ANY + # End of compatibility checks discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st" ) - assert discovery_info_by_udn_st == { - ssdp.ATTR_SSDP_EXT: "", - ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", - ssdp.ATTR_SSDP_SERVER: "mock-server", - ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + assert discovery_info_by_udn_st.ssdp_ext == "" + assert discovery_info_by_udn_st.ssdp_location == "http://1.1.1.1" + assert discovery_info_by_udn_st.ssdp_server == "mock-server" + assert discovery_info_by_udn_st.ssdp_st == "mock-st" + assert ( + discovery_info_by_udn_st.ssdp_usn + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert discovery_info_by_udn_st.ssdp_udn == ANY + assert discovery_info_by_udn_st.ssdp_headers["_timestamp"] == ANY + assert discovery_info_by_udn_st.upnp == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_SSDP_UDN: ANY, - "_timestamp": ANY, + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } + # Compatibility with old dict access (to be removed after 2022.6) + assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_EXT] == "" + assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_SERVER] == "mock-server" + assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info_by_udn_st[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + assert ( + discovery_info_by_udn_st[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert discovery_info_by_udn_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" + assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_UDN] == ANY + assert discovery_info_by_udn_st["_timestamp"] == ANY + # End of compatibility checks assert ( await ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 48868a3727b..ddd92bb943f 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -237,7 +237,9 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): ) result = await hass.config_entries.flow.async_init( - TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + TEST_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=data_entry_flow.BaseServiceInfo(), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -267,7 +269,9 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + TEST_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=data_entry_flow.BaseServiceInfo(), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0b4d7fa8799..6b710caed90 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2351,7 +2351,7 @@ async def test_async_setup_update_entry(hass): "discovery_source", ( (config_entries.SOURCE_DISCOVERY, {}), - (config_entries.SOURCE_SSDP, {}), + (config_entries.SOURCE_SSDP, BaseServiceInfo()), (config_entries.SOURCE_USB, BaseServiceInfo()), (config_entries.SOURCE_HOMEKIT, BaseServiceInfo()), (config_entries.SOURCE_DHCP, BaseServiceInfo()), From 7b81185d2afff5081eacca33a422dde3cd3a3a67 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Mon, 29 Nov 2021 17:15:38 +0100 Subject: [PATCH 0972/1452] Add tolo fan platform (#60502) --- .coveragerc | 1 + homeassistant/components/tolo/__init__.py | 10 +++- homeassistant/components/tolo/fan.py | 56 +++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tolo/fan.py diff --git a/.coveragerc b/.coveragerc index 519fa1b3937..beb7719db44 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1098,6 +1098,7 @@ omit = homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py homeassistant/components/tolo/climate.py + homeassistant/components/tolo/fan.py homeassistant/components/tolo/light.py homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 666e86c951b..4f41303c8f8 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -22,7 +22,15 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN -PLATFORMS = ["binary_sensor", "button", "climate", "light", "select", "sensor"] +PLATFORMS = [ + "binary_sensor", + "button", + "climate", + "fan", + "light", + "select", + "sensor", +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py new file mode 100644 index 00000000000..499a348bd0b --- /dev/null +++ b/homeassistant/components/tolo/fan.py @@ -0,0 +1,56 @@ +"""TOLO Sauna fan controls.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.fan import FanEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up fan controls for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([ToloFan(coordinator, entry)]) + + +class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): + """Sauna fan control.""" + + _attr_name = "Fan" + + def __init__( + self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + ) -> None: + """Initialize TOLO fan entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{entry.entry_id}_fan" + + @property + def is_on(self) -> bool: + """Return if sauna fan is running.""" + return self.coordinator.data.status.fan_on + + def turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on sauna fan.""" + self.coordinator.client.set_fan_on(True) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off sauna fan.""" + self.coordinator.client.set_fan_on(False) From 37430e7c9eef55825a90695f99de3017a08b0402 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Nov 2021 17:37:55 +0100 Subject: [PATCH 0973/1452] Add get method to ZeroconfServiceInfo (#60528) Co-authored-by: epenet --- homeassistant/components/zeroconf/__init__.py | 22 ++++++++++-- tests/components/zeroconf/test_init.py | 34 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 932c2dee377..33fe73de617 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -120,20 +120,38 @@ class ZeroconfServiceInfo(BaseServiceInfo): def __getitem__(self, name: str) -> Any: """ - Allow property access by name for compatibility reason. + Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ if not self._warning_logged: report( f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"zeroconf"}, + exclude_integrations={DOMAIN}, error_if_core=False, level=logging.DEBUG, ) self._warning_logged = True return getattr(self, name) + def get(self, name: str, default: Any = None) -> Any: + """ + Enable method for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + if hasattr(self, name): + return getattr(self, name) + return default + @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index d04ddd3dd4b..1bd96a306eb 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1030,3 +1030,37 @@ async def test_no_name(hass, mock_async_zeroconf): register_call = mock_async_zeroconf.async_register_service.mock_calls[-1] info = register_call.args[0] assert info.name == "Home._home-assistant._tcp.local." + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = zeroconf.ZeroconfServiceInfo( + host="mock_host", + port=None, + hostname="mock_hostname", + type="mock_type", + name="mock_name", + properties={}, + ) + + # Ensure first call get logged + assert discovery_info["host"] == "mock_host" + assert discovery_info.get("host") == "mock_host" + assert discovery_info.get("host", "fallback_host") == "mock_host" + assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" + assert "Detected code that accessed discovery_info['host']" in caplog.text + assert "Detected code that accessed discovery_info.get('host')" not in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["host"] == "mock_host" + assert discovery_info.get("host") == "mock_host" + assert "Detected code that accessed discovery_info['host']" not in caplog.text + assert "Detected code that accessed discovery_info.get('host')" not in caplog.text + + discovery_info._warning_logged = False + assert discovery_info.get("host") == "mock_host" + assert "Detected code that accessed discovery_info.get('host')" in caplog.text From 54df81cbabed16422cef22221b50624656578759 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:27:15 +0100 Subject: [PATCH 0974/1452] Use ZeroconfServiceInfo in vizio (#60115) Co-authored-by: epenet --- homeassistant/components/vizio/config_flow.py | 33 ++++++++--------- tests/components/vizio/const.py | 18 +++++----- tests/components/vizio/test_config_flow.py | 36 +++++++++++-------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 97d54f2e874..9cca89f77aa 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -11,6 +11,7 @@ from pyvizio.const import APP_HOME import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV from homeassistant.config_entries import ( SOURCE_IGNORE, @@ -26,14 +27,11 @@ from homeassistant.const import ( CONF_INCLUDE, CONF_NAME, CONF_PIN, - CONF_PORT, - CONF_TYPE, ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util.network import is_ip_address from .const import ( @@ -338,28 +336,25 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input=import_config) async def async_step_zeroconf( - self, discovery_info: DiscoveryInfoType | None = None + self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" + host = discovery_info.host # If host already has port, no need to add it again - if ":" not in discovery_info[CONF_HOST]: - discovery_info[ - CONF_HOST - ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + if ":" not in host: + host = f"{host}:{discovery_info.port}" # Set default name to discovered device name by stripping zeroconf service # (`type`) from `name` - num_chars_to_strip = len(discovery_info[CONF_TYPE]) + 1 - discovery_info[CONF_NAME] = discovery_info[CONF_NAME][:-num_chars_to_strip] + num_chars_to_strip = len(discovery_info.type) + 1 + name = discovery_info.name[:-num_chars_to_strip] - discovery_info[CONF_DEVICE_CLASS] = await async_guess_device_type( - discovery_info[CONF_HOST] - ) + device_class = await async_guess_device_type(host) # Set unique ID early for discovery flow so we can abort if needed unique_id = await VizioAsync.get_unique_id( - discovery_info[CONF_HOST], - discovery_info[CONF_DEVICE_CLASS], + host, + device_class, session=async_get_clientsession(self.hass, False), ) @@ -372,7 +367,13 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Form must be shown after discovery so user can confirm/update configuration # before ConfigEntry creation. self._must_show_form = True - return await self.async_step_user(user_input=discovery_info) + return await self.async_step_user( + user_input={ + CONF_HOST: host, + CONF_NAME: name, + CONF_DEVICE_CLASS: device_class, + } + ) async def async_step_pair_tv(self, user_input: dict[str, Any] = None) -> FlowResult: """ diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 963947f1bd3..b4a3fc04766 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -1,4 +1,5 @@ """Constants for the Vizio integration tests.""" +from homeassistant.components import zeroconf from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV, @@ -23,8 +24,6 @@ from homeassistant.const import ( CONF_INCLUDE, CONF_NAME, CONF_PIN, - CONF_PORT, - CONF_TYPE, ) from homeassistant.util import slugify @@ -198,10 +197,11 @@ ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" ZEROCONF_HOST = HOST.split(":")[0] ZEROCONF_PORT = HOST.split(":")[1] -MOCK_ZEROCONF_SERVICE_INFO = { - CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, - CONF_NAME: ZEROCONF_NAME, - CONF_HOST: ZEROCONF_HOST, - CONF_PORT: ZEROCONF_PORT, - "properties": {"name": "SB4031-D5"}, -} +MOCK_ZEROCONF_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( + host=ZEROCONF_HOST, + hostname="mock_hostname", + name=ZEROCONF_NAME, + port=ZEROCONF_PORT, + properties={"name": "SB4031-D5"}, + type=VIZIO_ZEROCONF_SERVICE_TYPE, +) diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 544ad2b38cd..d02008c6d3d 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -1,8 +1,11 @@ """Tests for Vizio config flow.""" +import dataclasses + import pytest import voluptuous as vol from homeassistant import data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( @@ -27,7 +30,6 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PIN, - CONF_PORT, ) from homeassistant.core import HomeAssistant @@ -728,7 +730,7 @@ async def test_zeroconf_flow( vizio_guess_device_type: pytest.fixture, ) -> None: """Test zeroconf config flow.""" - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -739,7 +741,15 @@ async def test_zeroconf_flow( # Apply discovery updates to entry to mimic when user hits submit without changing # defaults which were set from discovery parameters - user_input = result["data_schema"](discovery_info) + user_input = result["data_schema"]( + { + CONF_HOST: f"{discovery_info[zeroconf.ATTR_HOST]}:{discovery_info[zeroconf.ATTR_PORT]}", + CONF_NAME: discovery_info[zeroconf.ATTR_NAME][ + : -(len(discovery_info[zeroconf.ATTR_TYPE]) + 1) + ], + CONF_DEVICE_CLASS: "speaker", + } + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input @@ -768,7 +778,7 @@ async def test_zeroconf_flow_already_configured( entry.add_to_hass(hass) # Try rediscovering same device - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -794,10 +804,8 @@ async def test_zeroconf_flow_with_port_in_host( entry.add_to_hass(hass) # Try rediscovering same device, this time with port already in host - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() - discovery_info[ - CONF_HOST - ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) + discovery_info.host = f"{discovery_info.host}:{discovery_info.port}" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -814,7 +822,7 @@ async def test_zeroconf_dupe_fail( vizio_guess_device_type: pytest.fixture, ) -> None: """Test zeroconf config flow when device gets discovered multiple times.""" - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -823,7 +831,7 @@ async def test_zeroconf_dupe_fail( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -848,7 +856,7 @@ async def test_zeroconf_ignore( ) entry.add_to_hass(hass) - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -863,7 +871,7 @@ async def test_zeroconf_no_unique_id( ) -> None: """Test zeroconf discovery aborts when unique_id is None.""" - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -888,7 +896,7 @@ async def test_zeroconf_abort_when_ignored( ) entry.add_to_hass(hass) - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -916,7 +924,7 @@ async def test_zeroconf_flow_already_configured_hostname( entry.add_to_hass(hass) # Try rediscovering same device - discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) From d980ca7e0485fda6d7b20f41fb6817d46e60a084 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Nov 2021 18:33:25 +0100 Subject: [PATCH 0975/1452] Correct recorder migration._add_columns for PostgreSQL (#60547) --- homeassistant/components/recorder/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index fe3e1aeb84d..2e6e5a7bd12 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -220,7 +220,7 @@ def _add_columns(connection, table_name, columns_def): ) ) except (InternalError, OperationalError) as err: - raise_if_exception_missing_str(err, ["duplicate"]) + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Column %s already exists on %s, continuing", column_def.split(" ")[1], From 814a742518e88609e318de7ad0fa378d085bfa73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Nov 2021 18:34:38 +0100 Subject: [PATCH 0976/1452] Don't wait for Google Assistant service calls when reporting state (#59832) * Don't wait for Google Assistant service calls when reporting state * Update tests * Add test --- .../components/google_assistant/smart_home.py | 27 +- .../components/google_assistant/trait.py | 81 +++-- tests/components/google_assistant/__init__.py | 31 +- .../google_assistant/test_smart_home.py | 328 +++++++++++++++--- 4 files changed, 366 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index c9f6c20c7af..eb7b5e9c9eb 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -17,6 +17,8 @@ from .const import ( from .error import SmartHomeError from .helpers import GoogleEntity, RequestData, async_get_entities +EXECUTE_LIMIT = 2 # Wait 2 seconds for execute to finish + HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) @@ -207,16 +209,23 @@ async def handle_devices_execute(hass, data, payload): entities[entity_id] = GoogleEntity(hass, data.config, state) executions[entity_id] = [execution] - execute_results = await asyncio.gather( - *( - _entity_execute(entities[entity_id], data, execution) - for entity_id, execution in executions.items() + try: + execute_results = await asyncio.wait_for( + asyncio.shield( + asyncio.gather( + *( + _entity_execute(entities[entity_id], data, execution) + for entity_id, execution in executions.items() + ) + ) + ), + EXECUTE_LIMIT, ) - ) - - for entity_id, result in zip(executions, execute_results): - if result is not None: - results[entity_id] = result + for entity_id, result in zip(executions, execute_results): + if result is not None: + results[entity_id] = result + except asyncio.TimeoutError: + pass final_results = list(results.values()) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5801ae6811b..30ea244bac9 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -264,7 +264,7 @@ class BrightnessTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_BRIGHTNESS_PCT: params["brightness"], }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -359,7 +359,7 @@ class OnOffTrait(_Trait): service_domain, service, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -464,7 +464,7 @@ class ColorSettingTrait(_Trait): light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_COLOR_TEMP: temp}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -479,7 +479,7 @@ class ColorSettingTrait(_Trait): light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_HS_COLOR: color}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -496,7 +496,7 @@ class ColorSettingTrait(_Trait): light.ATTR_HS_COLOR: [color["hue"], saturation], light.ATTR_BRIGHTNESS: brightness, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -536,7 +536,8 @@ class SceneTrait(_Trait): self.state.domain, service, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=self.state.domain not in (button.DOMAIN, script.DOMAIN), + blocking=(not self.config.should_report_state) + and self.state.domain not in (button.DOMAIN, script.DOMAIN), context=data.context, ) @@ -570,7 +571,7 @@ class DockTrait(_Trait): self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -610,7 +611,7 @@ class LocatorTrait(_Trait): self.state.domain, vacuum.SERVICE_LOCATE, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -731,7 +732,7 @@ class StartStopTrait(_Trait): self.state.domain, vacuum.SERVICE_START, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) else: @@ -739,7 +740,7 @@ class StartStopTrait(_Trait): self.state.domain, vacuum.SERVICE_STOP, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) elif command == COMMAND_PAUSEUNPAUSE: @@ -748,7 +749,7 @@ class StartStopTrait(_Trait): self.state.domain, vacuum.SERVICE_PAUSE, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) else: @@ -756,7 +757,7 @@ class StartStopTrait(_Trait): self.state.domain, vacuum.SERVICE_START, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -776,7 +777,7 @@ class StartStopTrait(_Trait): self.state.domain, cover.SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) else: @@ -993,7 +994,7 @@ class TemperatureSettingTrait(_Trait): climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: self.state.entity_id, ATTR_TEMPERATURE: temp}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1041,7 +1042,7 @@ class TemperatureSettingTrait(_Trait): climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, svc_data, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1054,7 +1055,7 @@ class TemperatureSettingTrait(_Trait): climate.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1064,7 +1065,7 @@ class TemperatureSettingTrait(_Trait): climate.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1077,7 +1078,7 @@ class TemperatureSettingTrait(_Trait): climate.ATTR_PRESET_MODE: self.google_to_preset[target_mode], ATTR_ENTITY_ID: self.state.entity_id, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1089,7 +1090,7 @@ class TemperatureSettingTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_HVAC_MODE: self.google_to_hvac[target_mode], }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1170,7 +1171,7 @@ class HumiditySettingTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, humidifier.ATTR_HUMIDITY: params["humidity"], }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1219,7 +1220,7 @@ class LockUnlockTrait(_Trait): lock.DOMAIN, service, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1342,7 +1343,7 @@ class ArmDisArmTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, ATTR_CODE: data.config.secure_devices_pin, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1430,7 +1431,7 @@ class FanSpeedTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_FAN_MODE: params["fanSpeed"], }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1442,7 +1443,7 @@ class FanSpeedTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1458,7 +1459,7 @@ class FanSpeedTrait(_Trait): fan.DOMAIN, fan.SERVICE_SET_DIRECTION, {ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_DIRECTION: direction}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1599,7 +1600,7 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_PRESET_MODE: preset_mode, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1613,7 +1614,7 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, input_select.ATTR_OPTION: option, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1627,7 +1628,7 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, select.ATTR_OPTION: option, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1641,7 +1642,7 @@ class ModesTrait(_Trait): ATTR_MODE: requested_mode, ATTR_ENTITY_ID: self.state.entity_id, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1655,7 +1656,7 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_EFFECT: requested_effect, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) return @@ -1670,7 +1671,7 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_SOUND_MODE: sound_mode, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1744,7 +1745,7 @@ class InputSelectorTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_INPUT_SOURCE: requested_source, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1888,7 +1889,11 @@ class OpenCloseTrait(_Trait): _verify_pin_challenge(data, self.state, challenge) await self.hass.services.async_call( - cover.DOMAIN, service, svc_params, blocking=True, context=data.context + cover.DOMAIN, + service, + svc_params, + blocking=not self.config.should_report_state, + context=data.context, ) @@ -1950,7 +1955,7 @@ class VolumeTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_MEDIA_VOLUME_LEVEL: level, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -1986,7 +1991,7 @@ class VolumeTrait(_Trait): media_player.DOMAIN, svc, {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) else: @@ -2008,7 +2013,7 @@ class VolumeTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_MEDIA_VOLUME_MUTED: mute, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -2170,7 +2175,7 @@ class TransportControlTrait(_Trait): media_player.DOMAIN, service, service_attrs, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) @@ -2275,7 +2280,7 @@ class ChannelTrait(_Trait): media_player.ATTR_MEDIA_CONTENT_ID: channel_number, media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, }, - blocking=True, + blocking=not self.config.should_report_state, context=data.context, ) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f7537db18de..562bd4e16cd 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -20,25 +20,27 @@ class MockConfig(helpers.AbstractConfig): def __init__( self, *, - secure_devices_pin=None, - should_expose=None, - should_2fa=None, + agent_user_ids=None, + enabled=True, entity_config=None, hass=None, - local_sdk_webhook_id=None, local_sdk_user_id=None, - enabled=True, - agent_user_ids=None, + local_sdk_webhook_id=None, + secure_devices_pin=None, + should_2fa=None, + should_expose=None, + should_report_state=False, ): """Initialize config.""" super().__init__(hass) - self._should_expose = should_expose - self._should_2fa = should_2fa - self._secure_devices_pin = secure_devices_pin - self._entity_config = entity_config or {} - self._local_sdk_webhook_id = local_sdk_webhook_id - self._local_sdk_user_id = local_sdk_user_id self._enabled = enabled + self._entity_config = entity_config or {} + self._local_sdk_user_id = local_sdk_user_id + self._local_sdk_webhook_id = local_sdk_webhook_id + self._secure_devices_pin = secure_devices_pin + self._should_2fa = should_2fa + self._should_expose = should_expose + self._should_report_state = should_report_state self._store = mock_google_config_store(agent_user_ids) @property @@ -74,6 +76,11 @@ class MockConfig(helpers.AbstractConfig): """Expose it all.""" return self._should_expose is None or self._should_expose(state) + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return self._should_report_state + def should_2fa(self, state): """Expose it all.""" return self._should_2fa is None or self._should_2fa(state) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 9d259290956..911f66bb428 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,5 +1,6 @@ """Test Google Smart Home.""" -from unittest.mock import patch +import asyncio +from unittest.mock import ANY, call, patch import pytest @@ -25,7 +26,7 @@ from homeassistant.components.google_assistant import ( from homeassistant.config import async_process_ha_core_config from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ from homeassistant.core import EVENT_CALL_SERVICE, State -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry, entity_platform from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig @@ -367,7 +368,10 @@ async def test_query_message(hass): } -async def test_execute(hass): +@pytest.mark.parametrize( + "report_state,on,brightness,value", [(False, True, 20, 0.2), (True, ANY, ANY, ANY)] +) +async def test_execute(hass, report_state, on, brightness, value): """Test an execute command.""" await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) await hass.async_block_till_done() @@ -375,45 +379,82 @@ async def test_execute(hass): await hass.services.async_call( "light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True ) + await hass.async_block_till_done() events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) service_events = async_capture_events(hass, EVENT_CALL_SERVICE) - result = await sh.async_handle_message( - hass, - BASIC_CONFIG, - None, - { - "requestId": REQ_ID, - "inputs": [ - { - "intent": "action.devices.EXECUTE", - "payload": { - "commands": [ - { - "devices": [ - {"id": "light.non_existing"}, - {"id": "light.ceiling_lights"}, - {"id": "light.kitchen_lights"}, - ], - "execution": [ - { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, - { - "command": "action.devices.commands.BrightnessAbsolute", - "params": {"brightness": 20}, - }, - ], - } - ] - }, - } - ], - }, - const.SOURCE_CLOUD, - ) + with patch.object( + hass.services, "async_call", wraps=hass.services.async_call + ) as call_service_mock: + result = await sh.async_handle_message( + hass, + MockConfig(should_report_state=report_state), + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + {"id": "light.non_existing"}, + {"id": "light.ceiling_lights"}, + {"id": "light.kitchen_lights"}, + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + { + "command": "action.devices.commands.BrightnessAbsolute", + "params": {"brightness": 20}, + }, + ], + } + ] + }, + } + ], + }, + const.SOURCE_CLOUD, + ) + assert call_service_mock.call_count == 4 + expected_calls = [ + call( + "light", + "turn_on", + {"entity_id": "light.ceiling_lights"}, + blocking=not report_state, + context=ANY, + ), + call( + "light", + "turn_on", + {"entity_id": "light.kitchen_lights"}, + blocking=not report_state, + context=ANY, + ), + call( + "light", + "turn_on", + {"entity_id": "light.ceiling_lights", "brightness_pct": 20}, + blocking=not report_state, + context=ANY, + ), + call( + "light", + "turn_on", + {"entity_id": "light.kitchen_lights", "brightness_pct": 20}, + blocking=not report_state, + context=ANY, + ), + ] + call_service_mock.assert_has_awaits(expected_calls, any_order=True) + await hass.async_block_till_done() assert result == { "requestId": REQ_ID, @@ -428,9 +469,9 @@ async def test_execute(hass): "ids": ["light.ceiling_lights"], "status": "SUCCESS", "states": { - "on": True, + "on": on, "online": True, - "brightness": 20, + "brightness": brightness, "color": {"temperatureK": 2631}, }, }, @@ -440,12 +481,12 @@ async def test_execute(hass): "states": { "on": True, "online": True, - "brightness": 20, + "brightness": brightness, "color": { "spectrumHsv": { "hue": 345, "saturation": 0.75, - "value": 0.2, + "value": value, }, }, }, @@ -506,6 +547,209 @@ async def test_execute(hass): assert service_events[3].context == events[0].context +@pytest.mark.parametrize("report_state,on,brightness,value", [(False, False, ANY, ANY)]) +async def test_execute_times_out(hass, report_state, on, brightness, value): + """Test an execute command which times out.""" + orig_execute_limit = sh.EXECUTE_LIMIT + sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() + + await hass.services.async_call( + "light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True + ) + await hass.async_block_till_done() + + events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) + service_events = async_capture_events(hass, EVENT_CALL_SERVICE) + + platforms = entity_platform.async_get_platforms(hass, "demo") + assert platforms[0].domain == "light" + assert platforms[0].entities["light.ceiling_lights"] + + turn_on_wait = asyncio.Event() + + async def slow_turn_on(*args, **kwargs): + # Make DemoLigt.async_turn_on hang waiting for the turn_on_wait event + await turn_on_wait.wait(), + + with patch.object( + hass.services, "async_call", wraps=hass.services.async_call + ) as call_service_mock, patch.object( + DemoLight, "async_turn_on", wraps=slow_turn_on + ): + result = await sh.async_handle_message( + hass, + MockConfig(should_report_state=report_state), + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + {"id": "light.non_existing"}, + {"id": "light.ceiling_lights"}, + {"id": "light.kitchen_lights"}, + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + { + "command": "action.devices.commands.BrightnessAbsolute", + "params": {"brightness": 20}, + }, + ], + } + ] + }, + } + ], + }, + const.SOURCE_CLOUD, + ) + # Only the two first calls are executed + assert call_service_mock.call_count == 2 + expected_calls = [ + call( + "light", + "turn_on", + {"entity_id": "light.ceiling_lights"}, + blocking=not report_state, + context=ANY, + ), + call( + "light", + "turn_on", + {"entity_id": "light.kitchen_lights"}, + blocking=not report_state, + context=ANY, + ), + ] + call_service_mock.assert_has_awaits(expected_calls, any_order=True) + + turn_on_wait.set() + await hass.async_block_till_done() + # The remaining two calls should now have executed + assert call_service_mock.call_count == 4 + expected_calls.extend( + [ + call( + "light", + "turn_on", + {"entity_id": "light.ceiling_lights", "brightness_pct": 20}, + blocking=not report_state, + context=ANY, + ), + call( + "light", + "turn_on", + {"entity_id": "light.kitchen_lights", "brightness_pct": 20}, + blocking=not report_state, + context=ANY, + ), + ] + ) + call_service_mock.assert_has_awaits(expected_calls, any_order=True) + await hass.async_block_till_done() + + assert result == { + "requestId": REQ_ID, + "payload": { + "commands": [ + { + "ids": ["light.non_existing"], + "status": "ERROR", + "errorCode": "deviceOffline", + }, + { + "ids": ["light.ceiling_lights"], + "status": "SUCCESS", + "states": { + "on": on, + "online": True, + "brightness": brightness, + }, + }, + { + "ids": ["light.kitchen_lights"], + "status": "SUCCESS", + "states": { + "on": True, + "online": True, + "brightness": brightness, + "color": { + "spectrumHsv": { + "hue": 345, + "saturation": 0.75, + "value": value, + }, + }, + }, + }, + ] + }, + } + + assert len(events) == 1 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + "request_id": REQ_ID, + "entity_id": [ + "light.non_existing", + "light.ceiling_lights", + "light.kitchen_lights", + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + { + "command": "action.devices.commands.BrightnessAbsolute", + "params": {"brightness": 20}, + }, + ], + "source": "cloud", + } + + service_events = sorted( + service_events, key=lambda ev: ev.data["service_data"]["entity_id"] + ) + assert len(service_events) == 4 + assert service_events[0].data == { + "domain": "light", + "service": "turn_on", + "service_data": {"entity_id": "light.ceiling_lights"}, + } + assert service_events[1].data == { + "domain": "light", + "service": "turn_on", + "service_data": {"brightness_pct": 20, "entity_id": "light.ceiling_lights"}, + } + assert service_events[0].context == events[0].context + assert service_events[1].context == events[0].context + assert service_events[2].data == { + "domain": "light", + "service": "turn_on", + "service_data": {"entity_id": "light.kitchen_lights"}, + } + assert service_events[3].data == { + "domain": "light", + "service": "turn_on", + "service_data": {"brightness_pct": 20, "entity_id": "light.kitchen_lights"}, + } + assert service_events[2].context == events[0].context + assert service_events[3].context == events[0].context + + sh.EXECUTE_LIMIT = orig_execute_limit + + async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" hass.states.async_set( From 83acfda757ec7cb689f656c33f19514848e9e353 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:49:49 +0100 Subject: [PATCH 0977/1452] Add reboot button to Shelly devices (#60417) --- homeassistant/components/shelly/button.py | 98 ++++++++++++----------- tests/components/shelly/conftest.py | 2 + tests/components/shelly/test_button.py | 86 +++++++++++++++++--- 3 files changed, 131 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 7890eefd30c..46a98e4a649 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -1,9 +1,11 @@ """Button for Shelly.""" from __future__ import annotations -from typing import cast +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final, cast -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant @@ -17,6 +19,44 @@ from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name +@dataclass +class ShellyButtonDescriptionMixin: + """Mixin to describe a Button entity.""" + + press_action: Callable + + +@dataclass +class ShellyButtonDescription(ButtonEntityDescription, ShellyButtonDescriptionMixin): + """Class to describe a Button entity.""" + + +BUTTONS: Final = [ + ShellyButtonDescription( + key="ota_update", + name="OTA Update", + icon="mdi:package-up", + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda wrapper: wrapper.async_trigger_ota_update(), + ), + ShellyButtonDescription( + key="ota_update_beta", + name="OTA Update Beta", + icon="mdi:flask-outline", + entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda wrapper: wrapper.async_trigger_ota_update(beta=True), + ), + ShellyButtonDescription( + key="reboot", + name="Reboot", + icon="mdi:restart", + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda wrapper: wrapper.device.trigger_reboot(), + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -36,66 +76,34 @@ async def async_setup_entry( wrapper = cast(BlockDeviceWrapper, block_wrapper) if wrapper is not None: - async_add_entities( - [ - ShellyOtaUpdateStableButton(wrapper, config_entry), - ShellyOtaUpdateBetaButton(wrapper, config_entry), - ] - ) + async_add_entities([ShellyButton(wrapper, button) for button in BUTTONS]) -class ShellyOtaUpdateBaseButton(ButtonEntity): +class ShellyButton(ButtonEntity): """Defines a Shelly OTA update base button.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + entity_description: ShellyButtonDescription def __init__( self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, - entry: ConfigEntry, - name: str, - beta_channel: bool, - icon: str, + description: ShellyButtonDescription, ) -> None: """Initialize Shelly OTA update button.""" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} - ) + self.entity_description = description + self.wrapper = wrapper if isinstance(wrapper, RpcDeviceWrapper): device_name = get_rpc_device_name(wrapper.device) else: device_name = get_block_device_name(wrapper.device) - self._attr_name = f"{device_name} {name}" + self._attr_name = f"{device_name} {description.name}" self._attr_unique_id = slugify(self._attr_name) - self._attr_icon = icon - - self.beta_channel = beta_channel - self.entry = entry - self.wrapper = wrapper + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} + ) async def async_press(self) -> None: """Triggers the OTA update service.""" - await self.wrapper.async_trigger_ota_update(beta=self.beta_channel) - - -class ShellyOtaUpdateStableButton(ShellyOtaUpdateBaseButton): - """Defines a Shelly OTA update stable channel button.""" - - def __init__( - self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry - ) -> None: - """Initialize Shelly OTA update button.""" - super().__init__(wrapper, entry, "OTA Update", False, "mdi:package-up") - - -class ShellyOtaUpdateBetaButton(ShellyOtaUpdateBaseButton): - """Defines a Shelly OTA update beta channel button.""" - - def __init__( - self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry - ) -> None: - """Initialize Shelly OTA update button.""" - super().__init__(wrapper, entry, "OTA Update Beta", True, "mdi:flask-outline") - self._attr_entity_registry_enabled_default = False + await self.entity_description.press_action(self.wrapper) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 7fc8fba0e33..5391f3f74fe 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -138,6 +138,7 @@ async def coap_wrapper(hass): firmware_version="some fw string", update=AsyncMock(), trigger_ota_update=AsyncMock(), + trigger_reboot=AsyncMock(), initialized=True, ) @@ -173,6 +174,7 @@ async def rpc_wrapper(hass): firmware_version="some fw string", update=AsyncMock(), trigger_ota_update=AsyncMock(), + trigger_reboot=AsyncMock(), initialized=True, shutdown=AsyncMock(), ) diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 5ceed08b9d9..442e6ef248f 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,6 +1,7 @@ """Tests for Shelly button platform.""" from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get @@ -10,6 +11,14 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): """Test block device OTA button.""" assert coap_wrapper + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + BUTTON_DOMAIN, + DOMAIN, + "test_name_ota_update_beta", + suggested_object_id="test_name_ota_update_beta", + disabled_by=None, + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) ) @@ -27,21 +36,54 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): blocking=True, ) await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + assert coap_wrapper.device.trigger_ota_update.call_count == 1 + coap_wrapper.device.trigger_ota_update.assert_called_with(beta=False) # beta channel button - entity_registry = async_get(hass) - entry = entity_registry.async_get("button.test_name_ota_update_beta") state = hass.states.get("button.test_name_ota_update_beta") - assert entry - assert state is None + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update_beta"}, + blocking=True, + ) + await hass.async_block_till_done() + assert coap_wrapper.device.trigger_ota_update.call_count == 2 + coap_wrapper.device.trigger_ota_update.assert_called_with(beta=True) + + # reboot button + state = hass.states.get("button.test_name_reboot") + + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_reboot"}, + blocking=True, + ) + await hass.async_block_till_done() + assert coap_wrapper.device.trigger_reboot.call_count == 1 async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): """Test rpc device OTA button.""" assert rpc_wrapper + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + BUTTON_DOMAIN, + DOMAIN, + "test_name_ota_update_beta", + suggested_object_id="test_name_ota_update_beta", + disabled_by=None, + ) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) ) @@ -59,12 +101,36 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): blocking=True, ) await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + assert rpc_wrapper.device.trigger_ota_update.call_count == 1 + rpc_wrapper.device.trigger_ota_update.assert_called_with(beta=False) # beta channel button - entity_registry = async_get(hass) - entry = entity_registry.async_get("button.test_name_ota_update_beta") state = hass.states.get("button.test_name_ota_update_beta") - assert entry - assert state is None + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update_beta"}, + blocking=True, + ) + await hass.async_block_till_done() + assert rpc_wrapper.device.trigger_ota_update.call_count == 2 + rpc_wrapper.device.trigger_ota_update.assert_called_with(beta=True) + + # reboot button + state = hass.states.get("button.test_name_reboot") + + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_reboot"}, + blocking=True, + ) + await hass.async_block_till_done() + assert rpc_wrapper.device.trigger_reboot.call_count == 1 From 8626de24fc9a134c6df4d838693413591aecb005 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:58:22 +0100 Subject: [PATCH 0978/1452] Use correct value for current temperature for AVM Fritz!Smarthome thermostat devices (#60510) --- homeassistant/components/fritzbox/climate.py | 2 ++ tests/components/fritzbox/__init__.py | 3 ++- tests/components/fritzbox/test_climate.py | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index ef1285f0ec2..0f481773778 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -88,6 +88,8 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): @property def current_temperature(self) -> float: """Return the current temperature.""" + if self.device.has_temperature_sensor and self.device.temperature is not None: + return self.device.temperature # type: ignore [no-any-return] return self.device.actual_temperature # type: ignore [no-any-return] @property diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 80052125f99..05003ccdf51 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -68,6 +68,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): """Mock of a AVM Fritz!Box climate device.""" actual_temperature = 18.0 + temperature = 18.0 alert_state = "fake_state" battery_level = 23 battery_low = True @@ -79,7 +80,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): has_powermeter = False has_lightbulb = False has_switch = False - has_temperature_sensor = False + has_temperature_sensor = True has_thermostat = True holiday_active = "fake_holiday" lock = "fake_locked" diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 175b67df858..fc3b15c4199 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -186,7 +186,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_MIN_TEMP] == 8 assert state.attributes[ATTR_TEMPERATURE] == 19.5 - device.actual_temperature = 19 + device.temperature = 19 device.target_temperature = 20 next_update = dt_util.utcnow() + timedelta(seconds=200) @@ -200,6 +200,24 @@ async def test_update(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_TEMPERATURE] == 20 +async def test_automatic_offset(hass: HomeAssistant, fritz: Mock): + """Test when automtaic offset is configured on fritz!box device.""" + device = FritzDeviceClimateMock() + device.temperature = 18 + device.actual_temperature = 19 + device.target_temperature = 20 + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 + assert state.attributes[ATTR_MAX_TEMP] == 28 + assert state.attributes[ATTR_MIN_TEMP] == 8 + assert state.attributes[ATTR_TEMPERATURE] == 20 + + async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() From 847b10fa65eebf1fa97279b68bb9d164c4689b8e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 29 Nov 2021 22:58:04 +0100 Subject: [PATCH 0979/1452] Add `button` platform to NAM integration (#60410) --- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nam/button.py | 58 ++++++++++++++++++++++++ tests/components/nam/test_button.py | 48 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nam/button.py create mode 100644 tests/components/nam/test_button.py diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 98152956fb5..094c286b931 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -42,7 +42,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["button", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py new file mode 100644 index 00000000000..3a205606ccc --- /dev/null +++ b/homeassistant/components/nam/button.py @@ -0,0 +1,58 @@ +"""Support for the Nettigo Air Monitor service.""" +from __future__ import annotations + +import logging + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NAMDataUpdateCoordinator +from .const import DEFAULT_NAME, DOMAIN + +PARALLEL_UPDATES = 1 + +_LOGGER = logging.getLogger(__name__) + +RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( + key="restart", + name=f"{DEFAULT_NAME} Restart", + icon="mdi:restart", + entity_category=ENTITY_CATEGORY_CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a Nettigo Air Monitor entities from a config_entry.""" + coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + buttons: list[NAMButton] = [] + buttons.append(NAMButton(coordinator, RESTART_BUTTON)) + + async_add_entities(buttons, False) + + +class NAMButton(CoordinatorEntity, ButtonEntity): + """Define an Nettigo Air Monitor button.""" + + coordinator: NAMDataUpdateCoordinator + + def __init__( + self, + coordinator: NAMDataUpdateCoordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self.entity_description = description + + async def async_press(self) -> None: + """Triggers the restart.""" + await self.coordinator.nam.async_restart() diff --git a/tests/components/nam/test_button.py b/tests/components/nam/test_button.py new file mode 100644 index 00000000000..7cd731e7584 --- /dev/null +++ b/tests/components/nam/test_button.py @@ -0,0 +1,48 @@ +"""Test button of Nettigo Air Monitor integration.""" +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from tests.components.nam import init_integration + + +async def test_button(hass): + """Test states of the button.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("button.nettigo_air_monitor_restart") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ICON) == "mdi:restart" + + entry = registry.async_get("button.nettigo_air_monitor_restart") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-restart" + + +async def test_button_press(hass): + """Test button press.""" + await init_integration(hass) + + now = dt_util.utcnow() + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_restart" + ) as mock_restart, patch("homeassistant.core.dt_util.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.nettigo_air_monitor_restart"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_restart.assert_called_once() + + state = hass.states.get("button.nettigo_air_monitor_restart") + assert state + assert state.state == now.isoformat() From 914f7f85ec535d17240b6f54006602bd6a8f96c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Nov 2021 14:01:03 -0800 Subject: [PATCH 0980/1452] Add local only users (#57598) --- homeassistant/auth/__init__.py | 32 ++++++-- homeassistant/auth/auth_store.py | 27 ++++-- homeassistant/auth/models.py | 1 + homeassistant/components/almond/__init__.py | 4 +- homeassistant/components/auth/__init__.py | 23 +++++- homeassistant/components/auth/login_flow.py | 82 +++++++++++-------- .../components/cast/home_assistant_cast.py | 2 +- homeassistant/components/cloud/prefs.py | 2 +- homeassistant/components/config/auth.py | 6 +- homeassistant/components/hassio/__init__.py | 4 +- homeassistant/components/http/__init__.py | 8 +- homeassistant/components/http/auth.py | 43 ++++++++++ .../components/http/request_context.py | 4 + homeassistant/components/onboarding/views.py | 4 +- tests/auth/test_init.py | 25 +++++- tests/components/auth/test_init.py | 66 +++++++++++++++ tests/components/auth/test_login_flow.py | 38 +++++++++ tests/components/http/test_auth.py | 76 ++++++++++++++++- 18 files changed, 378 insertions(+), 69 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index abd5ddc71d5..21afef8b1be 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -214,11 +214,19 @@ class AuthManager: return None async def async_create_system_user( - self, name: str, group_ids: list[str] | None = None + self, + name: str, + *, + group_ids: list[str] | None = None, + local_only: bool | None = None, ) -> models.User: """Create a system user.""" user = await self._store.async_create_user( - name=name, system_generated=True, is_active=True, group_ids=group_ids or [] + name=name, + system_generated=True, + is_active=True, + group_ids=group_ids or [], + local_only=local_only, ) self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) @@ -226,13 +234,18 @@ class AuthManager: return user async def async_create_user( - self, name: str, group_ids: list[str] | None = None + self, + name: str, + *, + group_ids: list[str] | None = None, + local_only: bool | None = None, ) -> models.User: """Create a user.""" kwargs: dict[str, Any] = { "name": name, "is_active": True, "group_ids": group_ids or [], + "local_only": local_only, } if await self._user_should_be_owner(): @@ -304,13 +317,18 @@ class AuthManager: name: str | None = None, is_active: bool | None = None, group_ids: list[str] | None = None, + local_only: bool | None = None, ) -> None: """Update a user.""" kwargs: dict[str, Any] = {} - if name is not None: - kwargs["name"] = name - if group_ids is not None: - kwargs["group_ids"] = group_ids + + for attr_name, value in ( + ("name", name), + ("group_ids", group_ids), + ("local_only", local_only), + ): + if value is not None: + kwargs[attr_name] = value await self._store.async_update_user(user, **kwargs) if is_active is not None: diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 114329eda1e..8acb7c44398 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -86,6 +86,7 @@ class AuthStore: system_generated: bool | None = None, credentials: models.Credentials | None = None, group_ids: list[str] | None = None, + local_only: bool | None = None, ) -> models.User: """Create a new user.""" if self._users is None: @@ -108,14 +109,14 @@ class AuthStore: "perm_lookup": self._perm_lookup, } - if is_owner is not None: - kwargs["is_owner"] = is_owner - - if is_active is not None: - kwargs["is_active"] = is_active - - if system_generated is not None: - kwargs["system_generated"] = system_generated + for attr_name, value in ( + ("is_owner", is_owner), + ("is_active", is_active), + ("local_only", local_only), + ("system_generated", system_generated), + ): + if value is not None: + kwargs[attr_name] = value new_user = models.User(**kwargs) @@ -152,6 +153,7 @@ class AuthStore: name: str | None = None, is_active: bool | None = None, group_ids: list[str] | None = None, + local_only: bool | None = None, ) -> None: """Update a user.""" assert self._groups is not None @@ -166,7 +168,11 @@ class AuthStore: user.groups = groups user.invalidate_permission_cache() - for attr_name, value in (("name", name), ("is_active", is_active)): + for attr_name, value in ( + ("name", name), + ("is_active", is_active), + ("local_only", local_only), + ): if value is not None: setattr(user, attr_name, value) @@ -417,6 +423,8 @@ class AuthStore: is_active=user_dict["is_active"], system_generated=user_dict["system_generated"], perm_lookup=perm_lookup, + # New in 2021.11 + local_only=user_dict.get("local_only", False), ) for cred_dict in data["credentials"]: @@ -502,6 +510,7 @@ class AuthStore: "is_active": user.is_active, "name": user.name, "system_generated": user.system_generated, + "local_only": user.local_only, } for user in self._users.values() ] diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 758bbdb78e2..e604bf9d21c 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -39,6 +39,7 @@ class User: is_owner: bool = attr.ib(default=False) is_active: bool = attr.ib(default=False) system_generated: bool = attr.ib(default=False) + local_only: bool = attr.ib(default=False) groups: list[Group] = attr.ib(factory=list, eq=False, order=False) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 03fc1f26011..0dd7a76d4c4 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -177,7 +177,9 @@ async def _configure_almond_for_ha( user = await hass.auth.async_get_user(data["almond_user"]) if user is None: - user = await hass.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) + user = await hass.auth.async_create_system_user( + "Almond", group_ids=[GROUP_ID_ADMIN] + ) data["almond_user"] = user.id await store.async_save(data) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 359f67ed0a5..374a36683da 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -126,7 +126,10 @@ import voluptuous as vol from homeassistant.auth import InvalidAuthError from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials from homeassistant.components import websocket_api -from homeassistant.components.http.auth import async_sign_path +from homeassistant.components.http.auth import ( + async_sign_path, + async_user_not_allowed_do_auth, +) from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView @@ -299,9 +302,12 @@ class TokenView(HomeAssistantView): user = await hass.auth.async_get_or_create_user(credential) - if not user.is_active: + if user_access_error := async_user_not_allowed_do_auth(hass, user): return self.json( - {"error": "access_denied", "error_description": "User is not active"}, + { + "error": "access_denied", + "error_description": user_access_error, + }, status_code=HTTPStatus.FORBIDDEN, ) @@ -355,6 +361,17 @@ class TokenView(HomeAssistantView): {"error": "invalid_request"}, status_code=HTTPStatus.BAD_REQUEST ) + if user_access_error := async_user_not_allowed_do_auth( + hass, refresh_token.user + ): + return self.json( + { + "error": "access_denied", + "error_description": user_access_error, + }, + status_code=HTTPStatus.FORBIDDEN, + ) + try: access_token = hass.auth.async_create_access_token( refresh_token, remote_addr diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index ed5c544499e..ef2bb793662 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -74,6 +74,8 @@ import voluptuous as vol import voluptuous_serialize from homeassistant import data_entry_flow +from homeassistant.auth.models import Credentials +from homeassistant.components.http.auth import async_user_not_allowed_do_auth from homeassistant.components.http.ban import ( log_invalid_auth, process_success_login, @@ -81,6 +83,7 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import HomeAssistant from . import indieauth @@ -138,11 +141,9 @@ def _prepare_result_json(result): return data -class LoginFlowIndexView(HomeAssistantView): - """View to create a config flow.""" +class LoginFlowBaseView(HomeAssistantView): + """Base class for the login views.""" - url = "/auth/login_flow" - name = "api:auth:login_flow" requires_auth = False def __init__(self, flow_mgr, store_result): @@ -150,6 +151,46 @@ class LoginFlowIndexView(HomeAssistantView): self._flow_mgr = flow_mgr self._store_result = store_result + async def _async_flow_result_to_response(self, request, client_id, result): + """Convert the flow result to a response.""" + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.RESULT_TYPE_FORM: + # @log_invalid_auth does not work here since it returns HTTP 200 + # need manually log failed login attempts + if result.get("errors", {}).get("base") in ( + "invalid_auth", + "invalid_code", + ): + await process_wrong_login(request) + return self.json(_prepare_result_json(result)) + + result.pop("data") + + hass: HomeAssistant = request.app["hass"] + result_obj: Credentials = result.pop("result") + + # Result can be None if credential was never linked to a user before. + user = await hass.auth.async_get_user_by_credentials(result_obj) + + if user is not None and ( + user_access_error := async_user_not_allowed_do_auth(hass, user) + ): + return self.json_message( + f"Login blocked: {user_access_error}", HTTPStatus.FORBIDDEN + ) + + await process_success_login(request) + result["result"] = self._store_result(client_id, result_obj) + + return self.json(result) + + +class LoginFlowIndexView(LoginFlowBaseView): + """View to create a config flow.""" + + url = "/auth/login_flow" + name = "api:auth:login_flow" + async def get(self, request): """Do not allow index of flows in progress.""" # pylint: disable=no-self-use @@ -195,26 +236,16 @@ class LoginFlowIndexView(HomeAssistantView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - await process_success_login(request) - result.pop("data") - result["result"] = self._store_result(data["client_id"], result["result"]) - return self.json(result) - - return self.json(_prepare_result_json(result)) + return await self._async_flow_result_to_response( + request, data["client_id"], result + ) -class LoginFlowResourceView(HomeAssistantView): +class LoginFlowResourceView(LoginFlowBaseView): """View to interact with the flow manager.""" url = "/auth/login_flow/{flow_id}" name = "api:auth:login_flow:resource" - requires_auth = False - - def __init__(self, flow_mgr, store_result): - """Initialize the login flow resource view.""" - self._flow_mgr = flow_mgr - self._store_result = store_result async def get(self, request): """Do not allow getting status of a flow in progress.""" @@ -240,20 +271,7 @@ class LoginFlowResourceView(HomeAssistantView): except vol.Invalid: return self.json_message("User input malformed", HTTPStatus.BAD_REQUEST) - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - # @log_invalid_auth does not work here since it returns HTTP 200 - # need manually log failed login attempts - if result.get("errors") is not None and result["errors"].get("base") in ( - "invalid_auth", - "invalid_code", - ): - await process_wrong_login(request) - return self.json(_prepare_result_json(result)) - - result.pop("data") - result["result"] = self._store_result(client_id, result["result"]) - - return self.json(result) + return await self._async_flow_result_to_response(request, client_id, result) async def delete(self, request, flow_id): """Cancel a flow in progress.""" diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 6127e466099..e4b72f0bf40 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -28,7 +28,7 @@ async def async_setup_ha_cast( if user is None: user = await hass.auth.async_create_system_user( - "Home Assistant Cast", [auth.GROUP_ID_ADMIN] + "Home Assistant Cast", group_ids=[auth.GROUP_ID_ADMIN] ) hass.config_entries.async_update_entry( entry, data={**entry.data, "user_id": user.id} diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index edd0e5ddda4..11d4ebbb175 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -281,7 +281,7 @@ class CloudPreferences: return user.id user = await self._hass.auth.async_create_system_user( - "Home Assistant Cloud", [GROUP_ID_ADMIN] + "Home Assistant Cloud", group_ids=[GROUP_ID_ADMIN], local_only=True ) assert user is not None await self.async_update(cloud_user=user.id) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 5c2e4ad2ed5..9170b028482 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -66,11 +66,14 @@ async def websocket_delete(hass, connection, msg): vol.Required("type"): "config/auth/create", vol.Required("name"): str, vol.Optional("group_ids"): [str], + vol.Optional("local_only"): bool, } ) async def websocket_create(hass, connection, msg): """Create a user.""" - user = await hass.auth.async_create_user(msg["name"], msg.get("group_ids")) + user = await hass.auth.async_create_user( + msg["name"], group_ids=msg.get("group_ids"), local_only=msg.get("local_only") + ) connection.send_message( websocket_api.result_message(msg["id"], {"user": _user_info(user)}) @@ -86,6 +89,7 @@ async def websocket_create(hass, connection, msg): vol.Optional("name"): str, vol.Optional("is_active"): bool, vol.Optional("group_ids"): [str], + vol.Optional("local_only"): bool, } ) async def websocket_update(hass, connection, msg): diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6b61881b47b..e9776dcb7e1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -442,7 +442,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await hass.auth.async_update_user(user, name="Supervisor") if refresh_token is None: - user = await hass.auth.async_create_system_user("Supervisor", [GROUP_ID_ADMIN]) + user = await hass.auth.async_create_system_user( + "Supervisor", group_ids=[GROUP_ID_ADMIN] + ) refresh_token = await hass.auth.async_create_refresh_token(user) data["hassio_user"] = user.id await store.async_save(data) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7c30acfc118..b12b6f83e3a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,7 +1,6 @@ """Support to serve the Home Assistant API as WSGI application.""" from __future__ import annotations -from contextvars import ContextVar from ipaddress import ip_network import logging import os @@ -28,7 +27,7 @@ from .ban import setup_bans from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 from .cors import setup_cors from .forwarded import async_setup_forwarded -from .request_context import setup_request_context +from .request_context import current_request, setup_request_context from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView @@ -401,8 +400,3 @@ async def start_http_server_and_save_config( ] store.async_delay_save(lambda: conf, SAVE_DELAY) - - -current_request: ContextVar[web.Request | None] = ContextVar( - "current_request", default=None -) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index e4d7da6ac9b..19f7c429a1e 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import timedelta +from ipaddress import ip_address import logging import secrets from typing import Final @@ -12,10 +13,13 @@ from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware import jwt +from homeassistant.auth.models import User from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util +from homeassistant.util.network import is_local from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER +from .request_context import current_request _LOGGER = logging.getLogger(__name__) @@ -46,6 +50,42 @@ def async_sign_path( return f"{path}?{SIGN_QUERY_PARAM}={encoded}" +@callback +def async_user_not_allowed_do_auth( + hass: HomeAssistant, user: User, request: Request | None = None +) -> str | None: + """Validate that user is not allowed to do auth things.""" + if not user.is_active: + return "User is not active" + + if not user.local_only: + return None + + # User is marked as local only, check if they are allowed to do auth + if request is None: + request = current_request.get() + + if not request: + return "No request available to validate local access" + + if "cloud" in hass.config.components: + # pylint: disable=import-outside-toplevel + from hass_nabucasa import remote + + if remote.is_cloud_request.get(): + return "User is local only" + + try: + remote = ip_address(request.remote) + except ValueError: + return "Invalid remote IP" + + if is_local(remote): + return None + + return "User cannot authenticate remotely" + + @callback def setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" @@ -72,6 +112,9 @@ def setup_auth(hass: HomeAssistant, app: Application) -> None: if refresh_token is None: return False + if async_user_not_allowed_do_auth(hass, refresh_token.user, request): + return False + request[KEY_HASS_USER] = refresh_token.user request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id return True diff --git a/homeassistant/components/http/request_context.py b/homeassistant/components/http/request_context.py index 032f3bfd49e..6e036b9cdc8 100644 --- a/homeassistant/components/http/request_context.py +++ b/homeassistant/components/http/request_context.py @@ -8,6 +8,10 @@ from aiohttp.web import Application, Request, StreamResponse, middleware from homeassistant.core import callback +current_request: ContextVar[Request | None] = ContextVar( + "current_request", default=None +) + @callback def setup_request_context( diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 61a99d345ff..44d239fdb6b 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -129,7 +129,9 @@ class UserOnboardingView(_BaseOnboardingView): provider = _async_get_hass_provider(hass) await provider.async_initialize() - user = await hass.auth.async_create_user(data["name"], [GROUP_ID_ADMIN]) + user = await hass.auth.async_create_user( + data["name"], group_ids=[GROUP_ID_ADMIN] + ) await hass.async_add_executor_job( provider.data.add_auth, data["username"], data["password"] ) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index ef1430f99a6..53c2a4261ae 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -13,7 +13,7 @@ from homeassistant.auth import ( const as auth_const, models as auth_models, ) -from homeassistant.auth.const import MFA_SESSION_EXPIRATION +from homeassistant.auth.const import GROUP_ID_ADMIN, MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -390,6 +390,8 @@ async def test_generating_system_user(hass): user = await manager.async_create_system_user("Hass.io") token = await manager.async_create_refresh_token(user) assert user.system_generated + assert user.groups == [] + assert not user.local_only assert token is not None assert token.client_id is None @@ -397,6 +399,21 @@ async def test_generating_system_user(hass): assert len(events) == 1 assert events[0].data["user_id"] == user.id + # Passing arguments + user = await manager.async_create_system_user( + "Hass.io", group_ids=[GROUP_ID_ADMIN], local_only=True + ) + token = await manager.async_create_refresh_token(user) + assert user.system_generated + assert user.is_admin + assert user.local_only + assert token is not None + assert token.client_id is None + + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data["user_id"] == user.id + async def test_refresh_token_requires_client_for_user(hass): """Test create refresh token for a user with client_id.""" @@ -1038,15 +1055,19 @@ async def test_new_users(mock_hass): # first user in the system is owner and admin assert user.is_owner assert user.is_admin + assert not user.local_only assert user.groups == [] user = await manager.async_create_user("Hello 2") assert not user.is_admin assert user.groups == [] - user = await manager.async_create_user("Hello 3", ["system-admin"]) + user = await manager.async_create_user( + "Hello 3", group_ids=["system-admin"], local_only=True + ) assert user.is_admin assert user.groups[0].id == "system-admin" + assert user.local_only user_cred = await manager.async_get_or_create_user( auth_models.Credentials( diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 53cc291a5db..39c7c4897c4 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -109,6 +109,48 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == HTTPStatus.OK +async def test_auth_code_checks_local_only_user(hass, aiohttp_client): + """Test local only user cannot exchange auth code for refresh tokens when external.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.post( + "/auth/login_flow", + json={ + "client_id": CLIENT_ID, + "handler": ["insecure_example", None], + "redirect_uri": CLIENT_REDIRECT_URI, + }, + ) + assert resp.status == HTTPStatus.OK + step = await resp.json() + + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + ) + + assert resp.status == HTTPStatus.OK + step = await resp.json() + code = step["result"] + + # Exchange code for tokens + with patch( + "homeassistant.components.auth.async_user_not_allowed_do_auth", + return_value="User is local only", + ): + resp = await client.post( + "/auth/token", + data={ + "client_id": CLIENT_ID, + "grant_type": "authorization_code", + "code": code, + }, + ) + + assert resp.status == HTTPStatus.FORBIDDEN + error = await resp.json() + assert error["error"] == "access_denied" + + def test_auth_code_store_expiration(mock_credential): """Test that the auth code store will not return expired tokens.""" store, retrieve = auth._create_auth_code_store() @@ -264,6 +306,30 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): ) +async def test_refresh_token_checks_local_only_user(hass, aiohttp_client): + """Test that we can't refresh token for a local only user when external.""" + client = await async_setup_auth(hass, aiohttp_client) + refresh_token = await async_setup_user_refresh_token(hass) + refresh_token.user.local_only = True + + with patch( + "homeassistant.components.auth.async_user_not_allowed_do_auth", + return_value="User is local only", + ): + resp = await client.post( + "/auth/token", + data={ + "client_id": CLIENT_ID, + "grant_type": "refresh_token", + "refresh_token": refresh_token.token, + }, + ) + + assert resp.status == HTTPStatus.FORBIDDEN + result = await resp.json() + assert result["error"] == "access_denied" + + async def test_refresh_token_provider_rejected( hass, aiohttp_client, hass_admin_user, hass_admin_credential ): diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index 72881023fe5..fd0157c235e 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -116,6 +116,44 @@ async def test_login_exist_user(hass, aiohttp_client): assert len(step["result"]) > 1 +async def test_login_local_only_user(hass, aiohttp_client): + """Test logging in with local only user.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( + {"username": "test-user"} + ) + user = await hass.auth.async_get_or_create_user(cred) + await hass.auth.async_update_user(user, local_only=True) + + resp = await client.post( + "/auth/login_flow", + json={ + "client_id": CLIENT_ID, + "handler": ["insecure_example", None], + "redirect_uri": CLIENT_REDIRECT_URI, + }, + ) + assert resp.status == HTTPStatus.OK + step = await resp.json() + + with patch( + "homeassistant.components.auth.login_flow.async_user_not_allowed_do_auth", + return_value="User is local only", + ) as mock_not_allowed_do_auth: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "username": "test-user", + "password": "test-pass", + }, + ) + + assert len(mock_not_allowed_do_auth.mock_calls) == 1 + assert resp.status == HTTPStatus.FORBIDDEN + assert await resp.json() == {"message": "Login blocked: User is local only"} + + async def test_login_exist_user_ip_changes(hass, aiohttp_client): """Test logging in and the ip address changes results in an rejection.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 8e2703cd51b..1f1a3d32d2c 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -2,14 +2,18 @@ from datetime import timedelta from http import HTTPStatus from ipaddress import ip_network -from unittest.mock import patch +from unittest.mock import Mock, patch from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized import pytest from homeassistant.auth.providers import trusted_networks -from homeassistant.components.http.auth import async_sign_path, setup_auth +from homeassistant.components.http.auth import ( + async_sign_path, + async_user_not_allowed_do_auth, + setup_auth, +) from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.forwarded import async_setup_forwarded from homeassistant.setup import async_setup_component @@ -26,7 +30,8 @@ TRUSTED_NETWORKS = [ ip_network("FD01:DB8::1"), ] TRUSTED_ADDRESSES = ["100.64.0.1", "192.0.2.100", "FD01:DB8::1", "2001:DB8:ABCD::1"] -UNTRUSTED_ADDRESSES = ["198.51.100.1", "2001:DB8:FA1::1", "127.0.0.1", "::1"] +EXTERNAL_ADDRESSES = ["198.51.100.1", "2001:DB8:FA1::1"] +UNTRUSTED_ADDRESSES = [*EXTERNAL_ADDRESSES, "127.0.0.1", "::1"] async def mock_handler(request): @@ -270,3 +275,68 @@ async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_to await hass.auth.async_remove_refresh_token(refresh_token) req = await client.get(signed_path) assert req.status == HTTPStatus.UNAUTHORIZED + + +async def test_local_only_user_rejected(hass, app, aiohttp_client, hass_access_token): + """Test access with access token in header.""" + token = hass_access_token + setup_auth(hass, app) + set_mock_ip = mock_real_ip(app) + client = await aiohttp_client(app) + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) + assert req.status == HTTPStatus.OK + assert await req.json() == {"user_id": refresh_token.user.id} + + refresh_token.user.local_only = True + + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) + assert req.status == HTTPStatus.UNAUTHORIZED + + +async def test_async_user_not_allowed_do_auth(hass, app): + """Test for not allowing auth.""" + user = await hass.auth.async_create_user("Hello") + user.is_active = False + + # User not active + assert async_user_not_allowed_do_auth(hass, user) == "User is not active" + + user.is_active = True + user.local_only = True + + # No current request + assert ( + async_user_not_allowed_do_auth(hass, user) + == "No request available to validate local access" + ) + + trusted_request = Mock(remote="192.168.1.123") + untrusted_request = Mock(remote=UNTRUSTED_ADDRESSES[0]) + + # Is Remote IP and local only (cloud not loaded) + assert async_user_not_allowed_do_auth(hass, user, trusted_request) is None + assert ( + async_user_not_allowed_do_auth(hass, user, untrusted_request) + == "User cannot authenticate remotely" + ) + + # Mimic cloud loaded and validate local IP again + hass.config.components.add("cloud") + assert async_user_not_allowed_do_auth(hass, user, trusted_request) is None + assert ( + async_user_not_allowed_do_auth(hass, user, untrusted_request) + == "User cannot authenticate remotely" + ) + + # Is Cloud request and local only, even a local IP will fail + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + assert ( + async_user_not_allowed_do_auth(hass, user, trusted_request) + == "User is local only" + ) From 8a5df5f7ebb3e5cabc9e9f133bb6df38b17d2c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 29 Nov 2021 23:03:16 +0100 Subject: [PATCH 0981/1452] Fix ingress for non admin (#60120) --- .../components/hassio/websocket_api.py | 12 +++- tests/components/hassio/test_websocket_api.py | 66 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index dfc2b7dc01d..e2ffff5e1e3 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -1,11 +1,13 @@ """Websocekt API handlers for the hassio integration.""" import logging +import re import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -34,6 +36,11 @@ SCHEMA_WEBSOCKET_EVENT = vol.Schema( extra=vol.ALLOW_EXTRA, ) +# Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` +WS_NO_ADMIN_ENDPOINTS = re.compile( + r"^(?:" r"|/ingress/(session|validate_session)" r"|/addons/[^/]+/info" r")$" +) + _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -79,7 +86,6 @@ async def websocket_supervisor_event( connection.send_result(msg[WS_ID]) -@websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( { @@ -94,6 +100,10 @@ async def websocket_supervisor_api( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): """Websocket handler to call Supervisor API.""" + if not connection.user.is_admin and not WS_NO_ADMIN_ENDPOINTS.match( + msg[ATTR_ENDPOINT] + ): + raise Unauthorized() supervisor: HassIO = hass.data[DOMAIN] try: result = await supervisor.send_command( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 931c1527b78..5d11d13166e 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -156,3 +156,69 @@ async def test_websocket_supervisor_api_error( msg = await websocket_client.receive_json() assert msg["error"]["message"] == "example error" + + +async def test_websocket_non_admin_user( + hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock, hass_admin_user +): + """Test Supervisor websocket api error.""" + hass_admin_user.groups = [] + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + aioclient_mock.get( + "http://127.0.0.1/addons/test_addon/info", + json={"result": "ok", "data": {}}, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/session", + json={"result": "ok", "data": {}}, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/validate_session", + json={"result": "ok", "data": {}}, + ) + + await websocket_client.send_json( + { + WS_ID: 1, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/addons/test_addon/info", + ATTR_METHOD: "get", + } + ) + msg = await websocket_client.receive_json() + assert msg["result"] == {} + + await websocket_client.send_json( + { + WS_ID: 2, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/ingress/session", + ATTR_METHOD: "get", + } + ) + msg = await websocket_client.receive_json() + assert msg["result"] == {} + + await websocket_client.send_json( + { + WS_ID: 3, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/ingress/validate_session", + ATTR_METHOD: "get", + } + ) + msg = await websocket_client.receive_json() + assert msg["result"] == {} + + await websocket_client.send_json( + { + WS_ID: 4, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/supervisor/info", + ATTR_METHOD: "get", + } + ) + + msg = await websocket_client.receive_json() + assert msg["error"]["message"] == "Unauthorized" From a925451906e92cc6845fb0ea9ee70b84731f2dda Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 30 Nov 2021 00:13:53 +0000 Subject: [PATCH 0982/1452] [ci skip] Translation update --- .../alarm_control_panel/translations/ja.json | 26 ++++++++++++++++ .../alarmdecoder/translations/ja.json | 4 ++- .../components/balboa/translations/id.json | 28 +++++++++++++++++ .../components/balboa/translations/nl.json | 28 +++++++++++++++++ .../components/brunt/translations/bg.json | 23 +++++++++++++- .../components/brunt/translations/nl.json | 27 +++++++++++++++++ .../device_tracker/translations/ja.json | 2 +- .../components/group/translations/ja.json | 2 +- .../components/knx/translations/bg.json | 4 +++ .../components/mill/translations/bg.json | 12 ++++++++ .../components/nam/translations/nl.json | 21 +++++++++++-- .../components/netatmo/translations/ja.json | 2 +- .../components/person/translations/ja.json | 2 +- .../components/renault/translations/id.json | 10 ++++++- .../components/rfxtrx/translations/id.json | 10 +++++++ .../components/ridwell/translations/id.json | 1 + .../components/risco/translations/ja.json | 20 +++++++++++-- .../simplisafe/translations/id.json | 1 + .../simplisafe/translations/ja.json | 1 + .../components/tado/translations/id.json | 1 + .../tesla_wall_connector/translations/bg.json | 19 ++++++++++++ .../tesla_wall_connector/translations/ca.json | 10 +++++-- .../tesla_wall_connector/translations/id.json | 30 +++++++++++++++++++ .../tesla_wall_connector/translations/ja.json | 30 +++++++++++++++++++ .../tesla_wall_connector/translations/nl.json | 30 +++++++++++++++++++ .../tesla_wall_connector/translations/no.json | 13 +++++++- .../translations/zh-Hans.json | 30 +++++++++++++++++++ .../translations/zh-Hant.json | 30 +++++++++++++++++++ .../tolo/translations/select.id.json | 8 +++++ .../tolo/translations/select.nl.json | 8 +++++ .../tuya/translations/select.en.json | 8 ++--- .../tuya/translations/sensor.id.json | 3 +- .../components/unifi/translations/id.json | 12 ++++---- .../components/unifi/translations/nl.json | 12 ++++---- .../components/unifi/translations/no.json | 14 ++++----- .../unifi/translations/zh-Hans.json | 14 ++++++--- .../unifi/translations/zh-Hant.json | 14 ++++----- .../wolflink/translations/sensor.ja.json | 1 + .../components/zha/translations/ja.json | 1 + 39 files changed, 463 insertions(+), 49 deletions(-) create mode 100644 homeassistant/components/balboa/translations/id.json create mode 100644 homeassistant/components/balboa/translations/nl.json create mode 100644 homeassistant/components/brunt/translations/nl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/bg.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/id.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/ja.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/nl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/zh-Hans.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/zh-Hant.json create mode 100644 homeassistant/components/tolo/translations/select.id.json create mode 100644 homeassistant/components/tolo/translations/select.nl.json diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 76f829502e9..875d24fd3fe 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -1,12 +1,38 @@ { "device_automation": { "action_type": { + "arm_away": "\u8b66\u6212 {entity_name} \u96e2\u5e2d(away)", + "arm_home": "\u8b66\u6212 {entity_name} \u5728\u5b85", + "arm_night": "\u8b66\u6212 {entity_name} \u591c", + "arm_vacation": "\u8b66\u6212 {entity_name} \u4f11\u6687", + "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u30c8\u30ea\u30ac\u30fc {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u306f\u8b66\u6212 \u96e2\u5e2d(away)", + "is_armed_home": "{entity_name} \u306f\u8b66\u6212 \u5728\u5b85", + "is_armed_night": "{entity_name} \u306f\u8b66\u6212 \u591c", + "is_armed_vacation": "{entity_name} \u306f\u8b66\u6212 \u4f11\u6687", + "is_disarmed": "{entity_name} \u306f\u89e3\u9664", + "is_triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3059" + }, + "trigger_type": { + "armed_away": "{entity_name} \u8b66\u6212 \u96e2\u5e2d(away)", + "armed_home": "{entity_name} \u8b66\u6212 \u5728\u5b85", + "armed_night": "{entity_name} \u8b66\u6212 \u591c", + "armed_vacation": "{entity_name} \u8b66\u6212 \u4f11\u6687", + "disarmed": "{entity_name} \u89e3\u9664", + "triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3057\u305f" } }, "state": { "_": { "armed": "\u8b66\u6212", + "armed_away": "\u8b66\u6212 \u96e2\u5e2d(away)", + "armed_custom_bypass": "\u8b66\u6212 \u30ab\u30b9\u30bf\u30e0 \u30d0\u30a4\u30d1\u30b9", + "armed_home": "\u8b66\u6212 \u5728\u5b85", + "armed_night": "\u8b66\u6212 \u591c", + "armed_vacation": "\u8b66\u6212 \u4f11\u6687", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u89e3\u9664", "disarming": "\u89e3\u9664", diff --git a/homeassistant/components/alarmdecoder/translations/ja.json b/homeassistant/components/alarmdecoder/translations/ja.json index 7e508ba6ad5..461c3bcd42d 100644 --- a/homeassistant/components/alarmdecoder/translations/ja.json +++ b/homeassistant/components/alarmdecoder/translations/ja.json @@ -37,7 +37,9 @@ "step": { "arm_settings": { "data": { - "alt_night_mode": "\u4ee3\u66ff\u30ca\u30a4\u30c8\u30e2\u30fc\u30c9" + "alt_night_mode": "\u4ee3\u66ff\u30ca\u30a4\u30c8\u30e2\u30fc\u30c9", + "auto_bypass": "\u8b66\u6212\u306e\u30aa\u30fc\u30c8\u30d0\u30a4\u30d1\u30b9", + "code_arm_required": "\u8b66\u6212\u306b\u5fc5\u8981\u306a\u30b3\u30fc\u30c9" }, "title": "AlarmDecoder\u306e\u8a2d\u5b9a" }, diff --git a/homeassistant/components/balboa/translations/id.json b/homeassistant/components/balboa/translations/id.json new file mode 100644 index 00000000000..8f8cb97780a --- /dev/null +++ b/homeassistant/components/balboa/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Hubungkan ke perangkat Wi-Fi Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Selalu sinkronkan waktu Balboa Spa Client Anda dengan Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/nl.json b/homeassistant/components/balboa/translations/nl.json new file mode 100644 index 00000000000..04dc0decd0d --- /dev/null +++ b/homeassistant/components/balboa/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Maak verbinding met het Balboa Wi-Fi-apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Houd de tijd van uw Balboa Spa Client gesynchroniseerd met Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/bg.json b/homeassistant/components/brunt/translations/bg.json index e9a9c468402..71737c4fb26 100644 --- a/homeassistant/components/brunt/translations/bg.json +++ b/homeassistant/components/brunt/translations/bg.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430: {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/nl.json b/homeassistant/components/brunt/translations/nl.json new file mode 100644 index 00000000000..6cf4f7e2f94 --- /dev/null +++ b/homeassistant/components/brunt/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord opnieuw in voor: {username}", + "title": "Verifieer de integratie opnieuw" + }, + "user": { + "data": { + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/ja.json b/homeassistant/components/device_tracker/translations/ja.json index 872448abf44..53302c9eb29 100644 --- a/homeassistant/components/device_tracker/translations/ja.json +++ b/homeassistant/components/device_tracker/translations/ja.json @@ -12,7 +12,7 @@ "state": { "_": { "home": "\u5728\u5b85", - "not_home": "\u5916\u51fa" + "not_home": "\u96e2\u5e2d(away)" } }, "title": "\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc" diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index ec4f774b2e1..c49045fedd6 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -4,7 +4,7 @@ "closed": "\u9589\u9396", "home": "\u5728\u5b85", "locked": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", - "not_home": "\u5916\u51fa", + "not_home": "\u96e2\u5e2d(away)", "off": "\u30aa\u30d5", "ok": "OK", "on": "\u30aa\u30f3", diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 42ce4fc3608..52e22a0a80e 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/mill/translations/bg.json b/homeassistant/components/mill/translations/bg.json index a93b899406a..43611df6920 100644 --- a/homeassistant/components/mill/translations/bg.json +++ b/homeassistant/components/mill/translations/bg.json @@ -7,6 +7,18 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "cloud": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "local": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e." + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/nam/translations/nl.json b/homeassistant/components/nam/translations/nl.json index c6171ead0f4..f900dccc295 100644 --- a/homeassistant/components/nam/translations/nl.json +++ b/homeassistant/components/nam/translations/nl.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "device_unsupported": "Het apparaat wordt niet ondersteund." + "device_unsupported": "Het apparaat wordt niet ondersteund.", + "reauth_successful": "Herauthenticatie was succesvol", + "reauth_unsuccessful": "Herauthenticatie is mislukt, verwijder de integratie en stel het opnieuw in." }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Wilt u Nettigo Air Monitor instellen bij {host} ?" }, + "credentials": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer de gebruikersnaam en het wachtwoord in." + }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer de juiste gebruikersnaam en wachtwoord in voor host: {host}" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index ac3c4b258d2..ecb21ffe039 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -22,7 +22,7 @@ }, "device_automation": { "trigger_subtype": { - "away": "\u7559\u5b88(away)", + "away": "\u96e2\u5e2d(away)", "hg": "\u30d5\u30ed\u30b9\u30c8(frost)\u30ac\u30fc\u30c9", "schedule": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" }, diff --git a/homeassistant/components/person/translations/ja.json b/homeassistant/components/person/translations/ja.json index 432fd29c964..5b45acc7cb6 100644 --- a/homeassistant/components/person/translations/ja.json +++ b/homeassistant/components/person/translations/ja.json @@ -2,7 +2,7 @@ "state": { "_": { "home": "\u5728\u5b85", - "not_home": "\u5916\u51fa" + "not_home": "\u96e2\u5e2d(away)" } }, "title": "\u4eba" diff --git a/homeassistant/components/renault/translations/id.json b/homeassistant/components/renault/translations/id.json index 3fb9f05cd0c..eacdca285d2 100644 --- a/homeassistant/components/renault/translations/id.json +++ b/homeassistant/components/renault/translations/id.json @@ -9,6 +9,12 @@ "invalid_credentials": "Autentikasi tidak valid" }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Id akun Kamereon" + }, + "title": "Pilih id akun Kamereon" + }, "reauth_confirm": { "data": { "password": "Kata Sandi" @@ -18,9 +24,11 @@ }, "user": { "data": { + "locale": "Pelokalan", "password": "Kata Sandi", "username": "Email" - } + }, + "title": "Setel kredensial Renault" } } } diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json index 9836d252c68..5ef4220f84a 100644 --- a/homeassistant/components/rfxtrx/translations/id.json +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Kirim perintah: {subtype}", + "send_status": "Kirim pembaruan status: {subtype}" + }, + "trigger_type": { + "command": "Perintah yang diterima: {subtype}", + "status": "Status yang diterima: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Perangkat sudah dikonfigurasi", diff --git a/homeassistant/components/ridwell/translations/id.json b/homeassistant/components/ridwell/translations/id.json index a3e90a11d51..acbf2e35dad 100644 --- a/homeassistant/components/ridwell/translations/id.json +++ b/homeassistant/components/ridwell/translations/id.json @@ -13,6 +13,7 @@ "data": { "password": "Kata Sandi" }, + "description": "Masukkan kembali kata sandi untuk {username} :", "title": "Autentikasi Ulang Integrasi" }, "user": { diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index 017faaadd1c..1e574a07a32 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -20,8 +20,20 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u8b66\u6212 \u96e2\u5e2d(away)", + "armed_custom_bypass": "\u8b66\u6212 \u30ab\u30b9\u30bf\u30e0 \u30d0\u30a4\u30d1\u30b9", + "armed_home": "\u8b66\u6212 \u5728\u5b85", + "armed_night": "\u8b66\u6212 \u591c" + }, + "description": "Home Assistant\u306e\u30a2\u30e9\u30fc\u30e0\u3067\u8b66\u6212\u3059\u308b\u969b\u306b\u3001Risco\u306e\u30a2\u30e9\u30fc\u30e0\u72b6\u614b\u3092\u3069\u306e\u3088\u3046\u306b\u8a2d\u5b9a\u3059\u308b\u304b\u306e\u9078\u629e", + "title": "Risco states\u3092Home Assistant\u306b\u30de\u30c3\u30d7" + }, "init": { "data": { + "code_arm_required": "\u8b66\u6212\u306b\u306f\u3001PIN\u30b3\u30fc\u30c9\u304c\u5fc5\u8981", + "code_disarm_required": "\u89e3\u9664\u306b\u306f\u3001PIN\u30b3\u30fc\u30c9\u304c\u5fc5\u8981", "scan_interval": "Risco\u3092\u30dd\u30fc\u30ea\u30f3\u30b0\u3059\u308b\u983b\u5ea6(\u79d2\u5358\u4f4d)" }, "title": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" @@ -31,8 +43,12 @@ "A": "\u30b0\u30eb\u30fc\u30d7A", "B": "\u30b0\u30eb\u30fc\u30d7B", "C": "\u30b0\u30eb\u30fc\u30d7C", - "D": "\u30b0\u30eb\u30fc\u30d7D" - } + "D": "\u30b0\u30eb\u30fc\u30d7D", + "arm": "\u8b66\u6212 (\u96e2\u5e2d(AWAY))", + "partial_arm": "\u90e8\u5206\u7684\u306b\u8b66\u6212 (\u6ede\u5728(STAY))" + }, + "description": "Risco\u306b\u3088\u3063\u3066\u5831\u544a\u3055\u308c\u305f\u3059\u3079\u3066\u306e\u72b6\u614b\u306b\u3064\u3044\u3066\u3001Home Assistant\u30a2\u30e9\u30fc\u30e0\u304c\u3001\u3069\u306e\u72b6\u614b\u3060\u3068\u5831\u544a\u3059\u308b\u304b\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "Risco states\u3092Home Assistant\u306e\u72b6\u614b\u306b\u30de\u30c3\u30d7\u3059\u308b" } } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index a1be0833ba4..733af744e30 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -36,6 +36,7 @@ "password": "Kata Sandi", "username": "Email" }, + "description": "Sejak tahun 2021, SimpliSafe telah berpindah ke mekanisme autentikasi baru melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan masukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan klik Kirim.", "title": "Isi informasi Anda." } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 2dc0b2609ac..32e8e4f777e 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -8,6 +8,7 @@ "error": { "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "still_awaiting_mfa": "MFA email click\u3092\u307e\u3060\u5f85\u3063\u3066\u3044\u307e\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/tado/translations/id.json b/homeassistant/components/tado/translations/id.json index bdbfa19cb6d..e6a00f9ee07 100644 --- a/homeassistant/components/tado/translations/id.json +++ b/homeassistant/components/tado/translations/id.json @@ -25,6 +25,7 @@ "data": { "fallback": "Aktifkan mode alternatif." }, + "description": "Mode fallback akan beralih ke Smart Schedule pada pergantian jadwal berikutnya setelah menyesuaikan zona secara manual.", "title": "Sesuaikan opsi Tado." } } diff --git a/homeassistant/components/tesla_wall_connector/translations/bg.json b/homeassistant/components/tesla_wall_connector/translations/bg.json new file mode 100644 index 00000000000..678f40de649 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/ca.json b/homeassistant/components/tesla_wall_connector/translations/ca.json index 33facc1426c..7f0056f3413 100644 --- a/homeassistant/components/tesla_wall_connector/translations/ca.json +++ b/homeassistant/components/tesla_wall_connector/translations/ca.json @@ -1,14 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, + "flow_title": "{serial_number} ({host})", "step": { "user": { "data": { "host": "Amfitri\u00f3" - } + }, + "title": "Configuraci\u00f3 de Tesla Wall Connector" } } }, @@ -17,7 +22,8 @@ "init": { "data": { "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" - } + }, + "title": "Configuraci\u00f3 d'opcions de Tesla Wall Connector" } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/id.json b/homeassistant/components/tesla_wall_connector/translations/id.json new file mode 100644 index 00000000000..6214e3d153f --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Konfigurasikan Konektor Dinding Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frekuensi pembaruan" + }, + "title": "Konfigurasikan opsi untuk Konektor Dinding Tesla" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/ja.json b/homeassistant/components/tesla_wall_connector/translations/ja.json new file mode 100644 index 00000000000..5370b0b161a --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/ja.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "Tesla Wall Connector\u306e\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u5ea6" + }, + "title": "Tesla Wall Connector\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/nl.json b/homeassistant/components/tesla_wall_connector/translations/nl.json new file mode 100644 index 00000000000..b6182cdcf5a --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Configureer Tesla Wall Connector" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "title": "Configureer opties voor Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/no.json b/homeassistant/components/tesla_wall_connector/translations/no.json index df06035b1d8..ed6b4c30cfd 100644 --- a/homeassistant/components/tesla_wall_connector/translations/no.json +++ b/homeassistant/components/tesla_wall_connector/translations/no.json @@ -12,7 +12,18 @@ "user": { "data": { "host": "Vert" - } + }, + "title": "Konfigurer Tesla Wall Connector" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdateringsfrekvens" + }, + "title": "Konfigurer alternativer for Tesla Wall Connector" } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json new file mode 100644 index 00000000000..21ee02edbf0 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a" + }, + "title": "\u914d\u7f6e Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9891\u7387" + }, + "title": "Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668\u914d\u7f6e\u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json new file mode 100644 index 00000000000..69de7dd2eb3 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u8a2d\u5b9a\u7279\u65af\u62c9\u58c1\u639b\u5f0f\u5145\u96fb\u5ea7" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "title": "\u7279\u65af\u62c9\u58c1\u639b\u5f0f\u5145\u96fb\u5ea7\u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.id.json b/homeassistant/components/tolo/translations/select.id.json new file mode 100644 index 00000000000..262d67bb310 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.id.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "otomatis", + "manual": "manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.nl.json b/homeassistant/components/tolo/translations/select.nl.json new file mode 100644 index 00000000000..ea981d36e54 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "Automatisch", + "manual": "Handmatig" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index cd03cf8eeaf..55c8d3f2f90 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -14,6 +14,10 @@ "0": "Low sensitivity", "1": "High sensitivity" }, + "tuya__fingerbot_mode": { + "click": "Push", + "switch": "Switch" + }, "tuya__ipc_work_mode": { "0": "Low power mode", "1": "Continuous working mode" @@ -44,10 +48,6 @@ "on": "On", "power_off": "Off", "power_on": "On" - }, - "tuya__fingerbot_mode": { - "click": "Push", - "switch": "Switch" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.id.json b/homeassistant/components/tuya/translations/sensor.id.json index cf9fc8bd738..0697075be00 100644 --- a/homeassistant/components/tuya/translations/sensor.id.json +++ b/homeassistant/components/tuya/translations/sensor.id.json @@ -8,7 +8,8 @@ "reserve_1": "Cadangan 1", "reserve_2": "Cadangan 2", "reserve_3": "Cadangan 3", - "standby": "Siaga" + "standby": "Siaga", + "warm": "Pelestarian panas" } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json index e22b5a024a7..4c78c96155c 100644 --- a/homeassistant/components/unifi/translations/id.json +++ b/homeassistant/components/unifi/translations/id.json @@ -21,7 +21,7 @@ "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" }, - "title": "Siapkan UniFi Controller" + "title": "Siapkan UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Izinkan kontrol POE klien" }, "description": "Konfigurasikan kontrol klien \n\nBuat sakelar untuk nomor seri yang ingin dikontrol akses jaringannya.", - "title": "Opsi UniFi 2/3" + "title": "Opsi UniFi Network 2/3" }, "device_tracker": { "data": { "detection_time": "Tenggang waktu dalam detik dari terakhir terlihat hingga dianggap sebagai keluar", - "ignore_wired_bug": "Nonaktifkan bug logika kabel UniFi", + "ignore_wired_bug": "Nonaktifkan bug logika kabel UniFi Network", "ssid_filter": "Pilih SSID untuk melacak klien nirkabel", "track_clients": "Lacak klien jaringan", "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)", "track_wired_clients": "Sertakan klien jaringan berkabel" }, "description": "Konfigurasikan pelacakan perangkat", - "title": "Opsi UniFi 1/3" + "title": "Opsi UniFi Network 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "Lacak klien jaringan", "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)" }, - "description": "Konfigurasikan integrasi UniFi" + "description": "Konfigurasikan integrasi UniFi Network" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "Sensor waktu kerja untuk klien jaringan" }, "description": "Konfigurasikan sensor statistik", - "title": "Opsi UniFi 3/3" + "title": "Opsi UniFi Network 3/3" } } } diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 8be73aad793..5cadc27117c 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller site is al geconfigureerd", + "already_configured": "Unifi Network site is al geconfigureerd", "configuration_updated": "Configuratie bijgewerkt.", "reauth_successful": "Herauthenticatie was succesvol" }, @@ -21,7 +21,7 @@ "username": "Gebruikersnaam", "verify_ssl": "SSL-certificaat verifi\u00ebren" }, - "title": "Stel de UniFi-controller in" + "title": "Stel de UniFi Network controller in" } } }, @@ -34,7 +34,7 @@ "poe_clients": "Sta POE-controle van gebruikers toe" }, "description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.", - "title": "UniFi-opties 2/3" + "title": "UniFi Network opties 2/3" }, "device_tracker": { "data": { @@ -46,7 +46,7 @@ "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" }, "description": "Apparaattracking configureren", - "title": "UniFi-opties 1/3" + "title": "UniFi Network opties 1/3" }, "init": { "data": { @@ -60,7 +60,7 @@ "track_clients": "Volg netwerkclients", "track_devices": "Volg netwerkapparaten (Ubiquiti-apparaten)" }, - "description": "Configureer UniFi-integratie" + "description": "Configureer UniFi network integratie" }, "statistics_sensors": { "data": { @@ -68,7 +68,7 @@ "allow_uptime_sensors": "Uptime-sensoren voor netwerkclients" }, "description": "Configureer statistische sensoren", - "title": "UniFi-opties 3/3" + "title": "UniFi Network opties 3/3" } } } diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index b1ecb706345..ec43a8f295b 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Kontroller nettstedet er allerede konfigurert", + "already_configured": "UniFi Network-nettstedet er allerede konfigurert", "configuration_updated": "Konfigurasjonen er oppdatert.", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, @@ -21,7 +21,7 @@ "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" }, - "title": "Sett opp UniFi kontroller" + "title": "Sett opp UniFi Network" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Tillat POE-kontroll av klienter" }, "description": "Konfigurere klient-kontroller\n\nOpprette brytere for serienumre du \u00f8nsker \u00e5 kontrollere tilgang til nettverk for.", - "title": "UniFi-alternativ 2/3" + "title": "UniFi Network alternativer 2/3" }, "device_tracker": { "data": { "detection_time": "Tid i sekunder fra sist sett til den ble ansett borte", - "ignore_wired_bug": "Deaktiver UniFi kablet feillogikk", + "ignore_wired_bug": "Deaktiver UniFi Network kablet feillogikk", "ssid_filter": "Velg SSID-er for \u00e5 spore tr\u00e5dl\u00f8se klienter p\u00e5", "track_clients": "Spor nettverksklienter", "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkluder kablede nettverksklienter" }, "description": "Konfigurere enhetssporing", - "title": "UniFi-alternativ 1/3" + "title": "UniFi-nettverksalternativer 1/3" }, "init": { "data": { @@ -60,7 +60,7 @@ "track_clients": "Spor nettverksklienter", "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)" }, - "description": "Konfigurer UniFi-integrasjon" + "description": "Konfigurer UniFi Network-integrasjon" }, "statistics_sensors": { "data": { @@ -68,7 +68,7 @@ "allow_uptime_sensors": "Oppetidssensorer for nettverksklienter" }, "description": "Konfigurer statistikk sensorer", - "title": "UniFi-alternativ 3/3" + "title": "UniFi Network alternativer 3/3" } } } diff --git a/homeassistant/components/unifi/translations/zh-Hans.json b/homeassistant/components/unifi/translations/zh-Hans.json index 7fe1b741bd5..7bc8a032c6f 100644 --- a/homeassistant/components/unifi/translations/zh-Hans.json +++ b/homeassistant/components/unifi/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u7ad9\u70b9\u5df2\u914d\u7f6e\u5b8c\u6210" + "already_configured": "UniFi \u7f51\u7edc\u7ad9\u70b9\u5df2\u914d\u7f6e\u5b8c\u6210" }, "error": { "faulty_credentials": "\u9519\u8bef\u7684\u7528\u6237\u51ed\u636e", @@ -17,12 +17,15 @@ "username": "\u7528\u6237\u540d", "verify_ssl": "\u4f7f\u7528\u6b63\u786e\u8bc1\u4e66\u7684\u63a7\u5236\u5668" }, - "title": "\u914d\u7f6e UniFi \u63a7\u5236\u5668" + "title": "\u914d\u7f6e UniFi \u7f51\u7edc" } } }, "options": { "step": { + "client_control": { + "title": "UniFi \u7f51\u7edc\u9009\u9879 3/3" + }, "device_tracker": { "data": { "detection_time": "\u8ddd\u79bb\u4e0a\u6b21\u53d1\u73b0\u591a\u5c11\u79d2\u540e\u8ba4\u4e3a\u79bb\u5f00", @@ -32,11 +35,14 @@ "track_wired_clients": "\u5305\u62ec\u6709\u7ebf\u7f51\u7edc\u5ba2\u6237\u7aef" }, "description": "\u914d\u7f6e\u8bbe\u5907\u8ddf\u8e2a", - "title": "UniFi \u9009\u9879" + "title": "UniFi \u7f51\u7edc\u9009\u9879 1/3" + }, + "simple_options": { + "description": "\u914d\u7f6e UniFi \u7f51\u7edc\u96c6\u6210" }, "statistics_sensors": { "description": "\u914d\u7f6e\u7edf\u8ba1\u4f20\u611f\u5668", - "title": "UniFi \u9009\u9879" + "title": "UniFi \u7f51\u7edc\u9009\u9879 2/3" } } } diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index 22cfeed3d65..1b394bae317 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", + "already_configured": "UniFi \u7db2\u8def\u5df2\u7d93\u8a2d\u5b9a", "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, @@ -21,7 +21,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" }, - "title": "\u8a2d\u5b9a UniFi \u63a7\u5236\u5668" + "title": "\u8a2d\u5b9a UniFi \u7db2\u8def" } } }, @@ -34,19 +34,19 @@ "poe_clients": "\u5141\u8a31 POE \u63a7\u5236\u5ba2\u6236\u7aef" }, "description": "\u8a2d\u5b9a\u5ba2\u6236\u7aef\u63a7\u5236\n\n\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\u958b\u95dc\u5e8f\u865f\u3002", - "title": "UniFi \u9078\u9805 2/3" + "title": "UniFi \u7db2\u8def\u9078\u9805 2/3" }, "device_tracker": { "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", - "ignore_wired_bug": "\u95dc\u9589 UniFi \u6709\u7dda\u932f\u8aa4\u908f\u8f2f", + "ignore_wired_bug": "\u95dc\u9589 UniFi \u7db2\u8def\u6709\u7dda\u932f\u8aa4\u908f\u8f2f", "ssid_filter": "\u9078\u64c7\u6240\u8981\u8ffd\u8e64\u7684\u7121\u7dda\u7db2\u8def", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" }, "description": "\u8a2d\u5b9a\u88dd\u7f6e\u8ffd\u8e64", - "title": "UniFi \u9078\u9805 1/3" + "title": "UniFi \u7db2\u8def\u9078\u9805 1/3" }, "simple_options": { "data": { @@ -54,7 +54,7 @@ "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09" }, - "description": "\u8a2d\u5b9a UniFi \u6574\u5408" + "description": "\u8a2d\u5b9a UniFi \u7db2\u8def\u6574\u5408" }, "statistics_sensors": { "data": { @@ -62,7 +62,7 @@ "allow_uptime_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u4e0a\u7dda\u6642\u9593\u611f\u6e2c\u5668" }, "description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668", - "title": "UniFi \u9078\u9805 3/3" + "title": "UniFi \u7db2\u8def\u9078\u9805 3/3" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 8af9c32c079..0b9fa47c9ea 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -39,6 +39,7 @@ "kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9", "kombigerat": "\u30b3\u30f3\u30d3 \u30dc\u30a4\u30e9\u30fc", "nachlauf_heizkreispumpe": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u30dd\u30f3\u30d7\u306e\u4f5c\u52d5", + "nachspulen": "\u30d5\u30e9\u30c3\u30b7\u30e5\u5f8c(Post-flush)", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", "parallelbetrieb": "\u30d1\u30e9\u30ec\u30eb\u30e2\u30fc\u30c9", "partymodus": "\u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 3fd080778ab..7974d0722da 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -40,6 +40,7 @@ }, "config_panel": { "zha_alarm_options": { + "alarm_arm_requires_code": "\u8b66\u6212\u30a2\u30af\u30b7\u30e7\u30f3\u306b\u5fc5\u8981\u306a\u30b3\u30fc\u30c9", "alarm_failed_tries": "\u30a2\u30e9\u30fc\u30e0\u3092\u30c8\u30ea\u30ac\u30fc\u3055\u305b\u308b\u305f\u3081\u306b\u9023\u7d9a\u3057\u3066\u5931\u6557\u3057\u305f\u30b3\u30fc\u30c9 \u30a8\u30f3\u30c8\u30ea\u306e\u6570", "alarm_master_code": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30de\u30b9\u30bf\u30fc\u30b3\u30fc\u30c9", "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" From 9f26850a19bd7076a41a759df328f9ff0d1316cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 01:44:21 +0100 Subject: [PATCH 0983/1452] Add device class support for button entity (#60560) --- homeassistant/components/button/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index fe2d1cb6435..621effd5d16 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -6,6 +6,8 @@ from datetime import datetime, timedelta import logging from typing import final +import voluptuous as vol + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -28,6 +30,13 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) +DEVICE_CLASS_RESTART = "restart" +DEVICE_CLASS_UPDATE = "update" + +DEVICE_CLASSES = [DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE] + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Button entities.""" From 2f24fc0fd4a0cd8e1607f5e67548677ba29f8f6e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Nov 2021 20:00:39 -0500 Subject: [PATCH 0984/1452] Fix Flo returning stale data (#60491) * Fix Flo returning stale data * update tests * update coverage --- homeassistant/components/flo/device.py | 10 +++++++++- homeassistant/components/flo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flo/conftest.py | 7 +++++++ tests/components/flo/fixtures/ping_response.json | 8 ++++++++ tests/components/flo/test_device.py | 16 +++++++++++++++- tests/components/flo/test_sensor.py | 2 +- tests/components/flo/test_services.py | 10 +++++----- 9 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 tests/components/flo/fixtures/ping_response.json diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index f32aa7e6e32..cc32acb485c 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -42,7 +42,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): try: async with timeout(10): await asyncio.gather( - *[self._update_device(), self._update_consumption_data()] + *[ + self.send_presence_ping(), + self._update_device(), + self._update_consumption_data(), + ] ) except (RequestError) as error: raise UpdateFailed(error) from error @@ -188,6 +192,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Return the battery level for battery-powered device, e.g. leak detectors.""" return self._device_information["battery"]["level"] + async def send_presence_ping(self): + """Send Flo a presence ping.""" + await self.api_client.presence.ping() + async def async_set_mode_home(self): """Set the Flo location to home mode.""" await self.api_client.location.set_mode_home(self._flo_location_id) diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json index 11972f5056b..6d1e002012c 100644 --- a/homeassistant/components/flo/manifest.json +++ b/homeassistant/components/flo/manifest.json @@ -3,7 +3,7 @@ "name": "Flo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flo", - "requirements": ["aioflo==0.4.1"], + "requirements": ["aioflo==2021.11.0"], "codeowners": ["@dmulcahey"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index df0f31ea28a..b010a5128a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioemonitor==1.0.5 aioesphomeapi==10.3.0 # homeassistant.components.flo -aioflo==0.4.1 +aioflo==2021.11.0 # homeassistant.components.yi aioftp==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19df25b2390..8e7d32bfd7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioemonitor==1.0.5 aioesphomeapi==10.3.0 # homeassistant.components.flo -aioflo==0.4.1 +aioflo==2021.11.0 # homeassistant.components.guardian aioguardian==2021.11.0 diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 6a82c0f4791..1484b10eae2 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -44,6 +44,13 @@ def aioclient_mock_fixture(aioclient_mock): headers={"Content-Type": CONTENT_TYPE_JSON}, status=HTTPStatus.OK, ) + # Mocks the presence ping response for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/presence/me", + text=load_fixture("flo/ping_response.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + status=HTTPStatus.OK, + ) # Mocks the devices for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/devices/98765", diff --git a/tests/components/flo/fixtures/ping_response.json b/tests/components/flo/fixtures/ping_response.json new file mode 100644 index 00000000000..950519289e6 --- /dev/null +++ b/tests/components/flo/fixtures/ping_response.json @@ -0,0 +1,8 @@ +{ + "ipAddress": "11.111.111.111", + "userId": "12345abcde", + "action": "report", + "type": "user", + "appName": "legacy", + "userData": { "account": { "type": "personal" } } +} diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 5ac31a2bff9..3e08d289aef 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -1,9 +1,14 @@ """Define tests for device-related endpoints.""" from datetime import timedelta +from unittest.mock import patch + +from aioflo.errors import RequestError +import pytest from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.flo.device import FloDeviceDataUpdateCoordinator from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -67,10 +72,19 @@ async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock assert detector.model == "puck_v1" assert detector.manufacturer == "Flo by Moen" assert detector.device_name == "Kitchen Sink" + assert detector.serial_number == "111111111112" call_count = aioclient_mock.call_count async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90)) await hass.async_block_till_done() - assert aioclient_mock.call_count == call_count + 4 + assert aioclient_mock.call_count == call_count + 6 + + # test error sending device ping + with patch( + "homeassistant.components.flo.device.FloDeviceDataUpdateCoordinator.send_presence_ping", + side_effect=RequestError, + ): + with pytest.raises(UpdateFailed): + await valve._async_update_data() diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index f1572dae02c..ec044286b0d 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -50,4 +50,4 @@ async def test_manual_update_entity( {ATTR_ENTITY_ID: ["sensor.current_system_mode"]}, blocking=True, ) - assert aioclient_mock.call_count == call_count + 2 + assert aioclient_mock.call_count == call_count + 3 diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index 4941a118e48..699bd97ebed 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -26,7 +26,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo await hass.async_block_till_done() assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 8 await hass.services.async_call( FLO_DOMAIN, @@ -35,7 +35,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo blocking=True, ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 await hass.services.async_call( FLO_DOMAIN, @@ -44,7 +44,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo blocking=True, ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 8 + assert aioclient_mock.call_count == 10 await hass.services.async_call( FLO_DOMAIN, @@ -53,7 +53,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo blocking=True, ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 11 await hass.services.async_call( FLO_DOMAIN, @@ -66,4 +66,4 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo blocking=True, ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 12 From 1bfd98ab1498d5d1eba25824733bafb76a73f52c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Nov 2021 18:35:41 -0800 Subject: [PATCH 0985/1452] Bump frontend to 20211129.0 (#60564) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e507def0078..f42b28df92f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211123.0" + "home-assistant-frontend==20211130.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d5d73be25b2..199bca41619 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211123.0 +home-assistant-frontend==20211130.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index b010a5128a9..2e0cf967e13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.6.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211123.0 +home-assistant-frontend==20211130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e7d32bfd7c..9ee1d3227a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.6.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211123.0 +home-assistant-frontend==20211130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f0f88d56bdbb5b82d8ec60bbeb9cfded0c802142 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Nov 2021 18:53:42 -1000 Subject: [PATCH 0986/1452] Avoid probing configured ipp devices at discovery (#60551) - Each time these were seen by zeroconf, these devices were probed even if they were already configured. This is expensive and we want to avoid this when possible --- homeassistant/components/ipp/config_flow.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 543592cd4f0..432094eee8e 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -104,23 +104,28 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - port = discovery_info[zeroconf.ATTR_PORT] - zctype = discovery_info[zeroconf.ATTR_TYPE] - name = discovery_info[zeroconf.ATTR_NAME].replace(f".{zctype}", "") + host = discovery_info.host + + # Avoid probing devices that already have an entry + self._async_abort_entries_match({CONF_HOST: host}) + + port = discovery_info.port + zctype = discovery_info.type + name = discovery_info.name.replace(f".{zctype}", "") tls = zctype == "_ipps._tcp.local." - base_path = discovery_info[zeroconf.ATTR_PROPERTIES].get("rp", "ipp/print") + base_path = discovery_info.properties.get("rp", "ipp/print") self.context.update({"title_placeholders": {"name": name}}) self.discovery_info.update( { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: host, CONF_PORT: port, CONF_SSL: tls, CONF_VERIFY_SSL: False, CONF_BASE_PATH: f"/{base_path}", CONF_NAME: name, - CONF_UUID: discovery_info[zeroconf.ATTR_PROPERTIES].get("UUID"), + CONF_UUID: discovery_info.properties.get("UUID"), } ) From df90fdf641b8e641ca166bc4196cd5d54f645827 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 21:23:58 -0800 Subject: [PATCH 0987/1452] Add an available property on Stream (#60429) --- homeassistant/components/stream/__init__.py | 28 +++++++--- homeassistant/components/stream/worker.py | 60 +++++++++++++-------- tests/components/stream/test_hls.py | 6 +++ tests/components/stream/test_worker.py | 53 +++++++++++++----- 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 731a610abf9..58b0dd00bc9 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -204,6 +204,7 @@ class Stream: self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False + self._available = True def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -254,6 +255,11 @@ class Stream: if all(p.idle for p in self._outputs.values()): self.access_token = None + @property + def available(self) -> bool: + """Return False if the stream is started and known to be unavailable.""" + return self._available + def start(self) -> None: """Start a stream.""" if self._thread is None or not self._thread.is_alive(): @@ -280,18 +286,25 @@ class Stream: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel - from .worker import SegmentBuffer, stream_worker + from .worker import SegmentBuffer, StreamWorkerError, stream_worker segment_buffer = SegmentBuffer(self.hass, self.outputs) wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() - stream_worker( - self.source, - self.options, - segment_buffer, - self._thread_quit, - ) + + self._available = True + try: + stream_worker( + self.source, + self.options, + segment_buffer, + self._thread_quit, + ) + except StreamWorkerError as err: + _LOGGER.error("Error from stream worker: %s", str(err)) + self._available = False + segment_buffer.discontinuity() if not self.keepalive or self._thread_quit.is_set(): if self._fast_restart_once: @@ -300,6 +313,7 @@ class Stream: self._thread_quit.clear() continue break + # To avoid excessive restarts, wait before restarting # As the required recovery time may be different for different setups, start # with trying a short wait_timeout and increase it on each reconnection attempt. diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index a0ab48290f5..5176b93dedf 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import defaultdict, deque from collections.abc import Callable, Generator, Iterator, Mapping +import contextlib import datetime from io import BytesIO import logging @@ -31,6 +32,14 @@ from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) +class StreamWorkerError(Exception): + """An exception thrown while processing a stream.""" + + +class StreamEndedError(StreamWorkerError): + """Raised when the stream is complete, exposed for facilitating testing.""" + + class SegmentBuffer: """Buffer for writing a sequence of packets to the output as a segment.""" @@ -356,7 +365,7 @@ class TimestampValidator: # Discard packets missing DTS. Terminate if too many are missing. if packet.dts is None: if self._missing_dts >= MAX_MISSING_DTS: - raise StopIteration( + raise StreamWorkerError( f"No dts in {MAX_MISSING_DTS+1} consecutive packets" ) self._missing_dts += 1 @@ -367,7 +376,7 @@ class TimestampValidator: if packet.dts <= prev_dts: gap = packet.time_base * (prev_dts - packet.dts) if gap > MAX_TIMESTAMP_GAP: - raise StopIteration( + raise StreamWorkerError( f"Timestamp overflow detected: last dts = {prev_dts}, dts = {packet.dts}" ) return False @@ -410,15 +419,14 @@ def stream_worker( try: container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) - except av.AVError: - _LOGGER.error("Error opening stream %s", redact_credentials(str(source))) - return + except av.AVError as err: + raise StreamWorkerError( + "Error opening stream %s" % redact_credentials(str(source)) + ) from err try: video_stream = container.streams.video[0] - except (KeyError, IndexError): - _LOGGER.error("Stream has no video") - container.close() - return + except (KeyError, IndexError) as ex: + raise StreamWorkerError("Stream has no video") from ex try: audio_stream = container.streams.audio[0] except (KeyError, IndexError): @@ -469,10 +477,17 @@ def stream_worker( # dts. Use "or 1" to deal with this. start_dts = next_video_packet.dts - (next_video_packet.duration or 1) first_keyframe.dts = first_keyframe.pts = start_dts - except (av.AVError, StopIteration) as ex: - _LOGGER.error("Error demuxing stream while finding first packet: %s", str(ex)) + except StreamWorkerError as ex: container.close() - return + raise ex + except StopIteration as ex: + container.close() + raise StreamEndedError("Stream ended; no additional packets") from ex + except av.AVError as ex: + container.close() + raise StreamWorkerError( + "Error demuxing stream while finding first packet: %s" % str(ex) + ) from ex segment_buffer.set_streams(video_stream, audio_stream) segment_buffer.reset(start_dts) @@ -480,14 +495,15 @@ def stream_worker( # Mux the first keyframe, then proceed through the rest of the packets segment_buffer.mux_packet(first_keyframe) - while not quit_event.is_set(): - try: - packet = next(container_packets) - except (av.AVError, StopIteration) as ex: - _LOGGER.error("Error demuxing stream: %s", str(ex)) - break - segment_buffer.mux_packet(packet) + with contextlib.closing(container), contextlib.closing(segment_buffer): + while not quit_event.is_set(): + try: + packet = next(container_packets) + except StreamWorkerError as ex: + raise ex + except StopIteration as ex: + raise StreamEndedError("Stream ended; no additional packets") from ex + except av.AVError as ex: + raise StreamWorkerError("Error demuxing stream: %s" % str(ex)) from ex - # Close stream - segment_buffer.close() - container.close() + segment_buffer.mux_packet(packet) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 3bff13a936b..9c529d7abe5 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -135,6 +135,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): # Request stream stream.add_provider(HLS_PROVIDER) + assert stream.available stream.start() hls_client = await hls_stream(stream) @@ -161,6 +162,9 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream_worker_sync.resume() + # The stream worker reported end of stream and exited + assert not stream.available + # Stop stream, if it hasn't quit already stream.stop() @@ -181,6 +185,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Request stream stream.add_provider(HLS_PROVIDER) + assert stream.available stream.start() url = stream.endpoint_url(HLS_PROVIDER) @@ -267,6 +272,7 @@ async def test_stream_keepalive(hass): stream._thread.join() stream._thread = None assert av_open.call_count == 2 + assert not stream.available # Stop stream, if it hasn't quit already stream.stop() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 97fe4bd0d37..c65e10d65f3 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -37,7 +37,12 @@ from homeassistant.components.stream.const import ( TARGET_SEGMENT_DURATION_NON_LL_HLS, ) from homeassistant.components.stream.core import StreamSettings -from homeassistant.components.stream.worker import SegmentBuffer, stream_worker +from homeassistant.components.stream.worker import ( + SegmentBuffer, + StreamEndedError, + StreamWorkerError, + stream_worker, +) from homeassistant.setup import async_setup_component from tests.components.stream.common import generate_h264_video, generate_h265_video @@ -264,8 +269,15 @@ async def async_decode_stream(hass, packets, py_av=None): side_effect=py_av.capture_buffer.capture_output_segment, ): segment_buffer = SegmentBuffer(hass, stream.outputs) - stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) - await hass.async_block_till_done() + try: + stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) + except StreamEndedError: + # Tests only use a limited number of packets, then the worker exits as expected. In + # production, stream ending would be unexpected. + pass + finally: + # Wait for all packets to be flushed even when exceptions are thrown + await hass.async_block_till_done() return py_av.capture_buffer @@ -274,7 +286,7 @@ async def test_stream_open_fails(hass): """Test failure on stream open.""" stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) - with patch("av.open") as av_open: + with patch("av.open") as av_open, pytest.raises(StreamWorkerError): av_open.side_effect = av.error.InvalidDataError(-2, "error") segment_buffer = SegmentBuffer(hass, stream.outputs) stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) @@ -371,7 +383,10 @@ 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, packets) + py_av = MockPyAv() + with pytest.raises(StreamWorkerError, match=r"Timestamp overflow detected"): + await async_decode_stream(hass, packets, py_av=py_av) + decoded_stream = py_av.capture_buffer segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments @@ -425,7 +440,10 @@ 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, packets) + py_av = MockPyAv() + with pytest.raises(StreamWorkerError, match=r"No dts"): + await async_decode_stream(hass, packets, py_av=py_av) + decoded_stream = py_av.capture_buffer segments = decoded_stream.segments assert len(segments) == 0 assert len(decoded_stream.video_packets) == 0 @@ -466,7 +484,10 @@ 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, packets) + py_av = MockPyAv() + with pytest.raises(StreamWorkerError, match=r"No dts"): + await async_decode_stream(hass, packets, py_av=py_av) + decoded_stream = py_av.capture_buffer 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 @@ -477,9 +498,11 @@ async def test_no_video_stream(hass): """Test no video stream in the container means no resulting output.""" py_av = MockPyAv(video=False) - decoded_stream = await async_decode_stream( - hass, PacketSequence(TEST_SEQUENCE_LENGTH), py_av=py_av - ) + with pytest.raises(StreamWorkerError, match=r"Stream has no video"): + await async_decode_stream( + hass, PacketSequence(TEST_SEQUENCE_LENGTH), py_av=py_av + ) + decoded_stream = py_av.capture_buffer # Note: This failure scenario does not output an end of stream segments = decoded_stream.segments assert len(segments) == 0 @@ -616,6 +639,9 @@ async def test_stream_stopped_while_decoding(hass): worker_wake.set() stream.stop() + # Stream is still considered available when the worker was still active and asked to stop + assert stream.available + async def test_update_stream_source(hass): """Tests that the worker is re-invoked when the stream source is updated.""" @@ -646,6 +672,7 @@ async def test_update_stream_source(hass): stream.start() assert worker_open.wait(TIMEOUT) assert last_stream_source == STREAM_SOURCE + assert stream.available # Update the stream source, then the test wakes up the worker and assert # that it re-opens the new stream (the test again waits on thread_started) @@ -655,6 +682,7 @@ async def test_update_stream_source(hass): assert worker_open.wait(TIMEOUT) assert last_stream_source == STREAM_SOURCE + "-updated-source" worker_wake.set() + assert stream.available # Cleanup stream.stop() @@ -664,15 +692,16 @@ async def test_worker_log(hass, caplog): """Test that the worker logs the url without username and password.""" stream = Stream(hass, "https://abcd:efgh@foo.bar", {}) stream.add_provider(HLS_PROVIDER) - with patch("av.open") as av_open: + + with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: av_open.side_effect = av.error.InvalidDataError(-2, "error") segment_buffer = SegmentBuffer(hass, stream.outputs) stream_worker( "https://abcd:efgh@foo.bar", {}, segment_buffer, threading.Event() ) await hass.async_block_till_done() + assert str(err.value) == "Error opening stream https://****:****@foo.bar" assert "https://abcd:efgh@foo.bar" not in caplog.text - assert "https://****:****@foo.bar" in caplog.text async def test_durations(hass, record_worker_sync): From 890790a659ae1a6ec89ae59001e36f1298c95283 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 06:46:41 +0100 Subject: [PATCH 0988/1452] Use dataclass properties in arcam_fmj discovery (#60562) --- homeassistant/components/arcam_fmj/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index cbf707c14e6..2570fd1aea5 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -6,8 +6,9 @@ from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_UDN +from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES @@ -84,11 +85,11 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="confirm", description_placeholders=placeholders ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered device.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + host = urlparse(discovery_info.ssdp_location).hostname port = DEFAULT_PORT - uuid = get_uniqueid_from_udn(discovery_info[ATTR_UPNP_UDN]) + uuid = get_uniqueid_from_udn(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) await self._async_set_unique_id_and_update(host, port, uuid) From 8ca89b10eb6bbcb307cfeb8e8762eb8447413c0b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 22:25:28 -0800 Subject: [PATCH 0989/1452] Split StreamState class out of SegmentBuffer (#60423) This refactoring was pulled out of https://github.com/home-assistant/core/pull/53676 as an initial step towards reverting the addition of the SegmentBuffer class, which will be unrolled back into a for loop. The StreamState class holds the persistent state in stream that is used across stream worker instantiations, e.g. state across a retry or url expiration, which primarily handles discontinuities. By itself, this PR is not a large win until follow up PRs further simplify the SegmentBuffer class. --- homeassistant/components/stream/__init__.py | 8 +- homeassistant/components/stream/worker.py | 112 ++++++++++++-------- tests/components/stream/conftest.py | 10 +- tests/components/stream/test_worker.py | 19 ++-- 4 files changed, 89 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 58b0dd00bc9..070dd062e42 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -286,9 +286,9 @@ class Stream: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel - from .worker import SegmentBuffer, StreamWorkerError, stream_worker + from .worker import StreamState, StreamWorkerError, stream_worker - segment_buffer = SegmentBuffer(self.hass, self.outputs) + stream_state = StreamState(self.hass, self.outputs) wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() @@ -298,14 +298,14 @@ class Stream: stream_worker( self.source, self.options, - segment_buffer, + stream_state, self._thread_quit, ) except StreamWorkerError as err: _LOGGER.error("Error from stream worker: %s", str(err)) self._available = False - segment_buffer.discontinuity() + stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): if self._fast_restart_once: # The stream source is updated, restart without any delay. diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 5176b93dedf..b1d79e52800 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -40,28 +40,77 @@ class StreamEndedError(StreamWorkerError): """Raised when the stream is complete, exposed for facilitating testing.""" -class SegmentBuffer: - """Buffer for writing a sequence of packets to the output as a segment.""" +class StreamState: + """Responsible for trakcing output and playback state for a stream. + + Holds state used for playback to interpret a decoded stream. A source stream + may be reset (e.g. reconnecting to an rtsp stream) and this object tracks + the state to inform the player. + """ def __init__( self, hass: HomeAssistant, outputs_callback: Callable[[], Mapping[str, StreamOutput]], ) -> None: - """Initialize SegmentBuffer.""" + """Initialize StreamState.""" self._stream_id: int = 0 - self._hass = hass + self.hass = hass self._outputs_callback: Callable[ [], Mapping[str, StreamOutput] ] = outputs_callback # sequence gets incremented before the first segment so the first segment # has a sequence number of 0. self._sequence = -1 + + @property + def sequence(self) -> int: + """Return the current sequence for the latest segment.""" + return self._sequence + + def next_sequence(self) -> int: + """Increment the sequence number.""" + self._sequence += 1 + return self._sequence + + @property + def stream_id(self) -> int: + """Return the readonly stream_id attribute.""" + return self._stream_id + + def discontinuity(self) -> None: + """Mark the stream as having been restarted.""" + # Preserving sequence and stream_id here keep the HLS playlist logic + # simple to check for discontinuity at output time, and to determine + # the discontinuity sequence number. + self._stream_id += 1 + # Call discontinuity to remove incomplete segment from the HLS output + if hls_output := self._outputs_callback().get(HLS_PROVIDER): + cast(HlsStreamOutput, hls_output).discontinuity() + + @property + def outputs(self) -> list[StreamOutput]: + """Return the active stream outputs.""" + return list(self._outputs_callback().values()) + + +class StreamMuxer: + """StreamMuxer re-packages video/audio packets for output.""" + + def __init__( + self, + hass: HomeAssistant, + video_stream: av.video.VideoStream, + audio_stream: av.audio.stream.AudioStream | None, + stream_state: StreamState, + ) -> None: + """Initialize StreamMuxer.""" + self._hass = hass self._segment_start_dts: int = cast(int, None) self._memory_file: BytesIO = cast(BytesIO, None) self._av_output: av.container.OutputContainer = None - self._input_video_stream: av.video.VideoStream = None - self._input_audio_stream: av.audio.stream.AudioStream | None = None + self._input_video_stream: av.video.VideoStream = video_stream + self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None @@ -70,6 +119,7 @@ class SegmentBuffer: self._part_start_dts: int = cast(int, None) self._part_has_keyframe = False self._stream_settings: StreamSettings = hass.data[DOMAIN][ATTR_SETTINGS] + self._stream_state = stream_state self._start_time = datetime.datetime.utcnow() def make_new_av( @@ -77,14 +127,13 @@ class SegmentBuffer: memory_file: BytesIO, sequence: int, input_vstream: av.video.VideoStream, - input_astream: av.audio.stream.AudioStream, + input_astream: av.audio.stream.AudioStream | None, ) -> tuple[ av.container.OutputContainer, av.video.VideoStream, av.audio.stream.AudioStream | None, ]: """Make a new av OutputContainer and add output streams.""" - add_audio = input_astream and input_astream.name in AUDIO_CODECS container = av.open( memory_file, mode="w", @@ -135,24 +184,12 @@ class SegmentBuffer: output_vstream = container.add_stream(template=input_vstream) # Check if audio is requested output_astream = None - if add_audio: + if input_astream: output_astream = container.add_stream(template=input_astream) return container, output_vstream, output_astream - def set_streams( - self, - video_stream: av.video.VideoStream, - audio_stream: Any, - # no type hint for audio_stream until https://github.com/PyAV-Org/PyAV/pull/775 is merged - ) -> None: - """Initialize output buffer with streams from container.""" - self._input_video_stream = video_stream - self._input_audio_stream = audio_stream - def reset(self, video_dts: int) -> None: """Initialize a new stream segment.""" - # Keep track of the number of segments we've processed - self._sequence += 1 self._part_start_dts = self._segment_start_dts = video_dts self._segment = None self._memory_file = BytesIO() @@ -163,7 +200,7 @@ class SegmentBuffer: self._output_audio_stream, ) = self.make_new_av( memory_file=self._memory_file, - sequence=self._sequence, + sequence=self._stream_state.next_sequence(), input_vstream=self._input_video_stream, input_astream=self._input_audio_stream, ) @@ -201,12 +238,12 @@ class SegmentBuffer: # We have our first non-zero byte position. This means the init has just # been written. Create a Segment and put it to the queue of each output. self._segment = Segment( - sequence=self._sequence, - stream_id=self._stream_id, + sequence=self._stream_state.sequence, + stream_id=self._stream_state.stream_id, init=self._memory_file.getvalue(), # Fetch the latest StreamOutputs, which may have changed since the # worker started. - stream_outputs=self._outputs_callback().values(), + stream_outputs=self._stream_state.outputs, start_time=self._start_time, ) self._memory_file_pos = self._memory_file.tell() @@ -283,17 +320,6 @@ class SegmentBuffer: self._part_start_dts = adjusted_dts self._part_has_keyframe = False - def discontinuity(self) -> None: - """Mark the stream as having been restarted.""" - # Preserving sequence and stream_id here keep the HLS playlist logic - # simple to check for discontinuity at output time, and to determine - # the discontinuity sequence number. - self._stream_id += 1 - self._start_time = datetime.datetime.utcnow() - # Call discontinuity to remove incomplete segment from the HLS output - if hls_output := self._outputs_callback().get(HLS_PROVIDER): - cast(HlsStreamOutput, hls_output).discontinuity() - def close(self) -> None: """Close stream buffer.""" self._av_output.close() @@ -412,7 +438,7 @@ def unsupported_audio(packets: Iterator[av.Packet], audio_stream: Any) -> bool: def stream_worker( source: str, options: dict[str, str], - segment_buffer: SegmentBuffer, + stream_state: StreamState, quit_event: Event, ) -> None: """Handle consuming streams.""" @@ -431,6 +457,8 @@ def stream_worker( audio_stream = container.streams.audio[0] except (KeyError, IndexError): audio_stream = None + if audio_stream and audio_stream.name not in AUDIO_CODECS: + audio_stream = None # These formats need aac_adtstoasc bitstream filter, but auto_bsf not # compatible with empty_moov and manual bitstream filters not in PyAV if container.format.name in {"hls", "mpegts"}: @@ -489,13 +517,13 @@ def stream_worker( "Error demuxing stream while finding first packet: %s" % str(ex) ) from ex - segment_buffer.set_streams(video_stream, audio_stream) - segment_buffer.reset(start_dts) + muxer = StreamMuxer(stream_state.hass, video_stream, audio_stream, stream_state) + muxer.reset(start_dts) # Mux the first keyframe, then proceed through the rest of the packets - segment_buffer.mux_packet(first_keyframe) + muxer.mux_packet(first_keyframe) - with contextlib.closing(container), contextlib.closing(segment_buffer): + with contextlib.closing(container), contextlib.closing(muxer): while not quit_event.is_set(): try: packet = next(container_packets) @@ -506,4 +534,4 @@ def stream_worker( except av.AVError as ex: raise StreamWorkerError("Error demuxing stream: %s" % str(ex)) from ex - segment_buffer.mux_packet(packet) + muxer.mux_packet(packet) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 62c62593c57..10328a8f87b 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -23,7 +23,7 @@ import async_timeout import pytest from homeassistant.components.stream.core import Segment, StreamOutput -from homeassistant.components.stream.worker import SegmentBuffer +from homeassistant.components.stream.worker import StreamState TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout @@ -34,7 +34,7 @@ class WorkerSync: def __init__(self): """Initialize WorkerSync.""" self._event = None - self._original = SegmentBuffer.discontinuity + self._original = StreamState.discontinuity def pause(self): """Pause the worker before it finalizes the stream.""" @@ -45,7 +45,7 @@ class WorkerSync: logging.debug("waking blocked worker") self._event.set() - def blocking_discontinuity(self, stream: SegmentBuffer): + def blocking_discontinuity(self, stream_state: StreamState): """Intercept call to pause stream worker.""" # Worker is ending the stream, which clears all output buffers. # Block the worker thread until the test has a chance to verify @@ -55,7 +55,7 @@ class WorkerSync: self._event.wait() # Forward to actual implementation - self._original(stream) + self._original(stream_state) @pytest.fixture() @@ -63,7 +63,7 @@ def stream_worker_sync(hass): """Patch StreamOutput to allow test to synchronize worker stream end.""" sync = WorkerSync() with patch( - "homeassistant.components.stream.worker.SegmentBuffer.discontinuity", + "homeassistant.components.stream.worker.StreamState.discontinuity", side_effect=sync.blocking_discontinuity, autospec=True, ): diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index c65e10d65f3..3e9ea157934 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -38,8 +38,8 @@ from homeassistant.components.stream.const import ( ) from homeassistant.components.stream.core import StreamSettings from homeassistant.components.stream.worker import ( - SegmentBuffer, StreamEndedError, + StreamState, StreamWorkerError, stream_worker, ) @@ -255,6 +255,12 @@ class MockPyAv: return self.container +def run_worker(hass, stream, stream_source): + """Run the stream worker under test.""" + stream_state = StreamState(hass, stream.outputs) + stream_worker(stream_source, {}, stream_state, threading.Event()) + + async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE, {}) @@ -268,9 +274,8 @@ async def async_decode_stream(hass, packets, py_av=None): "homeassistant.components.stream.core.StreamOutput.put", side_effect=py_av.capture_buffer.capture_output_segment, ): - segment_buffer = SegmentBuffer(hass, stream.outputs) try: - stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) + run_worker(hass, stream, STREAM_SOURCE) except StreamEndedError: # Tests only use a limited number of packets, then the worker exits as expected. In # production, stream ending would be unexpected. @@ -288,8 +293,7 @@ async def test_stream_open_fails(hass): stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError): av_open.side_effect = av.error.InvalidDataError(-2, "error") - segment_buffer = SegmentBuffer(hass, stream.outputs) - stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) + run_worker(hass, stream, STREAM_SOURCE) await hass.async_block_till_done() av_open.assert_called_once() @@ -695,10 +699,7 @@ async def test_worker_log(hass, caplog): with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: av_open.side_effect = av.error.InvalidDataError(-2, "error") - segment_buffer = SegmentBuffer(hass, stream.outputs) - stream_worker( - "https://abcd:efgh@foo.bar", {}, segment_buffer, threading.Event() - ) + run_worker(hass, stream, "https://abcd:efgh@foo.bar") await hass.async_block_till_done() assert str(err.value) == "Error opening stream https://****:****@foo.bar" assert "https://abcd:efgh@foo.bar" not in caplog.text From cc543b200da8a2b33ca230fbaa6d6073d23408db Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 22:41:29 -0800 Subject: [PATCH 0990/1452] Update `nest` config flow to dramatically simplify end user setup with automated pub/sub subscription creation (#59260) * Configure nest pubsub subscriber automatically Update the config flow to configure the nest pubsub subscriber automatically. After completing the authentication step, the user is now asked for the google cloud console ID, which is needed to create a subscription. Home Assistant manages the lifecycle of a subscription only when it is created by the ConfigFlow. Otherwise (if specified in configuration.yaml) it treats it similarly as before. These are the considerations or failure modes taken into account: - Subscription is created with reasonable default values as previously recommended (e.g. retion only keeps 5-15 minutes of backlog messages) - Subscriptions are created with a naming scheme that makes it clear they came from home assistant, and with a random string - Subscriptions are cleaned up when the ConfigEntry is removed. If removal fails, a subscription that is orphaned will be deleted after 30 days - If the subscription gets into a bad state or deleted, the user can go through the re-auth flow to re-create it. - Users can still specifcy a CONF_SUBSCRIBER_ID in the configuration.yaml, and skip automatic subscriber creation * Remove unnecessary nest config flow diffs and merge in upstream changes * Incorporate review feedback into nest subscription config flow * Update text wording in nest config flow --- homeassistant/components/nest/__init__.py | 29 +- homeassistant/components/nest/api.py | 28 +- homeassistant/components/nest/config_flow.py | 186 ++++++++-- homeassistant/components/nest/const.py | 1 + homeassistant/components/nest/strings.json | 15 +- .../components/nest/translations/en.json | 2 +- tests/components/nest/common.py | 21 +- tests/components/nest/test_config_flow_sdm.py | 336 +++++++++++++++--- tests/components/nest/test_init_sdm.py | 139 +++++++- 9 files changed, 658 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 37901d060e1..0933f10e6ce 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -127,10 +127,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if CONF_PROJECT_ID not in config[DOMAIN]: return await async_setup_legacy(hass, config) - if CONF_SUBSCRIBER_ID not in config[DOMAIN]: - _LOGGER.error("Configuration option 'subscriber_id' required") - return False - # For setup of ConfigEntry below hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN] project_id = config[DOMAIN][CONF_PROJECT_ID] @@ -195,9 +191,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await async_setup_legacy_entry(hass, entry) subscriber = await api.new_subscriber(hass, entry) + if not subscriber: + return False + callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) - try: await subscriber.start_async() except AuthException as err: @@ -245,3 +243,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) return unload_ok + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle removal of pubsub subscriptions created during config flow.""" + if DATA_SDM not in entry.data or CONF_SUBSCRIBER_ID not in entry.data: + return + + subscriber = await api.new_subscriber(hass, entry) + if not subscriber: + return + _LOGGER.debug("Deleting subscriber '%s'", subscriber.subscriber_id) + try: + await subscriber.delete_subscription() + except GoogleNestException as err: + _LOGGER.warning( + "Unable to delete subscription '%s'; Will be automatically cleaned up by cloud console: %s", + subscriber.subscriber_id, + err, + ) + finally: + subscriber.stop_async() diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 17b473dbeaa..3934b0b3cf1 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -1,6 +1,9 @@ """API for Google Nest Device Access bound to Home Assistant OAuth.""" +from __future__ import annotations + import datetime +import logging from typing import cast from aiohttp import ClientSession @@ -23,7 +26,7 @@ from .const import ( SDM_SCOPES, ) -# See https://developers.google.com/nest/device-access/registration +_LOGGER = logging.getLogger(__name__) class AsyncConfigEntryAuth(AbstractAuth): @@ -71,14 +74,31 @@ class AsyncConfigEntryAuth(AbstractAuth): async def new_subscriber( hass: HomeAssistant, entry: ConfigEntry -) -> GoogleNestSubscriber: +) -> GoogleNestSubscriber | None: """Create a GoogleNestSubscriber.""" implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry ) ) + config = hass.data[DOMAIN][DATA_NEST_CONFIG] + if not ( + subscriber_id := entry.data.get( + CONF_SUBSCRIBER_ID, config.get(CONF_SUBSCRIBER_ID) + ) + ): + _LOGGER.error("Configuration option 'subscriber_id' required") + return None + return await new_subscriber_with_impl(hass, entry, subscriber_id, implementation) + +async def new_subscriber_with_impl( + hass: HomeAssistant, + entry: ConfigEntry, + subscriber_id: str, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, +) -> GoogleNestSubscriber: + """Create a GoogleNestSubscriber, used during ConfigFlow.""" config = hass.data[DOMAIN][DATA_NEST_CONFIG] session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) auth = AsyncConfigEntryAuth( @@ -87,6 +107,4 @@ async def new_subscriber( config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET], ) - return GoogleNestSubscriber( - auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] - ) + return GoogleNestSubscriber(auth, config[CONF_PROJECT_ID], subscriber_id) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index ec567aaa14e..31192b1a2b2 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -6,7 +6,22 @@ This configuration flow supports the following: - Legacy Nest API auth flow with where user enters an auth code manually NestFlowHandler is an implementation of AbstractOAuth2FlowHandler with -some overrides to support installed app and old APIs auth flow. +some overrides to support installed app and old APIs auth flow, reauth, +and other custom steps inserted in the middle of the flow. + +The notable config flow steps are: +- user: To dispatch between API versions +- auth: Inserted to add a hook for the installed app flow to accept a token +- async_oauth_create_entry: Overridden to handle when OAuth is complete. This + does not actually create the entry, but holds on to the OAuth token data + for later +- pubsub: Configure the pubsub subscription. Note that subscriptions created + by the config flow are deleted when removed. +- finish: Handles creating a new configuration entry or updating the existing + configuration entry for reauth. + +The SDM API config flow supports a hybrid of configuration.yaml (used as defaults) +and config flow. """ from __future__ import annotations @@ -17,20 +32,46 @@ import os from typing import Any import async_timeout +from google_nest_sdm.exceptions import ( + AuthException, + ConfigurationException, + GoogleNestException, +) import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.util import get_random_string from homeassistant.util.json import load_json -from .const import DATA_SDM, DOMAIN, OOB_REDIRECT_URI, SDM_SCOPES +from . import api +from .const import ( + CONF_CLOUD_PROJECT_ID, + CONF_PROJECT_ID, + CONF_SUBSCRIBER_ID, + DATA_NEST_CONFIG, + DATA_SDM, + DOMAIN, + OOB_REDIRECT_URI, + SDM_SCOPES, +) DATA_FLOW_IMPL = "nest_flow_implementation" +SUBSCRIPTION_FORMAT = "projects/{cloud_project_id}/subscriptions/home-assistant-{rnd}" +SUBSCRIPTION_RAND_LENGTH = 10 +CLOUD_CONSOLE_URL = "https://console.cloud.google.com/home/dashboard" _LOGGER = logging.getLogger(__name__) +def _generate_subscription_id(cloud_project_id: str) -> str: + """Create a new subscription id.""" + rnd = get_random_string(SUBSCRIPTION_RAND_LENGTH) + return SUBSCRIPTION_FORMAT.format(cloud_project_id=cloud_project_id, rnd=rnd) + + @callback def register_flow_implementation( hass: HomeAssistant, @@ -80,8 +121,10 @@ class NestFlowHandler( def __init__(self) -> None: """Initialize NestFlowHandler.""" super().__init__() - # When invoked for reauth, allows updating an existing config entry - self._reauth = False + # Allows updating an existing config entry + self._reauth_data: dict[str, Any] | None = None + # ConfigEntry data for SDM API + self._data: dict[str, Any] = {DATA_SDM: {}} @classmethod def register_sdm_api(cls, hass: HomeAssistant) -> None: @@ -110,35 +153,24 @@ class NestFlowHandler( } async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: - """Create an entry for the SDM flow.""" + """Complete OAuth setup and finish pubsub or finish.""" assert self.is_sdm_api(), "Step only supported for SDM API" - data[DATA_SDM] = {} - await self.async_set_unique_id(DOMAIN) - # Update existing config entry when in the reauth flow. This - # integration only supports one config entry so remove any prior entries - # added before the "single_instance_allowed" check was added - existing_entries = self._async_current_entries() - if existing_entries: - updated = False - for entry in existing_entries: - if updated: - await self.hass.config_entries.async_remove(entry.entry_id) - continue - updated = True - self.hass.config_entries.async_update_entry( - entry, data=data, unique_id=DOMAIN - ) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return await super().async_oauth_create_entry(data) + self._data.update(data) + if not self._configure_pubsub(): + _LOGGER.debug("Skipping Pub/Sub configuration") + return await self.async_step_finish() + return await self.async_step_pubsub() async def async_step_reauth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Perform reauth upon an API authentication error.""" assert self.is_sdm_api(), "Step only supported for SDM API" - self._reauth = True # Forces update of existing config entry + if user_input is None: + _LOGGER.error("Reauth invoked with empty config entry data") + return self.async_abort(reason="missing_configuration") + self._reauth_data = user_input + self._data.update(user_input) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -167,7 +199,7 @@ class NestFlowHandler( """Handle a flow initialized by the user.""" if self.is_sdm_api(): # Reauth will update an existing entry - if self._async_current_entries() and not self._reauth: + if self._async_current_entries() and not self._reauth_data: return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) return await self.async_step_init(user_input) @@ -199,6 +231,106 @@ class NestFlowHandler( ) return await super().async_step_auth(user_input) + def _configure_pubsub(self) -> bool: + """Return True if the config flow should configure Pub/Sub.""" + if self._reauth_data is not None and CONF_SUBSCRIBER_ID in self._reauth_data: + # Existing entry needs to be reconfigured + return True + if CONF_SUBSCRIBER_ID in self.hass.data[DOMAIN][DATA_NEST_CONFIG]: + # Hard coded configuration.yaml skips pubsub in config flow + return False + # No existing subscription configured, so create in config flow + return True + + async def async_step_pubsub( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure and create Pub/Sub subscriber.""" + # Populate data from the previous config entry during reauth, then + # overwrite with the user entered values. + data = {} + if self._reauth_data: + data.update(self._reauth_data) + if user_input: + data.update(user_input) + cloud_project_id = data.get(CONF_CLOUD_PROJECT_ID, "") + + errors = {} + config = self.hass.data[DOMAIN][DATA_NEST_CONFIG] + if cloud_project_id == config[CONF_PROJECT_ID]: + _LOGGER.error( + "Wrong Project ID. Device Access Project ID used, but expected Cloud Project ID" + ) + errors[CONF_CLOUD_PROJECT_ID] = "wrong_project_id" + + if user_input is not None and not errors: + # Create the subscriber id and/or verify it already exists. Note that + # the existing id is used, and create call below is idempotent + subscriber_id = data.get(CONF_SUBSCRIBER_ID, "") + if not subscriber_id: + subscriber_id = _generate_subscription_id(cloud_project_id) + _LOGGER.debug("Creating subscriber id '%s'", subscriber_id) + # Create a placeholder ConfigEntry to use since with the auth we've already created. + entry = ConfigEntry( + version=1, domain=DOMAIN, title="", data=self._data, source="" + ) + subscriber = await api.new_subscriber_with_impl( + self.hass, entry, subscriber_id, self.flow_impl + ) + try: + await subscriber.create_subscription() + except AuthException as err: + _LOGGER.error("Subscriber authentication error: %s", err) + return self.async_abort(reason="invalid_access_token") + except ConfigurationException as err: + _LOGGER.error("Configuration error creating subscription: %s", err) + errors[CONF_CLOUD_PROJECT_ID] = "bad_project_id" + except GoogleNestException as err: + _LOGGER.error("Error creating subscription: %s", err) + errors[CONF_CLOUD_PROJECT_ID] = "subscriber_error" + + if not errors: + self._data.update( + { + CONF_SUBSCRIBER_ID: subscriber_id, + CONF_CLOUD_PROJECT_ID: cloud_project_id, + } + ) + return await self.async_step_finish() + + return self.async_show_form( + step_id="pubsub", + data_schema=vol.Schema( + { + vol.Required(CONF_CLOUD_PROJECT_ID, default=cloud_project_id): str, + } + ), + description_placeholders={"url": CLOUD_CONSOLE_URL}, + errors=errors, + ) + + async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: + """Create an entry for the SDM flow.""" + assert self.is_sdm_api(), "Step only supported for SDM API" + await self.async_set_unique_id(DOMAIN) + # Update existing config entry when in the reauth flow. This + # integration only supports one config entry so remove any prior entries + # added before the "single_instance_allowed" check was added + existing_entries = self._async_current_entries() + if existing_entries: + updated = False + for entry in existing_entries: + if updated: + await self.hass.config_entries.async_remove(entry.entry_id) + continue + updated = True + self.hass.config_entries.async_update_entry( + entry, data=self._data, unique_id=DOMAIN + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return await super().async_oauth_create_entry(self._data) + async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 6fcd74299ba..a92a48bfd6c 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -7,6 +7,7 @@ DATA_NEST_CONFIG = "nest_config" CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" +CONF_CLOUD_PROJECT_ID = "cloud_project_id" SIGNAL_NEST_UPDATE = "nest_update" diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 84cfc3435a6..4dd3e5419b0 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -11,6 +11,13 @@ "code": "[%key:common::config_flow::data::access_token%]" } }, + "pubsub": { + "title": "Configure Google Cloud", + "description": "Visit the [Cloud Console]({url}) to find your Google Cloud Project ID.", + "data": { + "cloud_project_id": "Google Cloud Project ID" + } + }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nest integration needs to re-authenticate your account" @@ -34,7 +41,10 @@ "timeout": "Timeout validating code", "invalid_pin": "Invalid [%key:common::config_flow::data::pin%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "internal_error": "Internal error validating code" + "internal_error": "Internal error validating code", + "bad_project_id": "Please enter a valid Cloud Project ID (check Cloud Console)", + "wrong_project_id": "Please enter a valid Cloud Project ID (found Device Access Project ID)", + "subscriber_error": "Unknown subscriber error, see logs" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", @@ -42,7 +52,8 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index be35cf1b54e..e03de2b9bea 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -56,4 +56,4 @@ "doorbell_chime": "Doorbell pressed" } } -} \ No newline at end of file +} diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index c9572c528bb..eb44b19d540 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -32,7 +32,7 @@ FAKE_TOKEN = "some-token" FAKE_REFRESH_TOKEN = "some-refresh-token" -def create_config_entry(hass, token_expiration_time=None): +def create_config_entry(hass, token_expiration_time=None) -> MockConfigEntry: """Create a ConfigEntry and add it to Home Assistant.""" if token_expiration_time is None: token_expiration_time = time.time() + 86400 @@ -47,7 +47,9 @@ def create_config_entry(hass, token_expiration_time=None): "expires_at": token_expiration_time, }, } - MockConfigEntry(domain=DOMAIN, data=config_entry_data).add_to_hass(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data=config_entry_data) + config_entry.add_to_hass(hass) + return config_entry class FakeDeviceManager(DeviceManager): @@ -80,6 +82,14 @@ class FakeSubscriber(GoogleNestSubscriber): """Capture the callback set by Home Assistant.""" self._callback = callback + async def create_subscription(self): + """Create the subscription.""" + return + + async def delete_subscription(self): + """Delete the subscription.""" + return + async def start_async(self): """Return the fake device manager.""" return self._device_manager @@ -99,9 +109,12 @@ class FakeSubscriber(GoogleNestSubscriber): await self._callback(event_message) -async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): +async def async_setup_sdm_platform( + hass, platform, devices={}, structures={}, with_config=True +): """Set up the platform and prerequisites.""" - create_config_entry(hass) + if with_config: + create_config_entry(hass) device_manager = FakeDeviceManager(devices=devices, structures=structures) subscriber = FakeSubscriber(device_manager) with patch( diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 75ce8bcf939..5d6987f94f7 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,7 +1,13 @@ """Test the Google Nest Device Access config flow.""" +import copy from unittest.mock import patch +from google_nest_sdm.exceptions import ( + AuthException, + ConfigurationException, + GoogleNestException, +) import pytest from homeassistant import config_entries, setup @@ -11,12 +17,13 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .common import MockConfigEntry +from .common import FakeDeviceManager, FakeSubscriber, MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" -SUBSCRIBER_ID = "projects/example/subscriptions/subscriber-id-9876" +SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" +CLOUD_PROJECT_ID = "cloud-id-9876" CONFIG = { DOMAIN: { @@ -35,7 +42,19 @@ WEB_REDIRECT_URL = "https://example.com/auth/external/callback" APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" -def get_config_entry(hass: HomeAssistant) -> ConfigEntry: +@pytest.fixture +def device_manager() -> FakeDeviceManager: + """Create FakeDeviceManager.""" + return FakeDeviceManager(devices={}, structures={}) + + +@pytest.fixture +def subscriber(device_manager: FakeDeviceManager) -> FakeSubscriber: + """Create FakeSubscriber.""" + return FakeSubscriber(device_manager) + + +def get_config_entry(hass): """Return a single config entry.""" entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -71,7 +90,7 @@ class OAuthFixture: result["flow_id"], {"implementation": auth_domain} ) - async def async_oauth_web_flow(self, result: dict) -> ConfigEntry: + async def async_oauth_web_flow(self, result: dict) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" state = self.create_state(result, WEB_REDIRECT_URL) assert result["url"] == self.authorize_url(state, WEB_REDIRECT_URL) @@ -82,9 +101,9 @@ class OAuthFixture: assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" - return await self.async_finish_flow(result) + await self.async_mock_refresh(result) - async def async_oauth_app_flow(self, result: dict) -> ConfigEntry: + async def async_oauth_app_flow(self, result: dict) -> None: """Invoke the oauth flow for Installed Auth with fake responses.""" # Render form with a link to get an auth token assert result["type"] == "form" @@ -96,7 +115,25 @@ class OAuthFixture: state, APP_REDIRECT_URL ) # Simulate user entering auth token in form - return await self.async_finish_flow(result, {"code": "abcd"}) + await self.async_mock_refresh(result, {"code": "abcd"}) + + async def async_reauth(self, old_data: dict) -> dict: + """Initiate a reuath flow.""" + result = await self.hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_data + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + # Advance through the reauth flow + flows = self.hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + # Advance to the oauth flow + return await self.hass.config_entries.flow.async_configure( + flows[0]["flow_id"], {} + ) def create_state(self, result: dict, redirect_url: str) -> str: """Create state object based on redirect url.""" @@ -119,7 +156,7 @@ class OAuthFixture: "&access_type=offline&prompt=consent" ) - async def async_finish_flow(self, result, user_input: dict = None) -> ConfigEntry: + async def async_mock_refresh(self, result, user_input: dict = None) -> None: """Finish the OAuth flow exchanging auth token for refresh token.""" self.aioclient_mock.post( OAUTH2_TOKEN, @@ -131,6 +168,10 @@ class OAuthFixture: }, ) + async def async_finish_setup( + self, result: dict, user_input: dict = None + ) -> ConfigEntry: + """Finish the OAuth flow exchanging auth token for refresh token.""" with patch( "homeassistant.components.nest.async_setup_entry", return_value=True ) as mock_setup: @@ -139,7 +180,25 @@ class OAuthFixture: ) assert len(mock_setup.mock_calls) == 1 await self.hass.async_block_till_done() + return self.get_config_entry() + async def async_configure(self, result: dict, user_input: dict) -> dict: + """Advance to the next step in the config flow.""" + return await self.hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + async def async_pubsub_flow(self, result: dict, cloud_project_id="") -> ConfigEntry: + """Verify the pubsub creation step.""" + # Render form with a link to get an auth token + assert result["type"] == "form" + assert result["step_id"] == "pubsub" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + assert result["data_schema"]({}) == {"cloud_project_id": cloud_project_id} + + def get_config_entry(self) -> ConfigEntry: + """Get the config entry.""" return get_config_entry(self.hass) @@ -149,6 +208,13 @@ async def oauth(hass, hass_client_no_auth, aioclient_mock, current_request_with_ return OAuthFixture(hass, hass_client_no_auth, aioclient_mock) +async def async_setup_configflow(hass): + """Set up component so the pubsub subscriber is managed by config flow.""" + config = copy.deepcopy(CONFIG) + del config[DOMAIN]["subscriber_id"] # Create in config flow instead + return await setup.async_setup_component(hass, DOMAIN, config) + + async def test_web_full_flow(hass, oauth): """Check full flow.""" assert await setup.async_setup_component(hass, DOMAIN, CONFIG) @@ -159,7 +225,8 @@ async def test_web_full_flow(hass, oauth): result = await oauth.async_pick_flow(result, WEB_AUTH_DOMAIN) - entry = await oauth.async_oauth_web_flow(result) + await oauth.async_oauth_web_flow(result) + entry = await oauth.async_finish_setup(result) assert entry.title == "OAuth for Web" assert "token" in entry.data entry.data["token"].pop("expires_at") @@ -170,6 +237,8 @@ async def test_web_full_flow(hass, oauth): "type": "Bearer", "expires_in": 60, } + # Subscriber from configuration.yaml + assert "subscriber_id" not in entry.data async def test_web_reauth(hass, oauth): @@ -194,19 +263,10 @@ async def test_web_reauth(hass, oauth): "access_token": "some-revoked-token", } - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data - ) + result = await oauth.async_reauth(old_entry.data) - # Advance through the reauth flow - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" - - # Run the oauth flow - result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - - entry = await oauth.async_oauth_web_flow(result) + await oauth.async_oauth_web_flow(result) + entry = await oauth.async_finish_setup(result) # Verify existing tokens are replaced entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN @@ -217,6 +277,7 @@ async def test_web_reauth(hass, oauth): "expires_in": 60, } assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN + assert "subscriber_id" not in entry.data # not updated async def test_single_config_entry(hass): @@ -254,17 +315,12 @@ async def test_unexpected_existing_config_entries(hass, oauth): assert len(entries) == 2 # Invoke the reauth flow - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data - ) - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" + result = await oauth.async_reauth(old_entry.data) - flows = hass.config_entries.flow.async_progress() - - result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) await oauth.async_oauth_web_flow(result) + await oauth.async_finish_setup(result) + # Only a single entry now exists, and the other was cleaned up entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -277,9 +333,22 @@ async def test_unexpected_existing_config_entries(hass, oauth): "type": "Bearer", "expires_in": 60, } + assert "subscriber_id" not in entry.data # not updated -async def test_app_full_flow(hass, oauth, aioclient_mock): +async def test_reauth_missing_config_entry(hass): + """Test the reauth flow invoked missing existing data.""" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + # Invoke the reauth flow with no existing data + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=None + ) + assert result["type"] == "abort" + assert result["reason"] == "missing_configuration" + + +async def test_app_full_flow(hass, oauth): """Check full flow.""" assert await setup.async_setup_component(hass, DOMAIN, CONFIG) @@ -288,7 +357,8 @@ async def test_app_full_flow(hass, oauth, aioclient_mock): ) result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - entry = await oauth.async_oauth_app_flow(result) + await oauth.async_oauth_app_flow(result) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "OAuth for Apps" assert "token" in entry.data entry.data["token"].pop("expires_at") @@ -299,6 +369,8 @@ async def test_app_full_flow(hass, oauth, aioclient_mock): "type": "Bearer", "expires_in": 60, } + # Subscriber from configuration.yaml + assert "subscriber_id" not in entry.data async def test_app_reauth(hass, oauth): @@ -318,26 +390,11 @@ async def test_app_reauth(hass, oauth): }, ) - entry = get_config_entry(hass) - assert entry.data["token"] == { - "access_token": "some-revoked-token", - } - - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data - ) - - # Advance through the reauth flow - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" - - # Run the oauth flow - result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + result = await oauth.async_reauth(old_entry.data) await oauth.async_oauth_app_flow(result) # Verify existing tokens are replaced - entry = get_config_entry(hass) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN assert entry.data["token"] == { @@ -347,3 +404,186 @@ async def test_app_reauth(hass, oauth): "expires_in": 60, } assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN + assert "subscriber_id" not in entry.data # not updated + + +async def test_pubsub_subscription(hass, oauth, subscriber): + """Check flow that creates a pub/sub subscription.""" + assert await async_setup_configflow(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + await oauth.async_oauth_app_flow(result) + + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, + ): + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) + await hass.async_block_till_done() + + assert entry.title == "OAuth for Apps" + assert "token" in entry.data + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + assert "subscriber_id" in entry.data + assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID + + +async def test_pubsub_subscription_auth_failure(hass, oauth): + """Check flow that creates a pub/sub subscription.""" + assert await async_setup_configflow(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + await oauth.async_oauth_app_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", + side_effect=AuthException(), + ): + await oauth.async_pubsub_flow(result) + result = await oauth.async_configure( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "invalid_access_token" + + +async def test_pubsub_subscription_failure(hass, oauth): + """Check flow that creates a pub/sub subscription.""" + assert await async_setup_configflow(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + await oauth.async_oauth_app_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", + side_effect=GoogleNestException(), + ): + result = await oauth.async_configure( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert "errors" in result + assert "cloud_project_id" in result["errors"] + assert result["errors"]["cloud_project_id"] == "subscriber_error" + + +async def test_pubsub_subscription_configuration_failure(hass, oauth): + """Check flow that creates a pub/sub subscription.""" + assert await async_setup_configflow(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + await oauth.async_oauth_app_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", + side_effect=ConfigurationException(), + ): + result = await oauth.async_configure( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert "errors" in result + assert "cloud_project_id" in result["errors"] + assert result["errors"]["cloud_project_id"] == "bad_project_id" + + +async def test_pubsub_with_wrong_project_id(hass, oauth): + """Test a possible common misconfiguration mixing up project ids.""" + assert await async_setup_configflow(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) + await oauth.async_oauth_app_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + result = await oauth.async_configure( + result, {"cloud_project_id": PROJECT_ID} # SDM project id + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert "errors" in result + assert "cloud_project_id" in result["errors"] + assert result["errors"]["cloud_project_id"] == "wrong_project_id" + + +async def test_pubsub_subscriber_config_entry_reauth(hass, oauth, subscriber): + """Test the pubsub subscriber id is preserved during reauth.""" + assert await async_setup_configflow(hass) + + old_entry = create_config_entry( + hass, + { + "auth_implementation": APP_AUTH_DOMAIN, + "subscription_id": SUBSCRIBER_ID, + "cloud_project_id": CLOUD_PROJECT_ID, + "token": { + "access_token": "some-revoked-token", + }, + "sdm": {}, + }, + ) + result = await oauth.async_reauth(old_entry.data) + await oauth.async_oauth_app_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) + + # Configure Pub/Sub + await oauth.async_pubsub_flow(result, cloud_project_id=CLOUD_PROJECT_ID) + + # Verify existing tokens are replaced + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, + ): + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": "other-cloud-project-id"} + ) + await hass.async_block_till_done() + + entry = oauth.get_config_entry() + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN + assert ( + "projects/other-cloud-project-id/subscriptions" in entry.data["subscriber_id"] + ) + assert entry.data["cloud_project_id"] == "other-cloud-project-id" diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 59d1cbd0d69..fbfd6305487 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -9,7 +9,11 @@ import copy import logging from unittest.mock import patch -from google_nest_sdm.exceptions import AuthException, GoogleNestException +from google_nest_sdm.exceptions import ( + AuthException, + ConfigurationException, + GoogleNestException, +) from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -31,9 +35,10 @@ async def test_setup_success(hass, caplog): assert entries[0].state is ConfigEntryState.LOADED -async def async_setup_sdm(hass, config=CONFIG): +async def async_setup_sdm(hass, config=CONFIG, with_config=True): """Prepare test setup.""" - create_config_entry(hass) + if with_config: + create_config_entry(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ): @@ -111,17 +116,53 @@ async def test_subscriber_auth_failure(hass, caplog): async def test_setup_missing_subscriber_id(hass, caplog): - """Test successful setup.""" + """Test missing susbcriber id from config and config entry.""" config = copy.deepcopy(CONFIG) del config[DOMAIN]["subscriber_id"] - with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + + with caplog.at_level(logging.WARNING, logger="homeassistant.components.nest"): result = await async_setup_sdm(hass, config) - assert not result + assert result assert "Configuration option" in caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.SETUP_ERROR + + +async def test_setup_subscriber_id_config_entry(hass, caplog): + """Test successful setup with subscriber id in ConfigEntry.""" + config = copy.deepcopy(CONFIG) + subscriber_id = config[DOMAIN]["subscriber_id"] + del config[DOMAIN]["subscriber_id"] + + config_entry = create_config_entry(hass) + data = {**config_entry.data} + data["subscriber_id"] = subscriber_id + hass.config_entries.async_update_entry(config_entry, data=data) + + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm_platform(hass, PLATFORM, with_config=False) + assert not caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + +async def test_subscriber_configuration_failure(hass, caplog): + """Test configuration error.""" + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", + side_effect=ConfigurationException(), + ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + result = await async_setup_sdm(hass, CONFIG) + assert result + assert "Configuration error: " in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR async def test_empty_config(hass, caplog): @@ -133,3 +174,87 @@ async def test_empty_config(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 0 + + +async def test_unload_entry(hass, caplog): + """Test successful unload of a ConfigEntry.""" + await async_setup_sdm_platform(hass, PLATFORM) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED + + +async def test_remove_entry(hass, caplog): + """Test successful unload of a ConfigEntry.""" + await async_setup_sdm_platform(hass, PLATFORM) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_remove(entry.entry_id) + + entries = hass.config_entries.async_entries(DOMAIN) + assert not entries + + +async def test_remove_entry_deletes_subscriber(hass, caplog): + """Test ConfigEntry unload deletes a subscription.""" + config = copy.deepcopy(CONFIG) + subscriber_id = config[DOMAIN]["subscriber_id"] + del config[DOMAIN]["subscriber_id"] + + config_entry = create_config_entry(hass) + data = {**config_entry.data} + data["subscriber_id"] = subscriber_id + hass.config_entries.async_update_entry(config_entry, data=data) + + await async_setup_sdm_platform(hass, PLATFORM, with_config=False) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", + ) as delete: + assert await hass.config_entries.async_remove(entry.entry_id) + assert delete.called + + entries = hass.config_entries.async_entries(DOMAIN) + assert not entries + + +async def test_remove_entry_delete_subscriber_failure(hass, caplog): + """Test a failure when deleting the subscription.""" + config = copy.deepcopy(CONFIG) + subscriber_id = config[DOMAIN]["subscriber_id"] + del config[DOMAIN]["subscriber_id"] + + config_entry = create_config_entry(hass) + data = {**config_entry.data} + data["subscriber_id"] = subscriber_id + hass.config_entries.async_update_entry(config_entry, data=data) + + await async_setup_sdm_platform(hass, PLATFORM, with_config=False) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", + side_effect=GoogleNestException(), + ): + assert await hass.config_entries.async_remove(entry.entry_id) + + entries = hass.config_entries.async_entries(DOMAIN) + assert not entries From c4e5242b0c09fb62cacaee6a04b08f3988de45a0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 23:04:29 -0800 Subject: [PATCH 0991/1452] Add an entity service for saving nest event related snapshots (#58369) * Add an entity service for saving nest event related snapshots Add an entity service `nest.snapshot_event` for recording camera event related media to disk. This is based on `camera.snapshot` but takes in a parameter for a Nest API event_id. PR #58299 adds `nest_event_id` to events published by nest so that they can be hooked up to this service for capturing events. Future related work includes: - Height & Width parameters for the rendered image - Support video clips for new battery cameras - An API for proxying media related to events, separate from the camera image thumbnail - A Nest MediaSource for browsing media related to events * Revert debugging information * Add test coverage for OSError failure case * Add service description for nest snapshot service * Reduce unnecessary diffs. * Sort nest camera imports * Remove unnecessary if block in snapshot --- homeassistant/components/nest/camera_sdm.py | 46 +++++- homeassistant/components/nest/const.py | 2 + homeassistant/components/nest/services.yaml | 28 +++- tests/components/nest/test_camera_sdm.py | 153 +++++++++++++++++++- 4 files changed, 223 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 5385eb42b26..562b55ba652 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable import datetime import logging +import os from pathlib import Path from typing import Any @@ -19,6 +20,7 @@ from google_nest_sdm.device import Device from google_nest_sdm.event import ImageEventBase from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG +import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC @@ -26,12 +28,13 @@ from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_SUBSCRIBER, DOMAIN, SERVICE_SNAPSHOT_EVENT from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -64,6 +67,17 @@ async def async_setup_sdm_entry( entities.append(NestCamera(device)) async_add_entities(entities) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SNAPSHOT_EVENT, + { + vol.Required("nest_event_id"): cv.string, + vol.Required("filename"): cv.string, + }, + "_async_snapshot_event", + ) + class NestCamera(Camera): """Devices that support cameras.""" @@ -292,3 +306,33 @@ class NestCamera(Camera): except GoogleNestException as err: raise HomeAssistantError(f"Nest API error: {err}") from err return stream.answer_sdp + + async def _async_snapshot_event(self, nest_event_id: str, filename: str) -> None: + """Save media for a Nest event, based on `camera.snapshot`.""" + _LOGGER.debug("Taking snapshot for event id '%s'", nest_event_id) + if not self.hass.config.is_allowed_path(filename): + raise HomeAssistantError("No access to write snapshot '%s'" % filename) + # Fetch media associated with the event + if not (trait := self._device.traits.get(CameraEventImageTrait.NAME)): + raise HomeAssistantError("Camera does not support event image snapshots") + try: + event_image = await trait.generate_image(nest_event_id) + except GoogleNestException as err: + raise HomeAssistantError("Unable to create event snapshot") from err + try: + image = await event_image.contents() + except GoogleNestException as err: + raise HomeAssistantError("Unable to fetch event snapshot") from err + + _LOGGER.debug("Writing event snapshot to '%s'", filename) + + def _write_image() -> None: + """Executor helper to write image.""" + os.makedirs(os.path.dirname(filename), exist_ok=True) + with open(filename, "wb") as img_file: + img_file.write(image) + + try: + await self.hass.async_add_executor_job(_write_image) + except OSError as err: + raise HomeAssistantError("Failed to write snapshot image") from err diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index a92a48bfd6c..e98d563c574 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -22,3 +22,5 @@ SDM_SCOPES = [ ] API_URL = "https://smartdevicemanagement.googleapis.com/v1" OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" + +SERVICE_SNAPSHOT_EVENT = "snapshot_event" diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 98aacf60524..d432d2a3859 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,8 +1,30 @@ # Describes the format for available Nest services +snapshot_event: + name: Take event snapshot + description: Take a snapshot from a camera for an event. + target: + entity: + integration: nest + domain: camera + fields: + nest_event_id: + name: Nest Event Id + description: The nest_event_id from the event to snapshot. Can be populated by an automation trigger for a 'nest_event' with 'data_template'. + required: true + selector: + text: + filename: + name: Filename + description: A filename where the snapshot for the event is written. + required: true + example: "/tmp/snapshot_my_camera.jpg" + selector: + text: + set_away_mode: name: Set away mode - description: Set the away mode for a Nest structure. + description: Set the away mode for a Nest structure. For Legacy API. fields: away_mode: name: Away mode @@ -22,7 +44,7 @@ set_away_mode: set_eta: name: Set estimated time of arrival - description: Set or update the estimated time of arrival window for a Nest structure. + description: Set or update the estimated time of arrival window for a Nest structure. For Legacy API. fields: eta: name: ETA @@ -51,7 +73,7 @@ set_eta: cancel_eta: name: Cancel ETA - description: Cancel an existing estimated time of arrival window for a Nest structure. + description: Cancel an existing estimated time of arrival window for a Nest structure. For Legacy API. fields: trip_id: name: Trip ID diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 1ac1b4ca6f9..07184556819 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -7,7 +7,8 @@ pubsub subscriber. import datetime from http import HTTPStatus -from unittest.mock import patch +import os +from unittest.mock import mock_open, patch import aiohttp from google_nest_sdm.device import Device @@ -21,7 +22,9 @@ from homeassistant.components.camera import ( STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, ) +from homeassistant.components.nest.const import SERVICE_SNAPSHOT_EVENT from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -150,6 +153,20 @@ async def async_get_image(hass, width=None, height=None): ) +async def async_call_service_event_snapshot(hass, filename): + """Call the event snapshot service.""" + return await hass.services.async_call( + DOMAIN, + SERVICE_SNAPSHOT_EVENT, + { + ATTR_ENTITY_ID: "camera.my_camera", + "nest_event_id": "some-event-id", + "filename": filename, + }, + blocking=True, + ) + + async def test_no_devices(hass): """Test configuration that returns no devices.""" await async_setup_camera(hass) @@ -519,7 +536,7 @@ async def test_camera_image_from_last_event(hass, auth): async def test_camera_image_from_event_not_supported(hass, auth): """Test fallback to stream image when event images are not supported.""" - # Create a device that does not support the CameraEventImgae trait + # Create a device that does not support the CameraEventImage trait traits = DEVICE_TRAITS.copy() del traits["sdm.devices.traits.CameraEventImage"] subscriber = await async_setup_camera(hass, traits, auth=auth) @@ -865,3 +882,135 @@ async def test_camera_multiple_streams(hass, auth, hass_ws_client): assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" + + +async def test_service_snapshot_event_image(hass, auth, tmpdir): + """Test calling the snapshot_event service.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True): + assert await async_call_service_event_snapshot(hass, filename) + + assert os.path.exists(filename) + with open(filename, "rb") as f: + contents = f.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_service_snapshot_no_access_to_filename(hass, auth, tmpdir): + """Test calling the snapshot_event service with a disallowed file path.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object( + hass.config, "is_allowed_path", return_value=False + ), pytest.raises(HomeAssistantError, match=r"No access.*"): + assert await async_call_service_event_snapshot(hass, filename) + + assert not os.path.exists(filename) + + +async def test_camera_snapshot_from_event_not_supported(hass, auth, tmpdir): + """Test a camera that does not support snapshots.""" + # Create a device that does not support the CameraEventImage trait + traits = DEVICE_TRAITS.copy() + del traits["sdm.devices.traits.CameraEventImage"] + await async_setup_camera(hass, traits, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( + HomeAssistantError, match=r"Camera does not support.*" + ): + await async_call_service_event_snapshot(hass, filename) + assert not os.path.exists(filename) + + +async def test_service_snapshot_event_generate_url_failure(hass, auth, tmpdir): + """Test failure while creating a snapshot url.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + auth.responses = [ + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), + ] + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( + HomeAssistantError, match=r"Unable to create.*" + ): + await async_call_service_event_snapshot(hass, filename) + assert not os.path.exists(filename) + + +async def test_service_snapshot_event_image_fetch_invalid(hass, auth, tmpdir): + """Test failure when fetching an image snapshot.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), + ] + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( + HomeAssistantError, match=r"Unable to fetch.*" + ): + await async_call_service_event_snapshot(hass, filename) + assert not os.path.exists(filename) + + +async def test_service_snapshot_event_image_create_directory(hass, auth, tmpdir): + """Test creating the directory when writing the snapshot.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + filename = f"{tmpdir}/path/does/not/exist/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True): + assert await async_call_service_event_snapshot(hass, filename) + + assert os.path.exists(filename) + with open(filename, "rb") as f: + contents = f.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_service_snapshot_event_write_failure(hass, auth, tmpdir): + """Test a failure when writing the snapshot.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + filename = f"{tmpdir}/snapshot.jpg" + with patch.object(hass.config, "is_allowed_path", return_value=True), patch( + "homeassistant.components.nest.camera_sdm.open", mock_open(), create=True + ) as mocked_open, pytest.raises(HomeAssistantError, match=r"Failed to write.*"): + mocked_open.side_effect = IOError() + assert await async_call_service_event_snapshot(hass, filename) + + assert not os.path.exists(filename) From b40dc6c271c372648c46379d2f198c6e39e58cd3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:10:50 +0100 Subject: [PATCH 0992/1452] Use dataclass properties in rainmachine discovery (#60578) Co-authored-by: epenet --- homeassistant/components/rainmachine/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 6b11e82d023..39884681967 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -70,7 +70,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery via zeroconf.""" - ip_address = discovery_info[zeroconf.ATTR_HOST] + ip_address = discovery_info.host self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) # Handle IP change From b4d17e1fad87f38562006644e90a305859d71913 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:11:56 +0100 Subject: [PATCH 0993/1452] Use dataclass properties in system_bridge discovery (#60576) Co-authored-by: epenet --- homeassistant/components/system_bridge/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index ef3c1ccd2e4..26ccf83c345 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -151,7 +151,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - properties = discovery_info[zeroconf.ATTR_PROPERTIES] + properties = discovery_info.properties host = properties.get("ip") uuid = properties.get("uuid") From 0e3a229a1f4b8d495c260e6c4c8762c31b200e5a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:13:48 +0100 Subject: [PATCH 0994/1452] Use dataclass properties in volumio discovery (#60575) Co-authored-by: epenet --- homeassistant/components/volumio/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index e77aad05df2..f9703e3c2fb 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -98,10 +98,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self._host = discovery_info[zeroconf.ATTR_HOST] - self._port = discovery_info[zeroconf.ATTR_PORT] - self._name = discovery_info[zeroconf.ATTR_PROPERTIES]["volumioName"] - self._uuid = discovery_info[zeroconf.ATTR_PROPERTIES]["UUID"] + self._host = discovery_info.host + self._port = discovery_info.port + self._name = discovery_info.properties["volumioName"] + self._uuid = discovery_info.properties["UUID"] await self._set_uid_and_abort() From bb92dd24679d537d4902309e7226addb35fe9f17 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:31:25 +0100 Subject: [PATCH 0995/1452] Use dataclass properties in apple_tv discovery (#60557) --- homeassistant/components/apple_tv/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 306a1d9f793..11c41740c69 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -147,14 +147,14 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle device found via zeroconf.""" - service_type = discovery_info[zeroconf.ATTR_TYPE] - properties = discovery_info[zeroconf.ATTR_PROPERTIES] + service_type = discovery_info.type + properties = discovery_info.properties if service_type == "_mediaremotetv._tcp.local.": identifier = properties["UniqueIdentifier"] name = properties["Name"] elif service_type == "_touch-able._tcp.local.": - identifier = discovery_info[zeroconf.ATTR_NAME].split(".")[0] + identifier = discovery_info.name.split(".")[0] name = properties["CtlN"] else: return self.async_abort(reason="unknown") From 222da7e2d12d3f03680990f6c4eddc5622e79d66 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 30 Nov 2021 08:32:02 +0100 Subject: [PATCH 0996/1452] Add configuration_url to integration (#60565) --- homeassistant/components/p1_monitor/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 67ed6a160d8..1b0eedc7554 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_HOST, CURRENCY_EURO, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -268,6 +269,7 @@ class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity): identifiers={ (DOMAIN, f"{coordinator.config_entry.entry_id}_{service_key}") }, + configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", manufacturer="P1 Monitor", name=service, ) From e17759410c57b385ac60be5af5f86307ec74b0c2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 30 Nov 2021 20:33:14 +1300 Subject: [PATCH 0997/1452] Esphome button device class (#60569) --- homeassistant/components/esphome/button.py | 8 ++++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index b2b780ccaff..a16eca650ad 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -from aioesphomeapi import ButtonInfo -from aioesphomeapi.model import EntityState +from aioesphomeapi import ButtonInfo, EntityState from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry @@ -32,6 +31,11 @@ async def async_setup_entry( class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): """A button implementation for ESPHome.""" + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._static_info.device_class + @callback def _on_device_update(self) -> None: """Update the entity state when device info has changed.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 3b4eea3395c..680c79057af 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.3.0"], + "requirements": ["aioesphomeapi==10.4.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 2e0cf967e13..b4a32a3bbe4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.3.0 +aioesphomeapi==10.4.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ee1d3227a9..d7ffa92dc67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.3.0 +aioesphomeapi==10.4.0 # homeassistant.components.flo aioflo==2021.11.0 From b4f3e08b84c1e2de7d9a48d40e76f99644a3e137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:34:20 +0100 Subject: [PATCH 0998/1452] Bump actions/setup-python from 2.3.0 to 2.3.1 (#60572) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 12 ++++++------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 56da4acc490..3f8dea3b657 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -68,7 +68,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -99,7 +99,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 063044a5602..5bc99611abc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -137,7 +137,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -196,7 +196,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -245,7 +245,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -295,7 +295,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -337,7 +337,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -469,7 +469,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 6d11b3abae2..6c9c6700e9f 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.0 + uses: actions/setup-python@v2.3.1 with: python-version: ${{ env.DEFAULT_PYTHON }} From e0b315041bb8c56f78e1a679a8892037b601600b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:42:08 +0100 Subject: [PATCH 0999/1452] Use dataclass properties in octoprint discovery (#60579) Co-authored-by: epenet --- .../components/octoprint/config_flow.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index b2815b2d10f..6e6301a0ce5 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from yarl import URL from homeassistant import config_entries, data_entry_flow, exceptions -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -143,29 +143,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle discovery flow.""" - uuid = discovery_info[zeroconf.ATTR_PROPERTIES]["uuid"] + uuid = discovery_info.properties["uuid"] await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() self.context["title_placeholders"] = { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: discovery_info.host, } self.discovery_schema = _schema_with_defaults( - host=discovery_info[zeroconf.ATTR_HOST], - port=discovery_info[zeroconf.ATTR_PORT], - path=discovery_info[zeroconf.ATTR_PROPERTIES][CONF_PATH], + host=discovery_info.host, + port=discovery_info.port, + path=discovery_info.properties[CONF_PATH], ) return await self.async_step_user() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle ssdp discovery flow.""" - uuid = discovery_info["UDN"][5:] + uuid = discovery_info.upnp["UDN"][5:] await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() - url = URL(discovery_info["presentationURL"]) + url = URL(discovery_info.upnp["presentationURL"]) self.context["title_placeholders"] = { CONF_HOST: url.host, } From 9374ce87ba12e1b2ae08508805786dd6df631e00 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:42:30 +0100 Subject: [PATCH 1000/1452] Use dataclass properties in wled discovery (#60573) Co-authored-by: epenet --- homeassistant/components/wled/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index b2112e8962b..485afef4f6c 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -44,14 +44,14 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" # Hostname is format: wled-livingroom.local. - host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") + host = discovery_info.hostname.rstrip(".") name, _ = host.rsplit(".") self.context.update( { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: discovery_info.host, CONF_NAME: name, - CONF_MAC: discovery_info[zeroconf.ATTR_PROPERTIES].get(CONF_MAC), + CONF_MAC: discovery_info.properties.get(CONF_MAC), "title_placeholders": {"name": name}, } ) From d74145ed7b3c46680639f4ab77dbe09a047878f7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:46:40 +0100 Subject: [PATCH 1001/1452] Use dataclass properties in roku discovery (#60577) Co-authored-by: epenet --- homeassistant/components/roku/config_flow.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 996d6ca295f..688e243886b 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -8,11 +8,7 @@ from rokuecp import Roku, RokuError import voluptuous as vol from homeassistant.components import ssdp, zeroconf -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL, -) +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback @@ -91,9 +87,9 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): # If we already have the host configured do # not open connections to it if we can avoid it. - self._async_abort_entries_match({CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}) + self._async_abort_entries_match({CONF_HOST: discovery_info.host}) - self.discovery_info.update({CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}) + self.discovery_info.update({CONF_HOST: discovery_info.host}) try: info = await validate_input(self.hass, self.discovery_info) @@ -106,7 +102,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["serial_number"]) self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[zeroconf.ATTR_HOST]}, + updates={CONF_HOST: discovery_info.host}, ) self.context.update({"title_placeholders": {"name": info["title"]}}) @@ -116,9 +112,9 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] - serial_number = discovery_info[ATTR_UPNP_SERIAL] + host = urlparse(discovery_info.ssdp_location).hostname + name = discovery_info.upnp[ATTR_UPNP_FRIENDLY_NAME] + serial_number = discovery_info.upnp[ATTR_UPNP_SERIAL] await self.async_set_unique_id(serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) From 64afe738cccabd8473ec8a54adddeb6c186772e9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:53:03 +0100 Subject: [PATCH 1002/1452] Use dataclass properties in elgato discovery (#60588) Co-authored-by: epenet --- homeassistant/components/elgato/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 327aeead4dc..12d1b5d1d93 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -46,8 +46,8 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self.host = discovery_info[zeroconf.ATTR_HOST] - self.port = discovery_info[zeroconf.ATTR_PORT] or 9123 + self.host = discovery_info.host + self.port = discovery_info.port or 9123 try: await self._get_elgato_serial_number() From 5003a1515b04849625a9085d63926c2b9b0280cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 09:36:41 +0100 Subject: [PATCH 1003/1452] Use dataclass properties in nam discovery (#60596) Co-authored-by: epenet --- homeassistant/components/nam/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 00df0e0bf90..82032ac306e 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -126,7 +126,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self.host = discovery_info[zeroconf.ATTR_HOST] + self.host = discovery_info.host self.context["title_placeholders"] = {"host": self.host} # Do not probe the device if the host is already configured From b996f624db5e352fa9216f75e2b72c7e0b080b9e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 09:44:39 +0100 Subject: [PATCH 1004/1452] Ensure ESPHome device classes are valid (#60594) --- homeassistant/components/esphome/binary_sensor.py | 6 ++++-- homeassistant/components/esphome/button.py | 6 ++++-- homeassistant/components/esphome/cover.py | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 338f3787090..ffe322b6259 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from aioesphomeapi import BinarySensorInfo, BinarySensorState -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,8 +45,10 @@ class EsphomeBinarySensor( return self._state.state @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" + if self._static_info.device_class not in DEVICE_CLASSES: + return None return self._static_info.device_class @property diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index a16eca650ad..914ddb38da1 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -5,7 +5,7 @@ from typing import Any from aioesphomeapi import ButtonInfo, EntityState -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import DEVICE_CLASSES, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,8 +32,10 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): """A button implementation for ESPHome.""" @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" + if self._static_info.device_class not in DEVICE_CLASSES: + return None return self._static_info.device_class @callback diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index e055ffc5d03..0e23050646f 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -8,6 +8,7 @@ from aioesphomeapi import CoverInfo, CoverOperation, CoverState from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASSES, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -57,8 +58,10 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): return flags @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" + if self._static_info.device_class not in DEVICE_CLASSES: + return None return self._static_info.device_class @property From 0bb44c042c8a50d024a228a2c6d32b8332891eda Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:07:58 +0100 Subject: [PATCH 1005/1452] Use dataclass properties in brother discovery (#60601) Co-authored-by: epenet --- homeassistant/components/brother/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 7a814e2e77c..39a196aa6cb 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -84,13 +84,13 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: brother.local. - self.host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") + self.host = discovery_info.hostname.rstrip(".") # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) snmp_engine = get_snmp_engine(self.hass) - model = discovery_info[zeroconf.ATTR_PROPERTIES].get("product") + model = discovery_info.properties.get("product") try: self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) From a32a748cec73e5d6bea8cc0e637263b73064e88b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:08:55 +0100 Subject: [PATCH 1006/1452] Use dataclass properties in devolo_home_control discovery (#60600) Co-authored-by: epenet --- homeassistant/components/devolo_home_control/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index fab5c2b5008..e0e49197f45 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -49,7 +49,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Check if it is a gateway - if discovery_info[zeroconf.ATTR_PROPERTIES].get("MT") in SUPPORTED_MODEL_TYPES: + if discovery_info.properties.get("MT") in SUPPORTED_MODEL_TYPES: await self._async_handle_discovery_without_unique_id() return await self.async_step_zeroconf_confirm() return self.async_abort(reason="Not a devolo Home Control gateway.") From 2f79760fb422590fe62f676a8a07ae635525c618 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:15:46 +0100 Subject: [PATCH 1007/1452] Use dataclass properties in guardian discovery (#60586) Co-authored-by: epenet --- homeassistant/components/guardian/config_flow.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 78a4cbd90ea..b02d415b364 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -110,12 +110,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the configuration via zeroconf.""" self.discovery_info = { - CONF_IP_ADDRESS: discovery_info[zeroconf.ATTR_HOST], - CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_IP_ADDRESS: discovery_info.host, + CONF_PORT: discovery_info.port, } - pin = async_get_pin_from_discovery_hostname( - discovery_info[zeroconf.ATTR_HOSTNAME] - ) + pin = async_get_pin_from_discovery_hostname(discovery_info.hostname) await self._async_set_unique_id(pin) return await self._async_handle_discovery() From 071296865b225fcc6c22174c13f2e2a6e074b741 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:16:06 +0100 Subject: [PATCH 1008/1452] Use dataclass properties in lookin discovery (#60585) Co-authored-by: epenet --- homeassistant/components/lookin/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index af42febbb3f..2b4df9cc027 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -31,8 +31,8 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Start a discovery flow from zeroconf.""" - uid: str = discovery_info[zeroconf.ATTR_HOSTNAME][: -len(".local.")] - host: str = discovery_info[zeroconf.ATTR_HOST] + uid: str = discovery_info.hostname[: -len(".local.")] + host: str = discovery_info.host await self.async_set_unique_id(uid.upper()) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) From efebd1b6579b3170242b9e7624677b1c6c9fdc48 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:17:49 +0100 Subject: [PATCH 1009/1452] Use dataclass properties in nut discovery (#60603) Co-authored-by: epenet --- homeassistant/components/nut/config_flow.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 120c3754eca..fb0a2210a69 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Network UPS Tools (NUT) integration.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -23,7 +25,7 @@ from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) -def _base_schema(discovery_info): +def _base_schema(discovery_info: zeroconf.ZeroconfServiceInfo | None) -> vol.Schema: """Generate base schema.""" base_schema = {} if not discovery_info: @@ -83,7 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the nut config flow.""" self.nut_config = {} - self.discovery_info = {} + self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None self.ups_list = None self.title = None @@ -94,8 +96,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovery_info = discovery_info await self._async_handle_discovery_without_unique_id() self.context["title_placeholders"] = { - CONF_PORT: discovery_info[zeroconf.ATTR_PORT] or DEFAULT_PORT, - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_PORT: discovery_info.port or DEFAULT_PORT, + CONF_HOST: discovery_info.host, } return await self.async_step_user() @@ -106,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.discovery_info: user_input.update( { - CONF_HOST: self.discovery_info[CONF_HOST], + CONF_HOST: self.discovery_info.host, CONF_PORT: self.discovery_info.port or DEFAULT_PORT, } ) From 7182827818b81124112f67f812030264bb1be76e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:18:25 +0100 Subject: [PATCH 1010/1452] Use dataclass properties in modern_forms discovery (#60584) Co-authored-by: epenet --- homeassistant/components/modern_forms/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index 6a1059a0387..f8f3e2f1dc1 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -30,14 +30,14 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - host = discovery_info[zeroconf.ATTR_HOSTNAME].rstrip(".") + host = discovery_info.hostname.rstrip(".") name, _ = host.rsplit(".") self.context.update( { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: discovery_info.host, CONF_NAME: name, - CONF_MAC: discovery_info[zeroconf.ATTR_PROPERTIES].get(CONF_MAC), + CONF_MAC: discovery_info.properties.get(CONF_MAC), "title_placeholders": {"name": name}, } ) From 40a814221c3ce744fd181b5c26d467931adbad0b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:55:32 +0100 Subject: [PATCH 1011/1452] Use dataclass properties in forked_daapd discovery (#60587) Co-authored-by: epenet --- homeassistant/components/forked_daapd/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 28177bef97d..e3cf6fc7c1d 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -160,7 +160,7 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Prepare configuration for a discovered forked-daapd device.""" version_num = 0 - zeroconf_properties = discovery_info[zeroconf.ATTR_PROPERTIES] + zeroconf_properties = discovery_info.properties if zeroconf_properties.get("Machine Name"): with suppress(ValueError): version_num = int( @@ -173,7 +173,7 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Update title and abort if we already have an entry for this host for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) != discovery_info[zeroconf.ATTR_HOST]: + if entry.data.get(CONF_HOST) != discovery_info.host: continue self.hass.config_entries.async_update_entry( entry, @@ -182,8 +182,8 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") zeroconf_data = { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], - CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_HOST: discovery_info.host, + CONF_PORT: discovery_info.port, CONF_NAME: zeroconf_properties["Machine Name"], } self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data)) From 9b92787d59a7fefaedf0d5fe3a56fe85348c106d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:55:52 +0100 Subject: [PATCH 1012/1452] Use dataclass properties in daikin discovery (#60589) Co-authored-by: epenet --- homeassistant/components/daikin/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index e907aaa4d74..0084a89172f 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -130,15 +130,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) - devices = Discovery().poll(ip=discovery_info[zeroconf.ATTR_HOST]) + devices = Discovery().poll(ip=discovery_info.host) if not devices: _LOGGER.debug( "Could not find MAC-address for %s," " make sure the required UDP ports are open (see integration documentation)", - discovery_info[zeroconf.ATTR_HOST], + discovery_info.host, ) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(next(iter(devices))[KEY_MAC]) self._abort_if_unique_id_configured() - self.host = discovery_info[zeroconf.ATTR_HOST] + self.host = discovery_info.host return await self.async_step_user() From d537ec1d6f59a256708e7cd930cb9b0761b94c76 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:57:37 +0100 Subject: [PATCH 1013/1452] Use dataclass properties in bosch_shc discovery (#60559) --- .../components/bosch_shc/config_flow.py | 16 +++--------- .../components/bosch_shc/test_config_flow.py | 25 +------------------ 2 files changed, 4 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index c642df2a619..6ad1a374a5a 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -190,22 +190,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_bosch_shc") try: - hosts = ( - discovery_info[zeroconf.ATTR_HOST] - if isinstance(discovery_info[zeroconf.ATTR_HOST], list) - else [discovery_info[zeroconf.ATTR_HOST]] - ) - for host in hosts: - if host.startswith("169."): # skip link local address - continue - self.info = await self._get_info(host) - self.host = host - if self.info is None or self.host is None: - return self.async_abort(reason="cannot_connect") + self.info = await self._get_info(discovery_info.host) except SHCConnectionError: return self.async_abort(reason="cannot_connect") + self.host = discovery_info.host - local_name = discovery_info[zeroconf.ATTR_HOSTNAME][:-1] + local_name = discovery_info.hostname[:-1] node_name = local_name[: -len(".local")] await self.async_set_unique_id(self.info["unique_id"]) diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index b10b76b1042..065035bedbe 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -21,7 +21,7 @@ MOCK_SETTINGS = { "device": {"mac": "test-mac", "hostname": "test-host"}, } DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( - host=["169.1.1.1", "1.1.1.1"], + host="1.1.1.1", hostname="shc012345.local.", name="Bosch SHC [test-mac]._http._tcp.local.", port=0, @@ -527,29 +527,6 @@ async def test_zeroconf_cannot_connect(hass, mock_zeroconf): assert result["reason"] == "cannot_connect" -async def test_zeroconf_link_local(hass, mock_zeroconf): - """Test we get the form.""" - DISCOVERY_INFO_LINK_LOCAL = zeroconf.ZeroconfServiceInfo( - host=["169.1.1.1"], - hostname="shc012345.local.", - name="Bosch SHC [test-mac]._http._tcp.local.", - port=0, - properties={}, - type="_http._tcp.local.", - ) - - with patch( - "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=DISCOVERY_INFO_LINK_LOCAL, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( From fb94ed4e6b4cb1fd14d75159e461f70fd2516066 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:58:06 +0100 Subject: [PATCH 1014/1452] Use dataclass properties in bond discovery (#60590) Co-authored-by: epenet --- homeassistant/components/bond/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index cca8930532d..5fce8477a28 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -94,8 +94,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - name: str = discovery_info[zeroconf.ATTR_NAME] - host: str = discovery_info[zeroconf.ATTR_HOST] + name: str = discovery_info.name + host: str = discovery_info.host bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) for entry in self._async_current_entries(): From ba1cc00c2416dcffe05932c1b7d1775c73f6163c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:00:54 +0100 Subject: [PATCH 1015/1452] Use dataclass properties in shelly discovery (#60593) Co-authored-by: epenet --- homeassistant/components/shelly/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index ee24a302923..521fca79dc9 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -189,7 +189,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - host = discovery_info[zeroconf.ATTR_HOST] + host = discovery_info.host try: self.info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: From f0df3e4646c2c730100d133fea9b674deba0a2e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:01:28 +0100 Subject: [PATCH 1016/1452] Use dataclass properties in smappee discovery (#60602) Co-authored-by: epenet --- homeassistant/components/smappee/config_flow.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index 2fb5abaa05e..e57071b4938 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -42,15 +42,11 @@ class SmappeeFlowHandler( ) -> FlowResult: """Handle zeroconf discovery.""" - if not discovery_info[zeroconf.ATTR_HOSTNAME].startswith( - SUPPORTED_LOCAL_DEVICES - ): + if not discovery_info.hostname.startswith(SUPPORTED_LOCAL_DEVICES): return self.async_abort(reason="invalid_mdns") - serial_number = ( - discovery_info[zeroconf.ATTR_HOSTNAME] - .replace(".local.", "") - .replace("Smappee", "") + serial_number = discovery_info.hostname.replace(".local.", "").replace( + "Smappee", "" ) # Check if already configured (local) @@ -63,7 +59,7 @@ class SmappeeFlowHandler( self.context.update( { - CONF_IP_ADDRESS: discovery_info[zeroconf.ATTR_HOST], + CONF_IP_ADDRESS: discovery_info.host, CONF_SERIALNUMBER: serial_number, "title_placeholders": {"name": serial_number}, } From 416976dd39d72e96c35ddf64bd2a1ef076db2e9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:03:08 +0100 Subject: [PATCH 1017/1452] Use dataclass properties in esphome discovery (#60606) Co-authored-by: epenet --- homeassistant/components/esphome/config_flow.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 94c1f80a5e4..b73743ee950 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -143,22 +143,20 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = discovery_info[zeroconf.ATTR_HOSTNAME][:-1] + local_name = discovery_info.hostname[:-1] node_name = local_name[: -len(".local")] - address = discovery_info[zeroconf.ATTR_PROPERTIES].get("address", local_name) + address = discovery_info.properties.get("address", local_name) # Check if already configured await self.async_set_unique_id(node_name) - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} - ) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host}) for entry in self._async_current_entries(): already_configured = False if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( address, - discovery_info[zeroconf.ATTR_HOST], + discovery_info.host, ): # Is this address or IP address already configured? already_configured = True @@ -177,15 +175,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): entry, data={ **entry.data, - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: discovery_info.host, }, unique_id=node_name, ) return self.async_abort(reason="already_configured") - self._host = discovery_info[zeroconf.ATTR_HOST] - self._port = discovery_info[zeroconf.ATTR_PORT] + self._host = discovery_info.host + self._port = discovery_info.port self._name = node_name return await self.async_step_discovery_confirm() From 58661fa6366ac6f95ffb24af569bf390c16e0bd6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:04:33 +0100 Subject: [PATCH 1018/1452] Use dataclass properties in devolo_home_network discovery (#60608) Co-authored-by: epenet --- .../components/devolo_home_network/config_flow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 05d88aa1045..765d16177d9 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -77,16 +77,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zerooconf discovery.""" - if discovery_info[zeroconf.ATTR_PROPERTIES]["MT"] in ["2600", "2601"]: + if discovery_info.properties["MT"] in ["2600", "2601"]: return self.async_abort(reason="home_control") - await self.async_set_unique_id(discovery_info[zeroconf.ATTR_PROPERTIES]["SN"]) + await self.async_set_unique_id(discovery_info.properties["SN"]) self._abort_if_unique_id_configured() - self.context[CONF_HOST] = discovery_info[zeroconf.ATTR_HOST] + self.context[CONF_HOST] = discovery_info.host self.context["title_placeholders"] = { - PRODUCT: discovery_info[zeroconf.ATTR_PROPERTIES]["Product"], - CONF_NAME: discovery_info[zeroconf.ATTR_HOSTNAME].split(".")[0], + PRODUCT: discovery_info.properties["Product"], + CONF_NAME: discovery_info.hostname.split(".")[0], } return await self.async_step_zeroconf_confirm() From ae9320b61691a3d21830f954744d831cb57148fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:05:42 +0100 Subject: [PATCH 1019/1452] Use dataclass properties in tradfri discovery (#60592) Co-authored-by: epenet --- homeassistant/components/tradfri/config_flow.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index af3ed00e974..a0b63f94f4f 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -97,13 +97,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle homekit discovery.""" await self.async_set_unique_id( - discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] - ) - self._abort_if_unique_id_configured( - {CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info.host}) - host = discovery_info[zeroconf.ATTR_HOST] + host = discovery_info.host for entry in self._async_current_entries(): if entry.data.get(CONF_HOST) != host: @@ -113,9 +111,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not entry.unique_id: self.hass.config_entries.async_update_entry( entry, - unique_id=discovery_info[zeroconf.ATTR_PROPERTIES][ - zeroconf.ATTR_PROPERTIES_ID - ], + unique_id=discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID], ) return self.async_abort(reason="already_configured") From 56899d61fe2ae969e7d27799c26b8fcb2be2cf0b Mon Sep 17 00:00:00 2001 From: Adam Chyb Date: Tue, 30 Nov 2021 21:07:44 +1100 Subject: [PATCH 1020/1452] Add support for Kogan smart blinds to Tuya (#60552) --- homeassistant/components/tuya/cover.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index b5ac5645cd2..2598429dda3 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -10,6 +10,7 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASS_BLIND, DEVICE_CLASS_CURTAIN, DEVICE_CLASS_GARAGE, SUPPORT_CLOSE, @@ -68,6 +69,15 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { set_position=DPCode.PERCENT_CONTROL_3, device_class=DEVICE_CLASS_CURTAIN, ), + # switch_1 is an undocumented code that behaves identically to control + # It is used by the Kogan Smart Blinds Driver + TuyaCoverEntityDescription( + key=DPCode.SWITCH_1, + name="Blind", + current_position=DPCode.PERCENT_CONTROL, + set_position=DPCode.PERCENT_CONTROL, + device_class=DEVICE_CLASS_BLIND, + ), ), # Garage Door Opener # https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee @@ -183,9 +193,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): if device.function[description.key].type == "Boolean": self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE elif device.function[description.key].type == "Enum": - data_type = EnumTypeData.from_json( - device.status_range[description.key].values - ) + data_type = EnumTypeData.from_json(device.function[description.key].values) if "open" in data_type.range: self._attr_supported_features |= SUPPORT_OPEN if "close" in data_type.range: From 6e3f522d4b40898c7b21c3e0d89a15b081e2cefc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 11:16:56 +0100 Subject: [PATCH 1021/1452] Fix StrEnum backport return type issue (#60610) --- homeassistant/util/enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/enum.py b/homeassistant/util/enum.py index 88df4ba19c3..8bd01fa9ede 100644 --- a/homeassistant/util/enum.py +++ b/homeassistant/util/enum.py @@ -8,7 +8,7 @@ from typing import Any class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: + def __new__(cls, value: str, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def] """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") From 9b9801516b2cb78552ec6c8ba1a281766f3d5dfb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 11:54:06 +0100 Subject: [PATCH 1022/1452] Migrate button device classes to StrEnum (#60611) --- homeassistant/components/button/__init__.py | 25 ++++++++++++++++----- homeassistant/components/esphome/button.py | 13 ++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 621effd5d16..583310f0be9 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +from homeassistant.util.enum import StrEnum from .const import DOMAIN, SERVICE_PRESS @@ -30,12 +31,15 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) -DEVICE_CLASS_RESTART = "restart" -DEVICE_CLASS_UPDATE = "update" -DEVICE_CLASSES = [DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE] +class ButtonDeviceClass(StrEnum): + """Device class for buttons.""" -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + RESTART = "restart" + UPDATE = "update" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(ButtonDeviceClass)) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -70,16 +74,27 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class ButtonEntityDescription(EntityDescription): """A class that describes button entities.""" + device_class: ButtonDeviceClass | None = None + class ButtonEntity(RestoreEntity): """Representation of a Button entity.""" entity_description: ButtonEntityDescription _attr_should_poll = False - _attr_device_class: None = None + _attr_device_class: ButtonDeviceClass | None = None _attr_state: None = None __last_pressed: datetime | None = None + @property + def device_class(self) -> ButtonDeviceClass | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property @final def state(self) -> str | None: diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index 914ddb38da1..5b6f2c153c8 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -1,11 +1,12 @@ """Support for ESPHome buttons.""" from __future__ import annotations +from contextlib import suppress from typing import Any from aioesphomeapi import ButtonInfo, EntityState -from homeassistant.components.button import DEVICE_CLASSES, ButtonEntity +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,11 +33,11 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): """A button implementation for ESPHome.""" @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - if self._static_info.device_class not in DEVICE_CLASSES: - return None - return self._static_info.device_class + def device_class(self) -> ButtonDeviceClass | None: + """Return the class of this entity.""" + with suppress(ValueError): + return ButtonDeviceClass(self._static_info.device_class) + return None @callback def _on_device_update(self) -> None: From 1b8eba0afde7fdad8cc29f855486527cb2e5cfb6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 12:12:08 +0100 Subject: [PATCH 1023/1452] Add button device classes to WLED (#60613) --- homeassistant/components/wled/button.py | 24 +++++------ tests/components/wled/test_button.py | 56 +++++++++++++------------ 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 3e10ccb902f..191242fe0dc 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -1,7 +1,7 @@ """Support for WLED button.""" from __future__ import annotations -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant @@ -23,7 +23,7 @@ async def async_setup_entry( async_add_entities( [ WLEDRestartButton(coordinator), - WLEDUpgradeButton(coordinator), + WLEDUpdateButton(coordinator), ] ) @@ -31,7 +31,7 @@ async def async_setup_entry( class WLEDRestartButton(WLEDEntity, ButtonEntity): """Defines a WLED restart button.""" - _attr_icon = "mdi:restart" + _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -46,21 +46,21 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): await self.coordinator.wled.reset() -class WLEDUpgradeButton(WLEDEntity, ButtonEntity): - """Defines a WLED upgrade button.""" +class WLEDUpdateButton(WLEDEntity, ButtonEntity): + """Defines a WLED update button.""" - _attr_icon = "mdi:cellphone-arrow-down" + _attr_device_class = ButtonDeviceClass.UPDATE _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Upgrade" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_upgrade" + self._attr_name = f"{coordinator.data.info.name} Update" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" @property def available(self) -> bool: - """Return if the entity and an upgrade is available.""" + """Return if the entity and an update is available.""" current = self.coordinator.data.info.version beta = self.coordinator.data.info.version_latest_beta stable = self.coordinator.data.info.version_latest_stable @@ -82,13 +82,13 @@ class WLEDUpgradeButton(WLEDEntity, ButtonEntity): @wled_exception_handler async def async_press(self) -> None: - """Send out a restart command.""" + """Send out a update command.""" current = self.coordinator.data.info.version beta = self.coordinator.data.info.version_latest_beta stable = self.coordinator.data.info.version_latest_stable - # If we already run a pre-release, allow upgrading to a newer - # pre-release or newer stable, otherwise, offer a normal stable upgrades. + # If we already run a pre-release, allow update to a newer + # pre-release or newer stable, otherwise, offer a normal stable updates. version = stable if ( current is not None diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index a4ca9a86506..d6eea403d97 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -5,10 +5,14 @@ from freezegun import freeze_time import pytest from wled import WLEDConnectionError, WLEDError -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + SERVICE_PRESS, + ButtonDeviceClass, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ENTITY_CATEGORY_CONFIG, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -27,8 +31,8 @@ async def test_button_restart( state = hass.states.get("button.wled_rgb_light_restart") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:restart" assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.RESTART entry = entity_registry.async_get("button.wled_rgb_light_restart") assert entry @@ -93,31 +97,31 @@ async def test_button_connection_error( assert "Error communicating with API" in caplog.text -async def test_button_upgrade_stay_stable( +async def test_button_update_stay_stable( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: - """Test the upgrade button. + """Test the update button. - There is both an upgrade for beta and stable available, however, the device - is currently running a stable version. Therefore, the upgrade button should - upgrade the the next stable (even though beta is newer). + There is both an update for beta and stable available, however, the device + is currently running a stable version. Therefore, the update button should + update the the next stable (even though beta is newer). """ entity_registry = er.async_get(hass) - entry = entity_registry.async_get("button.wled_rgb_light_upgrade") + entry = entity_registry.async_get("button.wled_rgb_light_update") assert entry - assert entry.unique_id == "aabbccddeeff_upgrade" + assert entry.unique_id == "aabbccddeeff_update" assert entry.entity_category == ENTITY_CATEGORY_CONFIG - state = hass.states.get("button.wled_rgb_light_upgrade") + state = hass.states.get("button.wled_rgb_light_update") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:cellphone-arrow-down" assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.UPDATE await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_upgrade"}, + {ATTR_ENTITY_ID: "button.wled_rgb_light_update"}, blocking=True, ) await hass.async_block_till_done() @@ -126,19 +130,19 @@ async def test_button_upgrade_stay_stable( @pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) -async def test_button_upgrade_beta_to_stable( +async def test_button_update_beta_to_stable( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: - """Test the upgrade button. + """Test the update button. - There is both an upgrade for beta and stable available the device + There is both an update for beta and stable available the device is currently a beta, however, a newer stable is available. Therefore, the - upgrade button should upgrade to the next stable. + update button should update to the next stable. """ await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgbw_light_upgrade"}, + {ATTR_ENTITY_ID: "button.wled_rgbw_light_update"}, blocking=True, ) await hass.async_block_till_done() @@ -147,18 +151,18 @@ async def test_button_upgrade_beta_to_stable( @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) -async def test_button_upgrade_stay_beta( +async def test_button_update_stay_beta( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: - """Test the upgrade button. + """Test the update button. - There is an upgrade for beta and the device is currently a beta. Therefore, - the upgrade button should upgrade to the next beta. + There is an update for beta and the device is currently a beta. Therefore, + the update button should update to the next beta. """ await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_upgrade"}, + {ATTR_ENTITY_ID: "button.wled_rgb_light_update"}, blocking=True, ) await hass.async_block_till_done() @@ -167,10 +171,10 @@ async def test_button_upgrade_stay_beta( @pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) -async def test_button_no_upgrade_available( +async def test_button_no_update_available( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: - """Test the upgrade button. There is no update available.""" - state = hass.states.get("button.wled_websocket_upgrade") + """Test the update button. There is no update available.""" + state = hass.states.get("button.wled_websocket_update") assert state assert state.state == STATE_UNAVAILABLE From 7469f083fd02f885f660650936a860c585ecedd7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 12:54:06 +0100 Subject: [PATCH 1024/1452] Migrate number mode to StrEnum (#60614) --- homeassistant/components/demo/number.py | 11 ++++------- homeassistant/components/flux_led/number.py | 5 ++--- homeassistant/components/knx/schema.py | 8 ++++---- homeassistant/components/number/__init__.py | 16 ++++++++++++---- homeassistant/components/number/const.py | 9 +++++---- tests/components/demo/test_number.py | 12 +++++------- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index d471bdac85f..2625d8bca05 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,10 +1,7 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations -from typing import Literal - -from homeassistant.components.number import NumberEntity -from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER +from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.helpers.entity import DeviceInfo @@ -21,7 +18,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 42.0, "mdi:volume-high", False, - mode=MODE_SLIDER, + mode=NumberMode.SLIDER, ), DemoNumber( "pwm1", @@ -32,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 0.0, 1.0, 0.01, - MODE_BOX, + NumberMode.BOX, ), DemoNumber( "large_range", @@ -78,7 +75,7 @@ class DemoNumber(NumberEntity): min_value: float | None = None, max_value: float | None = None, step: float | None = None, - mode: Literal["auto", "box", "slider"] = MODE_AUTO, + mode: NumberMode = NumberMode.AUTO, ) -> None: """Initialize the Demo Number entity.""" self._attr_assumed_state = assumed diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 583b8cea0a8..28007181e5c 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -4,8 +4,7 @@ from __future__ import annotations from typing import cast from homeassistant import config_entries -from homeassistant.components.number import NumberEntity -from homeassistant.components.number.const import MODE_SLIDER +from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -47,7 +46,7 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): _attr_min_value = 1 _attr_max_value = 100 _attr_step = 1 - _attr_mode = MODE_SLIDER + _attr_mode = NumberMode.SLIDER _attr_icon = "mdi:speedometer" def __init__( diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 02e4803b163..218a84b2485 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -18,7 +18,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODES from homeassistant.components.cover import DEVICE_CLASSES as COVER_DEVICE_CLASSES -from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER +from homeassistant.components.number import NumberMode from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -786,14 +786,14 @@ class NumberSchema(KNXPlatformSchema): CONF_STEP = "step" DEFAULT_NAME = "KNX Number" - NUMBER_MODES: Final = [MODE_AUTO, MODE_BOX, MODE_SLIDER] - ENTITY_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, - vol.Optional(CONF_MODE, default=MODE_AUTO): vol.In(NUMBER_MODES), + vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce( + NumberMode + ), vol.Required(CONF_TYPE): numeric_type_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 89ad7d8b8c2..7e2f9093b80 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, Literal, final +from typing import Any, final import voluptuous as vol @@ -18,6 +18,7 @@ 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.util.enum import StrEnum from .const import ( ATTR_MAX, @@ -28,7 +29,6 @@ from .const import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DOMAIN, - MODE_AUTO, SERVICE_SET_VALUE, ) @@ -41,6 +41,14 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) +class NumberMode(StrEnum): + """Modes for number entities.""" + + AUTO = "auto" + BOX = "box" + SLIDER = "slider" + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" component = hass.data[DOMAIN] = EntityComponent( @@ -92,7 +100,7 @@ class NumberEntity(Entity): _attr_min_value: float = DEFAULT_MIN_VALUE _attr_state: None = None _attr_step: float - _attr_mode: Literal["auto", "slider", "box"] = MODE_AUTO + _attr_mode: NumberMode = NumberMode.AUTO _attr_value: float @property @@ -128,7 +136,7 @@ class NumberEntity(Entity): return step @property - def mode(self) -> Literal["auto", "slider", "box"]: + def mode(self) -> NumberMode: """Return the mode of the entity.""" return self._attr_mode diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 749463b11e5..50390e7ab81 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -7,10 +7,6 @@ ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" -MODE_AUTO: Final = "auto" -MODE_BOX: Final = "box" -MODE_SLIDER: Final = "slider" - DEFAULT_MIN_VALUE = 0.0 DEFAULT_MAX_VALUE = 100.0 DEFAULT_STEP = 1.0 @@ -18,3 +14,8 @@ DEFAULT_STEP = 1.0 DOMAIN = "number" SERVICE_SET_VALUE = "set_value" + +# MODE_* are deprecated as of 2021.12, use the NumberMode enum instead. +MODE_AUTO: Final = "auto" +MODE_BOX: Final = "box" +MODE_SLIDER: Final = "slider" diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 88e46f5c66d..64f690d9eac 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -3,15 +3,13 @@ import pytest import voluptuous as vol +from homeassistant.components.number import NumberMode from homeassistant.components.number.const import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, ATTR_VALUE, DOMAIN, - MODE_AUTO, - MODE_BOX, - MODE_SLIDER, SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE @@ -42,25 +40,25 @@ def test_default_setup_params(hass): assert state.attributes.get(ATTR_MIN) == 0.0 assert state.attributes.get(ATTR_MAX) == 100.0 assert state.attributes.get(ATTR_STEP) == 1.0 - assert state.attributes.get(ATTR_MODE) == MODE_SLIDER + assert state.attributes.get(ATTR_MODE) == NumberMode.SLIDER state = hass.states.get(ENTITY_PWM) assert state.attributes.get(ATTR_MIN) == 0.0 assert state.attributes.get(ATTR_MAX) == 1.0 assert state.attributes.get(ATTR_STEP) == 0.01 - assert state.attributes.get(ATTR_MODE) == MODE_BOX + assert state.attributes.get(ATTR_MODE) == NumberMode.BOX state = hass.states.get(ENTITY_LARGE_RANGE) assert state.attributes.get(ATTR_MIN) == 1.0 assert state.attributes.get(ATTR_MAX) == 1000.0 assert state.attributes.get(ATTR_STEP) == 1.0 - assert state.attributes.get(ATTR_MODE) == MODE_AUTO + assert state.attributes.get(ATTR_MODE) == NumberMode.AUTO state = hass.states.get(ENTITY_SMALL_RANGE) assert state.attributes.get(ATTR_MIN) == 1.0 assert state.attributes.get(ATTR_MAX) == 255.0 assert state.attributes.get(ATTR_STEP) == 1.0 - assert state.attributes.get(ATTR_MODE) == MODE_AUTO + assert state.attributes.get(ATTR_MODE) == NumberMode.AUTO async def test_set_value_bad_attr(hass): From 2a2a20fcb3e19f1229aae8340286aa0e511bc0ff Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 30 Nov 2021 14:04:24 +0100 Subject: [PATCH 1025/1452] Add mqtt sensor configurable state encoding for sensor and binary_sensor platform (#60447) * Add mqtt sensor state encoding * Make encoding attribute not specific to states * Move encoding attribute to schema base --- homeassistant/components/mqtt/__init__.py | 7 +++- .../components/mqtt/abbreviations.py | 1 + .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/sensor.py | 3 +- homeassistant/components/mqtt/trigger.py | 5 +-- tests/components/mqtt/test_binary_sensor.py | 33 +++++++++++++++++++ tests/components/mqtt/test_sensor.py | 29 ++++++++++++++++ 8 files changed, 76 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b99036efbe3..59f64972f6c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -57,6 +57,7 @@ from .const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_COMMAND_TOPIC, + CONF_ENCODING, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, @@ -65,6 +66,7 @@ from .const import ( DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, + DEFAULT_ENCODING, DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, @@ -200,7 +202,10 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_BASE = {vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA} +SCHEMA_BASE = { + vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, +} MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 9ad5ca4ce1c..c47b512b727 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -46,6 +46,7 @@ ABBREVIATIONS = { "dev_cla": "device_class", "dock_t": "docked_topic", "dock_tpl": "docked_template", + "e": "encoding", "en": "enabled_by_default", "err_t": "error_topic", "err_tpl": "error_template", diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 1928f79032d..3d988079c6f 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -28,7 +28,7 @@ from homeassistant.util import dt as dt_util from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_QOS, CONF_STATE_TOPIC, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -200,6 +200,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): "topic": self._config[CONF_STATE_TOPIC], "msg_callback": state_message_received, "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, } }, ) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c626592b0a3..9c788490ea3 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -13,6 +13,7 @@ CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" CONF_COMMAND_TOPIC = "command_topic" +CONF_ENCODING = "encoding" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" @@ -24,6 +25,7 @@ DATA_MQTT_CONFIG = "mqtt_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True +DEFAULT_ENCODING = "utf-8" DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7ddc8ec467a..72f0b339fe2 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -33,7 +33,7 @@ from homeassistant.util import dt as dt_util from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_QOS, CONF_STATE_TOPIC, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -252,6 +252,7 @@ class MqttSensor(MqttEntity, SensorEntity): "topic": self._config[CONF_STATE_TOPIC], "msg_callback": message_received, "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, } @callback diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index f9c035dea85..db366010bb2 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -10,13 +10,10 @@ from homeassistant.core import HassJob, callback from homeassistant.helpers import config_validation as cv, template from .. import mqtt -from .const import CONF_QOS, CONF_TOPIC +from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING, DEFAULT_QOS # mypy: allow-untyped-defs -CONF_ENCODING = "encoding" -DEFAULT_ENCODING = "utf-8" -DEFAULT_QOS = 0 TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index ba601fd094d..6e1bdd60972 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -373,6 +373,39 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( assert "template output: 'ILLEGAL'" in caplog.text +async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( + hass, mqtt_mock, caplog +): + """Test processing a raw value via MQTT.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "encoding": "", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{%if value|bitwise_and(1)-%}ON{%else%}OFF{%-endif-%}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test-topic", b"\x01") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test-topic", b"\x00") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + async def test_setting_sensor_value_via_mqtt_message_empty_template( hass, mqtt_mock, caplog ): diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 680bafb3b2b..2ebfdde4a52 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -894,6 +894,35 @@ async def test_entity_category(hass, mqtt_mock): async def test_value_template_with_entity_id(hass, mqtt_mock): + """Test processing a raw value via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "encoding": "", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value | bitwise_and(255) }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", b"\xff") + state = hass.states.get("sensor.test") + + assert state.state == "255" + + async_fire_mqtt_message(hass, "test-topic", b"\x01\x10") + state = hass.states.get("sensor.test") + + assert state.state == "16" + + +async def test_value_template_with_raw_data(hass, mqtt_mock): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, From f07e676c823784b815a4ac6150905defd40023bf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 30 Nov 2021 15:01:43 +0100 Subject: [PATCH 1026/1452] Allow template int filter to render from a bytes based integer (#60452) * Allow template int to render bytes * re-triggering tests * Add warning when base !=10 and rendering bytes * re-trigger tests * Re-trigger tests * remove period * Update homeassistant/helpers/template.py Co-authored-by: Erik Montnemery * Fix logger syntax * remove parentheses Co-authored-by: Erik Montnemery --- homeassistant/helpers/template.py | 18 +++++++++++++-- tests/helpers/test_template.py | 37 +++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 62e9f3fab6a..3c7e1cb2952 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1567,8 +1567,15 @@ def forgiving_float_filter(value, default=_SENTINEL): return default -def forgiving_int(value, default=_SENTINEL, base=10): +def forgiving_int(value, default=_SENTINEL, base=10, little_endian=False): """Try to convert value to an int, and warn if it fails.""" + if isinstance(value, bytes) and value: + if base != 10: + _LOGGER.warning( + "Template warning: 'int' got 'bytes' type input, ignoring base=%s parameter", + base, + ) + return convert_to_int(value, little_endian=little_endian) result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: warn_no_default("int", value, value) @@ -1576,8 +1583,15 @@ def forgiving_int(value, default=_SENTINEL, base=10): return result -def forgiving_int_filter(value, default=_SENTINEL, base=10): +def forgiving_int_filter(value, default=_SENTINEL, base=10, little_endian=False): """Try to convert value to an int, and warn if it fails.""" + if isinstance(value, bytes) and value: + if base != 10: + _LOGGER.warning( + "Template warning: 'int' got 'bytes' type input, ignoring base=%s parameter", + base, + ) + return convert_to_int(value, little_endian=little_endian) result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: warn_no_default("int", value, 0) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index bd405acf597..40e390b2070 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -252,7 +252,7 @@ def test_float_filter(hass): assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 -def test_int_filter(hass): +def test_int_filter(hass, caplog): """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ states.sensor.temperature.state | int }}") == 12 @@ -265,8 +265,25 @@ def test_int_filter(hass): assert render(hass, "{{ 'bad' | int(1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 + # Test with bytes based integer + variables = {"value": b"\xde\xad\xbe\xef"} + assert (render(hass, "{{ value | int }}", variables=variables)) == 0xDEADBEEF + assert ( + render(hass, "{{ value | int(little_endian=True) }}", variables=variables) + == 0xEFBEADDE + ) -def test_int_function(hass): + # Test with base and base parameter set + assert ( + render(hass, "{{ value | int(base=16) }}", variables=variables) + ) == 0xDEADBEEF + assert ( + "Template warning: 'int' got 'bytes' type input, ignoring base=16 parameter" + in caplog.text + ) + + +def test_int_function(hass, caplog): """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ int(states.sensor.temperature.state) }}") == 12 @@ -279,6 +296,22 @@ def test_int_function(hass): assert render(hass, "{{ int('bad', 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 + # Test with base and base parameter set + variables = {"value": b"\xde\xad\xbe\xef"} + assert (render(hass, "{{ int(value) }}", variables=variables)) == 0xDEADBEEF + assert ( + render(hass, "{{ int(value, little_endian=True) }}", variables=variables) + == 0xEFBEADDE + ) + + assert ( + render(hass, "{{ int(value, base=16) }}", variables=variables) + ) == 0xDEADBEEF + assert ( + "Template warning: 'int' got 'bytes' type input, ignoring base=16 parameter" + in caplog.text + ) + @pytest.mark.parametrize( "value, expected", From b5a6e03c21f3107e96aca4abe6e291f0c89d9b03 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 15:06:33 +0100 Subject: [PATCH 1027/1452] Fix device class shorthand attr in ButtonEntity (#60622) --- homeassistant/components/button/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 583310f0be9..49249baac61 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -82,7 +82,7 @@ class ButtonEntity(RestoreEntity): entity_description: ButtonEntityDescription _attr_should_poll = False - _attr_device_class: ButtonDeviceClass | None = None + _attr_device_class: ButtonDeviceClass | None _attr_state: None = None __last_pressed: datetime | None = None From 683bb13f50b3df7e4c0f297e22622cc4a99982de Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Dec 2021 03:20:40 +1300 Subject: [PATCH 1028/1452] Support unit of measurement in ESPHome numbers (#60591) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/number.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 680c79057af..025c98def88 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.4.0"], + "requirements": ["aioesphomeapi==10.5.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index c8baa1f112e..c26d0398398 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -52,6 +52,11 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """Return the increment/decrement step.""" return super()._static_info.step + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + return super()._static_info.unit_of_measurement + @esphome_state_property def value(self) -> float | None: """Return the state of the entity.""" diff --git a/requirements_all.txt b/requirements_all.txt index b4a32a3bbe4..d021d8cc687 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.4.0 +aioesphomeapi==10.5.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7ffa92dc67..cc1b51bddb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.4.0 +aioesphomeapi==10.5.0 # homeassistant.components.flo aioflo==2021.11.0 From 2366fbe84699858975a2f9e865d8529eb3070944 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 15:21:26 +0100 Subject: [PATCH 1029/1452] Add button device classes to HomeKit Controller (#60620) --- homeassistant/components/homekit_controller/button.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 19dc69c50a8..b83cc351fd5 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -10,7 +10,11 @@ from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback @@ -35,7 +39,7 @@ BUTTON_ENTITIES: dict[str, HomeKitButtonEntityDescription] = { CharacteristicsTypes.Vendor.HAA_UPDATE: HomeKitButtonEntityDescription( key=CharacteristicsTypes.Vendor.HAA_UPDATE, name="Update", - icon="mdi:update", + device_class=ButtonDeviceClass.UPDATE, entity_category=ENTITY_CATEGORY_CONFIG, write_value="#HAA@trcmd", ), @@ -61,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeKitButton(CharacteristicEntity, ButtonEntity): """Representation of a Button control on a homekit accessory.""" - entity_description = HomeKitButtonEntityDescription + entity_description: HomeKitButtonEntityDescription def __init__( self, From 6be1b0c704439e3d02e9a6db77093d8ca8f7d249 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 15:44:47 +0100 Subject: [PATCH 1030/1452] Add button device classes to Shelly (#60625) --- homeassistant/components/shelly/button.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 46a98e4a649..ec308814fd8 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -5,7 +5,11 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Final, cast -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant @@ -35,14 +39,14 @@ BUTTONS: Final = [ ShellyButtonDescription( key="ota_update", name="OTA Update", - icon="mdi:package-up", + device_class=ButtonDeviceClass.UPDATE, entity_category=ENTITY_CATEGORY_CONFIG, press_action=lambda wrapper: wrapper.async_trigger_ota_update(), ), ShellyButtonDescription( key="ota_update_beta", name="OTA Update Beta", - icon="mdi:flask-outline", + device_class=ButtonDeviceClass.UPDATE, entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_CONFIG, press_action=lambda wrapper: wrapper.async_trigger_ota_update(beta=True), @@ -50,7 +54,7 @@ BUTTONS: Final = [ ShellyButtonDescription( key="reboot", name="Reboot", - icon="mdi:restart", + device_class=ButtonDeviceClass.RESTART, entity_category=ENTITY_CATEGORY_CONFIG, press_action=lambda wrapper: wrapper.device.trigger_reboot(), ), From cd351cf22bf7fbceae0b4340321d0111f3107163 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 30 Nov 2021 15:45:06 +0100 Subject: [PATCH 1031/1452] Fix test naming switch (#60630) --- tests/components/mqtt/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 2ebfdde4a52..3b395c823e9 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -893,7 +893,7 @@ async def test_entity_category(hass, mqtt_mock): await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) -async def test_value_template_with_entity_id(hass, mqtt_mock): +async def test_value_template_with_raw_data(hass, mqtt_mock): """Test processing a raw value via MQTT.""" assert await async_setup_component( hass, @@ -922,7 +922,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): assert state.state == "16" -async def test_value_template_with_raw_data(hass, mqtt_mock): +async def test_value_template_with_entity_id(hass, mqtt_mock): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, From 6f22ffbedd8f9791c88343abdc75254feca487a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 15:47:18 +0100 Subject: [PATCH 1032/1452] Use dataclass properties in enphase_envoy discovery (#60627) Co-authored-by: epenet --- homeassistant/components/enphase_envoy/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 6093a30ca68..0b163e331d6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -104,9 +104,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info[zeroconf.ATTR_PROPERTIES]["serialnum"] + self.serial = discovery_info.properties["serialnum"] await self.async_set_unique_id(self.serial) - self.ip_address = discovery_info[zeroconf.ATTR_HOST] + self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): if ( From 3f22905709dc85cb4997af759402da382bf7a080 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 15:50:47 +0100 Subject: [PATCH 1033/1452] Use dataclass properties in plugwise discovery (#60631) Co-authored-by: epenet --- homeassistant/components/plugwise/config_flow.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index eaf728aa801..a120daf0083 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -109,14 +109,12 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info - _properties = discovery_info[zeroconf.ATTR_PROPERTIES] + _properties = discovery_info.properties # unique_id is needed here, to be able to determine whether the discovered device is known, or not. - unique_id = discovery_info[zeroconf.ATTR_HOSTNAME].split(".")[0] + unique_id = discovery_info.hostname.split(".")[0] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - {CONF_HOST: discovery_info[zeroconf.ATTR_HOST]} - ) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info.host}) if DEFAULT_USERNAME not in unique_id: self._username = STRETCH_USERNAME @@ -125,9 +123,9 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" self.context["title_placeholders"] = { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], + CONF_HOST: discovery_info.host, CONF_NAME: _name, - CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_PORT: discovery_info.port, CONF_USERNAME: self._username, } return await self.async_step_user_gateway() @@ -143,8 +141,8 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input.pop(FLOW_TYPE, None) if self.discovery_info: - user_input[CONF_HOST] = self.discovery_info[zeroconf.ATTR_HOST] - user_input[CONF_PORT] = self.discovery_info[zeroconf.ATTR_PORT] + user_input[CONF_HOST] = self.discovery_info.host + user_input[CONF_PORT] = self.discovery_info.port user_input[CONF_USERNAME] = self._username try: From 0d24862a28313031a452b5e92ee70b7aad66a841 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:06:18 +0100 Subject: [PATCH 1034/1452] Use dataclass properties in homekit_controller discovery (#60626) Co-authored-by: epenet --- .../components/homekit_controller/config_flow.py | 9 ++++----- tests/components/homekit_controller/common.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 557fc3894c7..0728048ec84 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -208,8 +208,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # homekit_python has code to do this, but not in a form we can # easily use, so do the bare minimum ourselves here instead. properties = { - key.lower(): value - for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() + key.lower(): value for (key, value) in discovery_info.properties.items() } if zeroconf.ATTR_PROPERTIES_ID not in properties: @@ -225,7 +224,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # It changes if a device is factory reset. hkid = properties[zeroconf.ATTR_PROPERTIES_ID] model = properties["md"] - name = discovery_info[zeroconf.ATTR_NAME].replace("._hap._tcp.local.", "") + name = discovery_info.name.replace("._hap._tcp.local.", "") status_flags = int(properties["sf"]) paired = not status_flags & 0x01 @@ -243,8 +242,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Set unique-id and error out if it's already configured existing_entry = await self.async_set_unique_id(normalize_hkid(hkid)) updated_ip_port = { - "AccessoryIP": discovery_info[zeroconf.ATTR_HOST], - "AccessoryPort": discovery_info[zeroconf.ATTR_PORT], + "AccessoryIP": discovery_info.host, + "AccessoryPort": discovery_info.port, } # If the device is already paired and known to us we should monitor c# diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index c3c182c8b51..49c63a761c2 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,6 +9,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FakeController +from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, @@ -118,17 +119,19 @@ async def device_config_changed(hass, accessories): accessories_obj.add_accessory(accessory) pairing.accessories = accessories_obj - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": { + discovery_info = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="mock_hostname", + name="TestDevice", + port=8080, + properties={ "md": "TestDevice", "id": "00:00:00:00:00:00", "c#": "2", "sf": "0", }, - } + type="mock_type", + ) # Config Flow will abort and notify us if the discovery event is of # interest - in this case c# has incremented From 16462df4510f53bf8dafe2c5a264ce165c847905 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Nov 2021 16:08:02 +0100 Subject: [PATCH 1035/1452] Add button device classes to MQTT (#60628) --- homeassistant/components/mqtt/button.py | 10 ++++- tests/components/mqtt/test_button.py | 57 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 4006b8bfab9..4b8931375b9 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -6,8 +6,8 @@ import functools import voluptuous as vol from homeassistant.components import button -from homeassistant.components.button import ButtonEntity -from homeassistant.const import CONF_NAME +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -25,6 +25,7 @@ DEFAULT_PAYLOAD_PRESS = "PRESS" PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, @@ -74,6 +75,11 @@ class MqttButton(MqttEntity, ButtonEntity): async def _subscribe_topics(self): """(Re)Subscribe to topics.""" + @property + def device_class(self) -> ButtonDeviceClass | None: + """Return the device class of the sensor.""" + return self._config.get(CONF_DEVICE_CLASS) + async def async_press(self, **kwargs): """Turn the device on. diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 0a1d8af2a0c..5eb92db7767 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -264,3 +264,60 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG ) + + +async def test_invalid_device_class(hass, mqtt_mock): + """Test device_class option with invalid value.""" + assert await async_setup_component( + hass, + button.DOMAIN, + { + button.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "device_class": "foobarnotreal", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.test") + assert state is None + + +async def test_valid_device_class(hass, mqtt_mock): + """Test device_class option with valid values.""" + assert await async_setup_component( + hass, + button.DOMAIN, + { + button.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "command_topic": "test-topic", + "device_class": "update", + }, + { + "platform": "mqtt", + "name": "Test 2", + "command_topic": "test-topic", + "device_class": "restart", + }, + { + "platform": "mqtt", + "name": "Test 3", + "command_topic": "test-topic", + }, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.test_1") + assert state.attributes["device_class"] == button.ButtonDeviceClass.UPDATE + state = hass.states.get("button.test_2") + assert state.attributes["device_class"] == button.ButtonDeviceClass.RESTART + state = hass.states.get("button.test_3") + assert "device_class" not in state.attributes From b8a1899d4873fdc4cea1672c673e9cfcaee9e7ec Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 30 Nov 2021 15:14:49 +0000 Subject: [PATCH 1036/1452] Remove homekit_controller's air quality entity in favor of separate sensor entities (#60480) --- .../homekit_controller/air_quality.py | 113 ------------------ .../components/homekit_controller/const.py | 1 - .../homekit_controller/test_air_quality.py | 100 ---------------- 3 files changed, 214 deletions(-) delete mode 100644 homeassistant/components/homekit_controller/air_quality.py delete mode 100644 tests/components/homekit_controller/test_air_quality.py diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py deleted file mode 100644 index df5a89f179e..00000000000 --- a/homeassistant/components/homekit_controller/air_quality.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Support for HomeKit Controller air quality sensors.""" -import logging - -from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes - -from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.core import callback - -from . import KNOWN_DEVICES, HomeKitEntity - -_LOGGER = logging.getLogger(__name__) - -AIR_QUALITY_TEXT = { - 0: "unknown", - 1: "excellent", - 2: "good", - 3: "fair", - 4: "inferior", - 5: "poor", -} - - -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 [ - CharacteristicsTypes.AIR_QUALITY, - CharacteristicsTypes.DENSITY_PM25, - CharacteristicsTypes.DENSITY_PM10, - CharacteristicsTypes.DENSITY_OZONE, - CharacteristicsTypes.DENSITY_NO2, - CharacteristicsTypes.DENSITY_SO2, - CharacteristicsTypes.DENSITY_VOC, - ] - - @property - def particulate_matter_2_5(self): - """Return the particulate matter 2.5 level.""" - return self.service.value(CharacteristicsTypes.DENSITY_PM25) - - @property - def particulate_matter_10(self): - """Return the particulate matter 10 level.""" - return self.service.value(CharacteristicsTypes.DENSITY_PM10) - - @property - def ozone(self): - """Return the O3 (ozone) level.""" - return self.service.value(CharacteristicsTypes.DENSITY_OZONE) - - @property - def sulphur_dioxide(self): - """Return the SO2 (sulphur dioxide) level.""" - return self.service.value(CharacteristicsTypes.DENSITY_SO2) - - @property - def nitrogen_dioxide(self): - """Return the NO2 (nitrogen dioxide) level.""" - return self.service.value(CharacteristicsTypes.DENSITY_NO2) - - @property - def air_quality_text(self): - """Return the Air Quality Index (AQI).""" - air_quality = self.service.value(CharacteristicsTypes.AIR_QUALITY) - return AIR_QUALITY_TEXT.get(air_quality, "unknown") - - @property - def volatile_organic_compounds(self): - """Return the volatile organic compounds (VOC) level.""" - return self.service.value(CharacteristicsTypes.DENSITY_VOC) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - data = {"air_quality_text": self.air_quality_text} - - if voc := self.volatile_organic_compounds: - data["volatile_organic_compounds"] = voc - - return data - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Homekit air quality sensor.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] - - @callback - def async_add_service(service): - if service.short_type != ServicesTypes.AIR_QUALITY_SENSOR: - return False - info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([HomeAirQualitySensor(conn, info)], True) - return True - - conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 6eb507c7214..eee244c8cf1 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -40,7 +40,6 @@ HOMEKIT_ACCESSORY_DISPATCH = { "leak": "binary_sensor", "fan": "fan", "fanv2": "fan", - "air-quality": "air_quality", "occupancy": "binary_sensor", "television": "media_player", "valve": "switch", diff --git a/tests/components/homekit_controller/test_air_quality.py b/tests/components/homekit_controller/test_air_quality.py deleted file mode 100644 index 2477c6bacfd..00000000000 --- a/tests/components/homekit_controller/test_air_quality.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Basic checks for HomeKit air quality sensor.""" -from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes - -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER -from homeassistant.helpers import entity_registry as er - -from tests.components.homekit_controller.common import setup_test_component - - -def create_air_quality_sensor_service(accessory): - """Define temperature characteristics.""" - service = accessory.add_service(ServicesTypes.AIR_QUALITY_SENSOR) - - cur_state = service.add_char(CharacteristicsTypes.AIR_QUALITY) - cur_state.value = 5 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_OZONE) - cur_state.value = 1111 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_NO2) - cur_state.value = 2222 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_SO2) - cur_state.value = 3333 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_PM25) - cur_state.value = 4444 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_PM10) - cur_state.value = 5555 - - cur_state = service.add_char(CharacteristicsTypes.DENSITY_VOC) - cur_state.value = 6666 - - -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" - - assert state.attributes["air_quality_text"] == "poor" - assert state.attributes["ozone"] == 1111 - assert state.attributes["nitrogen_dioxide"] == 2222 - assert state.attributes["sulphur_dioxide"] == 3333 - assert state.attributes["particulate_matter_2_5"] == 4444 - assert state.attributes["particulate_matter_10"] == 5555 - assert state.attributes["volatile_organic_compounds"] == 6666 - - -async def test_air_quality_sensor_read_state_even_if_air_quality_off(hass, utcnow): - """The air quality entity is disabled by default, the replacement sensors should always be available.""" - await setup_test_component(hass, create_air_quality_sensor_service) - - entity_registry = er.async_get(hass) - - sensors = [ - {"entity_id": "sensor.testdevice_air_quality"}, - { - "entity_id": "sensor.testdevice_pm10_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_pm2_5_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_pm10_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_ozone_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_sulphur_dioxide_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_nitrogen_dioxide_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - { - "entity_id": "sensor.testdevice_volatile_organic_compound_density", - "units": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - ] - - for sensor in sensors: - entry = entity_registry.async_get(sensor["entity_id"]) - assert entry is not None - assert entry.unit_of_measurement == sensor.get("units") From a84b12abe7df6b38110f42cf71b084e82959d4e2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 30 Nov 2021 07:16:00 -0800 Subject: [PATCH 1037/1452] Revert "Add an entity service for saving nest event related snapshots" (#60632) --- homeassistant/components/nest/camera_sdm.py | 46 +----- homeassistant/components/nest/const.py | 2 - homeassistant/components/nest/services.yaml | 28 +--- tests/components/nest/test_camera_sdm.py | 153 +------------------- 4 files changed, 6 insertions(+), 223 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 562b55ba652..5385eb42b26 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable import datetime import logging -import os from pathlib import Path from typing import Any @@ -20,7 +19,6 @@ from google_nest_sdm.device import Device from google_nest_sdm.event import ImageEventBase from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG -import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC @@ -28,13 +26,12 @@ from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN, SERVICE_SNAPSHOT_EVENT +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -67,17 +64,6 @@ async def async_setup_sdm_entry( entities.append(NestCamera(device)) async_add_entities(entities) - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_SNAPSHOT_EVENT, - { - vol.Required("nest_event_id"): cv.string, - vol.Required("filename"): cv.string, - }, - "_async_snapshot_event", - ) - class NestCamera(Camera): """Devices that support cameras.""" @@ -306,33 +292,3 @@ class NestCamera(Camera): except GoogleNestException as err: raise HomeAssistantError(f"Nest API error: {err}") from err return stream.answer_sdp - - async def _async_snapshot_event(self, nest_event_id: str, filename: str) -> None: - """Save media for a Nest event, based on `camera.snapshot`.""" - _LOGGER.debug("Taking snapshot for event id '%s'", nest_event_id) - if not self.hass.config.is_allowed_path(filename): - raise HomeAssistantError("No access to write snapshot '%s'" % filename) - # Fetch media associated with the event - if not (trait := self._device.traits.get(CameraEventImageTrait.NAME)): - raise HomeAssistantError("Camera does not support event image snapshots") - try: - event_image = await trait.generate_image(nest_event_id) - except GoogleNestException as err: - raise HomeAssistantError("Unable to create event snapshot") from err - try: - image = await event_image.contents() - except GoogleNestException as err: - raise HomeAssistantError("Unable to fetch event snapshot") from err - - _LOGGER.debug("Writing event snapshot to '%s'", filename) - - def _write_image() -> None: - """Executor helper to write image.""" - os.makedirs(os.path.dirname(filename), exist_ok=True) - with open(filename, "wb") as img_file: - img_file.write(image) - - try: - await self.hass.async_add_executor_job(_write_image) - except OSError as err: - raise HomeAssistantError("Failed to write snapshot image") from err diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index e98d563c574..a92a48bfd6c 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -22,5 +22,3 @@ SDM_SCOPES = [ ] API_URL = "https://smartdevicemanagement.googleapis.com/v1" OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" - -SERVICE_SNAPSHOT_EVENT = "snapshot_event" diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index d432d2a3859..98aacf60524 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,30 +1,8 @@ # Describes the format for available Nest services -snapshot_event: - name: Take event snapshot - description: Take a snapshot from a camera for an event. - target: - entity: - integration: nest - domain: camera - fields: - nest_event_id: - name: Nest Event Id - description: The nest_event_id from the event to snapshot. Can be populated by an automation trigger for a 'nest_event' with 'data_template'. - required: true - selector: - text: - filename: - name: Filename - description: A filename where the snapshot for the event is written. - required: true - example: "/tmp/snapshot_my_camera.jpg" - selector: - text: - set_away_mode: name: Set away mode - description: Set the away mode for a Nest structure. For Legacy API. + description: Set the away mode for a Nest structure. fields: away_mode: name: Away mode @@ -44,7 +22,7 @@ set_away_mode: set_eta: name: Set estimated time of arrival - description: Set or update the estimated time of arrival window for a Nest structure. For Legacy API. + description: Set or update the estimated time of arrival window for a Nest structure. fields: eta: name: ETA @@ -73,7 +51,7 @@ set_eta: cancel_eta: name: Cancel ETA - description: Cancel an existing estimated time of arrival window for a Nest structure. For Legacy API. + description: Cancel an existing estimated time of arrival window for a Nest structure. fields: trip_id: name: Trip ID diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 07184556819..1ac1b4ca6f9 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -7,8 +7,7 @@ pubsub subscriber. import datetime from http import HTTPStatus -import os -from unittest.mock import mock_open, patch +from unittest.mock import patch import aiohttp from google_nest_sdm.device import Device @@ -22,9 +21,7 @@ from homeassistant.components.camera import ( STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, ) -from homeassistant.components.nest.const import SERVICE_SNAPSHOT_EVENT from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -153,20 +150,6 @@ async def async_get_image(hass, width=None, height=None): ) -async def async_call_service_event_snapshot(hass, filename): - """Call the event snapshot service.""" - return await hass.services.async_call( - DOMAIN, - SERVICE_SNAPSHOT_EVENT, - { - ATTR_ENTITY_ID: "camera.my_camera", - "nest_event_id": "some-event-id", - "filename": filename, - }, - blocking=True, - ) - - async def test_no_devices(hass): """Test configuration that returns no devices.""" await async_setup_camera(hass) @@ -536,7 +519,7 @@ async def test_camera_image_from_last_event(hass, auth): async def test_camera_image_from_event_not_supported(hass, auth): """Test fallback to stream image when event images are not supported.""" - # Create a device that does not support the CameraEventImage trait + # Create a device that does not support the CameraEventImgae trait traits = DEVICE_TRAITS.copy() del traits["sdm.devices.traits.CameraEventImage"] subscriber = await async_setup_camera(hass, traits, auth=auth) @@ -882,135 +865,3 @@ async def test_camera_multiple_streams(hass, auth, hass_ws_client): assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" - - -async def test_service_snapshot_event_image(hass, auth, tmpdir): - """Test calling the snapshot_event service.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True): - assert await async_call_service_event_snapshot(hass, filename) - - assert os.path.exists(filename) - with open(filename, "rb") as f: - contents = f.read() - assert contents == IMAGE_BYTES_FROM_EVENT - - -async def test_service_snapshot_no_access_to_filename(hass, auth, tmpdir): - """Test calling the snapshot_event service with a disallowed file path.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object( - hass.config, "is_allowed_path", return_value=False - ), pytest.raises(HomeAssistantError, match=r"No access.*"): - assert await async_call_service_event_snapshot(hass, filename) - - assert not os.path.exists(filename) - - -async def test_camera_snapshot_from_event_not_supported(hass, auth, tmpdir): - """Test a camera that does not support snapshots.""" - # Create a device that does not support the CameraEventImage trait - traits = DEVICE_TRAITS.copy() - del traits["sdm.devices.traits.CameraEventImage"] - await async_setup_camera(hass, traits, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Camera does not support.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_generate_url_failure(hass, auth, tmpdir): - """Test failure while creating a snapshot url.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Unable to create.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_image_fetch_invalid(hass, auth, tmpdir): - """Test failure when fetching an image snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Unable to fetch.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_image_create_directory(hass, auth, tmpdir): - """Test creating the directory when writing the snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/path/does/not/exist/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True): - assert await async_call_service_event_snapshot(hass, filename) - - assert os.path.exists(filename) - with open(filename, "rb") as f: - contents = f.read() - assert contents == IMAGE_BYTES_FROM_EVENT - - -async def test_service_snapshot_event_write_failure(hass, auth, tmpdir): - """Test a failure when writing the snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), patch( - "homeassistant.components.nest.camera_sdm.open", mock_open(), create=True - ) as mocked_open, pytest.raises(HomeAssistantError, match=r"Failed to write.*"): - mocked_open.side_effect = IOError() - assert await async_call_service_event_snapshot(hass, filename) - - assert not os.path.exists(filename) From ffb9b4cd2d3138438227832ebbd3544162f59215 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:16:30 +0100 Subject: [PATCH 1038/1452] Use dataclass properties in hunterdouglas discovery (#60605) --- .../hunterdouglas_powerview/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index d2c78227fef..e0ebef7abbb 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -88,16 +88,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle DHCP discovery.""" - self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] - self.discovered_name = discovery_info[dhcp.HOSTNAME] + self.discovered_ip = discovery_info.ip + self.discovered_name = discovery_info.hostname return await self.async_step_discovery_confirm() async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self.discovered_ip = discovery_info[zeroconf.ATTR_HOST] - name = discovery_info[zeroconf.ATTR_NAME] + self.discovered_ip = discovery_info.host + name = discovery_info.name if name.endswith(POWERVIEW_SUFFIX): name = name[: -len(POWERVIEW_SUFFIX)] self.discovered_name = name @@ -107,8 +107,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle HomeKit discovery.""" - self.discovered_ip = discovery_info[zeroconf.ATTR_HOST] - name = discovery_info[zeroconf.ATTR_NAME] + self.discovered_ip = discovery_info.host + name = discovery_info.name if name.endswith(HAP_SUFFIX): name = name[: -len(HAP_SUFFIX)] self.discovered_name = name From ad75c217ceebd9543e1ab76604d82dc50f20c392 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:25:40 +0100 Subject: [PATCH 1039/1452] Use dataclass properties in kodi discovery (#60634) Co-authored-by: epenet --- homeassistant/components/kodi/config_flow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 6e7ff6bb2a4..461df3b09e6 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -104,13 +104,13 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - self._host = discovery_info[zeroconf.ATTR_HOST] - self._port = int(discovery_info[zeroconf.ATTR_PORT]) - self._name = discovery_info[zeroconf.ATTR_HOSTNAME][: -len(".local.")] - if not (uuid := discovery_info[zeroconf.ATTR_PROPERTIES].get("uuid")): + self._host = discovery_info.host + self._port = int(discovery_info.port) + self._name = discovery_info.hostname[: -len(".local.")] + if not (uuid := discovery_info.properties.get("uuid")): return self.async_abort(reason="no_uuid") - self._discovery_name = discovery_info[zeroconf.ATTR_NAME] + self._discovery_name = discovery_info.name await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured( From a90ef488a1e619cc790a1180268cbb2801fe2f15 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:26:02 +0100 Subject: [PATCH 1040/1452] Add return type annotation to StrEnum (#60624) --- homeassistant/util/enum.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/enum.py b/homeassistant/util/enum.py index 8bd01fa9ede..2ebd1678138 100644 --- a/homeassistant/util/enum.py +++ b/homeassistant/util/enum.py @@ -2,13 +2,15 @@ from __future__ import annotations from enum import Enum -from typing import Any +from typing import Any, TypeVar + +T = TypeVar("T", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - def __new__(cls, value: str, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def] + def __new__(cls: type[T], value: str, *args: Any, **kwargs: Any) -> T: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") From 30bb2c82c67660ebbeec8287bf9db0b7829a91e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:28:02 +0100 Subject: [PATCH 1041/1452] Use dataclass properties in freebox discovery (#60635) Co-authored-by: epenet --- homeassistant/components/freebox/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 76b99dc31d2..f0a7801823e 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -111,7 +111,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Initialize flow from zeroconf.""" - zeroconf_properties = discovery_info[zeroconf.ATTR_PROPERTIES] + zeroconf_properties = discovery_info.properties host = zeroconf_properties["api_domain"] port = zeroconf_properties["https_port"] return await self.async_step_user({CONF_HOST: host, CONF_PORT: port}) From 601ad8f71aa98330692b8085bcd09079eedccf87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:28:49 +0100 Subject: [PATCH 1042/1452] Use dataclass properties in xiaomi_aqara discovery (#60636) Co-authored-by: epenet --- homeassistant/components/xiaomi_aqara/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 2b486cb87ad..f9f8a761321 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -150,9 +150,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - name = discovery_info[zeroconf.ATTR_NAME] - self.host = discovery_info[zeroconf.ATTR_HOST] - mac_address = discovery_info[zeroconf.ATTR_PROPERTIES].get("mac") + name = discovery_info.name + self.host = discovery_info.host + mac_address = discovery_info.properties.get("mac") if not name or not self.host or not mac_address: return self.async_abort(reason="not_xiaomi_aqara") From d75785d7016ee55a49a8b21893c04ded805162e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:36:35 +0100 Subject: [PATCH 1043/1452] Use dataclass properties in hue discovery (#60598) Co-authored-by: epenet --- homeassistant/components/hue/config_flow.py | 23 +++++++++++++-------- tests/components/hue/test_config_flow.py | 20 ++++++++++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 8cb25e1acfe..9d4bc87889d 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -194,26 +194,31 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """ # Filter out non-Hue bridges #1 if ( - discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) + discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) not in HUE_MANUFACTURERURL ): return self.async_abort(reason="not_hue_bridge") # Filter out non-Hue bridges #2 if any( - name in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") + name in discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") for name in HUE_IGNORED_BRIDGE_NAMES ): return self.async_abort(reason="not_hue_bridge") if ( - not discovery_info.get(ssdp.ATTR_SSDP_LOCATION) - or ssdp.ATTR_UPNP_SERIAL not in discovery_info + not discovery_info.ssdp_location + or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp ): return self.async_abort(reason="not_hue_bridge") - host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) + url = urlparse(discovery_info.ssdp_location) + if not url.hostname: + return self.async_abort(reason="not_hue_bridge") + + bridge = await self._get_bridge( + url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + ) await self.async_set_unique_id(bridge.id) self._abort_if_unique_id_configured( @@ -232,8 +237,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host is already configured and delegate to the import step if not. """ bridge = await self._get_bridge( - discovery_info[zeroconf.ATTR_HOST], - discovery_info[zeroconf.ATTR_PROPERTIES]["bridgeid"], + discovery_info.host, + discovery_info.properties["bridgeid"], ) await self.async_set_unique_id(bridge.id) @@ -253,7 +258,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): as the unique identifier. Therefore, this method uses discovery without a unique ID. """ - self.bridge = await self._get_bridge(discovery_info[zeroconf.ATTR_HOST]) + self.bridge = await self._get_bridge(discovery_info.host) await self._async_handle_discovery_without_unique_id() return await self.async_step_link() diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 079c7db7d8b..ca5be5e28e9 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -423,6 +423,26 @@ async def test_bridge_ssdp_missing_serial(hass): assert result["reason"] == "not_hue_bridge" +async def test_bridge_ssdp_invalid_location(hass): + """Test if discovery info is a serial attribute.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http:///", + upnp={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" + + async def test_bridge_ssdp_espalexa(hass): """Test if discovery info is from an Espalexa based device.""" result = await hass.config_entries.flow.async_init( From 0a1f73e3f7feeff6590cc3038db612195d3dcc27 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 30 Nov 2021 15:37:58 +0000 Subject: [PATCH 1044/1452] Correct Temper USB sensor IoT class (#60619) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- homeassistant/components/temper/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index d80c44f8a87..0443987a87b 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": ["temperusb==1.5.3"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_polling" } From 071385e8d23ea78fa12f08ec3770e72a626c65e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:44:11 +0100 Subject: [PATCH 1045/1452] Use dataclass properties in tado discovery (#60641) Co-authored-by: epenet --- homeassistant/components/tado/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 779d330992e..e2a67c21b5d 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -88,8 +88,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle HomeKit discovery.""" self._async_abort_entries_match() properties = { - key.lower(): value - for (key, value) in discovery_info[zeroconf.ATTR_PROPERTIES].items() + key.lower(): value for (key, value) in discovery_info.properties.items() } await self.async_set_unique_id(properties[zeroconf.ATTR_PROPERTIES_ID]) self._abort_if_unique_id_configured() From ec923b877cdf420d3ee51e9cae9d8c334bcce95b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:02:24 +0100 Subject: [PATCH 1046/1452] Use dataclass properties in rachio discovery (#60637) Co-authored-by: epenet --- homeassistant/components/rachio/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 73712135785..9e93dba065e 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -86,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle HomeKit discovery.""" self._async_abort_entries_match() await self.async_set_unique_id( - discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) return await self.async_step_user() From 7295ab10aed9b334ecb9f3b9c4a58ab8b83d7ee3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:03:21 +0100 Subject: [PATCH 1047/1452] Use dataclass properties in doorbird discovery (#60639) Co-authored-by: epenet --- homeassistant/components/doorbird/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index c1345b16f91..31ddd1f6193 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -96,8 +96,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a discovered doorbird device.""" - macaddress = discovery_info[zeroconf.ATTR_PROPERTIES]["macaddress"] - host = discovery_info[zeroconf.ATTR_HOST] + macaddress = discovery_info.properties["macaddress"] + host = discovery_info.host if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") @@ -113,7 +113,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") chop_ending = "._axis-video._tcp.local." - friendly_hostname = discovery_info[zeroconf.ATTR_NAME] + friendly_hostname = discovery_info.name if friendly_hostname.endswith(chop_ending): friendly_hostname = friendly_hostname[: -len(chop_ending)] From 8a9f197918365c2088e338bcbdcf75ce257c256a Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Tue, 30 Nov 2021 09:04:24 -0700 Subject: [PATCH 1048/1452] Binary sensor platform for the Balboa Spa (#60409) --- .../components/balboa/binary_sensor.py | 61 +++++++++++++++ homeassistant/components/balboa/const.py | 2 +- tests/components/balboa/test_binary_sensor.py | 76 +++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/balboa/binary_sensor.py create mode 100644 tests/components/balboa/test_binary_sensor.py diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py new file mode 100644 index 00000000000..133ed2da9f4 --- /dev/null +++ b/homeassistant/components/balboa/binary_sensor.py @@ -0,0 +1,61 @@ +"""Support for Balboa Spa binary sensors.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOVING, + BinarySensorEntity, +) + +from .const import CIRC_PUMP, DOMAIN, FILTER +from .entity import BalboaEntity + +FILTER_STATES = [ + [False, False], # self.FILTER_OFF + [True, False], # self.FILTER_1 + [False, True], # self.FILTER_2 + [True, True], # self.FILTER_1_2 +] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the spa's binary sensors.""" + spa = hass.data[DOMAIN][entry.entry_id] + entities = [ + BalboaSpaFilter(hass, entry, spa, FILTER, index) for index in range(1, 3) + ] + if spa.have_circ_pump(): + entities.append(BalboaSpaCircPump(hass, entry, spa, CIRC_PUMP)) + + async_add_entities(entities) + + +class BalboaSpaBinarySensor(BalboaEntity, BinarySensorEntity): + """Representation of a Balboa Spa binary sensor entity.""" + + _attr_device_class = DEVICE_CLASS_MOVING + + +class BalboaSpaCircPump(BalboaSpaBinarySensor): + """Representation of a Balboa Spa circulation pump.""" + + @property + def is_on(self) -> bool: + """Return true if the filter is on.""" + return self._client.get_circ_pump() + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:water-pump" if self.is_on else "mdi:water-pump-off" + + +class BalboaSpaFilter(BalboaSpaBinarySensor): + """Representation of a Balboa Spa Filter.""" + + @property + def is_on(self) -> bool: + """Return true if the filter is on.""" + return FILTER_STATES[self._client.get_filtermode()][self._num - 1] + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:sync" if self.is_on else "mdi:sync-off" diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index 121c6be3bfa..59ca0041bf6 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -21,7 +21,7 @@ CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] CONF_SYNC_TIME = "sync_time" DEFAULT_SYNC_TIME = False FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] -PLATFORMS = ["climate"] +PLATFORMS = ["binary_sensor", "climate"] AUX = "Aux" CIRC_PUMP = "Circ Pump" diff --git a/tests/components/balboa/test_binary_sensor.py b/tests/components/balboa/test_binary_sensor.py new file mode 100644 index 00000000000..748801b7b8f --- /dev/null +++ b/tests/components/balboa/test_binary_sensor.py @@ -0,0 +1,76 @@ +"""Tests of the climate entity of the balboa integration.""" + +from unittest.mock import patch + +from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN, SIGNAL_UPDATE +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from . import init_integration_mocked + +ENTITY_BINARY_SENSOR = "binary_sensor.fakespa_" + +FILTER_MAP = [ + [STATE_OFF, STATE_OFF], + [STATE_ON, STATE_OFF], + [STATE_OFF, STATE_ON], + [STATE_ON, STATE_ON], +] + + +async def test_filters(hass: HomeAssistant): + """Test spa filters.""" + + config_entry = await _setup_binary_sensor_test(hass) + + for filter_mode in range(4): + for spa_filter in range(1, 3): + state = await _patch_filter(hass, config_entry, filter_mode, spa_filter) + assert state.state == FILTER_MAP[filter_mode][spa_filter - 1] + + +async def test_circ_pump(hass: HomeAssistant): + """Test spa circ pump.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.have_circ_pump", + return_value=True, + ): + config_entry = await _setup_binary_sensor_test(hass) + + state = await _patch_circ_pump(hass, config_entry, True) + assert state.state == STATE_ON + state = await _patch_circ_pump(hass, config_entry, False) + assert state.state == STATE_OFF + + +async def _patch_circ_pump(hass, config_entry, pump_state): + """Patch the circ pump state.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_circ_pump", + return_value=pump_state, + ): + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + return hass.states.get(f"{ENTITY_BINARY_SENSOR}circ_pump") + + +async def _patch_filter(hass, config_entry, filter_mode, num): + """Patch the filter state.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi.get_filtermode", + return_value=filter_mode, + ): + async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await hass.async_block_till_done() + return hass.states.get(f"{ENTITY_BINARY_SENSOR}filter{num}") + + +async def _setup_binary_sensor_test(hass): + """Prepare the test.""" + config_entry = await init_integration_mocked(hass) + await async_setup_component(hass, BALBOA_DOMAIN, config_entry) + await hass.async_block_till_done() + + return config_entry From be89c07cac5875ba75fb2e88ec996ade9fa3dbd8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:05:50 +0100 Subject: [PATCH 1049/1452] Use dataclass properties in nanoleaf discovery (#60580) Co-authored-by: epenet --- homeassistant/components/nanoleaf/config_flow.py | 14 ++++++-------- tests/components/nanoleaf/test_config_flow.py | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index e54d0646d21..6ae70b32d8e 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -106,20 +106,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle Nanoleaf Homekit and Zeroconf discovery.""" return await self._async_discovery_handler( - discovery_info[zeroconf.ATTR_HOST], - discovery_info[zeroconf.ATTR_NAME].replace( - f".{discovery_info[zeroconf.ATTR_TYPE]}", "" - ), - discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID], + discovery_info.host, + discovery_info.name.replace(f".{discovery_info.type}", ""), + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID], ) async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle Nanoleaf SSDP discovery.""" _LOGGER.debug("SSDP discovered: %s", discovery_info) return await self._async_discovery_handler( - discovery_info["_host"], - discovery_info["nl-devicename"], - discovery_info["nl-deviceid"], + discovery_info.ssdp_headers["_host"], + discovery_info.ssdp_headers["nl-devicename"], + discovery_info.ssdp_headers["nl-deviceid"], ) async def _async_discovery_handler( diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 3b2cd3777cb..4e4a48e9bfe 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -465,7 +465,8 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - upnp={ + upnp={}, + ssdp_headers={ "_host": TEST_HOST, "nl-devicename": TEST_NAME, "nl-deviceid": TEST_DEVICE_ID, From f444dd6d863a21ea55fa4a3062a92150926dce26 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:06:52 +0100 Subject: [PATCH 1050/1452] Use dataclass properties in sonos discovery (#60633) Co-authored-by: epenet --- homeassistant/components/sonos/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 59788d17a92..e6d2bb337b8 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,5 +1,5 @@ """Config flow for SONOS.""" -from typing import cast +import dataclasses import soco @@ -30,13 +30,13 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf.""" - hostname = discovery_info[zeroconf.ATTR_HOSTNAME] + hostname = discovery_info.hostname if hostname is None or not hostname.lower().startswith("sonos"): return self.async_abort(reason="not_sonos_device") await self.async_set_unique_id(self._domain, raise_on_progress=False) - host = discovery_info[zeroconf.ATTR_HOST] - mdns_name = discovery_info[zeroconf.ATTR_NAME] - properties = discovery_info[zeroconf.ATTR_PROPERTIES] + host = discovery_info.host + mdns_name = discovery_info.name + properties = discovery_info.properties boot_seqnum = properties.get("bootseq") model = properties.get("model") uid = hostname_to_uid(hostname) @@ -44,7 +44,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): discovery_manager.async_discovered_player( "Zeroconf", properties, host, uid, boot_seqnum, model, mdns_name ) - return await self.async_step_discovery(cast(dict, discovery_info)) + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) config_entries.HANDLERS.register(DOMAIN)(SonosDiscoveryFlowHandler) From 97c04d2e6753b7c5c2963999605d963c190eb2d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:15:23 +0100 Subject: [PATCH 1051/1452] Use dataclass properties in gogogate2 discovery (#60607) Co-authored-by: epenet --- homeassistant/components/gogogate2/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index e71046f7974..e97b62102c4 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -40,16 +40,16 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle homekit discovery.""" await self.async_set_unique_id( - discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) - return await self._async_discovery_handler(discovery_info[zeroconf.ATTR_HOST]) + return await self._async_discovery_handler(discovery_info.host) async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle dhcp discovery.""" - await self.async_set_unique_id(discovery_info[dhcp.MAC_ADDRESS]) - return await self._async_discovery_handler(discovery_info[dhcp.IP_ADDRESS]) + await self.async_set_unique_id(discovery_info.macaddress) + return await self._async_discovery_handler(discovery_info.ip) async def _async_discovery_handler(self, ip_address): """Start the user flow from any discovery.""" From de9e48174f921a84408d9fb0d48e59a7d0693336 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 30 Nov 2021 10:35:29 -0600 Subject: [PATCH 1052/1452] Change unnecessary Sonos coroutine to callback (#60643) --- homeassistant/components/sonos/number.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 372a89edda0..574eba95137 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.number import NumberEntity from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SONOS_CREATE_LEVELS @@ -16,7 +17,8 @@ LEVEL_TYPES = ("bass", "treble") async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sonos number platform from a config entry.""" - async def _async_create_entities(speaker: SonosSpeaker) -> None: + @callback + def _async_create_entities(speaker: SonosSpeaker) -> None: entities = [] for level_type in LEVEL_TYPES: entities.append(SonosLevelEntity(speaker, level_type)) From 19b4cc7119f0a79d68056bfd02bcec2bca7e7c1d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 30 Nov 2021 19:14:51 +0100 Subject: [PATCH 1053/1452] Hue handle device update (#60612) --- homeassistant/components/hue/scene.py | 18 +++++++++++++--- homeassistant/components/hue/v2/entity.py | 25 +++++++++++++++++++---- tests/components/hue/const.py | 2 +- tests/components/hue/test_light_v2.py | 11 ++-------- tests/components/hue/test_scene.py | 13 ++++++++++++ 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 55807ad0f2f..7335d2a048e 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -60,6 +60,19 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): super().__init__(bridge, controller, resource) self.resource = resource self.controller = controller + self.group = self.controller.get_group(self.resource.id) + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + await super().async_added_to_hass() + # Add value_changed callback for group to catch name changes. + self.async_on_remove( + self.bridge.api.groups.subscribe( + self._handle_event, + self.group.id, + (EventType.RESOURCE_UPDATED), + ) + ) @property def name(self) -> str: @@ -96,7 +109,6 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" - group = self.controller.get_group(self.resource.id) brightness = None if palette := self.resource.palette: if palette.dimming: @@ -108,8 +120,8 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): brightness = action.action.dimming.brightness break return { - "group_name": group.metadata.name, - "group_type": group.type.value, + "group_name": self.group.metadata.name, + "group_type": self.group.type.value, "name": self.resource.metadata.name, "speed": self.resource.speed, "brightness": brightness, diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 9bb81c16fa5..368a6cfe9d0 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -56,11 +56,11 @@ class HueBaseEntity(Entity): # creating a pretty name for device-less entities (e.g. groups/scenes) # should be handled in the platform instead return self.resource.type.value - dev_name = self.device.metadata.name - # if resource is a light, use the device name + # if resource is a light, use the name from metadata if self.resource.type == ResourceTypes.LIGHT: - return dev_name + return self.resource.name # for sensors etc, use devicename + pretty name of type + dev_name = self.device.metadata.name type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) @@ -76,6 +76,23 @@ class HueBaseEntity(Entity): (EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED), ) ) + # also subscribe to device update event to catch devicer changes (e.g. name) + if self.device is None: + return + self.async_on_remove( + self.bridge.api.devices.subscribe( + self._handle_event, + self.device.id, + EventType.RESOURCE_UPDATED, + ) + ) + # subscribe to zigbee_connectivity to catch availability changes + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + self.bridge.api.sensors.zigbee_connectivity.subscribe( + self._handle_event, + zigbee.id, + EventType.RESOURCE_UPDATED, + ) @property def available(self) -> bool: @@ -98,7 +115,7 @@ class HueBaseEntity(Entity): @callback def _handle_event(self, event_type: EventType, resource: CLIPResource) -> None: - """Handle status event for this resource.""" + """Handle status event for this resource (or it's parent).""" if event_type == EventType.RESOURCE_DELETED and resource.id == self.resource.id: self.logger.debug("Received delete for %s", self.entity_id) # non-device bound entities like groups and scenes need to be removed here diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 84b342c73be..03b2f1947cf 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -32,7 +32,7 @@ FAKE_LIGHT = { }, "id": "fake_light_id_1", "id_v1": "/lights/1", - "metadata": {"archetype": "unknown", "name": "Hue fake light 1"}, + "metadata": {"archetype": "unknown", "name": "Hue fake light"}, "mode": "normal", "on": {"on": False}, "owner": {"rid": "fake_device_id_1", "rtype": "device"}, diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index e608ef00e26..7843cab1574 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -173,7 +173,7 @@ async def test_light_added(hass, mock_bridge_v2): await setup_platform(hass, mock_bridge_v2, "light") - test_entity_id = "light.hue_mocked_device" + test_entity_id = "light.hue_fake_light" # verify entity does not exist before we start assert hass.states.get(test_entity_id) is None @@ -186,7 +186,7 @@ async def test_light_added(hass, mock_bridge_v2): test_entity = hass.states.get(test_entity_id) assert test_entity is not None assert test_entity.state == "off" - assert test_entity.attributes["friendly_name"] == FAKE_DEVICE["metadata"]["name"] + assert test_entity.attributes["friendly_name"] == FAKE_LIGHT["metadata"]["name"] async def test_light_availability(hass, mock_bridge_v2, v2_resources_test_data): @@ -212,13 +212,6 @@ async def test_light_availability(hass, mock_bridge_v2, v2_resources_test_data): "type": "zigbee_connectivity", }, ) - mock_bridge_v2.api.emit_event( - "update", - { - "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "type": "light", - }, - ) await hass.async_block_till_done() # the entity should now be available only when zigbee is connected diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 15684eb7e56..0f3d6255e86 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -118,6 +118,19 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity is not None assert test_entity.attributes["brightness"] == 35.0 + # test entity name changes on group name change + mock_bridge_v2.api.emit_event( + "update", + { + "type": "room", + "id": "6ddc9066-7e7d-4a03-a773-c73937968296", + "metadata": {"name": "Test Room 2"}, + }, + ) + await hass.async_block_till_done() + test_entity = hass.states.get(test_entity_id) + assert test_entity.name == "Test Room 2 - Mocked Scene" + # test delete mock_bridge_v2.api.emit_event("delete", updated_resource) await hass.async_block_till_done() From 153f15c93b3f0eed6126d832d1437bd312689d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 30 Nov 2021 19:18:14 +0100 Subject: [PATCH 1054/1452] Remove running binary_sensor for HAOS (#60597) --- homeassistant/components/hassio/binary_sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 5078e11c26e..b30c2d1c7f5 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -31,13 +31,16 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): target: str | None = None -ENTITY_DESCRIPTIONS = ( +COMMON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( device_class=DEVICE_CLASS_UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, name="Update Available", ), +) + +ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( HassioBinarySensorEntityDescription( device_class=DEVICE_CLASS_RUNNING, entity_registry_enabled_default=False, @@ -58,7 +61,7 @@ async def async_setup_entry( entities = [] - for entity_description in ENTITY_DESCRIPTIONS: + for entity_description in ADDON_ENTITY_DESCRIPTIONS: for addon in coordinator.data[DATA_KEY_ADDONS].values(): entities.append( HassioAddonBinarySensor( @@ -68,7 +71,8 @@ async def async_setup_entry( ) ) - if coordinator.is_hass_os: + if coordinator.is_hass_os: + for entity_description in COMMON_ENTITY_DESCRIPTIONS: entities.append( HassioOSBinarySensor( coordinator=coordinator, From 11b81ef88bc1432bb5939cbc35db00454fb8e279 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 30 Nov 2021 19:53:41 +0100 Subject: [PATCH 1055/1452] Config flow for trafikverket_weatherstation (#60078) * First commit * Added tests * Add requirements for test * Correction requirements tests * Add init to untested files * Fix review comments * Resolve last items from review * Add sync_abort_entries_match in import flow --- .coveragerc | 1 + .../trafikverket_weatherstation/__init__.py | 38 ++++++++++ .../config_flow.py | 70 +++++++++++++++++ .../trafikverket_weatherstation/const.py | 8 ++ .../trafikverket_weatherstation/manifest.json | 1 + .../trafikverket_weatherstation/sensor.py | 49 +++++++++--- .../trafikverket_weatherstation/strings.json | 17 +++++ .../translations/en.json | 17 +++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 4 + .../trafikverket_weatherstation/__init__.py | 1 + .../test_config_flow.py | 76 +++++++++++++++++++ 12 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/trafikverket_weatherstation/config_flow.py create mode 100644 homeassistant/components/trafikverket_weatherstation/const.py create mode 100644 homeassistant/components/trafikverket_weatherstation/strings.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/en.json create mode 100644 tests/components/trafikverket_weatherstation/__init__.py create mode 100644 tests/components/trafikverket_weatherstation/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index beb7719db44..6d042b3f46e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1136,6 +1136,7 @@ omit = homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py homeassistant/components/trafikverket_train/sensor.py + homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index 7feac4aad27..535ef304557 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -1 +1,39 @@ """The trafikverket_weatherstation component.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Trafikverket Weatherstation from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + title = entry.title + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + _LOGGER.debug("Loaded entry for %s", title) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Trafikverket Weatherstation config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + title = entry.title + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + _LOGGER.debug("Unloaded entry for %s", title) + return unload_ok + + return False diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py new file mode 100644 index 00000000000..44b8b124264 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -0,0 +1,70 @@ +"""Adds config flow for Trafikverket Weather integration.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +import homeassistant.helpers.config_validation as cv + +from .const import CONF_STATION, DOMAIN +from .sensor import SENSOR_TYPES + +SENSOR_LIST: dict[str, str | None] = { + description.key: description.name for (description) in SENSOR_TYPES +} + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): cv.multi_select( + SENSOR_LIST + ), + } +) + + +class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Trafikverket Weatherstation integration.""" + + VERSION = 1 + + entry: config_entries.ConfigEntry + + async def async_step_import(self, config: dict): + """Import a configuration from config.yaml.""" + + self.context.update( + {"title_placeholders": {CONF_NAME: f"YAML import {DOMAIN}"}} + ) + + self._async_abort_entries_match({CONF_NAME: config[CONF_NAME]}) + return await self.async_step_user(user_input=config) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + name = user_input[CONF_NAME] + api_key = user_input[CONF_API_KEY] + station = user_input[CONF_STATION] + conditions = user_input[CONF_MONITORED_CONDITIONS] + + return self.async_create_entry( + title=name, + data={ + CONF_NAME: name, + CONF_API_KEY: api_key, + CONF_STATION: station, + CONF_MONITORED_CONDITIONS: conditions, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/trafikverket_weatherstation/const.py b/homeassistant/components/trafikverket_weatherstation/const.py new file mode 100644 index 00000000000..bfda5782084 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/const.py @@ -0,0 +1,8 @@ +"""Adds constants for Trafikverket Weather integration.""" + +DOMAIN = "trafikverket_weatherstation" +CONF_STATION = "station" +PLATFORMS = ["sensor"] +ATTRIBUTION = "Data provided by Trafikverket" +ATTR_MEASURE_TIME = "measure_time" +ATTR_ACTIVE = "active" diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 6e123983e8b..202d2683d2d 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "requirements": ["pytrafikverket==0.1.6.2"], "codeowners": ["@endor-force"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 5fe3c462a56..32703aef4cc 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -28,18 +29,20 @@ from homeassistant.const import ( SPEED_METERS_PER_SECOND, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + ConfigType, + DiscoveryInfoType, +) from homeassistant.util import Throttle +from .const import ATTR_ACTIVE, ATTR_MEASURE_TIME, ATTRIBUTION, CONF_STATION, DOMAIN + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by Trafikverket" -ATTR_MEASURE_TIME = "measure_time" -ATTR_ACTIVE = "active" - -CONF_STATION = "station" - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SCAN_INTERVAL = timedelta(seconds=300) @@ -144,18 +147,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Trafikverket sensor platform.""" +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: + """Import Trafikverket Weather configuration from YAML.""" + _LOGGER.warning( + # Config flow added in Home Assistant Core 2021.12, remove import flow in 2022.4 + "Loading Trafikverket Weather 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, + ) + ) - sensor_name = config[CONF_NAME] - sensor_api = config[CONF_API_KEY] - sensor_station = config[CONF_STATION] + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Trafikverket sensor entry.""" + + sensor_name = entry.data[CONF_NAME] + sensor_api = entry.data[CONF_API_KEY] + sensor_station = entry.data[CONF_STATION] web_session = async_get_clientsession(hass) weather_api = TrafikverketWeather(web_session, sensor_api) - monitored_conditions = config[CONF_MONITORED_CONDITIONS] + monitored_conditions = entry.data[CONF_MONITORED_CONDITIONS] entities = [ TrafikverketWeatherStation( weather_api, sensor_name, sensor_station, description diff --git a/homeassistant/components/trafikverket_weatherstation/strings.json b/homeassistant/components/trafikverket_weatherstation/strings.json new file mode 100644 index 00000000000..7fa8071e389 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::username%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "station": "Station", + "conditions": "Monitored conditions" + } + } + } + } + } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/en.json b/homeassistant/components/trafikverket_weatherstation/translations/en.json new file mode 100644 index 00000000000..fd32bb80ab3 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "step": { + "user": { + "data": { + "name": "Name", + "api_key": "API Key", + "station": "Station", + "conditions": "Monitored conditions" + } + } + } + } + } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b8ba8c4aade..79e65b96234 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -307,6 +307,7 @@ FLOWS = [ "traccar", "tractive", "tradfri", + "trafikverket_weatherstation", "transmission", "tuya", "twentemilieu", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc1b51bddb2..26a2823334c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1180,6 +1180,10 @@ pytraccar==0.10.0 # homeassistant.components.tradfri pytradfri[async]==7.2.0 +# homeassistant.components.trafikverket_train +# homeassistant.components.trafikverket_weatherstation +pytrafikverket==0.1.6.2 + # homeassistant.components.usb pyudev==0.22.0 diff --git a/tests/components/trafikverket_weatherstation/__init__.py b/tests/components/trafikverket_weatherstation/__init__.py new file mode 100644 index 00000000000..836ee919195 --- /dev/null +++ b/tests/components/trafikverket_weatherstation/__init__.py @@ -0,0 +1 @@ +"""Tests for the Trafikverket weatherstation integration.""" diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py new file mode 100644 index 00000000000..f46dc73e031 --- /dev/null +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -0,0 +1,76 @@ +"""Test the Trafikverket weatherstation config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.core import HomeAssistant + +DOMAIN = "trafikverket_weatherstation" +CONF_STATION = "station" + + +async def test_form(hass: HomeAssistant) -> None: + """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"] == {} + + with patch( + "homeassistant.components.trafikverket_weatherstation.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "Vallby Vasteras", + CONF_API_KEY: "1234567890", + CONF_STATION: "Vallby", + CONF_MONITORED_CONDITIONS: ["air_temp", "road_temp"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Vallby Vasteras" + assert result2["data"] == { + "name": "Vallby Vasteras", + "api_key": "1234567890", + "station": "Vallby", + "monitored_conditions": ["air_temp", "road_temp"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.trafikverket_weatherstation.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Vallby Vasteras", + CONF_API_KEY: "1234567890", + CONF_STATION: "Vallby", + CONF_MONITORED_CONDITIONS: ["air_temp", "road_temp"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Vallby Vasteras" + assert result2["data"] == { + "name": "Vallby Vasteras", + "api_key": "1234567890", + "station": "Vallby", + "monitored_conditions": ["air_temp", "road_temp"], + } + assert len(mock_setup_entry.mock_calls) == 1 From 8630022e928dc077defce781f0d5f0cfa3017e7a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Dec 2021 08:19:14 +1300 Subject: [PATCH 1056/1452] Add mode to ESPHome numbers (#60653) --- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/number.py | 27 ++++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 025c98def88..527fc6bf768 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.5.0"], + "requirements": ["aioesphomeapi==10.6.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index c26d0398398..be27779437d 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -3,14 +3,19 @@ from __future__ import annotations import math -from aioesphomeapi import NumberInfo, NumberState +from aioesphomeapi import NumberInfo, NumberMode as EsphomeNumberMode, NumberState -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -30,6 +35,15 @@ async def async_setup_entry( ) +NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapper( + { + EsphomeNumberMode.AUTO: NumberMode.AUTO, + EsphomeNumberMode.BOX: NumberMode.BOX, + EsphomeNumberMode.SLIDER: NumberMode.SLIDER, + } +) + + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @@ -57,6 +71,13 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """Return the unit of measurement.""" return super()._static_info.unit_of_measurement + @property + def mode(self) -> NumberMode: + """Return the mode of the entity.""" + if self._static_info.mode: + return NUMBER_MODES.from_esphome(self._static_info.mode) + return NumberMode.AUTO + @esphome_state_property def value(self) -> float | None: """Return the state of the entity.""" diff --git a/requirements_all.txt b/requirements_all.txt index d021d8cc687..3324723c6df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.5.0 +aioesphomeapi==10.6.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26a2823334c..554404e4e0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.5.0 +aioesphomeapi==10.6.0 # homeassistant.components.flo aioflo==2021.11.0 From c2830b3e66af36f063dbd690e7bd9dcf9387182d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Dec 2021 08:26:02 +1300 Subject: [PATCH 1057/1452] Fix fields being None for discord notify service (#59736) Co-authored-by: Martin Hjelmare --- homeassistant/components/discord/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 16f30fbf051..c3f7de94af0 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -62,7 +62,7 @@ class DiscordNotificationService(BaseNotificationService): embed = None if ATTR_EMBED in data: embedding = data[ATTR_EMBED] - fields = embedding.get(ATTR_EMBED_FIELDS) + fields = embedding.get(ATTR_EMBED_FIELDS) or [] if embedding: embed = discord.Embed(**embedding) From 8954609f6bfaa88ef50b0ebf99238fbe1f677abb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Nov 2021 21:15:34 +0100 Subject: [PATCH 1058/1452] Use dataclass properties in axis discovery (#60558) Co-authored-by: epenet --- homeassistant/components/axis/config_flow.py | 24 +++++++++----------- tests/components/axis/test_device.py | 16 +++++++------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 74dbeb0b2c7..a47592fc877 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -156,21 +156,21 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): """Prepare configuration for a DHCP discovered Axis device.""" return await self._process_discovered_device( { - CONF_HOST: discovery_info[dhcp.IP_ADDRESS], - CONF_MAC: format_mac(discovery_info[dhcp.MAC_ADDRESS]), - CONF_NAME: discovery_info[dhcp.HOSTNAME], + CONF_HOST: discovery_info.ip, + CONF_MAC: format_mac(discovery_info.macaddress), + CONF_NAME: discovery_info.hostname, CONF_PORT: DEFAULT_PORT, } ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Prepare configuration for a SSDP discovered Axis device.""" - url = urlsplit(discovery_info["presentationURL"]) + url = urlsplit(discovery_info.upnp[ssdp.ATTR_UPNP_PRESENTATION_URL]) return await self._process_discovered_device( { CONF_HOST: url.hostname, - CONF_MAC: format_mac(discovery_info["serialNumber"]), - CONF_NAME: f"{discovery_info['friendlyName']}", + CONF_MAC: format_mac(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]), + CONF_NAME: f"{discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]}", CONF_PORT: url.port, } ) @@ -181,12 +181,10 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): """Prepare configuration for a Zeroconf discovered Axis device.""" return await self._process_discovered_device( { - CONF_HOST: discovery_info[zeroconf.ATTR_HOST], - CONF_MAC: format_mac( - discovery_info[zeroconf.ATTR_PROPERTIES]["macaddress"] - ), - CONF_NAME: discovery_info[zeroconf.ATTR_NAME].split(".", 1)[0], - CONF_PORT: discovery_info[zeroconf.ATTR_PORT], + CONF_HOST: discovery_info.host, + CONF_MAC: format_mac(discovery_info.properties["macaddress"]), + CONF_NAME: discovery_info.name.split(".", 1)[0], + CONF_PORT: discovery_info.port, } ) diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 2d2ba83f633..d43845e01ad 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -8,7 +8,7 @@ from axis.event_stream import OPERATION_INITIALIZED import pytest import respx -from homeassistant.components import axis +from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import ( CONF_EVENTS, CONF_MODEL, @@ -383,12 +383,14 @@ async def test_update_address(hass): mock_default_vapix_requests(respx, "2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, - data={ - "host": "2.3.4.5", - "port": 80, - "name": "name", - "properties": {"macaddress": MAC}, - }, + data=zeroconf.ZeroconfServiceInfo( + host="2.3.4.5", + hostname="mock_hostname", + name="name", + port=80, + properties={"macaddress": MAC}, + type="mock_type", + ), context={"source": SOURCE_ZEROCONF}, ) await hass.async_block_till_done() From 28ebd13d7581abf08cc4fee898a4c2b7250fb931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 30 Nov 2021 21:28:33 +0100 Subject: [PATCH 1059/1452] Mark calendar as a base platform (#60660) --- homeassistant/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index a917eb65b69..67740c54254 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -31,6 +31,7 @@ BASE_PLATFORMS = { "alarm_control_panel", "binary_sensor", "camera", + "calendar", "climate", "cover", "device_tracker", From 51ebfade5260e6f8c36ffc0e3e1d130a80a21740 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 30 Nov 2021 22:54:10 +0100 Subject: [PATCH 1060/1452] Fix ADR 0003 issues in trafikverket_weatherstation (#60664) * Fix ADR 0003 issues * Remove commented code --- .../trafikverket_weatherstation/config_flow.py | 11 ++++------- .../trafikverket_weatherstation/test_config_flow.py | 12 +++++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py index 44b8b124264..f079852be93 100644 --- a/homeassistant/components/trafikverket_weatherstation/config_flow.py +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -1,6 +1,8 @@ """Adds config flow for Trafikverket Weather integration.""" from __future__ import annotations +import json + import voluptuous as vol from homeassistant import config_entries @@ -10,18 +12,13 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_STATION, DOMAIN from .sensor import SENSOR_TYPES -SENSOR_LIST: dict[str, str | None] = { - description.key: description.name for (description) in SENSOR_TYPES -} +SENSOR_LIST: set[str] = {description.key for (description) in SENSOR_TYPES} DATA_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_STATION): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): cv.multi_select( - SENSOR_LIST - ), } ) @@ -51,7 +48,7 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): name = user_input[CONF_NAME] api_key = user_input[CONF_API_KEY] station = user_input[CONF_STATION] - conditions = user_input[CONF_MONITORED_CONDITIONS] + conditions = json.dumps(list(SENSOR_LIST)) return self.async_create_entry( title=name, diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index f46dc73e031..ecbd84f0eb4 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -1,12 +1,16 @@ """Test the Trafikverket weatherstation config flow.""" from __future__ import annotations +import json from unittest.mock import patch from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.components.trafikverket_weatherstation.sensor import SENSOR_TYPES +from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant +SENSOR_LIST: list[str | None] = {description.key for (description) in SENSOR_TYPES} + DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -30,7 +34,6 @@ async def test_form(hass: HomeAssistant) -> None: CONF_NAME: "Vallby Vasteras", CONF_API_KEY: "1234567890", CONF_STATION: "Vallby", - CONF_MONITORED_CONDITIONS: ["air_temp", "road_temp"], }, ) await hass.async_block_till_done() @@ -41,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: "name": "Vallby Vasteras", "api_key": "1234567890", "station": "Vallby", - "monitored_conditions": ["air_temp", "road_temp"], + "monitored_conditions": json.dumps(list(SENSOR_LIST)), } assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +63,6 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: CONF_NAME: "Vallby Vasteras", CONF_API_KEY: "1234567890", CONF_STATION: "Vallby", - CONF_MONITORED_CONDITIONS: ["air_temp", "road_temp"], }, ) await hass.async_block_till_done() @@ -71,6 +73,6 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: "name": "Vallby Vasteras", "api_key": "1234567890", "station": "Vallby", - "monitored_conditions": ["air_temp", "road_temp"], + "monitored_conditions": json.dumps(list(SENSOR_LIST)), } assert len(mock_setup_entry.mock_calls) == 1 From 98ce12c6eed9791a84051856ddffee898f891ea1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 00:37:34 +0100 Subject: [PATCH 1061/1452] Migrate cover device classes to StrEnum (#60655) --- homeassistant/components/cover/__init__.py | 68 ++++++++++++++-------- homeassistant/components/demo/cover.py | 7 ++- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 2f4abff749e..3b8558658eb 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -34,6 +34,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from homeassistant.util.enum import StrEnum # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -44,31 +45,38 @@ SCAN_INTERVAL = timedelta(seconds=15) ENTITY_ID_FORMAT = DOMAIN + ".{}" -# Refer to the cover dev docs for device class descriptions -DEVICE_CLASS_AWNING = "awning" -DEVICE_CLASS_BLIND = "blind" -DEVICE_CLASS_CURTAIN = "curtain" -DEVICE_CLASS_DAMPER = "damper" -DEVICE_CLASS_DOOR = "door" -DEVICE_CLASS_GARAGE = "garage" -DEVICE_CLASS_GATE = "gate" -DEVICE_CLASS_SHADE = "shade" -DEVICE_CLASS_SHUTTER = "shutter" -DEVICE_CLASS_WINDOW = "window" -DEVICE_CLASSES = [ - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_DAMPER, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHADE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, -] -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) +class CoverDeviceClass(StrEnum): + """Device class for cover.""" + + # Refer to the cover dev docs for device class descriptions + AWNING = "awning" + BLIND = "blind" + CURTAIN = "curtain" + DAMPER = "damper" + DOOR = "door" + GARAGE = "garage" + GATE = "gate" + SHADE = "shade" + SHUTTER = "shutter" + WINDOW = "window" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(CoverDeviceClass)) + +# DEVICE_CLASS* below are deprecated as of 2021.12 +# use the CoverDeviceClass enum instead. +DEVICE_CLASSES = [cls.value for cls in CoverDeviceClass] +DEVICE_CLASS_AWNING = CoverDeviceClass.AWNING.value +DEVICE_CLASS_BLIND = CoverDeviceClass.BLIND.value +DEVICE_CLASS_CURTAIN = CoverDeviceClass.CURTAIN.value +DEVICE_CLASS_DAMPER = CoverDeviceClass.DAMPER.value +DEVICE_CLASS_DOOR = CoverDeviceClass.DOOR.value +DEVICE_CLASS_GARAGE = CoverDeviceClass.GARAGE.value +DEVICE_CLASS_GATE = CoverDeviceClass.GATE.value +DEVICE_CLASS_SHADE = CoverDeviceClass.SHADE.value +DEVICE_CLASS_SHUTTER = CoverDeviceClass.SHUTTER.value +DEVICE_CLASS_WINDOW = CoverDeviceClass.WINDOW.value SUPPORT_OPEN = 1 SUPPORT_CLOSE = 2 @@ -175,6 +183,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class CoverEntityDescription(EntityDescription): """A class that describes cover entities.""" + device_class: CoverDeviceClass | str | None = None + class CoverEntity(Entity): """Base class for cover entities.""" @@ -182,6 +192,7 @@ class CoverEntity(Entity): entity_description: CoverEntityDescription _attr_current_cover_position: int | None = None _attr_current_cover_tilt_position: int | None = None + _attr_device_class: CoverDeviceClass | str | None _attr_is_closed: bool | None _attr_is_closing: bool | None = None _attr_is_opening: bool | None = None @@ -205,6 +216,15 @@ class CoverEntity(Entity): """ return self._attr_current_cover_tilt_position + @property + def device_class(self) -> CoverDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property @final def state(self) -> str | None: diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 444b6a2a90c..b4ca5541690 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,4 +1,6 @@ """Demo platform for the cover component.""" +from __future__ import annotations + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -8,6 +10,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN_TILT, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) from homeassistant.core import callback @@ -28,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass, "cover_4", "Garage Door", - device_class="garage", + device_class=CoverDeviceClass.GARAGE, supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), ), DemoCover( @@ -138,7 +141,7 @@ class DemoCover(CoverEntity): return self._is_opening @property - def device_class(self): + def device_class(self) -> CoverDeviceClass | None: """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class From 542aef2fe1ceebdb20df1aeb255e242a0e444df7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 00:38:45 +0100 Subject: [PATCH 1062/1452] Migrate switch device classes to StrEnum (#60658) --- homeassistant/components/demo/switch.py | 6 ++-- homeassistant/components/huawei_lte/switch.py | 8 ++--- homeassistant/components/switch/__init__.py | 36 +++++++++++++++---- homeassistant/components/upcloud/__init__.py | 3 -- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index dd969010bd7..09389f7c8cd 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,7 +1,7 @@ """Demo platform that has two fake switches.""" from __future__ import annotations -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.helpers.entity import DeviceInfo @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= False, "mdi:air-conditioner", False, - device_class="outlet", + device_class=SwitchDeviceClass.OUTLET, ), ] ) @@ -42,7 +42,7 @@ class DemoSwitch(SwitchEntity): state: bool, icon: str | None, assumed: bool, - device_class: str | None = None, + device_class: SwitchDeviceClass | None = None, ) -> None: """Initialize the Demo switch.""" self._attr_assumed_state = assumed diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index a4fd393346c..af2a382c4db 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -7,8 +7,8 @@ from typing import Any import attr from homeassistant.components.switch import ( - DEVICE_CLASS_SWITCH, DOMAIN as SWITCH_DOMAIN, + SwitchDeviceClass, SwitchEntity, ) from homeassistant.config_entries import ConfigEntry @@ -43,6 +43,7 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): key: str item: str + _attr_device_class = SwitchDeviceClass.SWITCH _raw_state: str | None = attr.ib(init=False, default=None) def _turn(self, state: bool) -> None: @@ -56,11 +57,6 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): """Turn switch off.""" self._turn(state=False) - @property - def device_class(self) -> str: - """Return device class.""" - return DEVICE_CLASS_SWITCH - async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" await super().async_added_to_hass() diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index b023b819a53..fec5a58f414 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -24,6 +24,7 @@ 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.enum import StrEnum DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) @@ -40,16 +41,25 @@ PROP_TO_ATTR = { "today_energy_kwh": ATTR_TODAY_ENERGY_KWH, } -DEVICE_CLASS_OUTLET = "outlet" -DEVICE_CLASS_SWITCH = "switch" - -DEVICE_CLASSES = [DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH] - -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) - _LOGGER = logging.getLogger(__name__) +class SwitchDeviceClass(StrEnum): + """Device class for switches.""" + + OUTLET = "outlet" + SWITCH = "switch" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(SwitchDeviceClass)) + +# DEVICE_CLASS* below are deprecated as of 2021.12 +# use the SwitchDeviceClass enum instead. +DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass] +DEVICE_CLASS_OUTLET = SwitchDeviceClass.OUTLET.value +DEVICE_CLASS_SWITCH = SwitchDeviceClass.SWITCH.value + + @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the switch is on based on the statemachine. @@ -89,12 +99,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class SwitchEntityDescription(ToggleEntityDescription): """A class that describes switch entities.""" + device_class: SwitchDeviceClass | str | None = None + class SwitchEntity(ToggleEntity): """Base class for switch entities.""" entity_description: SwitchEntityDescription _attr_current_power_w: float | None = None + _attr_device_class: SwitchDeviceClass | str | None _attr_today_energy_kwh: float | None = None @property @@ -102,6 +115,15 @@ class SwitchEntity(ToggleEntity): """Return the current power usage in W.""" return self._attr_current_power_w + @property + def device_class(self) -> SwitchDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property def today_energy_kwh(self) -> float | None: """Return the today total energy usage in kWh.""" diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 82d42e28589..2ecc6ec7522 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -47,7 +47,6 @@ CONF_SERVERS = "servers" DATA_UPCLOUD = "data_upcloud" DEFAULT_COMPONENT_NAME = "UpCloud {}" -DEFAULT_COMPONENT_DEVICE_CLASS = "power" CONFIG_ENTRY_DOMAINS = {BINARY_SENSOR_DOMAIN, SWITCH_DOMAIN} @@ -177,8 +176,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class UpCloudServerEntity(CoordinatorEntity): """Entity class for UpCloud servers.""" - _attr_device_class = DEFAULT_COMPONENT_DEVICE_CLASS - def __init__( self, coordinator: DataUpdateCoordinator[dict[str, upcloud_api.Server]], From c9589f763c31d116940db2b1ddb3762beec0fbc4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 1 Dec 2021 00:17:12 +0000 Subject: [PATCH 1063/1452] [ci skip] Translation update --- .../accuweather/translations/bg.json | 11 ++++- .../ambiclimate/translations/hu.json | 2 +- .../components/arcam_fmj/translations/bg.json | 3 +- .../aurora_abb_powerone/translations/sl.json | 10 +++++ .../azure_devops/translations/bg.json | 12 +++++ .../components/balboa/translations/sl.json | 18 ++++++++ .../binary_sensor/translations/sl.json | 21 +++++++++ .../components/blebox/translations/bg.json | 2 +- .../components/blink/translations/bg.json | 31 ++++++++++++- .../components/bond/translations/bg.json | 4 ++ .../components/brunt/translations/sl.json | 27 ++++++++++++ .../components/button/translations/sl.json | 8 ++++ .../components/daikin/translations/sl.json | 3 ++ .../components/demo/translations/hu.json | 2 +- .../devolo_home_network/translations/sl.json | 19 ++++++++ .../components/dlna_dmr/translations/sl.json | 23 ++++++++++ .../components/efergy/translations/sl.json | 19 ++++++++ .../environment_canada/translations/sl.json | 16 +++++++ .../evil_genius_labs/translations/sl.json | 15 +++++++ .../components/flux_led/translations/sl.json | 29 ++++++++++++ .../components/fronius/translations/bg.json | 3 +- .../components/fronius/translations/sl.json | 18 ++++++++ .../components/gogogate2/translations/bg.json | 18 +++++++- .../components/guardian/translations/bg.json | 2 + .../components/hlk_sw16/translations/bg.json | 21 +++++++++ .../components/hue/translations/bg.json | 13 ++---- .../components/hue/translations/sl.json | 7 ++- .../input_boolean/translations/hu.json | 2 +- .../components/iqvia/translations/bg.json | 3 ++ .../components/isy994/translations/bg.json | 1 + .../components/jellyfin/translations/sl.json | 21 +++++++++ .../components/juicenet/translations/bg.json | 2 +- .../components/knx/translations/sl.json | 44 +++++++++++++++++++ .../components/konnected/translations/sl.json | 1 + .../components/lookin/translations/sl.json | 21 +++++++++ .../meteo_france/translations/bg.json | 7 +++ .../components/mill/translations/sl.json | 23 ++++++++++ .../motion_blinds/translations/sl.json | 8 ++++ .../components/nam/translations/bg.json | 19 +++++++- .../components/nam/translations/sl.json | 26 +++++++++++ .../components/nest/translations/ca.json | 13 +++++- .../components/nest/translations/de.json | 13 +++++- .../components/nest/translations/en.json | 15 ++++++- .../components/nest/translations/et.json | 13 +++++- .../components/nest/translations/hu.json | 13 +++++- .../components/nest/translations/ja.json | 7 +++ .../components/nest/translations/no.json | 13 +++++- .../components/nest/translations/ru.json | 13 +++++- .../components/nest/translations/sl.json | 6 +++ .../components/nest/translations/tr.json | 13 +++++- .../components/nest/translations/zh-Hant.json | 13 +++++- .../components/netatmo/translations/bg.json | 3 +- .../components/octoprint/translations/sl.json | 22 ++++++++++ .../components/onvif/translations/bg.json | 3 +- .../components/openuv/translations/bg.json | 3 ++ .../ovo_energy/translations/bg.json | 6 +++ .../components/pi_hole/translations/bg.json | 8 ++++ .../components/plugwise/translations/bg.json | 12 +++++ .../components/poolsense/translations/bg.json | 3 +- .../components/rdw/translations/sl.json | 15 +++++++ .../components/rfxtrx/translations/bg.json | 1 + .../components/ridwell/translations/sl.json | 25 +++++++++++ .../components/risco/translations/bg.json | 7 ++- .../components/sense/translations/sl.json | 3 +- .../simplisafe/translations/bg.json | 12 ++++- .../simplisafe/translations/sl.json | 10 ++++- .../components/sonarr/translations/bg.json | 8 +++- .../components/spider/translations/bg.json | 19 ++++++++ .../stookalert/translations/sl.json | 14 ++++++ .../synology_dsm/translations/bg.json | 9 ++++ .../tesla_wall_connector/translations/hu.json | 30 +++++++++++++ .../tesla_wall_connector/translations/sl.json | 18 ++++++++ .../tesla_wall_connector/translations/tr.json | 30 +++++++++++++ .../components/tile/translations/bg.json | 3 ++ .../tolo/translations/select.sl.json | 7 +++ .../components/tolo/translations/sl.json | 22 ++++++++++ .../components/tradfri/translations/hu.json | 2 +- .../translations/ca.json | 17 +++++++ .../translations/en.json | 24 +++++----- .../translations/ja.json | 17 +++++++ .../translations/tr.json | 17 +++++++ .../tuya/translations/select.ca.json | 4 ++ .../tuya/translations/select.de.json | 4 ++ .../tuya/translations/select.et.json | 4 ++ .../tuya/translations/select.hu.json | 4 ++ .../tuya/translations/select.ja.json | 4 ++ .../tuya/translations/select.no.json | 4 ++ .../tuya/translations/select.ru.json | 4 ++ .../tuya/translations/select.sl.json | 34 ++++++++++++++ .../tuya/translations/select.tr.json | 4 ++ .../tuya/translations/select.zh-Hant.json | 4 ++ .../tuya/translations/sensor.sl.json | 10 +++++ .../components/tuya/translations/sl.json | 9 ++++ .../components/upb/translations/bg.json | 2 +- .../components/venstar/translations/sl.json | 20 +++++++++ .../components/vizio/translations/bg.json | 4 ++ .../vlc_telnet/translations/sl.json | 34 ++++++++++++++ .../components/volumio/translations/bg.json | 7 ++- .../components/wallbox/translations/sl.json | 12 +++++ .../components/watttime/translations/sl.json | 12 +++++ .../wled/translations/select.sl.json | 9 ++++ .../wolflink/translations/sensor.bg.json | 1 + .../xiaomi_aqara/translations/bg.json | 14 ++++++ .../xiaomi_miio/translations/sl.json | 3 +- 104 files changed, 1188 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/aurora_abb_powerone/translations/sl.json create mode 100644 homeassistant/components/balboa/translations/sl.json create mode 100644 homeassistant/components/brunt/translations/sl.json create mode 100644 homeassistant/components/button/translations/sl.json create mode 100644 homeassistant/components/devolo_home_network/translations/sl.json create mode 100644 homeassistant/components/dlna_dmr/translations/sl.json create mode 100644 homeassistant/components/efergy/translations/sl.json create mode 100644 homeassistant/components/environment_canada/translations/sl.json create mode 100644 homeassistant/components/evil_genius_labs/translations/sl.json create mode 100644 homeassistant/components/flux_led/translations/sl.json create mode 100644 homeassistant/components/fronius/translations/sl.json create mode 100644 homeassistant/components/hlk_sw16/translations/bg.json create mode 100644 homeassistant/components/jellyfin/translations/sl.json create mode 100644 homeassistant/components/knx/translations/sl.json create mode 100644 homeassistant/components/lookin/translations/sl.json create mode 100644 homeassistant/components/mill/translations/sl.json create mode 100644 homeassistant/components/nam/translations/sl.json create mode 100644 homeassistant/components/octoprint/translations/sl.json create mode 100644 homeassistant/components/rdw/translations/sl.json create mode 100644 homeassistant/components/ridwell/translations/sl.json create mode 100644 homeassistant/components/spider/translations/bg.json create mode 100644 homeassistant/components/stookalert/translations/sl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/hu.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/sl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/tr.json create mode 100644 homeassistant/components/tolo/translations/select.sl.json create mode 100644 homeassistant/components/tolo/translations/sl.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/ca.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/ja.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/tr.json create mode 100644 homeassistant/components/tuya/translations/select.sl.json create mode 100644 homeassistant/components/tuya/translations/sensor.sl.json create mode 100644 homeassistant/components/venstar/translations/sl.json create mode 100644 homeassistant/components/vlc_telnet/translations/sl.json create mode 100644 homeassistant/components/wallbox/translations/sl.json create mode 100644 homeassistant/components/watttime/translations/sl.json create mode 100644 homeassistant/components/wled/translations/select.sl.json diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json index 074602891cd..b037c01144f 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" @@ -7,8 +10,12 @@ "step": { "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447" - } + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + }, + "title": "AccuWeather" } } } diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 597645658d8..1e67873f1aa 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -10,7 +10,7 @@ }, "error": { "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", - "no_token": "Nem hiteles\u00edtett Ambiclimate" + "no_token": "Ambiclimate-al nem siker\u00fclt a hiteles\u00edt\u00e9s" }, "step": { "auth": { diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json index 26b157c0e5f..f24b5481b2c 100644 --- a/homeassistant/components/arcam_fmj/translations/bg.json +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -9,7 +9,8 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - } + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e." } } } diff --git a/homeassistant/components/aurora_abb_powerone/translations/sl.json b/homeassistant/components/aurora_abb_powerone/translations/sl.json new file mode 100644 index 00000000000..87fce100c77 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/sl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "unknown": "Nepri\u010dakovana napaka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/bg.json b/homeassistant/components/azure_devops/translations/bg.json index d9f03d82592..60c6d07d013 100644 --- a/homeassistant/components/azure_devops/translations/bg.json +++ b/homeassistant/components/azure_devops/translations/bg.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "flow_title": "{project_url}", + "step": { + "user": { + "data": { + "organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/sl.json b/homeassistant/components/balboa/translations/sl.json new file mode 100644 index 00000000000..0eec93b817d --- /dev/null +++ b/homeassistant/components/balboa/translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json index 02c4eedeba9..cc34982ad0a 100644 --- a/homeassistant/components/binary_sensor/translations/sl.json +++ b/homeassistant/components/binary_sensor/translations/sl.json @@ -30,6 +30,7 @@ "is_not_plugged_in": "{entity_name} je odklopljen", "is_not_powered": "{entity_name} ni napajan", "is_not_present": "{entity_name} ni prisoten", + "is_not_tampered": "{entity_name} ne zaznava nedovoljenih posegov", "is_not_unsafe": "{entity_name} je varen", "is_occupied": "{entity_name} je zaseden", "is_off": "{entity_name} je izklopljen", @@ -41,6 +42,7 @@ "is_problem": "{entity_name} zaznava te\u017eavo", "is_smoke": "{entity_name} zaznava dim", "is_sound": "{entity_name} zaznava zvok", + "is_tampered": "{entity_name} zaznava nedovoljeno poseganje", "is_unsafe": "{entity_name} ni varen", "is_vibration": "{entity_name} zaznava vibracije" }, @@ -50,6 +52,8 @@ "connected": "{entity_name} povezan", "gas": "{entity_name} za\u010del zaznavati plin", "hot": "{entity_name} je postal vro\u010d", + "is_not_tampered": "{entity_name} je prenehal zaznavati nedovoljena dejanja", + "is_tampered": "{entity_name} je za\u010del zaznavati nedovoljeno poseganje", "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", "moist": "{entity_name} postal vla\u017een", @@ -89,6 +93,19 @@ "vibration": "{entity_name} je za\u010del odkrivat vibracije" } }, + "device_class": { + "cold": "hladno", + "gas": "plin", + "heat": "toplota", + "moisture": "vlaga", + "motion": "gibanje", + "occupancy": "zasedenost", + "power": "mo\u010d", + "problem": "te\u017eava", + "smoke": "dim", + "sound": "zvok", + "vibration": "vibracija" + }, "state": { "_": { "off": "Izklju\u010den", @@ -164,6 +181,10 @@ "off": "OK", "on": "Te\u017eava" }, + "running": { + "off": "Ni v teku", + "on": "V teku" + }, "safety": { "off": "Varno", "on": "Nevarno" diff --git a/homeassistant/components/blebox/translations/bg.json b/homeassistant/components/blebox/translations/bg.json index 11108007b21..7f4a2894507 100644 --- a/homeassistant/components/blebox/translations/bg.json +++ b/homeassistant/components/blebox/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json index 2ac8a444100..32c84eeb1dc 100644 --- a/homeassistant/components/blink/translations/bg.json +++ b/homeassistant/components/blink/translations/bg.json @@ -1,7 +1,36 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0438\u043c\u0435\u0439\u043b", + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/bg.json b/homeassistant/components/bond/translations/bg.json index 6eb147e8ddd..7f67a133aa8 100644 --- a/homeassistant/components/bond/translations/bg.json +++ b/homeassistant/components/bond/translations/bg.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/brunt/translations/sl.json b/homeassistant/components/brunt/translations/sl.json new file mode 100644 index 00000000000..2a39d333e2f --- /dev/null +++ b/homeassistant/components/brunt/translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Geslo" + }, + "description": "Ponovno vnesite geslo za: {username}" + }, + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "title": "Nastavite Brunt integracijo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/sl.json b/homeassistant/components/button/translations/sl.json new file mode 100644 index 00000000000..84e3a3ff12b --- /dev/null +++ b/homeassistant/components/button/translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "pressed": "{entity_name} je pritisnjena" + } + }, + "title": "Gumb" +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/sl.json b/homeassistant/components/daikin/translations/sl.json index a9f8514146f..40c54ed0206 100644 --- a/homeassistant/components/daikin/translations/sl.json +++ b/homeassistant/components/daikin/translations/sl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Naprava je \u017ee konfigurirana" }, + "error": { + "api_password": "Neveljavna avtentikacija, uporabite API klju\u010d ali geslo." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index e77c21294b8..87810814aac 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -9,7 +9,7 @@ }, "options_1": { "data": { - "bool": "Opcion\u00e1lis logikai \u00e9rt\u00e9k", + "bool": "Opcion\u00e1lis logikai v\u00e1lt\u00f3", "constant": "\u00c1lland\u00f3", "int": "Numerikus bemenet" } diff --git a/homeassistant/components/devolo_home_network/translations/sl.json b/homeassistant/components/devolo_home_network/translations/sl.json new file mode 100644 index 00000000000..7f0bff8bc21 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "ip_address": "IP naslov" + }, + "description": "Ali \u017eelite za\u010deti z nastavitvijo?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/sl.json b/homeassistant/components/dlna_dmr/translations/sl.json new file mode 100644 index 00000000000..5a85ea9dd01 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "alternative_integration": "Naprava je bolje podprta z drugo integracijo", + "cannot_connect": "Povezava ni uspela" + }, + "error": { + "cannot_connect": "Povezava ni uspela" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/sl.json b/homeassistant/components/efergy/translations/sl.json new file mode 100644 index 00000000000..269215c5943 --- /dev/null +++ b/homeassistant/components/efergy/translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "api_key": "API Klju\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/sl.json b/homeassistant/components/environment_canada/translations/sl.json new file mode 100644 index 00000000000..5bbbcfbf23e --- /dev/null +++ b/homeassistant/components/environment_canada/translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "language": "Jezik vremenskih informacij", + "station": "ID vremenske postaje" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/sl.json b/homeassistant/components/evil_genius_labs/translations/sl.json new file mode 100644 index 00000000000..3d0d816aa12 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/sl.json b/homeassistant/components/flux_led/translations/sl.json new file mode 100644 index 00000000000..67c1d624911 --- /dev/null +++ b/homeassistant/components/flux_led/translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave" + }, + "error": { + "cannot_connect": "Povezava ni uspela" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_speed_pct": "U\u010dinek po meri: Hitrost v odstotkih za u\u010dinek, ki spreminja barve.", + "custom_effect_transition": "U\u010dinek po meri: Vrsta prehoda med barvami.", + "mode": "Izbrani na\u010din svetlosti." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/bg.json b/homeassistant/components/fronius/translations/bg.json index 5d235f77133..cbf1e2ae7c9 100644 --- a/homeassistant/components/fronius/translations/bg.json +++ b/homeassistant/components/fronius/translations/bg.json @@ -4,7 +4,8 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/fronius/translations/sl.json b/homeassistant/components/fronius/translations/sl.json new file mode 100644 index 00000000000..0eec93b817d --- /dev/null +++ b/homeassistant/components/fronius/translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/bg.json b/homeassistant/components/gogogate2/translations/bg.json index 94ea3d76554..48e0277066c 100644 --- a/homeassistant/components/gogogate2/translations/bg.json +++ b/homeassistant/components/gogogate2/translations/bg.json @@ -1,5 +1,21 @@ { "config": { - "flow_title": "{device} ({ip_address})" + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "flow_title": "{device} ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json index 9c063cbbd0d..de9699e4a21 100644 --- a/homeassistant/components/guardian/translations/bg.json +++ b/homeassistant/components/guardian/translations/bg.json @@ -1,11 +1,13 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "user": { "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/hlk_sw16/translations/bg.json b/homeassistant/components/hlk_sw16/translations/bg.json new file mode 100644 index 00000000000..d3c0c1a8e77 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index f5091f34333..062aa233562 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -34,17 +34,12 @@ }, "device_automation": { "trigger_subtype": { + "1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" } - }, - "options": { - "step": { - "init": { - "data": { - "allow_unreachable": "{name}" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/sl.json b/homeassistant/components/hue/translations/sl.json index c68971f36f9..fd2c04b8787 100644 --- a/homeassistant/components/hue/translations/sl.json +++ b/homeassistant/components/hue/translations/sl.json @@ -29,6 +29,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Prvi gumb", + "2": "Drugi gumb", + "3": "Tretji gumb", + "4": "\u010cetrti gumb", "button_1": "Prvi gumb", "button_2": "Drugi gumb", "button_3": "Tretji gumb", @@ -52,7 +56,8 @@ "step": { "init": { "data": { - "allow_hue_groups": "Dovoli skupine Hue" + "allow_hue_groups": "Dovoli skupine Hue", + "allow_hue_scenes": "Dovoli Hue scene" } } } diff --git a/homeassistant/components/input_boolean/translations/hu.json b/homeassistant/components/input_boolean/translations/hu.json index bdf99ca8f47..3f2e5be2694 100644 --- a/homeassistant/components/input_boolean/translations/hu.json +++ b/homeassistant/components/input_boolean/translations/hu.json @@ -5,5 +5,5 @@ "on": "Be" } }, - "title": "Logikai bemenet" + "title": "Logikai v\u00e1lt\u00f3" } \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/bg.json b/homeassistant/components/iqvia/translations/bg.json index 26125dcaa14..e59b361d2e0 100644 --- a/homeassistant/components/iqvia/translations/bg.json +++ b/homeassistant/components/iqvia/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "error": { "invalid_zip_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438\u044f\u0442 \u043a\u043e\u0434 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" }, diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index 64b2d259c20..04a015b0d61 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -8,6 +8,7 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/jellyfin/translations/sl.json b/homeassistant/components/jellyfin/translations/sl.json new file mode 100644 index 00000000000..4502cfe9345 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u017de konfigurirano. Mo\u017ena je samo ena konfiguracija." + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "url": "URL", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/bg.json b/homeassistant/components/juicenet/translations/bg.json index 084c18c771d..ae376d0bcb9 100644 --- a/homeassistant/components/juicenet/translations/bg.json +++ b/homeassistant/components/juicenet/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } diff --git a/homeassistant/components/knx/translations/sl.json b/homeassistant/components/knx/translations/sl.json new file mode 100644 index 00000000000..2e32080bfa0 --- /dev/null +++ b/homeassistant/components/knx/translations/sl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Storitev je \u017ee konfigurirana", + "single_instance_allowed": "\u017de konfigurirano. Mo\u017ena je samo ena konfiguracija." + }, + "error": { + "cannot_connect": "Povezava ni uspela" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Gostitelj", + "individual_address": "Posamezni naslov za povezavo", + "port": "Vrata" + } + }, + "routing": { + "data": { + "multicast_group": "Multicast skupina izbrana za usmerjanje", + "multicast_port": "Multicast vrata izbrana za usmerjanje" + } + }, + "tunnel": { + "description": "Prosimo, izberite prehod s seznama." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "individual_address": "Privzet individualni naslov" + } + }, + "tunnel": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/sl.json b/homeassistant/components/konnected/translations/sl.json index 88e0b696416..928d47c389d 100644 --- a/homeassistant/components/konnected/translations/sl.json +++ b/homeassistant/components/konnected/translations/sl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Naprava je \u017ee konfigurirana", "already_in_progress": "Konfiguracijski tok za napravo je \u017ee v teku.", + "cannot_connect": "Povezava ni uspela", "not_konn_panel": "Ni prepoznana kot Konnected.io naprava", "unknown": "Pri\u0161lo je do neznane napake" }, diff --git a/homeassistant/components/lookin/translations/sl.json b/homeassistant/components/lookin/translations/sl.json new file mode 100644 index 00000000000..8bcb70a23d3 --- /dev/null +++ b/homeassistant/components/lookin/translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "cannot_connect": "Povezava ni uspela", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "ip_address": "IP naslov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/bg.json b/homeassistant/components/meteo_france/translations/bg.json index c8c6c11aa02..9a8d591cd77 100644 --- a/homeassistant/components/meteo_france/translations/bg.json +++ b/homeassistant/components/meteo_france/translations/bg.json @@ -1,6 +1,13 @@ { "config": { "step": { + "cities": { + "data": { + "city": "\u0413\u0440\u0430\u0434" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0433\u0440\u0430\u0434 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "\u0413\u0440\u0430\u0434" diff --git a/homeassistant/components/mill/translations/sl.json b/homeassistant/components/mill/translations/sl.json new file mode 100644 index 00000000000..4c08cf164a8 --- /dev/null +++ b/homeassistant/components/mill/translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + } + }, + "local": { + "data": { + "ip_address": "IP naslov" + }, + "description": "Lokalni naslov IP naprave." + }, + "user": { + "data": { + "connection_type": "Izberite vrsto povezave" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sl.json b/homeassistant/components/motion_blinds/translations/sl.json index bb61b035201..80ea8b24fbb 100644 --- a/homeassistant/components/motion_blinds/translations/sl.json +++ b/homeassistant/components/motion_blinds/translations/sl.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "invalid_interface": "Neveljaven omre\u017eni vmesnik" + }, "step": { + "connect": { + "data": { + "interface": "Omre\u017eni vmesnik za uporabo" + } + }, "user": { "title": "Motion Blinds" } diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index efb0b252b1a..50368ce880d 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -2,14 +2,31 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{name}", "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430." + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0445\u043e\u0441\u0442\u0430: {host}" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/nam/translations/sl.json b/homeassistant/components/nam/translations/sl.json new file mode 100644 index 00000000000..fca06535159 --- /dev/null +++ b/homeassistant/components/nam/translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_unsuccessful": "Ponovna avtentikacija ni bila uspe\u0161na, odstranite integracijo in jo znova nastavite." + }, + "error": { + "invalid_auth": "Neveljavna avtentikacija" + }, + "step": { + "credentials": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo." + }, + "reauth_confirm": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite pravilno uporabni\u0161ko ime in geslo za gostitelja: {host}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index c17e5ca4bf7..ce7d4da99e6 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", @@ -12,10 +13,13 @@ "default": "Autenticaci\u00f3 exitosa" }, "error": { + "bad_project_id": "Introdueix un ID de projecte Cloud v\u00e0lid (consulta Cloud Console)", "internal_error": "Error intern al validar el codi", "invalid_pin": "Codi PIN inv\u00e0lid", + "subscriber_error": "Error de subscriptor desconegut, consulta els registres", "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi.", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "wrong_project_id": "Introdueix un ID de projecte Cloud v\u00e0lid (s'ha trobat un ID de projecte d'Acc\u00e9s de Dispositiu)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" }, + "pubsub": { + "data": { + "cloud_project_id": "ID de projecte de Google Cloud" + }, + "description": "V\u00e9s a [Cloud Console]({url}) per obtenir l'ID de projecte de Google Cloud.", + "title": "Configuraci\u00f3 de Google Cloud" + }, "reauth_confirm": { "description": "La integraci\u00f3 de Nest ha de tornar a autenticar-se amb el teu compte", "title": "Reautenticaci\u00f3 de la integraci\u00f3" diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 49563b55589..47d0505dca5 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", @@ -12,10 +13,13 @@ "default": "Erfolgreich authentifiziert" }, "error": { + "bad_project_id": "Bitte gib eine g\u00fcltige Cloud-Projekt-ID ein (\u00fcberpr\u00fcfe die Cloud-Konsole).", "internal_error": "Ein interner Fehler ist aufgetreten", "invalid_pin": "Ung\u00fcltiger PIN-Code", + "subscriber_error": "Unbekannter Abonnentenfehler, siehe Protokolle", "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "wrong_project_id": "Bitte gib eine g\u00fcltige Cloud-Projekt-ID ein (gefundene Ger\u00e4tezugriffs-Projekt-ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud-Projekt-ID" + }, + "description": "Rufe die [Cloud Console]( {url} ) auf, um deine Google Cloud-Projekt-ID zu finden.", + "title": "Google Cloud konfigurieren" + }, "reauth_confirm": { "description": "Die Nest-Integration muss das Konto neu authentifizieren", "title": "Integration erneut authentifizieren" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index e03de2b9bea..5b0b0e84afc 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", @@ -12,10 +13,13 @@ "default": "Successfully authenticated" }, "error": { + "bad_project_id": "Please enter a valid Cloud Project ID (check Cloud Console)", "internal_error": "Internal error validating code", "invalid_pin": "Invalid PIN Code", + "subscriber_error": "Unknown subscriber error, see logs", "timeout": "Timeout validating code", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "wrong_project_id": "Please enter a valid Cloud Project ID (found Device Access Project ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Pick Authentication Method" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "Visit the [Cloud Console]({url}) to find your Google Cloud Project ID.", + "title": "Configure Google Cloud" + }, "reauth_confirm": { "description": "The Nest integration needs to re-authenticate your account", "title": "Reauthenticate Integration" @@ -56,4 +67,4 @@ "doorbell_chime": "Doorbell pressed" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index f0e5c3d2359..90a78abbd63 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4su t\u00f5end", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", "reauth_successful": "Taastuvastamine \u00f5nnestus", @@ -12,10 +13,13 @@ "default": "Tuvastamine \u00f5nnestus" }, "error": { + "bad_project_id": "Sisesta kehtiv pilveprojekti ID (kontrolli Cloud Console'i)", "internal_error": "Sisemine viga tuvastuskoodi kinnitamisel", "invalid_pin": "Vale PIN kood", + "subscriber_error": "Tundmatu tellija t\u00f5rge, vt logisid", "timeout": "Tuvastuskoodi ajal\u00f5pp", - "unknown": "Tundmatu viga tuvastuskoodi kinnitamisel" + "unknown": "Tundmatu viga tuvastuskoodi kinnitamisel", + "wrong_project_id": "Sisesta kehtiv pilveprojekti ID (leitud seadme juurdep\u00e4\u00e4su projekti ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Vali tuvastusmeetod" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloudi projekti ID" + }, + "description": "K\u00fclasta [Cloud Console]({url}), et leida oma Google Cloudi projekti ID.", + "title": "Google Cloudi seadistamine" + }, "reauth_confirm": { "description": "Nesti sidumine peab konto taastuvastama", "title": "Taastuvasta sidumine" diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 3d73e84cbe0..2bb9d2dbaec 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse 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.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", @@ -12,10 +13,13 @@ "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { + "bad_project_id": "K\u00e9rem, adjon meg egy \u00e9rv\u00e9nyes Cloud Project ID-t (Cloud Consoleban l\u00e1that\u00f3).", "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", + "subscriber_error": "Ismeretlen el\u0151fizet\u0151i hiba, b\u0151vebben a napl\u00f3kban", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "wrong_project_id": "K\u00e9rem, adjon meg egy \u00e9rv\u00e9nyes Cloud Project ID-t (Device Access Project ID tal\u00e1lva)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "L\u00e1togasson el a [Cloud Console]({url}) oldalra, hogy megtal\u00e1lja a Google Cloud Project ID-t", + "title": "Google Cloud konfigur\u00e1l\u00e1sa" + }, "reauth_confirm": { "description": "A Nest integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kodat", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 77fb72e6b9e..c306b78bc51 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -42,6 +42,13 @@ "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" + }, + "description": "[Cloud Console]({url})\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3001Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u898b\u3064\u3051\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "Google Cloud\u306e\u8a2d\u5b9a" + }, "reauth_confirm": { "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 276fb862298..fcc6aeadddf 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", @@ -12,10 +13,13 @@ "default": "Vellykket godkjenning" }, "error": { + "bad_project_id": "Skriv inn en gyldig Cloud Project ID (sjekk Cloud Console)", "internal_error": "Intern feil ved validering av kode", "invalid_pin": "Ugyldig PIN kode", + "subscriber_error": "Ukjent abonnentfeil, se logger", "timeout": "Tidsavbrudd ved validering av kode", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "wrong_project_id": "Angi en gyldig Cloud Project ID (funnet Device Access Project ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Velg godkjenningsmetode" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "G\u00e5 til [Cloud Console]( {url} ) for \u00e5 finne Google Cloud Project ID.", + "title": "Konfigurer Google Cloud" + }, "reauth_confirm": { "description": "Nest-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt", "title": "Godkjenne integrering p\u00e5 nytt" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 8e090bd5ce2..f17f48b4560 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "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.", @@ -12,10 +13,13 @@ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { + "bad_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 Cloud Console)", "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", + "subscriber_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043f\u0438\u0441\u0447\u0438\u043a\u0430. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0445.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u043d\u0430\u0439\u0434\u0435\u043d Device Access Project ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [Cloud Console]({url}), \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0412\u0430\u0448 Google Cloud Project ID.", + "title": "Google Cloud" + }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 84f07fdcf42..836ae7761e8 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -11,6 +11,12 @@ "unknown": "Neznana napaka pri preverjanju kode" }, "step": { + "auth": { + "data": { + "code": "\u017deton za dostop" + }, + "title": "Pove\u017eite Google Ra\u010dun" + }, "init": { "data": { "flow_impl": "Ponudnik" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 11dd05897fd..03ff79824af 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", @@ -12,10 +13,13 @@ "default": "Ba\u015far\u0131yla do\u011fruland\u0131" }, "error": { + "bad_project_id": "L\u00fctfen ge\u00e7erli bir Cloud Project Kimli\u011fi girin (Cloud Console'a bak\u0131n)", "internal_error": "Kod do\u011frularken i\u00e7 hata olu\u015ftu", "invalid_pin": "Ge\u00e7ersiz PIN Kodu", + "subscriber_error": "Bilinmeyen abone hatas\u0131, g\u00fcnl\u00fcklere bak\u0131n\u0131z", "timeout": "Zaman a\u015f\u0131m\u0131 do\u011frulama kodu", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (bulunan Ayg\u0131t Eri\u015fimi Proje Kimli\u011fi)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Bulut Proje Kimli\u011fi" + }, + "description": "Google Cloud Project Kimli\u011finizi bulmak i\u00e7in [Cloud Console]({url}) adresini ziyaret edin.", + "title": "Google Cloud'u yap\u0131land\u0131r\u0131n" + }, "reauth_confirm": { "description": "Nest entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", "title": "Entegrasyonu Yeniden Do\u011frula" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index dbd8ad3b3df..50c5e31f0b9 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", @@ -12,10 +13,13 @@ "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "error": { + "bad_project_id": "\u8acb\u8f38\u5165\u6709\u6548 Cloud \u5c08\u6848 ID\uff08\u8acb\u53c3\u95b1 Cloud Console\uff09", "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", "invalid_pin": "\u7121\u6548\u7684 PIN \u78bc", + "subscriber_error": "\u672a\u77e5\u8a02\u95b1\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c", "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_project_id": "\u8acb\u8f38\u5165\u6709\u6548 Cloud \u5c08\u6848 ID\uff08\u53ef\u65bc Device Access Project ID \u4e2d\u627e\u5230\uff09" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud \u5c08\u6848 ID" + }, + "description": "\u958b\u555f [Cloud Console]({url}) \u9801\u9762\u4ee5\u67e5\u770b Google Cloud \u5c08\u6848 ID\u3002", + "title": "\u8a2d\u5b9a Google Cloud" + }, "reauth_confirm": { "description": "Nest \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 723b302203f..4b02d4b6c6e 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/octoprint/translations/sl.json b/homeassistant/components/octoprint/translations/sl.json new file mode 100644 index 00000000000..469e6df6535 --- /dev/null +++ b/homeassistant/components/octoprint/translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "port": "\u0160tevilka vrat", + "ssl": "Uporaba SSL", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json index 6ef4c15dd8b..e45e78b79ee 100644 --- a/homeassistant/components/onvif/translations/bg.json +++ b/homeassistant/components/onvif/translations/bg.json @@ -7,7 +7,7 @@ "auth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Username" + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } }, "configure": { @@ -22,6 +22,7 @@ "manual_input": { "data": { "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 9541f00f7f4..1bfee97d1e4 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index 16c1a3f9b0e..b7636becf45 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -11,6 +11,12 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } } } } diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 62a6b635be0..0a8f88b6b0d 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "api_key": { "data": { @@ -9,7 +15,9 @@ "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442", "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "name": "\u0418\u043c\u0435", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json index cf043c65495..0c1cf067319 100644 --- a/homeassistant/components/plugwise/translations/bg.json +++ b/homeassistant/components/plugwise/translations/bg.json @@ -1,6 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", "step": { + "user": { + "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:" + }, "user_gateway": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/poolsense/translations/bg.json b/homeassistant/components/poolsense/translations/bg.json index 0b38b9c4741..a89cca15270 100644 --- a/homeassistant/components/poolsense/translations/bg.json +++ b/homeassistant/components/poolsense/translations/bg.json @@ -12,7 +12,8 @@ "email": "Email", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?", + "title": "PoolSense" } } } diff --git a/homeassistant/components/rdw/translations/sl.json b/homeassistant/components/rdw/translations/sl.json new file mode 100644 index 00000000000..78b07d7baff --- /dev/null +++ b/homeassistant/components/rdw/translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown_license_plate": "Neznana registrska tablica" + }, + "step": { + "user": { + "data": { + "license_plate": "Registrska tablica" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index e35eaf15bd2..82061aa125d 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { diff --git a/homeassistant/components/ridwell/translations/sl.json b/homeassistant/components/ridwell/translations/sl.json new file mode 100644 index 00000000000..db0d9f2afc4 --- /dev/null +++ b/homeassistant/components/ridwell/translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Geslo" + } + }, + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite svoje uporabni\u0161ko ime in geslo:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index ac95bcfebf8..b9092f75d6c 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/sense/translations/sl.json b/homeassistant/components/sense/translations/sl.json index 8720f80a2c5..5d7c26c38d9 100644 --- a/homeassistant/components/sense/translations/sl.json +++ b/homeassistant/components/sense/translations/sl.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "E-po\u0161tni naslov", - "password": "Geslo" + "password": "Geslo", + "timeout": "Timeout" }, "title": "Pove\u017eite se s svojim Sense Energy monitor-jem" } diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 4013449a082..75f9480f5fd 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "input_auth_code": { @@ -10,6 +14,12 @@ "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/simplisafe/translations/sl.json b/homeassistant/components/simplisafe/translations/sl.json index dffa3c4ccef..bbbe8034d06 100644 --- a/homeassistant/components/simplisafe/translations/sl.json +++ b/homeassistant/components/simplisafe/translations/sl.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi." + "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi.", + "wrong_account": "Navedene uporabni\u0161ke poverilnice se ne ujemajo s tem ra\u010dunom SimpliSafe." }, "error": { "identifier_exists": "Ra\u010dun je \u017ee registriran" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Avtorizacijska koda" + }, + "description": "Vnesite avtorizacijsko kodo iz URL-ja spletne aplikacije SimpliSafe:", + "title": "Dokon\u010daj avtorizacijo" + }, "user": { "data": { "code": "Koda (uporablja se v uporabni\u0161kem vmesniku Home Assistant)", diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index 4acf9d803a7..29dc5bfd9a9 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/spider/translations/bg.json b/homeassistant/components/spider/translations/bg.json new file mode 100644 index 00000000000..bd20e92f8ec --- /dev/null +++ b/homeassistant/components/spider/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/sl.json b/homeassistant/components/stookalert/translations/sl.json new file mode 100644 index 00000000000..8cc117856ef --- /dev/null +++ b/homeassistant/components/stookalert/translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Storitev je \u017ee konfigurirana" + }, + "step": { + "user": { + "data": { + "province": "Pokrajina" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index 4b857bf5cf0..f77c82130a4 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -48,5 +48,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u041c\u0438\u043d\u0443\u0442\u0438 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0438\u044f\u0442\u0430" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/hu.json b/homeassistant/components/tesla_wall_connector/translations/hu.json new file mode 100644 index 00000000000..951d4de5a86 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "C\u00edm" + }, + "title": "Tesla Wall Connector konfigur\u00e1l\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" + }, + "title": "Tesla Wall Connector konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/sl.json b/homeassistant/components/tesla_wall_connector/translations/sl.json new file mode 100644 index 00000000000..0eec93b817d --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/tr.json b/homeassistant/components/tesla_wall_connector/translations/tr.json new file mode 100644 index 00000000000..5eaeba841e2 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Ana bilgisayar" + }, + "title": "Tesla Duvar Ba\u011flant\u0131s\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" + }, + "title": "Tesla Duvar Konekt\u00f6r\u00fc i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index cd419ccf91c..debdbdaaf87 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/tolo/translations/select.sl.json b/homeassistant/components/tolo/translations/select.sl.json new file mode 100644 index 00000000000..71dbd7a4ffb --- /dev/null +++ b/homeassistant/components/tolo/translations/select.sl.json @@ -0,0 +1,7 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "avtomatsko" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/sl.json b/homeassistant/components/tolo/translations/sl.json new file mode 100644 index 00000000000..e32b3eb95ca --- /dev/null +++ b/homeassistant/components/tolo/translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave" + }, + "error": { + "cannot_connect": "Povezava ni uspela" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ali \u017eelite za\u010deti z nastavitvijo?" + }, + "user": { + "data": { + "host": "Gostitelj" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index 5e9c281fdd6..01bb51b0af3 100644 --- a/homeassistant/components/tradfri/translations/hu.json +++ b/homeassistant/components/tradfri/translations/hu.json @@ -5,7 +5,7 @@ "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van" }, "error": { - "cannot_authenticate": "Sikertelen azonos\u00edt\u00e1s. A Gateway egy m\u00e1sik eszk\u00f6zzel van p\u00e1ros\u00edtva, mint p\u00e9ld\u00e1ul a Homekittel?", + "cannot_authenticate": "Sikertelen hiteles\u00edt\u00e9s. A Gateway egy m\u00e1sik eszk\u00f6zzel van p\u00e1ros\u00edtva, mint p\u00e9ld\u00e1ul a Homekittel?", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_key": "Nem siker\u00fclt regisztr\u00e1lni a megadott kulcs seg\u00edts\u00e9g\u00e9vel. Ha ez t\u00f6bbsz\u00f6r megt\u00f6rt\u00e9nik, pr\u00f3b\u00e1lja meg \u00fajraind\u00edtani a gatewayt.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ca.json b/homeassistant/components/trafikverket_weatherstation/translations/ca.json new file mode 100644 index 00000000000..00656cb09f2 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "conditions": "Condicions monitoritzades", + "name": "Nom d'usuari", + "station": "Estaci\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/en.json b/homeassistant/components/trafikverket_weatherstation/translations/en.json index fd32bb80ab3..883e67e9bfa 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/en.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/en.json @@ -1,17 +1,17 @@ { "config": { - "abort": { - "already_configured": "Account is already configured" - }, - "step": { - "user": { - "data": { - "name": "Name", - "api_key": "API Key", - "station": "Station", - "conditions": "Monitored conditions" + "abort": { + "already_configured": "Account is already configured" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "conditions": "Monitored conditions", + "name": "Username", + "station": "Station" + } } - } } } - } \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ja.json b/homeassistant/components/trafikverket_weatherstation/translations/ja.json new file mode 100644 index 00000000000..ec86eeea9cd --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "conditions": "\u30e2\u30cb\u30bf\u30fc\u306e\u72b6\u614b", + "name": "\u30e6\u30fc\u30b6\u30fc\u540d", + "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/tr.json b/homeassistant/components/trafikverket_weatherstation/translations/tr.json new file mode 100644 index 00000000000..858bc2c4c94 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "conditions": "\u0130zlenen ko\u015fullar", + "name": "Kullan\u0131c\u0131 Ad\u0131", + "station": "\u0130stasyon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ca.json b/homeassistant/components/tuya/translations/select.ca.json index f55c56a112b..d2b7935a075 100644 --- a/homeassistant/components/tuya/translations/select.ca.json +++ b/homeassistant/components/tuya/translations/select.ca.json @@ -14,6 +14,10 @@ "0": "Sensibilitat baixa", "1": "Sensibilitat alta" }, + "tuya__fingerbot_mode": { + "click": "Polsador", + "switch": "Interruptor" + }, "tuya__ipc_work_mode": { "0": "Mode de baix consum", "1": "Mode de funcionament continu" diff --git a/homeassistant/components/tuya/translations/select.de.json b/homeassistant/components/tuya/translations/select.de.json index f068b8b4425..7183ba9fc2a 100644 --- a/homeassistant/components/tuya/translations/select.de.json +++ b/homeassistant/components/tuya/translations/select.de.json @@ -14,6 +14,10 @@ "0": "Geringe Empfindlichkeit", "1": "Hohe Empfindlichkeit" }, + "tuya__fingerbot_mode": { + "click": "Dr\u00fccken", + "switch": "Schalter" + }, "tuya__ipc_work_mode": { "0": "Energiesparmodus", "1": "Kontinuierlicher Arbeitsmodus" diff --git a/homeassistant/components/tuya/translations/select.et.json b/homeassistant/components/tuya/translations/select.et.json index 58152555b56..c03901479d8 100644 --- a/homeassistant/components/tuya/translations/select.et.json +++ b/homeassistant/components/tuya/translations/select.et.json @@ -14,6 +14,10 @@ "0": "Madal tundlikkus", "1": "K\u00f5rge tundlikkus" }, + "tuya__fingerbot_mode": { + "click": "Vajutus", + "switch": "L\u00fcliti" + }, "tuya__ipc_work_mode": { "0": "Madala energiatarbega re\u017eiim", "1": "Pidev t\u00f6\u00f6re\u017eiim" diff --git a/homeassistant/components/tuya/translations/select.hu.json b/homeassistant/components/tuya/translations/select.hu.json index 6d9b4846e64..f503c93bd05 100644 --- a/homeassistant/components/tuya/translations/select.hu.json +++ b/homeassistant/components/tuya/translations/select.hu.json @@ -14,6 +14,10 @@ "0": "Alacsony \u00e9rz\u00e9kenys\u00e9g", "1": "Magas \u00e9rz\u00e9kenys\u00e9g" }, + "tuya__fingerbot_mode": { + "click": "Lenyom\u00e1s", + "switch": "Kapcsol\u00e1s" + }, "tuya__ipc_work_mode": { "0": "Alacsony fogyaszt\u00e1s\u00fa m\u00f3d", "1": "Folyamatos \u00fczemm\u00f3d" diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index fcc3466bf62..b2bb0a27204 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -14,6 +14,10 @@ "0": "\u4f4e\u611f\u5ea6", "1": "\u9ad8\u611f\u5ea6" }, + "tuya__fingerbot_mode": { + "click": "\u62bc\u3059", + "switch": "\u30b9\u30a4\u30c3\u30c1" + }, "tuya__ipc_work_mode": { "0": "\u4f4e\u96fb\u529b\u30e2\u30fc\u30c9", "1": "\u9023\u7d9a\u4f5c\u696d\u30e2\u30fc\u30c9" diff --git a/homeassistant/components/tuya/translations/select.no.json b/homeassistant/components/tuya/translations/select.no.json index e5bc8dbba53..459d8c4cfc0 100644 --- a/homeassistant/components/tuya/translations/select.no.json +++ b/homeassistant/components/tuya/translations/select.no.json @@ -14,6 +14,10 @@ "0": "Lav f\u00f8lsomhet", "1": "H\u00f8y f\u00f8lsomhet" }, + "tuya__fingerbot_mode": { + "click": "Trykk", + "switch": "Bryter" + }, "tuya__ipc_work_mode": { "0": "Lav effekt modus", "1": "Kontinuerlig arbeidsmodus" diff --git a/homeassistant/components/tuya/translations/select.ru.json b/homeassistant/components/tuya/translations/select.ru.json index 3c9401e4249..582c1188778 100644 --- a/homeassistant/components/tuya/translations/select.ru.json +++ b/homeassistant/components/tuya/translations/select.ru.json @@ -14,6 +14,10 @@ "0": "\u041d\u0438\u0437\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", "1": "\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" }, + "tuya__fingerbot_mode": { + "click": "\u041a\u043d\u043e\u043f\u043a\u0430", + "switch": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" + }, "tuya__ipc_work_mode": { "0": "\u0420\u0435\u0436\u0438\u043c \u043d\u0438\u0437\u043a\u043e\u0433\u043e \u044d\u043d\u0435\u0440\u0433\u043e\u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u044f", "1": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b" diff --git a/homeassistant/components/tuya/translations/select.sl.json b/homeassistant/components/tuya/translations/select.sl.json new file mode 100644 index 00000000000..cad127241aa --- /dev/null +++ b/homeassistant/components/tuya/translations/select.sl.json @@ -0,0 +1,34 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "0": "Onemogo\u010deno" + }, + "tuya__basic_nightvision": { + "0": "Samodejno", + "1": "Izklju\u010den", + "2": "Vklopljen" + }, + "tuya__led_type": { + "halogen": "Halogenska", + "incandescent": "\u017dare\u010de", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Izklju\u010den", + "pos": "Navedite lokacijo stikala", + "relay": "Navedite stanje vklopa/izklopa" + }, + "tuya__record_mode": { + "1": "Snemaj samo dogode", + "2": "Neprekinjeno snemanje" + }, + "tuya__relay_status": { + "last": "Zapomni si zadnje stanje", + "memory": "Zapomni si zadnje stanje", + "off": "Izklju\u010den", + "on": "Vklopljen", + "power_off": "Izklju\u010den", + "power_on": "Vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.tr.json b/homeassistant/components/tuya/translations/select.tr.json index e6bdd2038b0..56a875b7d88 100644 --- a/homeassistant/components/tuya/translations/select.tr.json +++ b/homeassistant/components/tuya/translations/select.tr.json @@ -14,6 +14,10 @@ "0": "D\u00fc\u015f\u00fck hassasiyet", "1": "Y\u00fcksek hassasiyet" }, + "tuya__fingerbot_mode": { + "click": "Bildirim", + "switch": "Anahtar" + }, "tuya__ipc_work_mode": { "0": "D\u00fc\u015f\u00fck g\u00fc\u00e7 modu", "1": "S\u00fcrekli \u00e7al\u0131\u015fma modu" diff --git a/homeassistant/components/tuya/translations/select.zh-Hant.json b/homeassistant/components/tuya/translations/select.zh-Hant.json index 86cd7342a7f..31656f70859 100644 --- a/homeassistant/components/tuya/translations/select.zh-Hant.json +++ b/homeassistant/components/tuya/translations/select.zh-Hant.json @@ -14,6 +14,10 @@ "0": "\u4f4e\u654f\u611f\u5ea6", "1": "\u9ad8\u654f\u611f\u5ea6" }, + "tuya__fingerbot_mode": { + "click": "\u63a8", + "switch": "\u958b\u95dc" + }, "tuya__ipc_work_mode": { "0": "\u4f4e\u529f\u8017\u6a21\u5f0f", "1": "\u6301\u7e8c\u5de5\u4f5c\u6a21\u5f0f" diff --git a/homeassistant/components/tuya/translations/sensor.sl.json b/homeassistant/components/tuya/translations/sensor.sl.json new file mode 100644 index 00000000000..68196055212 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.sl.json @@ -0,0 +1,10 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Temperatura vreli\u0161\u010da", + "cooling": "Hlajenje", + "heating": "Ogrevanje", + "heating_temp": "Temperatura ogrevanja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json index b07ad70adac..52d2fd3e973 100644 --- a/homeassistant/components/tuya/translations/sl.json +++ b/homeassistant/components/tuya/translations/sl.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "region": "Regija" + } + } + } + }, "options": { "abort": { "cannot_connect": "Povezovanje ni uspelo." diff --git a/homeassistant/components/upb/translations/bg.json b/homeassistant/components/upb/translations/bg.json index c7fc1a35b8e..45443313c72 100644 --- a/homeassistant/components/upb/translations/bg.json +++ b/homeassistant/components/upb/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/venstar/translations/sl.json b/homeassistant/components/venstar/translations/sl.json new file mode 100644 index 00000000000..d0af0651d27 --- /dev/null +++ b/homeassistant/components/venstar/translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "pin": "PIN koda", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/bg.json b/homeassistant/components/vizio/translations/bg.json index d10f511e3b3..962de7286c7 100644 --- a/homeassistant/components/vizio/translations/bg.json +++ b/homeassistant/components/vizio/translations/bg.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vlc_telnet/translations/sl.json b/homeassistant/components/vlc_telnet/translations/sl.json new file mode 100644 index 00000000000..978406ddba0 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/sl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Storitev je \u017ee konfigurirana", + "cannot_connect": "Povezava ni uspela", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "Ali se \u017eelite povezati z dodatkom {addon} ?" + }, + "reauth_confirm": { + "data": { + "password": "Geslo" + }, + "description": "Vnesite pravilno geslo za gostitelja: {host}" + }, + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "port": "Vrata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/bg.json b/homeassistant/components/volumio/translations/bg.json index a610a1f2a64..58bf7bc6cef 100644 --- a/homeassistant/components/volumio/translations/bg.json +++ b/homeassistant/components/volumio/translations/bg.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/wallbox/translations/sl.json b/homeassistant/components/wallbox/translations/sl.json new file mode 100644 index 00000000000..3b7b34cb86d --- /dev/null +++ b/homeassistant/components/wallbox/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/sl.json b/homeassistant/components/watttime/translations/sl.json new file mode 100644 index 00000000000..56d6a5b830a --- /dev/null +++ b/homeassistant/components/watttime/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Geslo" + }, + "description": "Ponovno vnesite geslo za {username} :" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.sl.json b/homeassistant/components/wled/translations/select.sl.json new file mode 100644 index 00000000000..b752ae28d89 --- /dev/null +++ b/homeassistant/components/wled/translations/select.sl.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Izklju\u010den", + "1": "Vklopljen", + "2": "Dokler se naprava znova ne za\u017eene" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 1889d3efb34..4a402cfe75b 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -2,6 +2,7 @@ "state": { "wolflink__state": { "1_x_warmwasser": "1 x DHW", + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", "urlaubsmodus": "\u0412\u0430\u043a\u0430\u043d\u0446\u0438\u043e\u043d\u0435\u043d \u0440\u0435\u0436\u0438\u043c", diff --git a/homeassistant/components/xiaomi_aqara/translations/bg.json b/homeassistant/components/xiaomi_aqara/translations/bg.json index 0fbc7416296..61cc221fa2c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/bg.json +++ b/homeassistant/components/xiaomi_aqara/translations/bg.json @@ -3,12 +3,26 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "invalid_mac": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d Mac \u0430\u0434\u0440\u0435\u0441" + }, "flow_title": "{name}", "step": { "select": { "data": { "select_ip": "IP \u0430\u0434\u0440\u0435\u0441" } + }, + "settings": { + "data": { + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430" + } + }, + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "mac": "Mac \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/sl.json b/homeassistant/components/xiaomi_miio/translations/sl.json index 45aad4ab691..472317182bf 100644 --- a/homeassistant/components/xiaomi_miio/translations/sl.json +++ b/homeassistant/components/xiaomi_miio/translations/sl.json @@ -4,7 +4,8 @@ "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { - "no_device_selected": "Izbrana ni nobena naprava, izberite eno napravo." + "no_device_selected": "Izbrana ni nobena naprava, izberite eno napravo.", + "wrong_token": "Napaka kontrolne vsote, napa\u010den \u017eeton" }, "step": { "gateway": { From c0021e576883d82c2e54406d04a5a6caa8ee8a3b Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 1 Dec 2021 04:32:57 +0100 Subject: [PATCH 1064/1452] Upgrade aionanoleaf to 0.1.1 (#60670) --- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index f8caf5be7eb..d2c2d1ff643 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.0.4"], + "requirements": ["aionanoleaf==0.1.1"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3324723c6df..707d59ba468 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.4 +aionanoleaf==0.1.1 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 554404e4e0c..2fec3518e71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.4 +aionanoleaf==0.1.1 # homeassistant.components.notion aionotion==3.0.2 From 9efec244b97cc1c35208fa8c41dad6f77ffc7f79 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 1 Dec 2021 05:26:35 +0100 Subject: [PATCH 1065/1452] Fix yale_smart_alarm strings (#60657) --- homeassistant/components/yale_smart_alarm/strings.json | 3 ++- homeassistant/components/yale_smart_alarm/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index 4fb61f5a5f1..cec588a3cc8 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/yale_smart_alarm/translations/en.json b/homeassistant/components/yale_smart_alarm/translations/en.json index a439971fb3f..e198b0329b9 100644 --- a/homeassistant/components/yale_smart_alarm/translations/en.json +++ b/homeassistant/components/yale_smart_alarm/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication" From 971560125503982bbfcc2605211cf2921b105800 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 07:08:17 +0100 Subject: [PATCH 1066/1452] Add button device classes to NAM (#60621) --- homeassistant/components/nam/button.py | 8 ++++++-- tests/components/nam/test_button.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index 3a205606ccc..73b5169abcd 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -3,7 +3,11 @@ from __future__ import annotations import logging -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant @@ -20,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", name=f"{DEFAULT_NAME} Restart", - icon="mdi:restart", + device_class=ButtonDeviceClass.RESTART, entity_category=ENTITY_CATEGORY_CONFIG, ) diff --git a/tests/components/nam/test_button.py b/tests/components/nam/test_button.py index 7cd731e7584..c8ed00b3376 100644 --- a/tests/components/nam/test_button.py +++ b/tests/components/nam/test_button.py @@ -1,8 +1,8 @@ """Test button of Nettigo Air Monitor integration.""" from unittest.mock import patch -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -18,7 +18,7 @@ async def test_button(hass): state = hass.states.get("button.nettigo_air_monitor_restart") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ICON) == "mdi:restart" + assert state.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART entry = registry.async_get("button.nettigo_air_monitor_restart") assert entry From 3770a726019fc185e03a404d434ea788023e3ae5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 07:25:10 +0100 Subject: [PATCH 1067/1452] Migrate media player device classes to StrEnum (#60656) --- .../components/media_player/__init__.py | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0b2a7959c13..12d0eae29d4 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -63,6 +63,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass +from homeassistant.util.enum import StrEnum from .const import ( ATTR_APP_ID, @@ -140,13 +141,24 @@ ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16 SCAN_INTERVAL = dt.timedelta(seconds=10) -DEVICE_CLASS_TV = "tv" -DEVICE_CLASS_SPEAKER = "speaker" -DEVICE_CLASS_RECEIVER = "receiver" -DEVICE_CLASSES = [DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER, DEVICE_CLASS_RECEIVER] +class MediaPlayerDeviceClass(StrEnum): + """Device class for media players.""" -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + TV = "tv" + SPEAKER = "speaker" + RECEIVER = "receiver" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass)) + + +# DEVICE_CLASS* below are deprecated as of 2021.12 +# use the MediaPlayerDeviceClass enum instead. +DEVICE_CLASSES = [cls.value for cls in MediaPlayerDeviceClass] +DEVICE_CLASS_TV = MediaPlayerDeviceClass.TV.value +DEVICE_CLASS_SPEAKER = MediaPlayerDeviceClass.SPEAKER.value +DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { @@ -373,6 +385,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class MediaPlayerEntityDescription(EntityDescription): """A class that describes media player entities.""" + device_class: MediaPlayerDeviceClass | str | None = None + class MediaPlayerEntity(Entity): """ABC for media player entities.""" @@ -382,6 +396,7 @@ class MediaPlayerEntity(Entity): _attr_app_id: str | None = None _attr_app_name: str | None = None + _attr_device_class: MediaPlayerDeviceClass | str | None _attr_group_members: list[str] | None = None _attr_is_volume_muted: bool | None = None _attr_media_album_artist: str | None = None @@ -413,6 +428,15 @@ class MediaPlayerEntity(Entity): _attr_volume_level: float | None = None # Implement these for your media player + @property + def device_class(self) -> MediaPlayerDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property def state(self) -> str | None: """State of the player.""" From 44714081d190b91f0688eaa70c074d1ccb0c6aa5 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 1 Dec 2021 14:41:52 +0800 Subject: [PATCH 1068/1452] Update IZone to new version of library (#60676) --- homeassistant/components/izone/climate.py | 8 ++++++-- homeassistant/components/izone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 67d121d760e..db47b087c0d 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -209,7 +209,8 @@ class ControllerDevice(ClimateEntity): return self.async_write_ha_state() for zone in self.zones.values(): - zone.async_schedule_update_ha_state() + if zone.hass is not None: + zone.async_schedule_update_ha_state() self.async_on_remove( async_dispatcher_connect( @@ -244,7 +245,8 @@ class ControllerDevice(ClimateEntity): self._available = available self.async_write_ha_state() for zone in self.zones.values(): - zone.async_schedule_update_ha_state() + if zone.hass is not None: + zone.async_schedule_update_ha_state() @property def unique_id(self): @@ -495,6 +497,8 @@ class ZoneDevice(ClimateEntity): """Handle zone data updates.""" if zone is not self._zone: return + if not self.available: + return self._name = zone.name.title() self.async_write_ha_state() diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 6da770f5c0b..82927fef795 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,7 +2,7 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.6"], + "requirements": ["python-izone==1.1.8"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 707d59ba468..d77c0a04207 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1892,7 +1892,7 @@ python-gitlab==1.6.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.1.6 +python-izone==1.1.8 # homeassistant.components.joaoapps_join python-join-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fec3518e71..1f2037ba864 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1136,7 +1136,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.izone -python-izone==1.1.6 +python-izone==1.1.8 # homeassistant.components.juicenet python-juicenet==1.0.2 From 908b7ca9c29908fd79c67fd621f60650d97ad9b9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 1 Dec 2021 08:08:59 +0100 Subject: [PATCH 1069/1452] Bump xiaomi_miio dependency (#60650) --- homeassistant/components/xiaomi_miio/__init__.py | 6 +++--- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 12 +++++++++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9d983d32252..62935f5e38b 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -26,8 +26,8 @@ from miio import ( FanP10, FanP11, FanZA5, + RoborockVacuum, Timer, - Vacuum, VacuumStatus, ) from miio.gateway.gateway import GatewayException @@ -212,7 +212,7 @@ class VacuumCoordinatorDataAttributes: fan_speeds_reverse: str = "fan_speeds_reverse" -def _async_update_data_vacuum(hass, device: Vacuum): +def _async_update_data_vacuum(hass, device: RoborockVacuum): def update() -> VacuumCoordinatorData: timer = [] @@ -313,7 +313,7 @@ async def async_create_miio_device_and_coordinator( or model.startswith(ROBOROCK_GENERIC) or model.startswith(ROCKROBO_GENERIC) ): - device = Vacuum(host, token) + device = RoborockVacuum(host, token) update_method = _async_update_data_vacuum coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData] # Pedestal fans diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 37c6b8f8a09..0556b6f3ea5 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.4", "python-miio==0.5.8"], + "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index d77c0a04207..86a88c3b915 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ python-kasa==0.4.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.8 +python-miio==0.5.9 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f2037ba864..00b958e67a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1145,7 +1145,7 @@ python-juicenet==1.0.2 python-kasa==0.4.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.8 +python-miio==0.5.9 # homeassistant.components.nest python-nest==4.1.0 diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index f2fef4bba4b..6c8b808eb94 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -115,7 +115,9 @@ def mirobo_is_got_error_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: + with patch( + "homeassistant.components.xiaomi_miio.RoborockVacuum" + ) as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -149,7 +151,9 @@ def mirobo_old_speeds_fixture(request): 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC ) - with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: + with patch( + "homeassistant.components.xiaomi_miio.RoborockVacuum" + ) as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -209,7 +213,9 @@ def mirobo_is_on_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: + with patch( + "homeassistant.components.xiaomi_miio.RoborockVacuum" + ) as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum From 12ff5dee7400516ff02dfd7c080ed756f6f802d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 08:09:55 +0100 Subject: [PATCH 1070/1452] Migrate sensor device classes to StrEnum (#60654) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- homeassistant/components/demo/sensor.py | 26 ++-- homeassistant/components/sensor/__init__.py | 139 +++++++++++++++----- homeassistant/const.py | 12 +- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 413017ad2f1..ec9cc648e1a 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -3,17 +3,15 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -40,7 +38,7 @@ async def async_setup_platform( "sensor_1", "Outside Temperature", 15.6, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, STATE_CLASS_MEASUREMENT, TEMP_CELSIUS, 12, @@ -49,7 +47,7 @@ async def async_setup_platform( "sensor_2", "Outside Humidity", 54, - DEVICE_CLASS_HUMIDITY, + SensorDeviceClass.HUMIDITY, STATE_CLASS_MEASUREMENT, PERCENTAGE, None, @@ -58,7 +56,7 @@ async def async_setup_platform( "sensor_3", "Carbon monoxide", 54, - DEVICE_CLASS_CO, + SensorDeviceClass.CO, STATE_CLASS_MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, None, @@ -67,7 +65,7 @@ async def async_setup_platform( "sensor_4", "Carbon dioxide", 54, - DEVICE_CLASS_CO2, + SensorDeviceClass.CO2, STATE_CLASS_MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, 14, @@ -76,7 +74,7 @@ async def async_setup_platform( "sensor_5", "Power consumption", 100, - DEVICE_CLASS_POWER, + SensorDeviceClass.POWER, STATE_CLASS_MEASUREMENT, POWER_WATT, None, @@ -85,7 +83,7 @@ async def async_setup_platform( "sensor_6", "Today energy", 15, - DEVICE_CLASS_ENERGY, + SensorDeviceClass.ENERGY, STATE_CLASS_MEASUREMENT, ENERGY_KILO_WATT_HOUR, None, @@ -113,7 +111,7 @@ class DemoSensor(SensorEntity): unique_id: str, name: str, state: StateType, - device_class: str | None, + device_class: SensorDeviceClass, state_class: str | None, unit_of_measurement: str | None, battery: StateType, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index b91b4b3051e..3abe4371bfe 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -13,7 +13,7 @@ import ciso8601 import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, @@ -53,6 +53,7 @@ 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, StateType +from homeassistant.util.enum import StrEnum from .const import CONF_STATE_CLASS # noqa: F401 @@ -66,38 +67,101 @@ DOMAIN: Final = "sensor" 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 - DEVICE_CLASS_CURRENT, # current (A) - DEVICE_CLASS_DATE, # date (ISO8601) - DEVICE_CLASS_ENERGY, # energy (kWh, Wh) - DEVICE_CLASS_FREQUENCY, # frequency (Hz, kHz, MHz, GHz) - 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 N2O (µg/m³) - DEVICE_CLASS_NITROGEN_MONOXIDE, # Amount of NO (µ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_VOLATILE_ORGANIC_COMPOUNDS, # Amount of VOC (µg/m³) - DEVICE_CLASS_GAS, # gas (m³ or ft³) -] -DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + +class SensorDeviceClass(StrEnum): + """Device class for sensors.""" + + # Air Quality Index + AQI = "aqi" + + # % of battery that is left + BATTERY = "battery" + + # ppm (parts per million) Carbon Monoxide gas concentration + CO = "carbon_monoxide" + + # ppm (parts per million) Carbon Dioxide gas concentration + CO2 = "carbon_dioxide" + + # current (A) + CURRENT = "current" + + # date (ISO8601) + DATE = "date" + + # energy (kWh, Wh) + ENERGY = "energy" + + # frequency (Hz, kHz, MHz, GHz) + FREQUENCY = "frequency" + + # gas (m³ or ft³) + GAS = "gas" + + # % of humidity in the air + HUMIDITY = "humidity" + + # current light level (lx/lm) + ILLUMINANCE = "illuminance" + + # Amount of money (currency) + MONETARY = "monetary" + + # Amount of NO2 (µg/m³) + NITROGEN_DIOXIDE = "nitrogen_dioxide" + + # Amount of NO (µg/m³) + NITROGEN_MONOXIDE = "nitrogen_monoxide" + + # Amount of N2O (µg/m³) + NITROUS_OXIDE = "nitrous_oxide" + + # Amount of O3 (µg/m³) + OZONE = "ozone" + + # Particulate matter <= 0.1 μm (µg/m³) + PM1 = "pm1" + + # Particulate matter <= 10 μm (µg/m³) + PM10 = "pm10" + + # Particulate matter <= 2.5 μm (µg/m³) + PM25 = "pm25" + + # power factor (%) + POWER_FACTOR = "power_factor" + + # power (W/kW) + POWER = "power" + + # pressure (hPa/mbar) + PRESSURE = "pressure" + + # signal strength (dB/dBm) + SIGNAL_STRENGTH = "signal_strength" + + # Amount of SO2 (µg/m³) + SULPHUR_DIOXIDE = "sulphur_dioxide" + + # temperature (C/F) + TEMPERATURE = "temperature" + + # timestamp (ISO8601) + TIMESTAMP = "timestamp" + + # Amount of VOC (µg/m³) + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + + # voltage (V) + VOLTAGE = "voltage" + + +DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) + +# DEVICE_CLASSES is deprecated as of 2021.12 +# use the SensorDeviceClass enum instead. +DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" @@ -141,6 +205,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" + device_class: SensorDeviceClass | str | None = None last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 native_unit_of_measurement: str | None = None state_class: str | None = None @@ -172,6 +237,7 @@ class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription + _attr_device_class: SensorDeviceClass | str | None _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 _attr_native_unit_of_measurement: str | None _attr_native_value: StateType | date | datetime = None @@ -186,6 +252,15 @@ class SensorEntity(Entity): # Temporary private attribute to track if deprecation has been logged. __datetime_as_string_deprecation_logged = False + @property + def device_class(self) -> SensorDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property def state_class(self) -> str | None: """Return the state class of this entity, from STATE_CLASSES, if any.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 45a88e2fe43..2ed777c12af 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -233,6 +233,8 @@ EVENT_TIME_CHANGED: Final = "time_changed" # #### DEVICE CLASSES #### +# DEVICE_CLASS_* below are deprecated as of 2021.12 +# use the SensorDeviceClass enum instead. DEVICE_CLASS_AQI: Final = "aqi" DEVICE_CLASS_BATTERY: Final = "battery" DEVICE_CLASS_CO: Final = "carbon_monoxide" @@ -241,6 +243,7 @@ DEVICE_CLASS_CURRENT: Final = "current" DEVICE_CLASS_DATE: Final = "date" DEVICE_CLASS_ENERGY: Final = "energy" DEVICE_CLASS_FREQUENCY: Final = "frequency" +DEVICE_CLASS_GAS: Final = "gas" DEVICE_CLASS_HUMIDITY: Final = "humidity" DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" DEVICE_CLASS_MONETARY: Final = "monetary" @@ -248,19 +251,18 @@ 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_PM25: Final = "pm25" +DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" +DEVICE_CLASS_POWER: Final = "power" 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_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" -DEVICE_CLASS_GAS: Final = "gas" +DEVICE_CLASS_VOLTAGE: Final = "voltage" # #### STATES #### STATE_ON: Final = "on" From 2b8f245e278593897b390a68e1346b2fc4637811 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 08:12:09 +0100 Subject: [PATCH 1071/1452] Migrate binary sensor device classes to StrEnum (#60651) --- .../components/binary_sensor/__init__.py | 187 ++++++++++-------- .../components/demo/binary_sensor.py | 20 +- homeassistant/components/sia/binary_sensor.py | 18 +- .../components/sia/sia_entity_base.py | 4 +- .../components/upcloud/binary_sensor.py | 8 +- 5 files changed, 135 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index aff7f9a3135..c9301eb7978 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -18,6 +18,7 @@ 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, StateType +from homeassistant.util.enum import StrEnum _LOGGER = logging.getLogger(__name__) @@ -26,118 +27,124 @@ SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" -# On means low, Off means normal -DEVICE_CLASS_BATTERY = "battery" -# On means charging, Off means not charging -DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" +class BinarySensorDeviceClass(StrEnum): + """Device class for binary sensors.""" -# On means cold, Off means normal -DEVICE_CLASS_COLD = "cold" + # On means low, Off means normal + BATTERY = "battery" -# On means connected, Off means disconnected -DEVICE_CLASS_CONNECTIVITY = "connectivity" + # On means charging, Off means not charging + BATTERY_CHARGING = "battery_charging" -# On means open, Off means closed -DEVICE_CLASS_DOOR = "door" + # On means cold, Off means normal + COLD = "cold" -# On means open, Off means closed -DEVICE_CLASS_GARAGE_DOOR = "garage_door" + # On means connected, Off means disconnected + CONNECTIVITY = "connectivity" -# On means gas detected, Off means no gas (clear) -DEVICE_CLASS_GAS = "gas" + # On means open, Off means closed + DOOR = "door" -# On means hot, Off means normal -DEVICE_CLASS_HEAT = "heat" + # On means open, Off means closed + GARAGE_DOOR = "garage_door" -# On means light detected, Off means no light -DEVICE_CLASS_LIGHT = "light" + # On means gas detected, Off means no gas (clear) + GAS = "gas" -# On means open (unlocked), Off means closed (locked) -DEVICE_CLASS_LOCK = "lock" + # On means hot, Off means normal + HEAT = "heat" -# On means wet, Off means dry -DEVICE_CLASS_MOISTURE = "moisture" + # On means light detected, Off means no light + LIGHT = "light" -# On means motion detected, Off means no motion (clear) -DEVICE_CLASS_MOTION = "motion" + # On means open (unlocked), Off means closed (locked) + LOCK = "lock" -# On means moving, Off means not moving (stopped) -DEVICE_CLASS_MOVING = "moving" + # On means wet, Off means dry + MOISTURE = "moisture" -# On means occupied, Off means not occupied (clear) -DEVICE_CLASS_OCCUPANCY = "occupancy" + # On means motion detected, Off means no motion (clear) + MOTION = "motion" -# On means open, Off means closed -DEVICE_CLASS_OPENING = "opening" + # On means moving, Off means not moving (stopped) + MOVING = "moving" -# On means plugged in, Off means unplugged -DEVICE_CLASS_PLUG = "plug" + # On means occupied, Off means not occupied (clear) + OCCUPANCY = "occupancy" -# On means power detected, Off means no power -DEVICE_CLASS_POWER = "power" + # On means open, Off means closed + OPENING = "opening" -# On means home, Off means away -DEVICE_CLASS_PRESENCE = "presence" + # On means plugged in, Off means unplugged + PLUG = "plug" -# On means problem detected, Off means no problem (OK) -DEVICE_CLASS_PROBLEM = "problem" + # On means power detected, Off means no power + POWER = "power" -# On means running, Off means not running -DEVICE_CLASS_RUNNING = "running" + # On means home, Off means away + PRESENCE = "presence" -# On means unsafe, Off means safe -DEVICE_CLASS_SAFETY = "safety" + # On means problem detected, Off means no problem (OK) + PROBLEM = "problem" -# On means smoke detected, Off means no smoke (clear) -DEVICE_CLASS_SMOKE = "smoke" + # On means running, Off means not running + RUNNING = "running" -# On means sound detected, Off means no sound (clear) -DEVICE_CLASS_SOUND = "sound" + # On means unsafe, Off means safe + SAFETY = "safety" -# On means tampering detected, Off means no tampering (clear) -DEVICE_CLASS_TAMPER = "tamper" + # On means smoke detected, Off means no smoke (clear) + SMOKE = "smoke" -# On means update available, Off means up-to-date -DEVICE_CLASS_UPDATE = "update" + # On means sound detected, Off means no sound (clear) + SOUND = "sound" -# On means vibration detected, Off means no vibration -DEVICE_CLASS_VIBRATION = "vibration" + # On means tampering detected, Off means no tampering (clear) + TAMPER = "tamper" -# On means open, Off means closed -DEVICE_CLASS_WINDOW = "window" + # On means update available, Off means up-to-date + UPDATE = "update" -DEVICE_CLASSES = [ - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_COLD, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_RUNNING, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_TAMPER, - DEVICE_CLASS_UPDATE, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_WINDOW, -] + # On means vibration detected, Off means no vibration + VIBRATION = "vibration" -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + # On means open, Off means closed + WINDOW = "window" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)) + +# DEVICE_CLASS* below are deprecated as of 2021.12 +# use the BinarySensorDeviceClass enum instead. +DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass] +DEVICE_CLASS_BATTERY = BinarySensorDeviceClass.BATTERY.value +DEVICE_CLASS_BATTERY_CHARGING = BinarySensorDeviceClass.BATTERY_CHARGING.value +DEVICE_CLASS_COLD = BinarySensorDeviceClass.COLD.value +DEVICE_CLASS_CONNECTIVITY = BinarySensorDeviceClass.CONNECTIVITY.value +DEVICE_CLASS_DOOR = BinarySensorDeviceClass.DOOR.value +DEVICE_CLASS_GARAGE_DOOR = BinarySensorDeviceClass.GARAGE_DOOR.value +DEVICE_CLASS_GAS = BinarySensorDeviceClass.GAS.value +DEVICE_CLASS_HEAT = BinarySensorDeviceClass.HEAT.value +DEVICE_CLASS_LIGHT = BinarySensorDeviceClass.LIGHT.value +DEVICE_CLASS_LOCK = BinarySensorDeviceClass.LOCK.value +DEVICE_CLASS_MOISTURE = BinarySensorDeviceClass.MOISTURE.value +DEVICE_CLASS_MOTION = BinarySensorDeviceClass.MOTION.value +DEVICE_CLASS_MOVING = BinarySensorDeviceClass.MOVING.value +DEVICE_CLASS_OCCUPANCY = BinarySensorDeviceClass.OCCUPANCY.value +DEVICE_CLASS_OPENING = BinarySensorDeviceClass.OPENING.value +DEVICE_CLASS_PLUG = BinarySensorDeviceClass.PLUG.value +DEVICE_CLASS_POWER = BinarySensorDeviceClass.POWER.value +DEVICE_CLASS_PRESENCE = BinarySensorDeviceClass.PRESENCE.value +DEVICE_CLASS_PROBLEM = BinarySensorDeviceClass.PROBLEM.value +DEVICE_CLASS_RUNNING = BinarySensorDeviceClass.RUNNING.value +DEVICE_CLASS_SAFETY = BinarySensorDeviceClass.SAFETY.value +DEVICE_CLASS_SMOKE = BinarySensorDeviceClass.SMOKE.value +DEVICE_CLASS_SOUND = BinarySensorDeviceClass.SOUND.value +DEVICE_CLASS_TAMPER = BinarySensorDeviceClass.TAMPER.value +DEVICE_CLASS_UPDATE = BinarySensorDeviceClass.UPDATE.value +DEVICE_CLASS_VIBRATION = BinarySensorDeviceClass.VIBRATION.value +DEVICE_CLASS_WINDOW = BinarySensorDeviceClass.WINDOW.value async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -166,14 +173,26 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class BinarySensorEntityDescription(EntityDescription): """A class that describes binary sensor entities.""" + device_class: BinarySensorDeviceClass | str | None = None + class BinarySensorEntity(Entity): """Represent a binary sensor.""" entity_description: BinarySensorEntityDescription + _attr_device_class: BinarySensorDeviceClass | str | None _attr_is_on: bool | None = None _attr_state: None = None + @property + def device_class(self) -> BinarySensorDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 7cf96d33f5c..8710308fc67 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,7 +1,6 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers.entity import DeviceInfo @@ -14,10 +13,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities( [ DemoBinarySensor( - "binary_1", "Basement Floor Wet", False, DEVICE_CLASS_MOISTURE + "binary_1", + "Basement Floor Wet", + False, + BinarySensorDeviceClass.MOISTURE, ), DemoBinarySensor( - "binary_2", "Movement Backyard", True, DEVICE_CLASS_MOTION + "binary_2", "Movement Backyard", True, BinarySensorDeviceClass.MOTION ), ] ) @@ -31,7 +33,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoBinarySensor(BinarySensorEntity): """representation of a Demo binary sensor.""" - def __init__(self, unique_id, name, state, device_class): + def __init__( + self, + unique_id: str, + name: str, + state: bool, + device_class: BinarySensorDeviceClass, + ) -> None: """Initialize the demo sensor.""" self._unique_id = unique_id self._name = name @@ -55,7 +63,7 @@ class DemoBinarySensor(BinarySensorEntity): return self._unique_id @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return self._sensor_type diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index eec4f9b2717..980596367b2 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -8,9 +8,7 @@ from typing import Any from pysiaalarm import SIAEvent from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -81,11 +79,11 @@ class SIABinarySensorBase(SIABaseEntity, BinarySensorEntity): entry: ConfigEntry, account_data: dict[str, Any], zone: int, - device_class: str, + device_class: BinarySensorDeviceClass, ) -> None: """Initialize a base binary sensor.""" - super().__init__(entry, account_data, zone, device_class) - + super().__init__(entry, account_data, zone) + self._attr_device_class = device_class self._attr_unique_id = SIA_UNIQUE_ID_FORMAT_BINARY.format( self._entry.entry_id, self._account, self._zone, self._attr_device_class ) @@ -111,7 +109,7 @@ class SIABinarySensorMoisture(SIABinarySensorBase): zone: int, ) -> None: """Initialize a Moisture binary sensor.""" - super().__init__(entry, account_data, zone, DEVICE_CLASS_MOISTURE) + super().__init__(entry, account_data, zone, BinarySensorDeviceClass.MOISTURE) self._attr_entity_registry_enabled_default = False def update_state(self, sia_event: SIAEvent) -> None: @@ -132,7 +130,7 @@ class SIABinarySensorSmoke(SIABinarySensorBase): zone: int, ) -> None: """Initialize a Smoke binary sensor.""" - super().__init__(entry, account_data, zone, DEVICE_CLASS_SMOKE) + super().__init__(entry, account_data, zone, BinarySensorDeviceClass.SMOKE) self._attr_entity_registry_enabled_default = False def update_state(self, sia_event: SIAEvent) -> None: @@ -152,7 +150,9 @@ class SIABinarySensorPower(SIABinarySensorBase): account_data: dict[str, Any], ) -> None: """Initialize a Power binary sensor.""" - super().__init__(entry, account_data, SIA_HUB_ZONE, DEVICE_CLASS_POWER) + super().__init__( + entry, account_data, SIA_HUB_ZONE, BinarySensorDeviceClass.POWER + ) self._attr_entity_registry_enabled_default = True def update_state(self, sia_event: SIAEvent) -> None: diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 14334d1f8cb..f4937e326eb 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -29,13 +29,13 @@ class SIABaseEntity(RestoreEntity): entry: ConfigEntry, account_data: dict[str, Any], zone: int, - device_class: str, + device_class: str | None = None, ) -> None: """Create SIABaseEntity object.""" self._entry: ConfigEntry = entry self._account_data: dict[str, Any] = account_data self._zone: int = zone - self._attr_device_class: str = device_class + self._attr_device_class = device_class self._port: int = self._entry.data[CONF_PORT] self._account: str = self._account_data[CONF_ACCOUNT] diff --git a/homeassistant/components/upcloud/binary_sensor.py b/homeassistant/components/upcloud/binary_sensor.py index de55a577610..ebdc30b69f1 100644 --- a/homeassistant/components/upcloud/binary_sensor.py +++ b/homeassistant/components/upcloud/binary_sensor.py @@ -2,7 +2,11 @@ import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, + BinarySensorDeviceClass, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant @@ -29,3 +33,5 @@ async def async_setup_entry( class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorEntity): """Representation of an UpCloud server sensor.""" + + _attr_device_class = BinarySensorDeviceClass.POWER From 19361216dea8caa6dff267477561889ca0e5e3aa Mon Sep 17 00:00:00 2001 From: LJU Date: Wed, 1 Dec 2021 08:38:38 +0100 Subject: [PATCH 1072/1452] Fix key reference placeholder (#60681) Fix typo for placeholder invalid acces token --- homeassistant/components/nest/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 4dd3e5419b0..1d3dfda1708 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -53,7 +53,7 @@ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]" + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" From cc8e02c733a0f96870ec3f896ad06bbf64167ff9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 08:43:37 +0100 Subject: [PATCH 1073/1452] Upgrade pre-commit to 2.16.0 (#60680) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6184e2dc224..6f580cb5159 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.1.0 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 -pre-commit==2.15.0 +pre-commit==2.16.0 pylint==2.12.1 pipdeptree==2.2.0 pylint-strict-informational==0.1 From 8240b8c72e9add4e85ca037852f57f1cc00a5f0b Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 1 Dec 2021 00:19:01 -0800 Subject: [PATCH 1074/1452] Update screenlogic use asyncio API (#60466) Co-authored-by: J. Nick Koston --- .../components/screenlogic/__init__.py | 130 +++++++++--------- .../components/screenlogic/climate.py | 29 ++-- .../components/screenlogic/config_flow.py | 16 +-- homeassistant/components/screenlogic/const.py | 2 - .../components/screenlogic/manifest.json | 2 +- .../components/screenlogic/services.py | 14 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../screenlogic/test_config_flow.py | 37 +---- 9 files changed, 88 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 894a8a5e527..bc7761857d5 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -1,5 +1,4 @@ """The Screenlogic integration.""" -import asyncio from datetime import timedelta import logging @@ -11,6 +10,7 @@ from screenlogicpy.const import ( SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT, + ScreenLogicWarning, ) from homeassistant.config_entries import ConfigEntry @@ -20,7 +20,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -28,35 +28,35 @@ from homeassistant.helpers.update_coordinator import ( ) from .config_flow import async_discover_gateways_by_unique_id, name_for_mac -from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN from .services import async_load_screenlogic_services, async_unload_screenlogic_services _LOGGER = logging.getLogger(__name__) -REQUEST_REFRESH_DELAY = 1 +REQUEST_REFRESH_DELAY = 2 +HEATER_COOLDOWN_DELAY = 6 -PLATFORMS = ["switch", "sensor", "binary_sensor", "climate", "light"] +# These seem to be constant across all controller models +PRIMARY_CIRCUIT_IDS = [500, 505] # [Spa, Pool] - -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) - return True +PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" + connect_info = await async_get_connect_info(hass, entry) - gateway = await hass.async_add_executor_job(get_new_gateway, hass, entry) + gateway = ScreenLogicGateway(**connect_info) - # The api library uses a shared socket connection and does not handle concurrent - # requests very well. - api_lock = asyncio.Lock() + try: + await gateway.async_connect() + except ScreenLogicError as ex: + _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) + raise ConfigEntryNotReady from ex coordinator = ScreenlogicDataUpdateCoordinator( - hass, config_entry=entry, gateway=gateway, api_lock=api_lock + hass, config_entry=entry, gateway=gateway ) async_load_screenlogic_services(hass) @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(async_update_listener)) - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -75,8 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) - hass.data[DOMAIN][entry.entry_id]["listener"]() if unload_ok: + coordinator = hass.data[DOMAIN][entry.entry_id] + await coordinator.gateway.async_disconnect() hass.data[DOMAIN].pop(entry.entry_id) async_unload_screenlogic_services(hass) @@ -89,11 +90,11 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -def get_connect_info(hass: HomeAssistant, entry: ConfigEntry): +async def async_get_connect_info(hass: HomeAssistant, entry: ConfigEntry): """Construct connect_info from configuration entry and returns it to caller.""" mac = entry.unique_id - # Attempt to re-discover named gateway to follow IP changes - discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] + # Attempt to rediscover gateway to follow IP changes + discovered_gateways = await async_discover_gateways_by_unique_id(hass) if mac in discovered_gateways: connect_info = discovered_gateways[mac] else: @@ -108,28 +109,13 @@ def get_connect_info(hass: HomeAssistant, entry: ConfigEntry): return connect_info -def get_new_gateway(hass: HomeAssistant, entry: ConfigEntry): - """Instantiate a new ScreenLogicGateway, connect to it and return it to caller.""" - - connect_info = get_connect_info(hass, entry) - - try: - gateway = ScreenLogicGateway(**connect_info) - except ScreenLogicError as ex: - _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) - raise ConfigEntryNotReady from ex - - return gateway - - class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage the data update for the Screenlogic component.""" - def __init__(self, hass, *, config_entry, gateway, api_lock): + def __init__(self, hass, *, config_entry, gateway): """Initialize the Screenlogic Data Update Coordinator.""" self.config_entry = config_entry self.gateway = gateway - self.api_lock = api_lock self.screenlogic_data = {} interval = timedelta( @@ -140,41 +126,39 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_interval=interval, - # We don't want an immediate refresh since the device - # takes a moment to reflect the state change + # Debounced option since the device takes + # a moment to reflect the knock-on changes request_refresh_debouncer=Debouncer( hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False ), ) - def reconnect_gateway(self): - """Instantiate a new ScreenLogicGateway, connect to it and update. Return new gateway to caller.""" - - connect_info = get_connect_info(self.hass, self.config_entry) - - try: - gateway = ScreenLogicGateway(**connect_info) - gateway.update() - except ScreenLogicError as error: - raise UpdateFailed(error) from error - - return gateway - async def _async_update_data(self): """Fetch data from the Screenlogic gateway.""" try: - async with self.api_lock: - await self.hass.async_add_executor_job(self.gateway.update) + await self.gateway.async_update() except ScreenLogicError as error: - _LOGGER.warning("ScreenLogicError - attempting reconnect: %s", error) - - async with self.api_lock: - self.gateway = await self.hass.async_add_executor_job( - self.reconnect_gateway - ) + _LOGGER.warning("Update error - attempting reconnect: %s", error) + await self._async_reconnect_update_data() + except ScreenLogicWarning as warn: + raise UpdateFailed(f"Incomplete update: {warn}") from warn return self.gateway.get_data() + async def _async_reconnect_update_data(self): + """Attempt to reconnect to the gateway and fetch data.""" + try: + # Clean up the previous connection as we're about to create a new one + await self.gateway.async_disconnect() + + connect_info = await async_get_connect_info(self.hass, self.config_entry) + self.gateway = ScreenLogicGateway(**connect_info) + + await self.gateway.async_update() + + except (ScreenLogicError, ScreenLogicWarning) as ex: + raise UpdateFailed(ex) from ex + class ScreenlogicEntity(CoordinatorEntity): """Base class for all ScreenLogic entities.""" @@ -233,6 +217,17 @@ class ScreenlogicEntity(CoordinatorEntity): name=self.gateway_name, ) + async def _async_refresh(self): + """Refresh the data from the gateway.""" + await self.coordinator.async_refresh() + # Second debounced refresh to catch any secondary + # changes in the device + await self.coordinator.async_request_refresh() + + async def _async_refresh_timed(self, now): + """Refresh from a timed called.""" + await self.coordinator.async_request_refresh() + class ScreenLogicCircuitEntity(ScreenlogicEntity): """ScreenLogic circuit entity.""" @@ -255,15 +250,18 @@ class ScreenLogicCircuitEntity(ScreenlogicEntity): """Send the OFF command.""" await self._async_set_circuit(ON_OFF.OFF) - async def _async_set_circuit(self, circuit_value) -> None: - async with self.coordinator.api_lock: - success = await self.hass.async_add_executor_job( - self.gateway.set_circuit, self._data_key, circuit_value + # Turning off spa or pool circuit may require more time for the + # heater to reflect changes depending on the pool controller, + # so we schedule an extra refresh a bit farther out + if self._data_key in PRIMARY_CIRCUIT_IDS: + async_call_later( + self.hass, HEATER_COOLDOWN_DELAY, self._async_refresh_timed ) - if success: + async def _async_set_circuit(self, circuit_value) -> None: + if await self.gateway.async_set_circuit(self._data_key, circuit_value): _LOGGER.debug("Turn %s %s", self._data_key, circuit_value) - await self.coordinator.async_request_refresh() + await self._async_refresh() else: _LOGGER.warning( "Failed to set_circuit %s %s", self._data_key, circuit_value diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index dc9e185ca9f..65200bb7dda 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -138,13 +138,10 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") - async with self.coordinator.api_lock: - success = await self.hass.async_add_executor_job( - self.gateway.set_heat_temp, int(self._data_key), int(temperature) - ) - - if success: - await self.coordinator.async_request_refresh() + if await self.gateway.async_set_heat_temp( + int(self._data_key), int(temperature) + ): + await self._async_refresh() else: raise HomeAssistantError( f"Failed to set_temperature {temperature} on body {self.body['body_type']['value']}" @@ -157,13 +154,8 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): else: mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode] - async with self.coordinator.api_lock: - success = await self.hass.async_add_executor_job( - self.gateway.set_heat_mode, int(self._data_key), int(mode) - ) - - if success: - await self.coordinator.async_request_refresh() + if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)): + await self._async_refresh() else: raise HomeAssistantError( f"Failed to set_hvac_mode {mode} on body {self.body['body_type']['value']}" @@ -176,13 +168,8 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): if self.hvac_mode == HVAC_MODE_OFF: return - async with self.coordinator.api_lock: - success = await self.hass.async_add_executor_job( - self.gateway.set_heat_mode, int(self._data_key), int(mode) - ) - - if success: - await self.coordinator.async_request_refresh() + if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)): + await self._async_refresh() else: raise HomeAssistantError( f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}" diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index e8f2fca1be5..ccb2dd38bb8 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -56,18 +56,6 @@ def name_for_mac(mac): return f"Pentair: {short_mac(mac)}" -async def async_get_mac_address(hass, ip_address, port): - """Connect to a screenlogic gateway and return the mac address.""" - connected_socket = await hass.async_add_executor_job( - login.create_socket, - ip_address, - port, - ) - if not connected_socket: - raise ScreenLogicError("Unknown socket error") - return await hass.async_add_executor_job(login.gateway_connect, connected_socket) - - class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow to setup screen logic devices.""" @@ -155,9 +143,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ip_address = user_input[CONF_IP_ADDRESS] port = user_input[CONF_PORT] try: - mac = format_mac( - await async_get_mac_address(self.hass, ip_address, port) - ) + mac = format_mac(await login.async_get_mac_address(ip_address, port)) except ScreenLogicError as ex: _LOGGER.debug(ex) errors[CONF_IP_ADDRESS] = "cannot_connect" diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index 2a1a3c23d2e..bfa9d09cab8 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -14,5 +14,3 @@ SUPPORTED_COLOR_MODES = { } LIGHT_CIRCUIT_FUNCTIONS = {CIRCUIT_FUNCTION.INTELLIBRITE, CIRCUIT_FUNCTION.LIGHT} - -DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index abef9ec99ed..b6134216049 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.4.1"], + "requirements": ["screenlogicpy==0.5.3"], "codeowners": ["@dieselrabbit"], "dhcp": [ { diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index d5edda12abb..c046a4478fe 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -59,13 +59,13 @@ def async_load_screenlogic_services(hass: HomeAssistant): color_num, ) try: - async with coordinator.api_lock: - if not await hass.async_add_executor_job( - coordinator.gateway.set_color_lights, color_num - ): - raise HomeAssistantError( - f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" - ) + if not await coordinator.gateway.async_set_color_lights(color_num): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" + ) + # Debounced refresh to catch any secondary + # changes in the device + await coordinator.async_request_refresh() except ScreenLogicError as error: raise HomeAssistantError(error) from error diff --git a/requirements_all.txt b/requirements_all.txt index 86a88c3b915..21e951ed1db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2115,7 +2115,7 @@ scapy==2.4.5 schiene==0.23 # homeassistant.components.screenlogic -screenlogicpy==0.4.1 +screenlogicpy==0.5.3 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00b958e67a5..3978378f604 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1257,7 +1257,7 @@ samsungtvws==1.6.0 scapy==2.4.5 # homeassistant.components.screenlogic -screenlogicpy==0.4.1 +screenlogicpy==0.5.3 # homeassistant.components.emulated_kasa # homeassistant.components.sense diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index 375bcddac24..1bb441f145c 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -50,8 +50,6 @@ async def test_flow_discovery(hass): assert result["step_id"] == "gateway_select" with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -66,7 +64,6 @@ async def test_flow_discovery(hass): CONF_IP_ADDRESS: "1.1.1.1", CONF_PORT: 80, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -102,15 +99,10 @@ async def test_flow_discover_error(hass): assert result["step_id"] == "gateway_entry" with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.create_socket", - return_value=True, - ), patch( - "homeassistant.components.screenlogic.config_flow.login.gateway_connect", + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", return_value="00-C0-33-01-01-01", ): result3 = await hass.config_entries.flow.async_configure( @@ -128,7 +120,6 @@ async def test_flow_discover_error(hass): CONF_IP_ADDRESS: "1.1.1.1", CONF_PORT: 80, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -149,15 +140,10 @@ async def test_dhcp(hass): assert result["step_id"] == "gateway_entry" with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.create_socket", - return_value=True, - ), patch( - "homeassistant.components.screenlogic.config_flow.login.gateway_connect", + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", return_value="00-C0-33-01-01-01", ): result3 = await hass.config_entries.flow.async_configure( @@ -175,7 +161,6 @@ async def test_dhcp(hass): CONF_IP_ADDRESS: "1.1.1.1", CONF_PORT: 80, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -210,15 +195,10 @@ async def test_form_manual_entry(hass): assert result2["step_id"] == "gateway_entry" with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.create_socket", - return_value=True, - ), patch( - "homeassistant.components.screenlogic.config_flow.login.gateway_connect", + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", return_value="00-C0-33-01-01-01", ): result3 = await hass.config_entries.flow.async_configure( @@ -236,7 +216,6 @@ async def test_form_manual_entry(hass): CONF_IP_ADDRESS: "1.1.1.1", CONF_PORT: 80, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -251,8 +230,8 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.screenlogic.config_flow.login.create_socket", - return_value=None, + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", + side_effect=ScreenLogicError("Failed to connect to host at 1.1.1.1:80"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -272,8 +251,6 @@ async def test_option_flow(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ), patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ): @@ -299,8 +276,6 @@ async def test_option_flow_defaults(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ), patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ): @@ -327,8 +302,6 @@ async def test_option_flow_input_floor(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.screenlogic.async_setup", return_value=True - ), patch( "homeassistant.components.screenlogic.async_setup_entry", return_value=True, ): From 73a4dba2ae6073cd3e37b37fa253050f30d896eb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 09:28:19 +0100 Subject: [PATCH 1075/1452] Use dataclass properties in yeelight discovery (#60640) Co-authored-by: epenet --- homeassistant/components/yeelight/config_flow.py | 16 ++++++++-------- homeassistant/components/yeelight/manifest.json | 3 ++- tests/components/yeelight/__init__.py | 16 ++++++++++------ tests/components/yeelight/test_config_flow.py | 3 ++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index ba1ab4d24c7..0419824492a 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -8,7 +8,7 @@ from yeelight.aio import AsyncBulb from yeelight.main import get_known_models from homeassistant import config_entries, exceptions -from homeassistant.components import dhcp, zeroconf +from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback @@ -60,28 +60,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery from homekit.""" - self._discovered_ip = discovery_info[zeroconf.ATTR_HOST] + self._discovered_ip = discovery_info.host return await self._async_handle_discovery() async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery from dhcp.""" - self._discovered_ip = discovery_info[dhcp.IP_ADDRESS] + self._discovered_ip = discovery_info.ip return await self._async_handle_discovery() async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle discovery from zeroconf.""" - self._discovered_ip = discovery_info[zeroconf.ATTR_HOST] + self._discovered_ip = discovery_info.host await self.async_set_unique_id( - "{0:#0{1}x}".format(int(discovery_info[zeroconf.ATTR_NAME][-26:-18]), 18) + "{0:#0{1}x}".format(int(discovery_info.name[-26:-18]), 18) ) return await self._async_handle_discovery_with_unique_id() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle discovery from ssdp.""" - self._discovered_ip = urlparse(discovery_info["location"]).hostname - await self.async_set_unique_id(discovery_info["id"]) + self._discovered_ip = urlparse(discovery_info.ssdp_headers["location"]).hostname + await self.async_set_unique_id(discovery_info.ssdp_headers["id"]) return await self._async_handle_discovery_with_unique_id() async def _async_handle_discovery_with_unique_id(self): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 13eb73cdd0c..60098514125 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -16,5 +16,6 @@ "zeroconf": [{ "type": "_miio._udp.local.", "name": "yeelink-*" }], "homekit": { "models": ["YL*"] - } + }, + "after_dependencies": ["ssdp"] } diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index a93cea7a94c..6f7ae807c9d 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -8,7 +8,7 @@ from async_upnp_client.search import SsdpSearchListener from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -179,11 +179,15 @@ def _patch_discovery(no_device=False, capabilities=None): YeelightScanner._scanner = None # Clear class scanner to reset hass def _generate_fake_ssdp_listener(*args, **kwargs): - return _patched_ssdp_listener( - None if no_device else capabilities or CAPABILITIES, - *args, - **kwargs, - ) + info = None + if not no_device: + info = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={}, + ssdp_headers=capabilities or CAPABILITIES, + ) + return _patched_ssdp_listener(info, *args, **kwargs) return patch( "homeassistant.components.yeelight.scanner.SsdpSearchListener", diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index b101fd3413d..aa5e7f98a45 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -55,7 +55,8 @@ DEFAULT_CONFIG = { SSDP_INFO = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - upnp=CAPABILITIES, + upnp={}, + ssdp_headers=CAPABILITIES, ) From 74f7f28f1c57312114b83d41e9f1a415a4168616 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 10:16:44 +0100 Subject: [PATCH 1076/1452] Use device class enums in WLED (#60684) --- homeassistant/components/wled/binary_sensor.py | 4 ++-- homeassistant/components/wled/sensor.py | 12 +++++------- tests/components/wled/test_binary_sensor.py | 6 +++--- tests/components/wled/test_sensor.py | 13 ++++--------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index 45df589de61..ce082691ffe 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -33,7 +33,7 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): """Defines a WLED firmware binary sensor.""" _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - _attr_device_class = DEVICE_CLASS_UPDATE + _attr_device_class = BinarySensorDeviceClass.UPDATE def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 271cba9055b..0bde4107688 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -8,16 +8,14 @@ from datetime import datetime, timedelta from wled import Device as WLEDDevice from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_MILLIAMPERE, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -52,7 +50,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( key="estimated_current", name="Estimated Current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=lambda device: device.info.leds.power, @@ -68,13 +66,13 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Max Current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, value_fn=lambda device: device.info.leds.max_power, ), WLEDSensorEntityDescription( key="uptime", name="Uptime", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: (utcnow() - timedelta(seconds=device.info.uptime)), @@ -102,7 +100,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( key="wifi_rssi", name="Wi-Fi RSSI", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.rssi if device.info.wifi else None, diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py index 61a883b780c..5c40f8833e5 100644 --- a/tests/components/wled/test_binary_sensor.py +++ b/tests/components/wled/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from homeassistant.components.binary_sensor import DEVICE_CLASS_UPDATE +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -25,7 +25,7 @@ async def test_update_available( state = hass.states.get("binary_sensor.wled_rgb_light_firmware") assert state - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_UPDATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE assert state.state == STATE_ON assert ATTR_ICON not in state.attributes @@ -44,7 +44,7 @@ async def test_no_update_available( state = hass.states.get("binary_sensor.wled_websocket_firmware") assert state - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_UPDATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE assert state.state == STATE_OFF assert ATTR_ICON not in state.attributes diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 7810915825d..bc401f574a6 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -4,12 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TIMESTAMP, - DOMAIN as SENSOR_DOMAIN, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.components.wled.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -98,7 +93,7 @@ async def test_sensors( assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_MILLIAMPERE ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert state.state == "470" entry = registry.async_get("sensor.wled_rgb_light_estimated_current") @@ -108,7 +103,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_uptime") assert state - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "2019-11-11T09:10:00+00:00" @@ -143,7 +138,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT From 6c7c7acdef145da2232edffdb9244638c07e4c92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 10:24:58 +0100 Subject: [PATCH 1077/1452] Use device class enums in Verisure (#60685) --- homeassistant/components/verisure/binary_sensor.py | 7 +++---- homeassistant/components/verisure/sensor.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index a14efc7d4b1..ba276aa3675 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,8 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -40,7 +39,7 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator - _attr_device_class = DEVICE_CLASS_OPENING + _attr_device_class = BinarySensorDeviceClass.OPENING def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -87,7 +86,7 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator _attr_name = "Verisure Ethernet status" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 2016c4dbb83..ea65b1fbcd8 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -2,9 +2,8 @@ from __future__ import annotations from homeassistant.components.sensor import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -51,7 +50,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = STATE_CLASS_MEASUREMENT @@ -106,7 +105,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator - _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT From 79ebc1b79ac9cf1cac8a30cae3d32947448d4a53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 10:25:26 +0100 Subject: [PATCH 1078/1452] Use device class enums in TwenteMilieu (#60686) --- homeassistant/components/twentemilieu/sensor.py | 16 ++++++++++------ tests/components/twentemilieu/test_sensor.py | 10 +++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index e75d7f372ac..43bfba2b017 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -6,9 +6,13 @@ from datetime import date from twentemilieu import WasteType -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, DEVICE_CLASS_DATE +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -41,28 +45,28 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( waste_type=WasteType.NON_RECYCLABLE, name="Non-recyclable Waste Pickup", icon="mdi:delete-empty", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", waste_type=WasteType.ORGANIC, name="Organic Waste Pickup", icon="mdi:delete-empty", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", waste_type=WasteType.PAPER, name="Paper Waste Pickup", icon="mdi:delete-empty", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", waste_type=WasteType.PACKAGES, name="Packages Waste Pickup", icon="mdi:delete-empty", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, ), ) diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 62acbe686a9..502bc2e3089 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -1,11 +1,11 @@ """Tests for the Twente Milieu sensors.""" +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.twentemilieu.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_DATE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant @@ -29,7 +29,7 @@ async def test_waste_pickup_sensors( assert entry.unique_id == "twentemilieu_12345_Non-recyclable" assert state.state == "2021-11-01" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -40,7 +40,7 @@ async def test_waste_pickup_sensors( assert entry.unique_id == "twentemilieu_12345_Organic" assert state.state == "2021-11-02" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -51,7 +51,7 @@ async def test_waste_pickup_sensors( assert entry.unique_id == "twentemilieu_12345_Plastic" assert state.state == "2021-11-03" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -62,7 +62,7 @@ async def test_waste_pickup_sensors( assert entry.unique_id == "twentemilieu_12345_Paper" assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From bbd179200cafad9fc0a8b0f4a9e280dbc807b46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 1 Dec 2021 10:36:15 +0100 Subject: [PATCH 1079/1452] Use device class enum in UptimeRobot (#60688) --- homeassistant/components/uptimerobot/binary_sensor.py | 4 ++-- tests/components/uptimerobot/test_binary_sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 09ce3262d81..43aed00708d 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -27,7 +27,7 @@ async def async_setup_entry( BinarySensorEntityDescription( key=str(monitor.id), name=monitor.friendly_name, - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, ), monitor=monitor, ) diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py index 8a5c4f623e0..25ca76a2914 100644 --- a/tests/components/uptimerobot/test_binary_sensor.py +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyuptimerobot import UptimeRobotAuthenticationException -from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.uptimerobot.const import ( ATTRIBUTION, COORDINATOR_UPDATE_INTERVAL, @@ -29,7 +29,7 @@ async def test_presentation(hass: HomeAssistant) -> None: entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) assert entity.state == STATE_ON - assert entity.attributes["device_class"] == DEVICE_CLASS_CONNECTIVITY + assert entity.attributes["device_class"] == BinarySensorDeviceClass.CONNECTIVITY assert entity.attributes["attribution"] == ATTRIBUTION assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] From 52112a25b0f7eda1556cdb94176e4715ccd8ff33 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 10:54:42 +0100 Subject: [PATCH 1080/1452] Use dataclass properties in emonitor discovery (#60695) Co-authored-by: epenet --- homeassistant/components/emonitor/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py index a745f652be4..a77289d469e 100644 --- a/homeassistant/components/emonitor/config_flow.py +++ b/homeassistant/components/emonitor/config_flow.py @@ -65,10 +65,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] - await self.async_set_unique_id(format_mac(discovery_info[dhcp.MAC_ADDRESS])) + self.discovered_ip = discovery_info.ip + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip}) - name = name_short_mac(short_mac(discovery_info[dhcp.MAC_ADDRESS])) + name = name_short_mac(short_mac(discovery_info.macaddress)) self.context["title_placeholders"] = {"name": name} try: self.discovered_info = await fetch_mac_and_title( From 49f3a7ce462f5a67b3c9393db0ef67661da5042b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 1 Dec 2021 10:55:30 +0100 Subject: [PATCH 1081/1452] Use device class enum in Supervisor (#60687) --- homeassistant/components/hassio/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index b30c2d1c7f5..b5525fe9ce4 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -4,8 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_RUNNING, - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -33,7 +32,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): COMMON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, name="Update Available", @@ -42,7 +41,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( HassioBinarySensorEntityDescription( - device_class=DEVICE_CLASS_RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, entity_registry_enabled_default=False, key=ATTR_STATE, name="Running", From 06f12fc5837506b06a0f58aa48e25b6c3b38d180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 10:58:28 +0100 Subject: [PATCH 1082/1452] Use device class enum in Mill (#60699) --- homeassistant/components/mill/sensor.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 64db26c371c..6d53ae22a4e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -4,13 +4,9 @@ from __future__ import annotations import mill from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -45,14 +41,14 @@ from .const import ( HEATER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=CONSUMPTION_YEAR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, name="Year consumption", ), SensorEntityDescription( key=CONSUMPTION_TODAY, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, name="Day consumption", @@ -62,21 +58,21 @@ HEATER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, name="Temperature", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, name="Humidity", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=BATTERY, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, name="Battery", state_class=STATE_CLASS_MEASUREMENT, @@ -84,7 +80,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key=ECO2, - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Estimated CO2", ), From ca55216d67d1eb8a4a9977b175bf85cfeee8ea76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 10:58:43 +0100 Subject: [PATCH 1083/1452] Use device class enum in Tractive (#60700) --- homeassistant/components/tractive/binary_sensor.py | 4 ++-- homeassistant/components/tractive/sensor.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index dfd28eed98d..453b5cf5b0c 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -76,7 +76,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, name="Battery Charging", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b9afbeba757..2bbd755da2c 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -4,11 +4,14 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, - DEVICE_CLASS_BATTERY, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TIME_MINUTES, @@ -133,7 +136,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_BATTERY_LEVEL, name="Battery Level", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, entity_class=TractiveHardwareSensor, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), From 739ce9bc75e2e2b351b4d6335c95f17d05d80d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 10:58:52 +0100 Subject: [PATCH 1084/1452] Use device class enum in Surepetcare (#60701) --- homeassistant/components/surepetcare/binary_sensor.py | 9 ++++----- homeassistant/components/surepetcare/sensor.py | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index eecf82abc52..d53252fae9b 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -8,8 +8,7 @@ from surepy.entities.pet import Pet as SurepyPet from surepy.enums import EntityType, Location from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PRESENCE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -67,7 +66,7 @@ class SurePetcareBinarySensor(SurePetcareEntity, BinarySensorEntity): class Hub(SurePetcareBinarySensor): """Sure Petcare Hub.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property @@ -94,7 +93,7 @@ class Hub(SurePetcareBinarySensor): class Pet(SurePetcareBinarySensor): """Sure Petcare Pet.""" - _attr_device_class = DEVICE_CLASS_PRESENCE + _attr_device_class = BinarySensorDeviceClass.PRESENCE @callback def _update_attr(self, surepy_entity: SurepyEntity) -> None: @@ -117,7 +116,7 @@ class Pet(SurePetcareBinarySensor): class DeviceConnectivity(SurePetcareBinarySensor): """Sure Petcare Device.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index cc4fe01fffa..e53f319bdc5 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -7,11 +7,10 @@ from surepy.entities import SurepyEntity from surepy.entities.devices import Felaqua as SurepyFelaqua from surepy.enums import EntityType -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, VOLUME_MILLILITERS, @@ -52,7 +51,7 @@ async def async_setup_entry( class SureBattery(SurePetcareEntity, SensorEntity): """A sensor implementation for Sure Petcare batteries.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_native_unit_of_measurement = PERCENTAGE From 38c2c879c98f218678132de9d80284c96b908e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 10:59:12 +0100 Subject: [PATCH 1085/1452] Use device class enum in Open Garage (#60702) --- homeassistant/components/opengarage/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index ed42b5fef3d..e8a316853c3 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -5,13 +5,11 @@ import logging from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_CENTIMETERS, PERCENTAGE, @@ -33,7 +31,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="rssi", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, @@ -41,13 +39,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="temp", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="humid", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), From 3e9c72df5c742f5b6e3a73873188dc3cbe57149e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 10:59:24 +0100 Subject: [PATCH 1086/1452] Use device class enum in Airthings (#60704) --- homeassistant/components/airthings/sensor.py | 25 +++++++------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index b2960ff6066..db9b688d6a4 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -5,6 +5,7 @@ from airthings import AirthingsDevice from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, StateType, @@ -14,14 +15,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_MBAR, @@ -46,32 +39,32 @@ SENSORS: dict[str, SensorEntityDescription] = { ), "temp": SensorEntityDescription( key="temp", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, name="Temperature", ), "humidity": SensorEntityDescription( key="humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, name="Humidity", ), "pressure": SensorEntityDescription( key="pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_MBAR, name="Pressure", ), "battery": SensorEntityDescription( key="battery", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, name="Battery", ), "co2": SensorEntityDescription( key="co2", - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="CO2", ), @@ -96,7 +89,7 @@ SENSORS: dict[str, SensorEntityDescription] = { "rssi": SensorEntityDescription( key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, name="RSSI", entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -104,13 +97,13 @@ SENSORS: dict[str, SensorEntityDescription] = { "pm1": SensorEntityDescription( key="pm1", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM1, + device_class=SensorDeviceClass.PM1, name="PM1", ), "pm25": SensorEntityDescription( key="pm25", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, name="PM25", ), } From bcb2fefbe56a29187d0284774490f5d5370d98e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 11:00:17 +0100 Subject: [PATCH 1087/1452] Use device class enum in Tibber (#60705) --- homeassistant/components/tibber/sensor.py | 50 ++++++++++------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index df79f287cf7..e0657d26a0a 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -9,16 +9,10 @@ from random import randrange import aiohttp from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -54,123 +48,123 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="averagePower", name="average power", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="power", name="power", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="powerProduction", name="power production", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="minPower", name="min power", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="maxPower", name="max power", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="accumulatedConsumption", name="accumulated consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="accumulatedProduction", name="accumulated production", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", name="accumulated production current hour", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterConsumption", name="last meter consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterProduction", name="last meter production", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="voltagePhase1", name="voltage phase1", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="voltagePhase2", name="voltage phase2", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="voltagePhase3", name="voltage phase3", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="currentL1", name="current L1", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="currentL2", name="current L2", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="currentL3", name="current L3", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="signalStrength", name="signal strength", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -178,19 +172,19 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="accumulatedReward", name="accumulated reward", - device_class=DEVICE_CLASS_MONETARY, + device_class=SensorDeviceClass.MONETARY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="accumulatedCost", name="accumulated cost", - device_class=DEVICE_CLASS_MONETARY, + device_class=SensorDeviceClass.MONETARY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="powerFactor", name="power factor", - device_class=DEVICE_CLASS_POWER_FACTOR, + device_class=SensorDeviceClass.POWER_FACTOR, native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), From 76e08aa93b669dd463e93658c3034f7d79d63883 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:06:38 +0100 Subject: [PATCH 1088/1452] Use dataclass properties in goalzero discovery (#60703) Co-authored-by: epenet --- homeassistant/components/goalzero/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 50927dd7729..2d8c0c848c9 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -30,9 +30,9 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.ip_address = discovery_info[dhcp.IP_ADDRESS] + self.ip_address = discovery_info.ip - await self.async_set_unique_id(discovery_info[dhcp.MAC_ADDRESS]) + await self.async_set_unique_id(discovery_info.macaddress) self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) self._async_abort_entries_match({CONF_HOST: self.ip_address}) From ad66522bce6e9ab9bd79b2d221a86ae1841cb124 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:07:10 +0100 Subject: [PATCH 1089/1452] Use dataclass properties in fritzbox discovery (#60698) Co-authored-by: epenet --- homeassistant/components/fritzbox/config_flow.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index e51a1a5f528..0841757d147 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -9,11 +9,6 @@ from requests.exceptions import HTTPError import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_UDN, -) from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult @@ -121,11 +116,11 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + host = urlparse(discovery_info.ssdp_location).hostname assert isinstance(host, str) self.context[CONF_HOST] = host - if uuid := discovery_info.get(ATTR_UPNP_UDN): + if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] await self.async_set_unique_id(uuid) @@ -143,7 +138,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") self._host = host - self._name = str(discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host) + self._name = str(discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or host) self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() From 160e6febc3a8b8f171927c8407ccbf632de3d2ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:08:36 +0100 Subject: [PATCH 1090/1452] Use dataclass properties in directv discovery (#60692) Co-authored-by: epenet --- homeassistant/components/directv/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 338af62364d..34a09a04811 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -9,7 +9,6 @@ from directv import DIRECTV, DIRECTVError import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant @@ -69,11 +68,13 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP discovery.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + host = urlparse(discovery_info.ssdp_location).hostname receiver_id = None - if discovery_info.get(ATTR_UPNP_SERIAL): - receiver_id = discovery_info[ATTR_UPNP_SERIAL][4:] # strips off RID- + if discovery_info.upnp.get(ssdp.ATTR_UPNP_SERIAL): + receiver_id = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL][ + 4: + ] # strips off RID- self.context.update({"title_placeholders": {"name": host}}) From 0782c6c4462b027867a9b8edebe17d83db534ff2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:33:40 +0100 Subject: [PATCH 1091/1452] Use dataclass properties in guardian discovery (#60710) Co-authored-by: epenet --- homeassistant/components/guardian/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index b02d415b364..ea4589ddd42 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -100,7 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle the configuration via dhcp.""" self.discovery_info = { - CONF_IP_ADDRESS: discovery_info[dhcp.IP_ADDRESS], + CONF_IP_ADDRESS: discovery_info.ip, CONF_PORT: DEFAULT_PORT, } return await self._async_handle_discovery() From 694f6d0abe78d583ba66a821bfda40cb449cbf36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:38:46 +0100 Subject: [PATCH 1092/1452] Use dataclass properties in heos discovery (#60712) Co-authored-by: epenet --- homeassistant/components/heos/config_flow.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index b84a3a23607..5e534e3e986 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult from .const import DATA_DISCOVERED_HOSTS, DOMAIN @@ -21,11 +22,13 @@ class HeosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Heos device.""" # Store discovered host - hostname = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - friendly_name = f"{discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME]} ({hostname})" + hostname = urlparse(discovery_info.ssdp_location or "").hostname + friendly_name = ( + f"{discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]} ({hostname})" + ) self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = hostname # Abort if other flows in progress or an entry already exists From eeafa36abc0704e631f00b5075ee25b0b24d20c7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 1 Dec 2021 05:43:51 -0500 Subject: [PATCH 1093/1452] Add SmartStart provisioning support to zwave_js WS API (#59037) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 394 ++++++++++- tests/components/zwave_js/test_api.py | 846 ++++++++++++++++++++++- 2 files changed, 1213 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 38d5b99147d..de2662bfa27 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -15,7 +15,10 @@ from zwave_js_server.const import ( CommandClass, InclusionStrategy, LogLevel, + Protocols, + QRCodeVersion, SecurityClass, + ZwaveFeature, ) from zwave_js_server.exceptions import ( BaseZwaveJSServerError, @@ -25,7 +28,12 @@ from zwave_js_server.exceptions import ( SetValueFailed, ) from zwave_js_server.firmware import begin_firmware_update -from zwave_js_server.model.controller import ControllerStatistics, InclusionGrant +from zwave_js_server.model.controller import ( + ControllerStatistics, + InclusionGrant, + ProvisioningEntry, + QRProvisioningInformation, +) from zwave_js_server.model.firmware import ( FirmwareUpdateFinished, FirmwareUpdateProgress, @@ -33,12 +41,14 @@ from zwave_js_server.model.firmware import ( from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage from zwave_js_server.model.node import Node, NodeStatistics +from zwave_js_server.model.utils import async_parse_qr_code_string from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import ( + ERR_INVALID_FORMAT, ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, @@ -80,8 +90,6 @@ TYPE = "type" PROPERTY = "property" PROPERTY_KEY = "property_key" VALUE = "value" -INCLUSION_STRATEGY = "inclusion_strategy" -PIN = "pin" # constants for log config commands CONFIG = "config" @@ -106,6 +114,113 @@ CLIENT_SIDE_AUTH = "client_side_auth" # constants for migration DRY_RUN = "dry_run" +# constants for inclusion +INCLUSION_STRATEGY = "inclusion_strategy" +PIN = "pin" +FORCE_SECURITY = "force_security" +PLANNED_PROVISIONING_ENTRY = "planned_provisioning_entry" +QR_PROVISIONING_INFORMATION = "qr_provisioning_information" +QR_CODE_STRING = "qr_code_string" + +DSK = "dsk" + +VERSION = "version" +GENERIC_DEVICE_CLASS = "generic_device_class" +SPECIFIC_DEVICE_CLASS = "specific_device_class" +INSTALLER_ICON_TYPE = "installer_icon_type" +MANUFACTURER_ID = "manufacturer_id" +PRODUCT_TYPE = "product_type" +PRODUCT_ID = "product_id" +APPLICATION_VERSION = "application_version" +MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval" +UUID = "uuid" +SUPPORTED_PROTOCOLS = "supported_protocols" + +FEATURE = "feature" +UNPROVISION = "unprovision" + +# https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 +MINIMUM_QR_STRING_LENGTH = 52 + + +def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: + """Handle provisioning entry dict to ProvisioningEntry.""" + info = ProvisioningEntry( + dsk=info[DSK], + security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + additional_properties={ + k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES) + }, + ) + return info + + +def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: + """Convert QR provisioning information dict to QRProvisioningInformation.""" + protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])] + info = QRProvisioningInformation( + version=QRCodeVersion(info[VERSION]), + security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + dsk=info[DSK], + generic_device_class=info[GENERIC_DEVICE_CLASS], + specific_device_class=info[SPECIFIC_DEVICE_CLASS], + installer_icon_type=info[INSTALLER_ICON_TYPE], + manufacturer_id=info[MANUFACTURER_ID], + product_type=info[PRODUCT_TYPE], + product_id=info[PRODUCT_ID], + application_version=info[APPLICATION_VERSION], + max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), + uuid=info.get(UUID), + supported_protocols=protocols if protocols else None, + ) + return info + + +# Helper schemas +PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(DSK): str, + vol.Required(SECURITY_CLASSES): vol.All( + cv.ensure_list, + [vol.Coerce(SecurityClass)], + ), + }, + # Provisioning entries can have extra keys for SmartStart + extra=vol.ALLOW_EXTRA, + ), + convert_planned_provisioning_entry, +) + +QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(VERSION): vol.Coerce(QRCodeVersion), + vol.Required(SECURITY_CLASSES): vol.All( + cv.ensure_list, + [vol.Coerce(SecurityClass)], + ), + vol.Required(DSK): str, + vol.Required(GENERIC_DEVICE_CLASS): int, + vol.Required(SPECIFIC_DEVICE_CLASS): int, + vol.Required(INSTALLER_ICON_TYPE): int, + vol.Required(MANUFACTURER_ID): int, + vol.Required(PRODUCT_TYPE): int, + vol.Required(PRODUCT_ID): int, + vol.Required(APPLICATION_VERSION): str, + vol.Optional(MAX_INCLUSION_REQUEST_INTERVAL): vol.Any(int, None), + vol.Optional(UUID): vol.Any(str, None), + vol.Optional(SUPPORTED_PROTOCOLS): vol.All( + cv.ensure_list, + [vol.Coerce(Protocols)], + ), + } + ), + convert_qr_provisioning_information, +) + +QR_CODE_STRING_SCHEMA = vol.All(str, vol.Length(min=MINIMUM_QR_STRING_LENGTH)) + def async_get_entry(orig_func: Callable) -> Callable: """Decorate async function to get entry.""" @@ -194,6 +309,11 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_grant_security_classes) websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin) + websocket_api.async_register_command(hass, websocket_provision_smart_start_node) + websocket_api.async_register_command(hass, websocket_unprovision_smart_start_node) + websocket_api.async_register_command(hass, websocket_get_provisioning_entries) + websocket_api.async_register_command(hass, websocket_parse_qr_code_string) + websocket_api.async_register_command(hass, websocket_supports_feature) websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_remove_node) @@ -434,9 +554,24 @@ async def websocket_ping_node( { vol.Required(TYPE): "zwave_js/add_node", vol.Required(ENTRY_ID): str, - vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.In( - [strategy.value for strategy in InclusionStrategy] + vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.All( + vol.Coerce(int), + vol.In( + [ + strategy.value + for strategy in InclusionStrategy + if strategy != InclusionStrategy.SMART_START + ] + ), ), + vol.Optional(FORCE_SECURITY): bool, + vol.Exclusive( + PLANNED_PROVISIONING_ENTRY, "options" + ): PLANNED_PROVISIONING_ENTRY_SCHEMA, + vol.Exclusive( + QR_PROVISIONING_INFORMATION, "options" + ): QR_PROVISIONING_INFORMATION_SCHEMA, + vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA, } ) @websocket_api.async_response @@ -452,6 +587,12 @@ async def websocket_add_node( """Add a node to the Z-Wave network.""" controller = client.driver.controller inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY]) + force_security = msg.get(FORCE_SECURITY) + provisioning = ( + msg.get(PLANNED_PROVISIONING_ENTRY) + or msg.get(QR_PROVISIONING_INFORMATION) + or msg.get(QR_CODE_STRING) + ) @callback def async_cleanup() -> None: @@ -542,7 +683,18 @@ async def websocket_add_node( ), ] - result = await controller.async_begin_inclusion(inclusion_strategy) + try: + result = await controller.async_begin_inclusion( + inclusion_strategy, force_security=force_security, provisioning=provisioning + ) + except ValueError as err: + connection.send_error( + msg[ID], + ERR_INVALID_FORMAT, + err.args[0], + ) + return + connection.send_result( msg[ID], result, @@ -554,9 +706,10 @@ async def websocket_add_node( { vol.Required(TYPE): "zwave_js/grant_security_classes", vol.Required(ENTRY_ID): str, - vol.Required(SECURITY_CLASSES): [ - vol.In([sec_cls.value for sec_cls in SecurityClass]) - ], + vol.Required(SECURITY_CLASSES): vol.All( + cv.ensure_list, + [vol.Coerce(SecurityClass)], + ), vol.Optional(CLIENT_SIDE_AUTH, default=False): bool, } ) @@ -570,7 +723,7 @@ async def websocket_grant_security_classes( entry: ConfigEntry, client: Client, ) -> None: - """Add a node to the Z-Wave network.""" + """Choose SecurityClass grants as part of S2 inclusion process.""" inclusion_grant = InclusionGrant( [SecurityClass(sec_cls) for sec_cls in msg[SECURITY_CLASSES]], msg[CLIENT_SIDE_AUTH], @@ -597,11 +750,179 @@ async def websocket_validate_dsk_and_enter_pin( entry: ConfigEntry, client: Client, ) -> None: - """Add a node to the Z-Wave network.""" + """Validate DSK and enter PIN as part of S2 inclusion process.""" await client.driver.controller.async_validate_dsk_and_enter_pin(msg[PIN]) connection.send_result(msg[ID]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/provision_smart_start_node", + vol.Required(ENTRY_ID): str, + vol.Exclusive( + PLANNED_PROVISIONING_ENTRY, "options" + ): PLANNED_PROVISIONING_ENTRY_SCHEMA, + vol.Exclusive( + QR_PROVISIONING_INFORMATION, "options" + ): QR_PROVISIONING_INFORMATION_SCHEMA, + vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_provision_smart_start_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Pre-provision a smart start node.""" + try: + cv.has_at_least_one_key( + PLANNED_PROVISIONING_ENTRY, QR_PROVISIONING_INFORMATION, QR_CODE_STRING + )(msg) + except vol.Invalid as err: + connection.send_error( + msg[ID], + ERR_INVALID_FORMAT, + err.args[0], + ) + return + + provisioning_info = ( + msg.get(PLANNED_PROVISIONING_ENTRY) + or msg.get(QR_PROVISIONING_INFORMATION) + or msg[QR_CODE_STRING] + ) + + if ( + QR_PROVISIONING_INFORMATION in msg + and provisioning_info.version == QRCodeVersion.S2 + ): + connection.send_error( + msg[ID], + ERR_INVALID_FORMAT, + "QR code version S2 is not supported for this command", + ) + return + await client.driver.controller.async_provision_smart_start_node(provisioning_info) + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/unprovision_smart_start_node", + vol.Required(ENTRY_ID): str, + vol.Exclusive(DSK, "input"): str, + vol.Exclusive(NODE_ID, "input"): int, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_unprovision_smart_start_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Unprovision a smart start node.""" + try: + cv.has_at_least_one_key(DSK, NODE_ID)(msg) + except vol.Invalid as err: + connection.send_error( + msg[ID], + ERR_INVALID_FORMAT, + err.args[0], + ) + return + dsk_or_node_id = msg.get(DSK) or msg[NODE_ID] + await client.driver.controller.async_unprovision_smart_start_node(dsk_or_node_id) + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_provisioning_entries", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_get_provisioning_entries( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Get provisioning entries (entries that have been pre-provisioned).""" + provisioning_entries = ( + await client.driver.controller.async_get_provisioning_entries() + ) + connection.send_result( + msg[ID], [dataclasses.asdict(entry) for entry in provisioning_entries] + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/parse_qr_code_string", + vol.Required(ENTRY_ID): str, + vol.Required(QR_CODE_STRING): QR_CODE_STRING_SCHEMA, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_parse_qr_code_string( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Parse a QR Code String and return QRProvisioningInformation dict.""" + qr_provisioning_information = await async_parse_qr_code_string( + client, msg[QR_CODE_STRING] + ) + connection.send_result(msg[ID], dataclasses.asdict(qr_provisioning_information)) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/supports_feature", + vol.Required(ENTRY_ID): str, + vol.Required(FEATURE): vol.Coerce(ZwaveFeature), + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_supports_feature( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Check if controller supports a particular feature.""" + supported = await client.driver.controller.async_supports_feature(msg[FEATURE]) + connection.send_result( + msg[ID], + {"supported": supported}, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -659,6 +980,7 @@ async def websocket_stop_exclusion( { vol.Required(TYPE): "zwave_js/remove_node", vol.Required(ENTRY_ID): str, + vol.Optional(UNPROVISION): bool, } ) @websocket_api.async_response @@ -707,7 +1029,7 @@ async def websocket_remove_node( controller.on("node removed", node_removed), ] - result = await controller.async_begin_exclusion() + result = await controller.async_begin_exclusion(msg.get(UNPROVISION)) connection.send_result( msg[ID], result, @@ -720,9 +1042,24 @@ async def websocket_remove_node( vol.Required(TYPE): "zwave_js/replace_failed_node", vol.Required(ENTRY_ID): str, vol.Required(NODE_ID): int, - vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.In( - [strategy.value for strategy in InclusionStrategy] + vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.All( + vol.Coerce(int), + vol.In( + [ + strategy.value + for strategy in InclusionStrategy + if strategy != InclusionStrategy.SMART_START + ] + ), ), + vol.Optional(FORCE_SECURITY): bool, + vol.Exclusive( + PLANNED_PROVISIONING_ENTRY, "options" + ): PLANNED_PROVISIONING_ENTRY_SCHEMA, + vol.Exclusive( + QR_PROVISIONING_INFORMATION, "options" + ): QR_PROVISIONING_INFORMATION_SCHEMA, + vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA, } ) @websocket_api.async_response @@ -739,6 +1076,12 @@ async def websocket_replace_failed_node( controller = client.driver.controller node_id = msg[NODE_ID] inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY]) + force_security = msg.get(FORCE_SECURITY) + provisioning = ( + msg.get(PLANNED_PROVISIONING_ENTRY) + or msg.get(QR_PROVISIONING_INFORMATION) + or msg.get(QR_CODE_STRING) + ) @callback def async_cleanup() -> None: @@ -842,7 +1185,21 @@ async def websocket_replace_failed_node( ), ] - result = await controller.async_replace_failed_node(node_id, inclusion_strategy) + try: + result = await controller.async_replace_failed_node( + node_id, + inclusion_strategy, + force_security=force_security, + provisioning=provisioning, + ) + except ValueError as err: + connection.send_error( + msg[ID], + ERR_INVALID_FORMAT, + err.args[0], + ) + return + connection.send_result( msg[ID], result, @@ -1309,13 +1666,12 @@ async def websocket_subscribe_log_updates( { vol.Optional(ENABLED): cv.boolean, vol.Optional(LEVEL): vol.All( - cv.string, + str, vol.Lower, - vol.In([log_level.value for log_level in LogLevel]), - lambda val: LogLevel(val), # pylint: disable=unnecessary-lambda + vol.Coerce(LogLevel), ), vol.Optional(LOG_TO_FILE): cv.boolean, - vol.Optional(FILENAME): cv.string, + vol.Optional(FILENAME): str, vol.Optional(FORCE_CONSOLE): cv.boolean, } ), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e6bfbb45393..4ca733786dc 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -9,7 +9,10 @@ from zwave_js_server.const import ( CommandClass, InclusionStrategy, LogLevel, + Protocols, + QRCodeVersion, SecurityClass, + ZwaveFeature, ) from zwave_js_server.event import Event from zwave_js_server.exceptions import ( @@ -19,31 +22,49 @@ from zwave_js_server.exceptions import ( NotFoundError, SetValueFailed, ) +from zwave_js_server.model.controller import ( + ProvisioningEntry, + QRProvisioningInformation, +) from zwave_js_server.model.node import Node from zwave_js_server.model.value import _get_value_id_from_dict, get_value_id from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( + APPLICATION_VERSION, CLIENT_SIDE_AUTH, COMMAND_CLASS_ID, CONFIG, + DSK, ENABLED, ENTRY_ID, ERR_NOT_LOADED, + FEATURE, FILENAME, FORCE_CONSOLE, + GENERIC_DEVICE_CLASS, ID, INCLUSION_STRATEGY, + INSTALLER_ICON_TYPE, LEVEL, LOG_TO_FILE, + MANUFACTURER_ID, NODE_ID, OPTED_IN, PIN, + PLANNED_PROVISIONING_ENTRY, + PRODUCT_ID, + PRODUCT_TYPE, PROPERTY, PROPERTY_KEY, + QR_CODE_STRING, + QR_PROVISIONING_INFORMATION, SECURITY_CLASSES, + SPECIFIC_DEVICE_CLASS, TYPE, + UNPROVISION, VALUE, + VERSION, ) from homeassistant.components.zwave_js.const import ( CONF_DATA_COLLECTION_OPTED_IN, @@ -421,9 +442,10 @@ async def test_add_node( client.async_send_command.return_value = {"success": True} + # Test inclusion with no provisioning input await ws_client.send_json( { - ID: 3, + ID: 1, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, @@ -542,6 +564,193 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 planned provisioning entry + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + PLANNED_PROVISIONING_ENTRY: { + DSK: "test", + SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": ProvisioningEntry( + "test", [SecurityClass.S2_UNAUTHENTICATED] + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR provisioning information + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": QRProvisioningInformation( + QRCodeVersion.S2, + [SecurityClass.S2_UNAUTHENTICATED], + "test", + 1, + 1, + 1, + 1, + 1, + 1, + "test", + None, + None, + None, + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR code string + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": "90testtesttesttesttesttesttesttesttesttesttesttesttest", + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test Smart Start QR provisioning information with S2 inclusion strategy fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 1, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test QR provisioning information with S0 inclusion strategy fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S0, + QR_PROVISIONING_INFORMATION: { + VERSION: 1, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {" success": True} + + # Test ValueError is caught as failure + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + assert len(client.async_send_command.call_args_list) == 0 + # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.controller.Controller.async_begin_inclusion", @@ -549,7 +758,7 @@ async def test_add_node( ): await ws_client.send_json( { - ID: 4, + ID: 7, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, } @@ -565,7 +774,7 @@ async def test_add_node( await hass.async_block_till_done() await ws_client.send_json( - {ID: 5, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + {ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -661,6 +870,465 @@ async def test_validate_dsk_and_enter_pin(hass, integration, client, hass_ws_cli assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_provision_smart_start_node(hass, integration, client, hass_ws_client): + """Test provision_smart_start_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + # Test provisioning entry + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + PLANNED_PROVISIONING_ENTRY: { + DSK: "test", + SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.provision_smart_start_node", + "entry": ProvisioningEntry( + "test", [SecurityClass.S2_UNAUTHENTICATED] + ).to_dict(), + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test QR provisioning information + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + QR_PROVISIONING_INFORMATION: { + VERSION: 1, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.provision_smart_start_node", + "entry": QRProvisioningInformation( + QRCodeVersion.SMART_START, + [SecurityClass.S2_UNAUTHENTICATED], + "test", + 1, + 1, + 1, + 1, + 1, + 1, + "test", + None, + None, + None, + ).to_dict(), + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test QR code string + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.provision_smart_start_node", + "entry": "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test QR provisioning information with S2 version throws error + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + assert len(client.async_send_command.call_args_list) == 0 + + # Test no provisioning parameter provided causes failure + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_provision_smart_start_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 8, + TYPE: "zwave_js/provision_smart_start_node", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_unprovision_smart_start_node(hass, integration, client, hass_ws_client): + """Test unprovision_smart_start_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {} + + # Test node ID as input + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/unprovision_smart_start_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 1, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.unprovision_smart_start_node", + "dskOrNodeId": 1, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {} + + # Test DSK as input + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/unprovision_smart_start_node", + ENTRY_ID: entry.entry_id, + DSK: "test", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.unprovision_smart_start_node", + "dskOrNodeId": "test", + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {} + + # Test not including DSK or node ID as input fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/unprovision_smart_start_node", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + assert len(client.async_send_command.call_args_list) == 0 + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_unprovision_smart_start_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/unprovision_smart_start_node", + ENTRY_ID: entry.entry_id, + DSK: "test", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/unprovision_smart_start_node", + ENTRY_ID: entry.entry_id, + DSK: "test", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_get_provisioning_entries(hass, integration, client, hass_ws_client): + """Test get_provisioning_entries websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = { + "entries": [{"dsk": "test", "securityClasses": [0], "fake": "test"}] + } + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/get_provisioning_entries", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == [ + { + "dsk": "test", + "security_classes": [SecurityClass.S2_UNAUTHENTICATED], + "additional_properties": {"fake": "test"}, + } + ] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.get_provisioning_entries", + } + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_get_provisioning_entries", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/get_provisioning_entries", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 7, TYPE: "zwave_js/get_provisioning_entries", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_parse_qr_code_string(hass, integration, client, hass_ws_client): + """Test parse_qr_code_string websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = { + "qrProvisioningInformation": { + "version": 0, + "securityClasses": [0], + "dsk": "test", + "genericDeviceClass": 1, + "specificDeviceClass": 1, + "installerIconType": 1, + "manufacturerId": 1, + "productType": 1, + "productId": 1, + "applicationVersion": "test", + "maxInclusionRequestInterval": 1, + "uuid": "test", + "supportedProtocols": [0], + } + } + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/parse_qr_code_string", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "version": 0, + "security_classes": [SecurityClass.S2_UNAUTHENTICATED], + "dsk": "test", + "generic_device_class": 1, + "specific_device_class": 1, + "installer_icon_type": 1, + "manufacturer_id": 1, + "product_type": 1, + "product_id": 1, + "application_version": "test", + "max_inclusion_request_interval": 1, + "uuid": "test", + "supported_protocols": [Protocols.ZWAVE], + } + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "utils.parse_qr_code_string", + "qr": "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + + # Test FailedZWaveCommand is caught + with patch( + "homeassistant.components.zwave_js.api.async_parse_qr_code_string", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/parse_qr_code_string", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/parse_qr_code_string", + ENTRY_ID: entry.entry_id, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_supports_feature(hass, integration, client, hass_ws_client): + """Test supports_feature websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"supported": True} + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/supports_feature", + ENTRY_ID: entry.entry_id, + FEATURE: ZwaveFeature.SMART_START, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == {"supported": True} + + async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_client): """Test cancelling the inclusion and exclusion process.""" entry = integration @@ -754,12 +1422,17 @@ async def test_remove_node( client.async_send_command.return_value = {"success": True} await ws_client.send_json( - {ID: 3, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} + {ID: 1, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() assert msg["success"] + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_exclusion", + } + event = Event( type="exclusion started", data={ @@ -792,6 +1465,28 @@ async def test_remove_node( ) assert device is None + # Test unprovision parameter + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/remove_node", + ENTRY_ID: entry.entry_id, + UNPROVISION: True, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_exclusion", + "unprovision": True, + } + # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.controller.Controller.async_begin_exclusion", @@ -847,11 +1542,12 @@ async def test_replace_failed_node( client.async_send_command.return_value = {"success": True} + # Test replace failed node with no provisioning information # Order of events we receive for a successful replacement is `inclusion started`, # `inclusion stopped`, `node removed`, `node added`, then interview stages. await ws_client.send_json( { - ID: 3, + ID: 1, TYPE: "zwave_js/replace_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -997,6 +1693,140 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 planned provisioning entry + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + PLANNED_PROVISIONING_ENTRY: { + DSK: "test", + SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.replace_failed_node", + "nodeId": 67, + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": ProvisioningEntry( + "test", [SecurityClass.S2_UNAUTHENTICATED] + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR provisioning information + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.replace_failed_node", + "nodeId": 67, + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": QRProvisioningInformation( + QRCodeVersion.S2, + [SecurityClass.S2_UNAUTHENTICATED], + "test", + 1, + 1, + 1, + 1, + 1, + 1, + "test", + None, + None, + None, + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR code string + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.replace_failed_node", + "nodeId": 67, + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": "90testtesttesttesttesttesttesttesttesttesttesttesttest", + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test ValueError is caught as failure + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, + QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", + } + ) + + msg = await ws_client.receive_json() + assert not msg["success"] + + assert len(client.async_send_command.call_args_list) == 0 + # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.controller.Controller.async_replace_failed_node", @@ -1004,7 +1834,7 @@ async def test_replace_failed_node( ): await ws_client.send_json( { - ID: 4, + ID: 7, TYPE: "zwave_js/replace_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -1022,7 +1852,7 @@ async def test_replace_failed_node( await ws_client.send_json( { - ID: 5, + ID: 8, TYPE: "zwave_js/replace_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -2324,7 +3154,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): ) msg = await ws_client.receive_json() assert not msg["success"] - assert "error" in msg and "value must be one of" in msg["error"]["message"] + assert "error" in msg and msg["error"]["code"] == "invalid_format" # Test error without service data await ws_client.send_json( From c6cbfe8c373993ff5360e11a68f2428b2b1fa50f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 12:02:14 +0100 Subject: [PATCH 1094/1452] Migrate humidifier device classes to StrEnum (#60706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/demo/humidifier.py | 14 ++++----- .../components/humidifier/__init__.py | 29 +++++++++++++++++-- homeassistant/components/humidifier/const.py | 2 ++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index 7ee5c0fc6ef..9065c5971fb 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,12 +1,8 @@ """Demo platform that offers a fake humidifier device.""" from __future__ import annotations -from homeassistant.components.humidifier import HumidifierEntity -from homeassistant.components.humidifier.const import ( - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, - SUPPORT_MODES, -) +from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity +from homeassistant.components.humidifier.const import SUPPORT_MODES SUPPORT_FLAGS = 0 @@ -19,13 +15,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name="Humidifier", mode=None, target_humidity=68, - device_class=DEVICE_CLASS_HUMIDIFIER, + device_class=HumidifierDeviceClass.HUMIDIFIER, ), DemoHumidifier( name="Dehumidifier", mode=None, target_humidity=54, - device_class=DEVICE_CLASS_DEHUMIDIFIER, + device_class=HumidifierDeviceClass.DEHUMIDIFIER, ), DemoHumidifier( name="Hygrostat", @@ -54,7 +50,7 @@ class DemoHumidifier(HumidifierEntity): target_humidity: int, available_modes: list[str] | None = None, is_on: bool = True, - device_class: str | None = None, + device_class: HumidifierDeviceClass | None = None, ) -> None: """Initialize the humidifier device.""" self._attr_name = name diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 48a71662be1..e6ec8dc72f2 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -26,8 +26,9 @@ 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.enum import StrEnum -from .const import ( +from .const import ( # noqa: F401 ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, @@ -49,9 +50,19 @@ SCAN_INTERVAL = timedelta(seconds=60) ENTITY_ID_FORMAT = DOMAIN + ".{}" -DEVICE_CLASSES = [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) +class HumidifierDeviceClass(StrEnum): + """Device class for humidifiers.""" + + HUMIDIFIER = "humidifier" + DEHUMIDIFIER = "dehumidifier" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(HumidifierDeviceClass)) + +# DEVICE_CLASSES below is deprecated as of 2021.12 +# use the HumidifierDeviceClass enum instead. +DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass] @bind_hass @@ -108,12 +119,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class HumidifierEntityDescription(ToggleEntityDescription): """A class that describes humidifier entities.""" + device_class: HumidifierDeviceClass | str | None = None + class HumidifierEntity(ToggleEntity): """Base class for humidifier entities.""" entity_description: HumidifierEntityDescription _attr_available_modes: list[str] | None + _attr_device_class: HumidifierDeviceClass | str | None _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY _attr_mode: str | None @@ -133,6 +147,15 @@ class HumidifierEntity(ToggleEntity): return data + @property + def device_class(self) -> HumidifierDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + @final @property def state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index c2508770187..a3df2756af9 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -19,6 +19,8 @@ DEFAULT_MAX_HUMIDITY = 100 DOMAIN = "humidifier" +# DEVICE_CLASS_* below are deprecated as of 2021.12 +# use the HumidifierDeviceClass enum instead. DEVICE_CLASS_HUMIDIFIER = "humidifier" DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier" From 2ec49d4ffdf97a05c4ecf7afc058acf87e158b79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 12:09:31 +0100 Subject: [PATCH 1095/1452] Migrate sensor state classes to StrEnum (#60709) --- homeassistant/components/demo/sensor.py | 16 ++++---- homeassistant/components/sensor/__init__.py | 41 +++++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index ec9cc648e1a..20631e3eee6 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -4,9 +4,9 @@ from __future__ import annotations from typing import Any from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -39,7 +39,7 @@ async def async_setup_platform( "Outside Temperature", 15.6, SensorDeviceClass.TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, TEMP_CELSIUS, 12, ), @@ -48,7 +48,7 @@ async def async_setup_platform( "Outside Humidity", 54, SensorDeviceClass.HUMIDITY, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, PERCENTAGE, None, ), @@ -57,7 +57,7 @@ async def async_setup_platform( "Carbon monoxide", 54, SensorDeviceClass.CO, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, None, ), @@ -66,7 +66,7 @@ async def async_setup_platform( "Carbon dioxide", 54, SensorDeviceClass.CO2, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, 14, ), @@ -75,7 +75,7 @@ async def async_setup_platform( "Power consumption", 100, SensorDeviceClass.POWER, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, POWER_WATT, None, ), @@ -84,7 +84,7 @@ async def async_setup_platform( "Today energy", 15, SensorDeviceClass.ENERGY, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, ENERGY_KILO_WATT_HOUR, None, ), @@ -112,7 +112,7 @@ class DemoSensor(SensorEntity): name: str, state: StateType, device_class: SensorDeviceClass, - state_class: str | None, + state_class: SensorStateClass | None, unit_of_measurement: str | None, battery: StateType, ) -> None: diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 3abe4371bfe..972914a0c68 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -163,20 +163,29 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) # use the SensorDeviceClass enum instead. DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] -# The state represents a measurement in present time + +class SensorStateClass(StrEnum): + """State class for sensors.""" + + # The state represents a measurement in present time + MEASUREMENT = "measurement" + + # The state represents a total amount, e.g. net energy consumption + TOTAL = "total" + + # The state represents a monotonically increasing total, e.g. an amount of consumed gas + TOTAL_INCREASING = "total_increasing" + + +STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) + + +# STATE_CLASS* is deprecated as of 2021.12 +# use the SensorStateClass enum instead. STATE_CLASS_MEASUREMENT: Final = "measurement" -# The state represents a total amount, e.g. net energy consumption STATE_CLASS_TOTAL: Final = "total" -# 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_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, -] - -STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(STATE_CLASSES)) +STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -208,7 +217,7 @@ class SensorEntityDescription(EntityDescription): device_class: SensorDeviceClass | str | None = None last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 native_unit_of_measurement: str | None = None - state_class: str | None = None + state_class: SensorStateClass | str | None = None unit_of_measurement: None = None # Type override, use native_unit_of_measurement def __post_init__(self) -> None: @@ -241,7 +250,7 @@ class SensorEntity(Entity): _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 _attr_native_unit_of_measurement: str | None _attr_native_value: StateType | date | datetime = None - _attr_state_class: str | None + _attr_state_class: SensorStateClass | str | None _attr_state: None = None # Subclasses of SensorEntity should not set this _attr_unit_of_measurement: None = ( None # Subclasses of SensorEntity should not set this @@ -262,8 +271,8 @@ class SensorEntity(Entity): return None @property - def state_class(self) -> str | None: - """Return the state class of this entity, from STATE_CLASSES, if any.""" + def state_class(self) -> SensorStateClass | str | None: + """Return the state class of this entity, if any.""" if hasattr(self, "_attr_state_class"): return self._attr_state_class if hasattr(self, "entity_description"): @@ -293,7 +302,7 @@ class SensorEntity(Entity): """Return state attributes.""" if last_reset := self.last_reset: if ( - self.state_class == STATE_CLASS_MEASUREMENT + self.state_class == SensorStateClass.MEASUREMENT and not self._last_reset_reported ): self._last_reset_reported = True From cbab0ba9c06d6a89b3267ce3e156cd2ca7752424 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 12:29:41 +0100 Subject: [PATCH 1096/1452] Drop base ATTR constants in zeroconf (#60561) Co-authored-by: epenet --- .../components/lutron_caseta/config_flow.py | 4 +-- .../components/samsungtv/config_flow.py | 4 +-- .../components/xiaomi_miio/config_flow.py | 8 +++--- homeassistant/components/zeroconf/__init__.py | 8 ------ .../homekit_controller/test_config_flow.py | 28 +++++++------------ tests/components/ipp/test_config_flow.py | 5 ++-- tests/components/vizio/test_config_flow.py | 7 ++--- tests/components/volumio/test_config_flow.py | 8 +++--- 8 files changed, 26 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index dac0d6c5f94..d75fc77c66e 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -67,14 +67,14 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - hostname = discovery_info[zeroconf.ATTR_HOSTNAME] + hostname = discovery_info.hostname if hostname is None or not hostname.startswith("lutron-"): return self.async_abort(reason="not_lutron_device") self.lutron_id = hostname.split("-")[1].replace(".local.", "") await self.async_set_unique_id(self.lutron_id) - host = discovery_info[zeroconf.ATTR_HOST] + host = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: host}) self.data[CONF_HOST] = host diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 45b2789d478..ba464a17b82 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -302,8 +302,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) - self._mac = format_mac(discovery_info[zeroconf.ATTR_PROPERTIES]["deviceid"]) - self._host = discovery_info[zeroconf.ATTR_HOST] + self._mac = format_mac(discovery_info.properties["deviceid"]) + self._host = discovery_info.host await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 4e4f1688a04..04e2b58ad0f 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -160,11 +160,11 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - name = discovery_info[zeroconf.ATTR_NAME] - self.host = discovery_info[zeroconf.ATTR_HOST] - self.mac = discovery_info[zeroconf.ATTR_PROPERTIES].get("mac") + name = discovery_info.name + self.host = discovery_info.host + self.mac = discovery_info.properties.get("mac") if self.mac is None: - poch = discovery_info[zeroconf.ATTR_PROPERTIES].get("poch", "") + poch = discovery_info.properties.get("poch", "") if (result := search(r"mac=\w+", poch)) is not None: self.mac = result.group(0).split("=")[1] diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 33fe73de617..5d029b56c0b 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -75,14 +75,6 @@ MAX_PROPERTY_VALUE_LEN = 230 # Dns label max length MAX_NAME_LEN = 63 -# Attributes for ZeroconfServiceInfo -ATTR_HOST: Final = "host" -ATTR_HOSTNAME: Final = "hostname" -ATTR_NAME: Final = "name" -ATTR_PORT: Final = "port" -ATTR_PROPERTIES: Final = "properties" -ATTR_TYPE: Final = "type" - # Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES] ATTR_PROPERTIES_ID: Final = "id" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 5026840a4e1..21a3792cd62 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -273,7 +273,7 @@ async def test_id_missing(hass, controller): discovery_info = get_device_discovery_info(device) # Remove id from device - del discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID] + del discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] # Device is discovered result = await hass.config_entries.flow.async_init( @@ -289,10 +289,8 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) - discovery_info[zeroconf.ATTR_PROPERTIES][ - zeroconf.ATTR_PROPERTIES_ID - ] = "AA:BB:CC:DD:EE:FF" - discovery_info[zeroconf.ATTR_PROPERTIES]["md"] = "HHKBridge1,1" + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "AA:BB:CC:DD:EE:FF" + discovery_info.properties["md"] = "HHKBridge1,1" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -319,9 +317,7 @@ async def test_discovery_ignored_hk_bridge(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info[zeroconf.ATTR_PROPERTIES][ - zeroconf.ATTR_PROPERTIES_ID - ] = "AA:BB:CC:DD:EE:FF" + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -348,9 +344,7 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) - discovery_info[zeroconf.ATTR_PROPERTIES][ - zeroconf.ATTR_PROPERTIES_ID - ] = "AA:BB:CC:DD:EE:FF" + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( @@ -382,7 +376,7 @@ async def test_discovery_broken_pairing_flag(hass, controller): discovery_info = get_device_discovery_info(device) # Make sure that we are pairable - assert discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] != 0x0 + assert discovery_info.properties["sf"] != 0x0 # Device is discovered result = await hass.config_entries.flow.async_init( @@ -454,7 +448,7 @@ async def test_discovery_already_configured(hass, controller): discovery_info = get_device_discovery_info(device) # Set device as already paired - discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] = 0x00 + discovery_info.properties["sf"] = 0x00 # Device is discovered result = await hass.config_entries.flow.async_init( @@ -490,11 +484,9 @@ async def test_discovery_already_configured_update_csharp(hass, controller): discovery_info = get_device_discovery_info(device) # Set device as already paired - discovery_info[zeroconf.ATTR_PROPERTIES]["sf"] = 0x00 - discovery_info[zeroconf.ATTR_PROPERTIES]["c#"] = 99999 - discovery_info[zeroconf.ATTR_PROPERTIES][ - zeroconf.ATTR_PROPERTIES_ID - ] = "AA:BB:CC:DD:EE:FF" + discovery_info.properties["sf"] = 0x00 + discovery_info.properties["c#"] = 99999 + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 1f24d9fd7cd..8eefa4251f7 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -2,7 +2,6 @@ import dataclasses from unittest.mock import patch -from homeassistant.components import zeroconf from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL @@ -282,7 +281,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) discovery_info.properties = { - **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], + **MOCK_ZEROCONF_IPP_SERVICE_INFO.properties, "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", } @@ -304,7 +303,7 @@ async def test_zeroconf_empty_unique_id( discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) discovery_info.properties = { - **MOCK_ZEROCONF_IPP_SERVICE_INFO[zeroconf.ATTR_PROPERTIES], + **MOCK_ZEROCONF_IPP_SERVICE_INFO.properties, "UUID": "", } result = await hass.config_entries.flow.async_init( diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index d02008c6d3d..817f23d52c5 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -5,7 +5,6 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components import zeroconf from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( @@ -743,10 +742,8 @@ async def test_zeroconf_flow( # defaults which were set from discovery parameters user_input = result["data_schema"]( { - CONF_HOST: f"{discovery_info[zeroconf.ATTR_HOST]}:{discovery_info[zeroconf.ATTR_PORT]}", - CONF_NAME: discovery_info[zeroconf.ATTR_NAME][ - : -(len(discovery_info[zeroconf.ATTR_TYPE]) + 1) - ], + CONF_HOST: f"{discovery_info.host}:{discovery_info.port}", + CONF_NAME: discovery_info.name[: -(len(discovery_info.type) + 1)], CONF_DEVICE_CLASS: "speaker", } ) diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 53eaef2ddf0..3eb254784d1 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -27,10 +27,10 @@ TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( ) TEST_DISCOVERY_RESULT = { - "host": TEST_DISCOVERY[zeroconf.ATTR_HOST], - "port": TEST_DISCOVERY[zeroconf.ATTR_PORT], - "id": TEST_DISCOVERY[zeroconf.ATTR_PROPERTIES]["UUID"], - "name": TEST_DISCOVERY[zeroconf.ATTR_PROPERTIES]["volumioName"], + "host": TEST_DISCOVERY.host, + "port": TEST_DISCOVERY.port, + "id": TEST_DISCOVERY.properties["UUID"], + "name": TEST_DISCOVERY.properties["volumioName"], } From 9a8c1cf6c45f2d85aaf649a05ff71ca47a636301 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 12:30:36 +0100 Subject: [PATCH 1097/1452] Use dataclass properties in konnected discovery (#60717) Co-authored-by: epenet --- homeassistant/components/konnected/config_flow.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 82a720bfff3..230a40f35b9 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -9,11 +9,11 @@ from urllib.parse import urlparse import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_ZONE, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import ( @@ -238,7 +239,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered konnected panel. This flow is triggered by the SSDP component. It will check if the @@ -247,16 +248,16 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug(discovery_info) try: - if discovery_info[ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER: + if discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER: return self.async_abort(reason="not_konn_panel") if not any( - name in discovery_info[ATTR_UPNP_MODEL_NAME] + name in discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] for name in KONN_PANEL_MODEL_NAMES ): _LOGGER.warning( "Discovered unrecognized Konnected device %s", - discovery_info.get(ATTR_UPNP_MODEL_NAME, "Unknown"), + discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "Unknown"), ) return self.async_abort(reason="not_konn_panel") @@ -266,7 +267,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Malformed Konnected SSDP info") else: # extract host/port from ssdp_location - netloc = urlparse(discovery_info["ssdp_location"]).netloc.split(":") + netloc = urlparse(discovery_info.ssdp_location).netloc.split(":") self._async_abort_entries_match( {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} ) From 2ba21d6bf3a90b95bd830d2ea091ca8b4ae9bd2b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 1 Dec 2021 12:48:06 +0100 Subject: [PATCH 1098/1452] Fix point device identifiers (#60719) --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 6865664af7c..dee0ae1a492 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -315,7 +315,7 @@ class MinutPointEntity(Entity): connections={ (device_registry.CONNECTION_NETWORK_MAC, device["device_mac"]) }, - identifiers=device["device_id"], + identifiers={(DOMAIN, device["device_id"])}, manufacturer="Minut", model=f"Point v{device['hardware_version']}", name=device["description"], From d5dc963b6f63f71985ee9c8edb9ae2e1936cb03f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 12:52:08 +0100 Subject: [PATCH 1099/1452] Guard config flow coverage in Codecov (#60718) --- codecov.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/codecov.yml b/codecov.yml index 8dee6ee3686..2522ccd90e9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,6 +6,19 @@ coverage: default: target: 90 threshold: 0.09 + config-flows: + target: auto + threshold: 0 + paths: + - homeassistant/components/*/config_flow.py + patch: + default: + target: auto + config-flows: + target: 100 + threshold: 0 + paths: + - homeassistant/components/*/config_flow.py comment: false # To make partial tests possible, From 95286791fe85f0f684364df93803e378ed0a3ce5 Mon Sep 17 00:00:00 2001 From: Oleksandr Kapshuk Date: Wed, 1 Dec 2021 14:17:02 +0200 Subject: [PATCH 1100/1452] Add tuya zndb device category (#59477) --- homeassistant/components/tuya/base.py | 14 ++ homeassistant/components/tuya/const.py | 6 +- homeassistant/components/tuya/sensor.py | 282 ++++++++++++++++-------- homeassistant/components/tuya/switch.py | 14 +- 4 files changed, 225 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index f7b4453fea8..928899940f4 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -89,6 +89,20 @@ class EnumTypeData: return cls(**json.loads(data)) +@dataclass +class ElectricityTypeData: + """Electricity Type Data.""" + + electriccurrent: str | None = None + power: str | None = None + voltage: str | None = None + + @classmethod + def from_json(cls, data: str) -> ElectricityTypeData: + """Load JSON string and return a ElectricityTypeData object.""" + return cls(**json.loads(data.lower())) + + class TuyaEntity(Entity): """Tuya base device.""" diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index e551c6a5229..dd229011c2a 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -205,6 +205,7 @@ class DPCode(str, Enum): FILTER_RESET = "filter_reset" # Filter (cartridge) reset FLOODLIGHT_LIGHTNESS = "floodlight_lightness" FLOODLIGHT_SWITCH = "floodlight_switch" + FORWARD_ENERGY_TOTAL = "forward_energy_total" GAS_SENSOR_STATE = "gas_sensor_state" GAS_SENSOR_STATUS = "gas_sensor_status" GAS_SENSOR_VALUE = "gas_sensor_value" @@ -234,6 +235,9 @@ class DPCode(str, Enum): PERCENT_STATE = "percent_state" PERCENT_STATE_2 = "percent_state_2" PERCENT_STATE_3 = "percent_state_3" + PHASE_A = "phase_a" + PHASE_B = "phase_b" + PHASE_C = "phase_c" PIR = "pir" # Motion sensor PM1 = "pm1" PM10 = "pm10" @@ -401,7 +405,7 @@ UNITS = ( ), UnitOfMeasurement( unit=ENERGY_KILO_WATT_HOUR, - aliases={"kwh", "kilowatt-hour"}, + aliases={"kwh", "kilowatt-hour", "kW·h"}, device_classes={DEVICE_CLASS_ENERGY}, ), UnitOfMeasurement( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index b09d0ca905f..4ba7fb461c3 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1,6 +1,7 @@ """Support for Tuya sensors.""" from __future__ import annotations +from dataclasses import dataclass from typing import cast from tuya_iot import TuyaDevice, TuyaDeviceManager @@ -9,6 +10,7 @@ from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -17,6 +19,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PM1, @@ -27,8 +30,11 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, + POWER_KILO_WATT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -36,7 +42,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import HomeAssistantTuyaData -from .base import EnumTypeData, IntegerTypeData, TuyaEntity +from .base import ElectricityTypeData, EnumTypeData, IntegerTypeData, TuyaEntity from .const import ( DEVICE_CLASS_TUYA_STATUS, DEVICE_CLASS_UNITS, @@ -46,9 +52,17 @@ from .const import ( UnitOfMeasurement, ) + +@dataclass +class TuyaSensorEntityDescription(SensorEntityDescription): + """Describes Tuya sensor entity.""" + + subkey: str | None = None + + # Commonly used battery sensors, that are re-used in the sensors down below. -BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( + TuyaSensorEntityDescription( key=DPCode.BATTERY_PERCENTAGE, name="Battery", native_unit_of_measurement=PERCENTAGE, @@ -56,20 +70,20 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, name="Battery State", icon="mdi:battery", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.BATTERY_VALUE, name="Battery", device_class=DEVICE_CLASS_BATTERY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VA_BATTERY, name="Battery", device_class=DEVICE_CLASS_BATTERY, @@ -82,23 +96,23 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( # default status set of each category (that don't have a set instruction) # end up being a sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { +SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { # Smart Kettle # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 "bh": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Current Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, name="Current Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.STATUS, name="Status", device_class=DEVICE_CLASS_TUYA_STATUS, @@ -107,19 +121,19 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # CO2 Detector # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy "co2bj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, @@ -130,7 +144,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # CO Detector # https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v "cobj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO_VALUE, name="Carbon Monoxide", device_class=DEVICE_CLASS_CO, @@ -141,101 +155,76 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Air Quality Monitor # No specification on Tuya portal "hjjcy": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), ), - # Switch - # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s - "kg": ( - SensorEntityDescription( - key=DPCode.CUR_CURRENT, - name="Current", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key=DPCode.CUR_POWER, - name="Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key=DPCode.CUR_VOLTAGE, - name="Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_registry_enabled_default=False, - ), - ), # Formaldehyde Detector # Note: Not documented "jqbj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", state_class=STATE_CLASS_MEASUREMENT, @@ -245,40 +234,65 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Methane Detector # https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm "jwbj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, name="Methane", state_class=STATE_CLASS_MEASUREMENT, ), *BATTERY_SENSORS, ), + # Switch + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "kg": ( + TuyaSensorEntityDescription( + key=DPCode.CUR_CURRENT, + name="Current", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_POWER, + name="Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_VOLTAGE, + name="Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 "ldcg": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.BRIGHT_STATE, name="Luminosity", icon="mdi:brightness-6", ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, name="Luminosity", device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, @@ -295,48 +309,48 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # PM2.5 Sensor # https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu "pm2.5": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM1, name="Particulate Matter 1.0 µm", device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM10, name="Particulate Matter 10.0 µm", device_class=DEVICE_CLASS_PM10, @@ -347,7 +361,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Heater # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm "qn": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.WORK_POWER, name="Power", device_class=DEVICE_CLASS_POWER, @@ -357,7 +371,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Gas Detector # https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw "rqbj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, icon="mdi:gas-cylinder", device_class=STATE_CLASS_MEASUREMENT, @@ -373,19 +387,19 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Smart Camera # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 "sp": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.SENSOR_TEMPERATURE, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.SENSOR_HUMIDITY, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.WIRELESS_ELECTRICITY, name="Battery", device_class=DEVICE_CLASS_BATTERY, @@ -393,42 +407,44 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { state_class=STATE_CLASS_MEASUREMENT, ), ), + # Fingerbot + "szjqr": BATTERY_SENSORS, # Solar Light # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 "tyndj": BATTERY_SENSORS, # Volatile Organic Compound Sensor # Note: Undocumented in cloud API docs, based on test device "voc": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, @@ -439,31 +455,31 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Temperature and Humidity Sensor # https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34 "wsdcg": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, name="Luminosity", device_class=DEVICE_CLASS_ILLUMINANCE, @@ -474,7 +490,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.PRESSURE_VALUE, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, @@ -484,7 +500,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Smoke Detector # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 "ywbj": ( - SensorEntityDescription( + TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, name="Smoke Amount", icon="mdi:smoke-detector", @@ -496,8 +512,88 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": BATTERY_SENSORS, - # Fingerbot - "szjqr": BATTERY_SENSORS, + # Smart Electricity Meter + # https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7 + "zndb": ( + TuyaSensorEntityDescription( + key=DPCode.FORWARD_ENERGY_TOTAL, + name="Total Energy", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_A, + name="Phase A Current", + device_class=DEVICE_CLASS_CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + subkey="electriccurrent", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_A, + name="Phase A Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_KILO_WATT, + subkey="power", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_A, + name="Phase A Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + subkey="voltage", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_B, + name="Phase B Current", + device_class=DEVICE_CLASS_CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + subkey="electriccurrent", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_B, + name="Phase B Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_KILO_WATT, + subkey="power", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_B, + name="Phase B Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + subkey="voltage", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_C, + name="Phase C Current", + device_class=DEVICE_CLASS_CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + subkey="electriccurrent", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_C, + name="Phase C Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_KILO_WATT, + subkey="power", + ), + TuyaSensorEntityDescription( + key=DPCode.PHASE_C, + name="Phase C Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + subkey="voltage", + ), + ), } # Socket (duplicate of `kg`) @@ -545,6 +641,8 @@ async def async_setup_entry( class TuyaSensorEntity(TuyaEntity, SensorEntity): """Tuya Sensor Entity.""" + entity_description: TuyaSensorEntityDescription + _status_range: TuyaDeviceStatusRange | None = None _type_data: IntegerTypeData | EnumTypeData | None = None _uom: UnitOfMeasurement | None = None @@ -553,12 +651,14 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): self, device: TuyaDevice, device_manager: TuyaDeviceManager, - description: SensorEntityDescription, + description: TuyaSensorEntityDescription, ) -> None: """Init Tuya sensor.""" super().__init__(device, device_manager) self.entity_description = description - self._attr_unique_id = f"{super().unique_id}{description.key}" + self._attr_unique_id = ( + f"{super().unique_id}{description.key}{description.subkey or ''}" + ) if status_range := device.status_range.get(description.key): self._status_range = cast(TuyaDeviceStatusRange, status_range) @@ -614,6 +714,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): "Integer", "String", "Enum", + "Json", ): return None @@ -636,5 +737,12 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): ): return None + # Get subkey value from Json string. + if self._status_range.type == "Json": + if self.entity_description.subkey is None: + return None + values = ElectricityTypeData.from_json(value) + return getattr(values, self.entity_description.subkey) + # Valid string or enum value return value diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 97361a5c2c7..acc6e2e57e0 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -385,6 +385,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Fingerbot + "szjqr": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + icon="mdi:cursor-pointer", + ), + ), # IoT Switch? # Note: Undocumented "tdq": ( @@ -449,12 +457,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), - # Fingerbot - "szjqr": ( + # Smart Electricity Meter + # https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7 + "zndb": ( SwitchEntityDescription( key=DPCode.SWITCH, name="Switch", - icon="mdi:cursor-pointer", ), ), } From b04b314a9b4b3fc3dc7ee1347b7dc6c1c52f6909 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 1 Dec 2021 12:20:00 +0000 Subject: [PATCH 1101/1452] Extend Docker build caching opportunities (#60661) --- Dockerfile | 15 ++++++++++++--- Dockerfile.dev | 9 +++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index c802ba9b273..a4d5ce3045d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,12 +7,21 @@ ENV \ WORKDIR /usr/src -## Setup Home Assistant +## Setup Home Assistant Core dependencies +COPY requirements.txt homeassistant/ +COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ +RUN \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + -r homeassistant/requirements.txt +COPY requirements_all.txt homeassistant/ +RUN \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + -r homeassistant/requirements_all.txt + +## Setup Home Assistant Core COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ && python3 -m compileall homeassistant/homeassistant diff --git a/Dockerfile.dev b/Dockerfile.dev index 5ebaa644ce5..727358dae9e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -30,11 +30,12 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements.txt requirements_test.txt requirements_test_pre_commit.txt ./ +COPY requirements.txt ./ COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN pip3 install -r requirements.txt \ - && pip3 install -r requirements_test.txt \ - && rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/ +RUN pip3 install -r requirements.txt +COPY requirements_test.txt requirements_test_pre_commit.txt ./ +RUN pip3 install -r requirements_test.txt +RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/ # Set the default shell to bash instead of sh ENV SHELL /bin/bash From e5e1e7b7e04288c18b119764ad3ad3b7123d22c2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 1 Dec 2021 13:30:25 +0100 Subject: [PATCH 1102/1452] Update pyfronius to 0.7.1 (#60722) --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 217598aaed4..902a20b9b4e 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -3,7 +3,7 @@ "name": "Fronius", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.7.0"], + "requirements": ["pyfronius==0.7.1"], "codeowners": ["@nielstron", "@farmio"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 21e951ed1db..93e7c008cf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1507,7 +1507,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.7.0 +pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3978378f604..718047891d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -910,7 +910,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.7.0 +pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 From 72d8882c7986d1cb869795c10603a6a1052ed28b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Dec 2021 04:51:10 -0800 Subject: [PATCH 1103/1452] Handle errors response to be None (#60679) Co-authored-by: Philip Allgaier Co-authored-by: Franck Nijhof --- .../auth/providers/insecure_example.py | 4 +- homeassistant/components/auth/login_flow.py | 16 ++++-- tests/components/auth/test_login_flow.py | 56 ++++++++++++------- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 9ad6da27ce3..dc003c3e6f3 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -102,7 +102,7 @@ class ExampleLoginFlow(LoginFlow): self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle the step of the form.""" - errors = {} + errors = None if user_input is not None: try: @@ -110,7 +110,7 @@ class ExampleLoginFlow(LoginFlow): user_input["username"], user_input["password"] ) except InvalidAuthError: - errors["base"] = "invalid_auth" + errors = {"base": "invalid_auth"} if not errors: user_input.pop("password") diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index ef2bb793662..a21854b7770 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -154,14 +154,18 @@ class LoginFlowBaseView(HomeAssistantView): async def _async_flow_result_to_response(self, request, client_id, result): """Convert the flow result to a response.""" if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - if result["type"] == data_entry_flow.RESULT_TYPE_FORM: - # @log_invalid_auth does not work here since it returns HTTP 200 - # need manually log failed login attempts - if result.get("errors", {}).get("base") in ( + # @log_invalid_auth does not work here since it returns HTTP 200. + # We need to manually log failed login attempts. + if ( + result["type"] == data_entry_flow.RESULT_TYPE_FORM + and (errors := result.get("errors")) + and errors.get("base") + in ( "invalid_auth", "invalid_code", - ): - await process_wrong_login(request) + ) + ): + await process_wrong_login(request) return self.json(_prepare_result_json(result)) result.pop("data") diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index fd0157c235e..1fa06045de6 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -54,33 +54,41 @@ async def test_invalid_username_password(hass, aiohttp_client): step = await resp.json() # Incorrect username - resp = await client.post( - f"/auth/login_flow/{step['flow_id']}", - json={ - "client_id": CLIENT_ID, - "username": "wrong-user", - "password": "test-pass", - }, - ) + with patch( + "homeassistant.components.auth.login_flow.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "username": "wrong-user", + "password": "test-pass", + }, + ) assert resp.status == HTTPStatus.OK step = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 assert step["step_id"] == "init" assert step["errors"]["base"] == "invalid_auth" # Incorrect password - resp = await client.post( - f"/auth/login_flow/{step['flow_id']}", - json={ - "client_id": CLIENT_ID, - "username": "test-user", - "password": "wrong-pass", - }, - ) + with patch( + "homeassistant.components.auth.login_flow.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "username": "test-user", + "password": "wrong-pass", + }, + ) assert resp.status == HTTPStatus.OK step = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 assert step["step_id"] == "init" assert step["errors"]["base"] == "invalid_auth" @@ -105,15 +113,23 @@ async def test_login_exist_user(hass, aiohttp_client): assert resp.status == HTTPStatus.OK step = await resp.json() - resp = await client.post( - f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, - ) + with patch( + "homeassistant.components.auth.login_flow.process_success_login" + ) as mock_process_success_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "username": "test-user", + "password": "test-pass", + }, + ) assert resp.status == HTTPStatus.OK step = await resp.json() assert step["type"] == "create_entry" assert len(step["result"]) > 1 + assert len(mock_process_success_login.mock_calls) == 1 async def test_login_local_only_user(hass, aiohttp_client): From c28b45cd83d467daad0598178958fb532cb18385 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 13:54:36 +0100 Subject: [PATCH 1104/1452] Migrate entity categories to StrEnum (#60720) --- homeassistant/const.py | 9 ++------- homeassistant/helpers/entity.py | 27 ++++++++++++++++++++++++--- tests/helpers/test_entity.py | 4 ++-- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2ed777c12af..fb8dfc6089a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -703,16 +703,11 @@ PRECISION_TENTHS: Final = 0.1 # cloud, alexa, or google_home components CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] -# Config: An entity which allows changing the configuration of a device +# ENTITY_CATEGOR* below are deprecated as of 2021.12 +# use the EntityCategory enum instead. ENTITY_CATEGORY_CONFIG: Final = "config" -# Diagnostic: An entity exposing some configuration parameter or diagnostics of a device ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" -# System: An entity which is not useful for the user to interact with ENTITY_CATEGORY_SYSTEM: Final = "system" - -# Entity categories which will: -# - Not be exposed to cloud, alexa, or google_home components -# - Not be included in indirect service calls to devices or areas ENTITY_CATEGORIES: Final[list[str]] = [ ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4d301e7d1b6..c303b97eb5f 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -43,6 +43,7 @@ from homeassistant.helpers.event import Event, async_track_entity_registry_updat from homeassistant.helpers.typing import StateType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify +from homeassistant.util.enum import StrEnum _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 @@ -179,6 +180,24 @@ class DeviceInfo(TypedDict, total=False): via_device: tuple[str, str] +class EntityCategory(StrEnum): + """Category of an entity. + + An entity with a category will: + - Not be exposed to cloud, Alexa, or Google Assistant components + - Not be included in indirect service calls to devices or areas + """ + + # Config: An entity which allows changing the configuration of a device + CONFIG = "config" + + # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device + DIAGNOSTIC = "diagnostic" + + # System: An entity which is not useful for the user to interact with + SYSTEM = "system" + + @dataclass class EntityDescription: """A class that describes Home Assistant entities.""" @@ -187,7 +206,9 @@ class EntityDescription: key: str device_class: str | None = None - entity_category: Literal["config", "diagnostic", "system"] | None = None + entity_category: EntityCategory | Literal[ + "config", "diagnostic", "system" + ] | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None @@ -246,7 +267,7 @@ class Entity(ABC): _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None - _attr_entity_category: str | None + _attr_entity_category: EntityCategory | str | None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_extra_state_attributes: MutableMapping[str, Any] @@ -414,7 +435,7 @@ class Entity(ABC): return self._attr_attribution @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | str | None: """Return the category of the entity, if any.""" if hasattr(self, "_attr_entity_category"): return self._attr_entity_category diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index e334e6d2c56..d4051613c03 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -819,13 +819,13 @@ async def test_entity_category_property(hass): key="abc", entity_category="ignore_me" ) mock_entity1.entity_id = "hello.world" - mock_entity1._attr_entity_category = "config" + mock_entity1._attr_entity_category = entity.EntityCategory.CONFIG assert mock_entity1.entity_category == "config" mock_entity2 = entity.Entity() mock_entity2.hass = hass mock_entity2.entity_description = entity.EntityDescription( - key="abc", entity_category="config" + key="abc", entity_category=entity.EntityCategory.CONFIG ) mock_entity2.entity_id = "hello.world" assert mock_entity2.entity_category == "config" From ddc17cb0017a84728f84769b074ab1610b83255f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 13:57:23 +0100 Subject: [PATCH 1105/1452] Use state class enum in Opengarage (#60727) --- homeassistant/components/opengarage/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index e8a316853c3..5a409146577 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -4,10 +4,10 @@ from __future__ import annotations import logging from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ENTITY_CATEGORY_DIAGNOSTIC, @@ -27,7 +27,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="dist", native_unit_of_measurement=LENGTH_CENTIMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="rssi", @@ -35,19 +35,19 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temp", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humid", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 92e608ce049d2602a6a9861808190eaff7958c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 13:57:54 +0100 Subject: [PATCH 1106/1452] Use state class enum in Airthings (#60728) --- homeassistant/components/airthings/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index db9b688d6a4..7f3d3693a4b 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -4,10 +4,10 @@ from __future__ import annotations from airthings import AirthingsDevice from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, StateType, ) from homeassistant.config_entries import ConfigEntry @@ -133,7 +133,7 @@ async def async_setup_entry( class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity): """Representation of a Airthings Sensor device.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, From 2a1f0cadaa72d2225d5fbd3ce545ed0e492d0188 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 13:58:56 +0100 Subject: [PATCH 1107/1452] Use dataclass properties in broadlink discovery (#60689) Co-authored-by: epenet --- homeassistant/components/broadlink/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 07fce107812..8a32ba02ee8 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -57,8 +57,8 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: dhcp.DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle dhcp discovery.""" - host = discovery_info[dhcp.IP_ADDRESS] - unique_id = discovery_info[dhcp.MAC_ADDRESS].lower().replace(":", "") + host = discovery_info.ip + unique_id = discovery_info.macaddress.lower().replace(":", "") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) From c7c2b810a7396f624744eb1f8368e15d3a81bedc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 13:59:34 +0100 Subject: [PATCH 1108/1452] Use dataclass properties in keenetic_ndms2 discovery (#60716) Co-authored-by: epenet --- .../components/keenetic_ndms2/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 96caea06304..a4ef406dd92 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_CONSIDER_HOME, @@ -104,20 +104,20 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered device.""" - friendly_name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") + friendly_name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") # Filter out items not having "keenetic" in their name if "keenetic" not in friendly_name.lower(): return self.async_abort(reason="not_keenetic_ndms2") # Filters out items having no/empty UDN - if not discovery_info.get(ssdp.ATTR_UPNP_UDN): + if not discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): return self.async_abort(reason="no_udn") - host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + host = urlparse(discovery_info.ssdp_location).hostname + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host}) From 63ed0af644af7338a5ff7512d27b4255a6010935 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:00:46 +0100 Subject: [PATCH 1109/1452] Use dataclass properties in hyperion discovery (#60714) Co-authored-by: epenet --- homeassistant/components/hyperion/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index f5beb185830..3b5785dd533 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -11,7 +11,6 @@ from hyperion import client, const import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( SOURCE_REAUTH, ConfigEntry, @@ -189,9 +188,11 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): # SSDP requires user confirmation. self._require_confirm = True - self._data[CONF_HOST] = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + self._data[CONF_HOST] = urlparse(discovery_info.ssdp_location).hostname try: - self._port_ui = urlparse(discovery_info[ATTR_SSDP_LOCATION]).port + self._port_ui = ( + urlparse(discovery_info.ssdp_location).port or const.DEFAULT_PORT_UI + ) except ValueError: self._port_ui = const.DEFAULT_PORT_UI @@ -204,7 +205,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): except ValueError: self._data[CONF_PORT] = const.DEFAULT_PORT_JSON - if not (hyperion_id := discovery_info.get(ATTR_UPNP_SERIAL)): + if not (hyperion_id := discovery_info.get(ssdp.ATTR_UPNP_SERIAL)): return self.async_abort(reason="no_id") # For discovery mechanisms, we set the unique_id as early as possible to From 1edc0a056084c9264b63ae8270988dbc4c1bfbfb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:01:21 +0100 Subject: [PATCH 1110/1452] Use dataclass properties in huawei_lte discovery (#60713) Co-authored-by: epenet --- .../components/huawei_lte/config_flow.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 61f6727f64a..9c9bbb7ee54 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -203,22 +203,25 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP initiated config flow.""" - await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() # Attempt to distinguish from other non-LTE Huawei router devices, at least # some ones we are interested in have "Mobile Wi-Fi" friendlyName. - if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower(): + if ( + "mobile" + not in discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower() + ): return self.async_abort(reason="not_huawei_lte") url = url_normalize( - discovery_info.get( + discovery_info.upnp.get( ssdp.ATTR_UPNP_PRESENTATION_URL, - f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", + f"http://{urlparse(discovery_info.ssdp_location or '').hostname}/", ) ) - if serial_number := discovery_info.get(ssdp.ATTR_UPNP_SERIAL): + if serial_number := discovery_info.upnp.get(ssdp.ATTR_UPNP_SERIAL): await self.async_set_unique_id(serial_number) self._abort_if_unique_id_configured() else: @@ -227,7 +230,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input = {CONF_URL: url} self.context["title_placeholders"] = { - CONF_NAME: discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + CONF_NAME: discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) } return await self._async_show_user_form(user_input) From 0047790db69d47e6a60964d974f05b0f8cb1a7b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:02:11 +0100 Subject: [PATCH 1111/1452] Use dataclass properties in harmony discovery (#60711) Co-authored-by: epenet --- homeassistant/components/harmony/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 1c735b0747d..4dca2192c6b 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.remote import ( ) from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, HARMONY_DATA, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( @@ -81,12 +82,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Harmony device.""" _LOGGER.debug("SSDP discovery_info: %s", discovery_info) - parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) - friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] + parsed_url = urlparse(discovery_info.ssdp_location) + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] self._async_abort_entries_match({CONF_HOST: parsed_url.hostname}) From 6544b440d20dcf70615062888abff5c91fe0cd08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:02:58 +0100 Subject: [PATCH 1112/1452] Use dataclass properties in fritz discovery (#60697) Co-authored-by: epenet --- homeassistant/components/fritz/config_flow.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 37f3d766d5d..3a0cc2b1301 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -14,11 +14,6 @@ from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_UDN, -) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback @@ -117,15 +112,16 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" - ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION]) + ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "") self._host = ssdp_location.hostname self._port = ssdp_location.port self._name = ( - discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or self.fritz_tools.model + discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or self.fritz_tools.model ) self.context[CONF_HOST] = self._host - if uuid := discovery_info.get(ATTR_UPNP_UDN): + if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] await self.async_set_unique_id(uuid) From 16942fc8e956c82ed13ced4efbfa48854fdd4200 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:28:40 +0100 Subject: [PATCH 1113/1452] Use dataclass properties in denonavr discovery (#60691) Co-authored-by: epenet --- .../components/denonavr/config_flow.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index ffb73327d31..2d5cef14f5b 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -211,7 +211,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the @@ -219,21 +219,23 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """ # Filter out non-Denon AVRs#1 if ( - discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER) + discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) not in SUPPORTED_MANUFACTURERS ): return self.async_abort(reason="not_denonavr_manufacturer") # Check if required information is present to set the unique_id if ( - ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info - or ssdp.ATTR_UPNP_SERIAL not in discovery_info + ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info.upnp + or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp ): return self.async_abort(reason="not_denonavr_missing") - self.model_name = discovery_info[ssdp.ATTR_UPNP_MODEL_NAME].replace("*", "") - self.serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL] - self.host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname + self.model_name = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME].replace( + "*", "" + ) + self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + self.host = urlparse(discovery_info.ssdp_location).hostname if self.model_name in IGNORED_MODELS: return self.async_abort(reason="not_denonavr_manufacturer") @@ -245,7 +247,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context.update( { "title_placeholders": { - "name": discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host) + "name": discovery_info.upnp.get( + ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host + ) } } ) From 59f87b9488a915d62ce4ade7187adce51e46f584 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:38:17 +0100 Subject: [PATCH 1114/1452] Use dataclass properties in netgear discovery (#60730) Co-authored-by: epenet --- homeassistant/components/netgear/config_flow.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 6ce97fdbe60..29a0bc40ed6 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -123,11 +123,11 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_ssdp(self, discovery_info: dict) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Initialize flow from ssdp.""" updated_data = {} - device_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + device_url = urlparse(discovery_info.ssdp_location) if device_url.hostname: updated_data[CONF_HOST] = device_url.hostname if device_url.scheme == "https": @@ -137,14 +137,16 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) - await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) updated_data[CONF_PORT] = DEFAULT_PORT for model in MODELS_V2: - if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( + if discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( model - ) or discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith(model): + ) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith( + model + ): updated_data[CONF_PORT] = ORBI_PORT self.placeholders.update(updated_data) From 6a8c732b37faca96f369d5f11fc010c837c87a8f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 14:40:38 +0100 Subject: [PATCH 1115/1452] Add Tailscale integration (#59764) * Add Tailscale integration * Use DeviceEntryType * Fix tests * Adjust to new Pylint version * Use enums for device classes * Update homeassistant/components/tailscale/config_flow.py Co-authored-by: Martin Hjelmare * Pass empty string as default Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 1 + .../components/tailscale/__init__.py | 30 ++ .../components/tailscale/binary_sensor.py | 112 ++++++++ .../components/tailscale/config_flow.py | 122 +++++++++ homeassistant/components/tailscale/const.py | 13 + .../components/tailscale/coordinator.py | 39 +++ .../components/tailscale/manifest.json | 10 + .../components/tailscale/strings.json | 27 ++ .../components/tailscale/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/tailscale/__init__.py | 1 + tests/components/tailscale/conftest.py | 76 ++++++ .../tailscale/fixtures/devices.json | 127 +++++++++ .../tailscale/test_binary_sensor.py | 41 +++ .../components/tailscale/test_config_flow.py | 257 ++++++++++++++++++ tests/components/tailscale/test_init.py | 72 +++++ 20 files changed, 973 insertions(+) create mode 100644 homeassistant/components/tailscale/__init__.py create mode 100644 homeassistant/components/tailscale/binary_sensor.py create mode 100644 homeassistant/components/tailscale/config_flow.py create mode 100644 homeassistant/components/tailscale/const.py create mode 100644 homeassistant/components/tailscale/coordinator.py create mode 100644 homeassistant/components/tailscale/manifest.json create mode 100644 homeassistant/components/tailscale/strings.json create mode 100644 homeassistant/components/tailscale/translations/en.json create mode 100644 tests/components/tailscale/__init__.py create mode 100644 tests/components/tailscale/conftest.py create mode 100644 tests/components/tailscale/fixtures/devices.json create mode 100644 tests/components/tailscale/test_binary_sensor.py create mode 100644 tests/components/tailscale/test_config_flow.py create mode 100644 tests/components/tailscale/test_init.py diff --git a/.strict-typing b/.strict-typing index ce04c74702b..caaef80fe38 100644 --- a/.strict-typing +++ b/.strict-typing @@ -127,6 +127,7 @@ homeassistant.components.switcher_kis.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* homeassistant.components.tag.* +homeassistant.components.tailscale.* homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* diff --git a/CODEOWNERS b/CODEOWNERS index bf9b88d5eec..7b872ade7ff 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -528,6 +528,7 @@ homeassistant/components/system_bridge/* @timmo001 homeassistant/components/tado/* @michaelarnauts @noltari homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei +homeassistant/components/tailscale/* @frenck homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tapsaff/* @bazwilliams homeassistant/components/tasmota/* @emontnemery diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py new file mode 100644 index 00000000000..f5e1c59b222 --- /dev/null +++ b/homeassistant/components/tailscale/__init__.py @@ -0,0 +1,30 @@ +"""The Tailscale integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import TailscaleDataUpdateCoordinator + +PLATFORMS = (BINARY_SENSOR_DOMAIN,) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Tailscale from a config entry.""" + coordinator = TailscaleDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(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 Tailscale config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py new file mode 100644 index 00000000000..37a25055bfc --- /dev/null +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -0,0 +1,112 @@ +"""Support for Tailscale binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from tailscale import Device as TailscaleDevice + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +@dataclass +class TailscaleBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + is_on_fn: Callable[[TailscaleDevice], bool | None] + + +@dataclass +class TailscaleBinarySensorEntityDescription( + BinarySensorEntityDescription, TailscaleBinarySensorEntityDescriptionMixin +): + """Describes a Tailscale binary sensor entity.""" + + +BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( + TailscaleBinarySensorEntityDescription( + key="update_available", + name="Client", + device_class=BinarySensorDeviceClass.UPDATE, + is_on_fn=lambda device: device.update_available, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Tailscale binary sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TailscaleBinarySensorEntity( + coordinator=coordinator, + device=device, + description=description, + ) + for device in coordinator.data.values() + for description in BINARY_SENSORS + ) + + +class TailscaleBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): + """Defines a Tailscale binary sensor.""" + + entity_description: TailscaleBinarySensorEntityDescription + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + device: TailscaleDevice, + description: TailscaleBinarySensorEntityDescription, + ) -> None: + """Initialize a Tailscale binary sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self.device_id = device.device_id + self._attr_name = f"{device.hostname} {description.name}" + self._attr_unique_id = f"{device.device_id}_{description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + device: TailscaleDevice = self.coordinator.data[self.device_id] + + configuration_url = "https://login.tailscale.com/admin/machines/" + if device.addresses: + configuration_url += device.addresses[0] + + return DeviceInfo( + configuration_url=configuration_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, device.device_id)}, + manufacturer="Tailscale Inc.", + model=device.os, + name=device.hostname, + sw_version=device.client_version, + ) + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return bool( + self.entity_description.is_on_fn(self.coordinator.data[self.device_id]) + ) diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py new file mode 100644 index 00000000000..cda4020a290 --- /dev/null +++ b/homeassistant/components/tailscale/config_flow.py @@ -0,0 +1,122 @@ +"""Config flow to configure the Tailscale integration.""" +from __future__ import annotations + +from typing import Any + +from tailscale import Tailscale, TailscaleAuthenticationError, TailscaleError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_TAILNET, DOMAIN + + +async def validate_input(hass: HomeAssistant, *, tailnet: str, api_key: str) -> None: + """Try using the give tailnet & api key against the Tailscale API.""" + session = async_get_clientsession(hass) + tailscale = Tailscale( + session=session, + api_key=api_key, + tailnet=tailnet, + ) + await tailscale.devices() + + +class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Tailscale.""" + + VERSION = 1 + + reauth_entry: ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + try: + await validate_input( + self.hass, + tailnet=user_input[CONF_TAILNET], + api_key=user_input[CONF_API_KEY], + ) + except TailscaleAuthenticationError: + errors["base"] = "invalid_auth" + except TailscaleError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(user_input[CONF_TAILNET]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_TAILNET], + data={ + CONF_TAILNET: user_input[CONF_TAILNET], + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + else: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_TAILNET, default=user_input.get(CONF_TAILNET, "") + ): str, + vol.Required( + CONF_API_KEY, default=user_input.get(CONF_API_KEY, "") + ): str, + } + ), + errors=errors, + ) + + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle initiation of re-authentication with Tailscale.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Tailscale.""" + errors = {} + + if user_input is not None and self.reauth_entry: + try: + await validate_input( + self.hass, + tailnet=self.reauth_entry.data[CONF_TAILNET], + api_key=user_input[CONF_API_KEY], + ) + except TailscaleAuthenticationError: + errors["base"] = "invalid_auth" + except TailscaleError: + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self.reauth_entry, + data={ + **self.reauth_entry.data, + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) diff --git a/homeassistant/components/tailscale/const.py b/homeassistant/components/tailscale/const.py new file mode 100644 index 00000000000..7cdf0cddf71 --- /dev/null +++ b/homeassistant/components/tailscale/const.py @@ -0,0 +1,13 @@ +"""Constants for the Tailscale integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "tailscale" + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=1) + +CONF_TAILNET: Final = "tailnet" diff --git a/homeassistant/components/tailscale/coordinator.py b/homeassistant/components/tailscale/coordinator.py new file mode 100644 index 00000000000..daebfe807c1 --- /dev/null +++ b/homeassistant/components/tailscale/coordinator.py @@ -0,0 +1,39 @@ +"""DataUpdateCoordinator for the Tailscale integration.""" +from __future__ import annotations + +from tailscale import Device, Tailscale, TailscaleAuthenticationError + +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.update_coordinator import DataUpdateCoordinator + +from .const import CONF_TAILNET, DOMAIN, LOGGER, SCAN_INTERVAL + + +class TailscaleDataUpdateCoordinator(DataUpdateCoordinator): + """The Tailscale Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Tailscale coordinator.""" + self.config_entry = entry + + session = async_get_clientsession(hass) + self.tailscale = Tailscale( + session=session, + api_key=entry.data[CONF_API_KEY], + tailnet=entry.data[CONF_TAILNET], + ) + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self) -> dict[str, Device]: + """Fetch devices from Tailscale.""" + try: + return await self.tailscale.devices() + except TailscaleAuthenticationError as err: + raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json new file mode 100644 index 00000000000..973ae420d40 --- /dev/null +++ b/homeassistant/components/tailscale/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "tailscale", + "name": "Tailscale", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tailscale", + "requirements": ["tailscale==0.1.2"], + "codeowners": ["@frenck"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/tailscale/strings.json b/homeassistant/components/tailscale/strings.json new file mode 100644 index 00000000000..247d6032c03 --- /dev/null +++ b/homeassistant/components/tailscale/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo).", + "data": { + "tailnet": "Tailnet", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "description":"Tailscale API tokens are valid for 90-days. You can create a fresh Tailscale API key at https://login.tailscale.com/admin/settings/authkeys.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/tailscale/translations/en.json b/homeassistant/components/tailscale/translations/en.json new file mode 100644 index 00000000000..f1e79785cbf --- /dev/null +++ b/homeassistant/components/tailscale/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "Tailscale API tokens are valid for 90-days. You can create a fresh Tailscale API key at https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "API Key", + "tailnet": "Tailnet" + }, + "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 79e65b96234..c2648ec04cd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -295,6 +295,7 @@ FLOWS = [ "synology_dsm", "system_bridge", "tado", + "tailscale", "tasmota", "tellduslive", "tesla_wall_connector", diff --git a/mypy.ini b/mypy.ini index cc1337bfc3c..483b8bd22f1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1408,6 +1408,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tailscale.*] +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.tautulli.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 93e7c008cf1..0a65c414efd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2274,6 +2274,9 @@ systembridge==2.2.3 # homeassistant.components.tahoma tahoma-api==0.0.16 +# homeassistant.components.tailscale +tailscale==0.1.2 + # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 718047891d7..cca40724fa4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,6 +1351,9 @@ surepy==0.7.2 # homeassistant.components.system_bridge systembridge==2.2.3 +# homeassistant.components.tailscale +tailscale==0.1.2 + # homeassistant.components.tellduslive tellduslive==0.10.11 diff --git a/tests/components/tailscale/__init__.py b/tests/components/tailscale/__init__.py new file mode 100644 index 00000000000..cdae3b16d0b --- /dev/null +++ b/tests/components/tailscale/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tailscale integration.""" diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py new file mode 100644 index 00000000000..12f11a5656d --- /dev/null +++ b/tests/components/tailscale/conftest.py @@ -0,0 +1,76 @@ +"""Fixtures for Tailscale integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from tailscale.models import Devices + +from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="homeassistant.github", + domain=DOMAIN, + data={CONF_TAILNET: "homeassistant.github", CONF_API_KEY: "tskey-MOCK"}, + unique_id="homeassistant.github", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.tailscale.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_tailscale_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Tailscale client.""" + with patch( + "homeassistant.components.tailscale.config_flow.Tailscale", autospec=True + ) as tailscale_mock: + tailscale = tailscale_mock.return_value + tailscale.devices.return_value = Devices.parse_raw( + load_fixture("tailscale/devices.json") + ).devices + yield tailscale + + +@pytest.fixture +def mock_tailscale(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked Tailscale client.""" + fixture: str = "tailscale/devices.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + devices = Devices.parse_raw(load_fixture(fixture)).devices + with patch( + "homeassistant.components.tailscale.coordinator.Tailscale", autospec=True + ) as tailscale_mock: + tailscale = tailscale_mock.return_value + tailscale.devices.return_value = devices + yield tailscale + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tailscale: MagicMock +) -> MockConfigEntry: + """Set up the Tailscale integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/tailscale/fixtures/devices.json b/tests/components/tailscale/fixtures/devices.json new file mode 100644 index 00000000000..1d7ea756399 --- /dev/null +++ b/tests/components/tailscale/fixtures/devices.json @@ -0,0 +1,127 @@ +{ + "devices": [ + { + "addresses": [ + "100.11.11.111" + ], + "id": "123456", + "user": "frenck", + "name": "frencks-iphone.homeassistant.github", + "hostname": "Frencks-iPhone", + "clientVersion": "1.12.3-td91ea7286-ge1bbbd90c", + "updateAvailable": true, + "os": "iOS", + "created": "2021-08-19T09:25:22Z", + "lastSeen": "2021-09-16T06:11:23Z", + "keyExpiryDisabled": false, + "expires": "2022-02-15T09:25:22Z", + "authorized": true, + "isExternal": false, + "machineKey": "mkey:mock", + "nodeKey": "nodekey:mock", + "blocksIncomingConnections": false, + "enabledRoutes": [], + "advertisedRoutes": [], + "clientConnectivity": { + "endpoints": [ + "192.0.0.1:41641", + "192.168.11.154:41641" + ], + "derp": "", + "mappingVariesByDestIP": false, + "latency": {}, + "clientSupports": { + "hairPinning": false, + "ipv6": false, + "pcp": false, + "pmp": false, + "udp": true, + "upnp": false + } + } + }, + { + "addresses": [ + "100.11.11.111" + ], + "id": "123457", + "user": "frenck", + "name": "router.homeassistant.github", + "hostname": "router", + "clientVersion": "1.14.0-t5cff36945-g809e87bba", + "updateAvailable": true, + "os": "linux", + "created": "2021-08-29T09:49:06Z", + "lastSeen": "2021-11-15T20:37:03Z", + "keyExpiryDisabled": false, + "expires": "2022-02-25T09:49:06Z", + "authorized": true, + "isExternal": false, + "machineKey": "mkey:mock", + "nodeKey": "nodekey:mock", + "blocksIncomingConnections": false, + "enabledRoutes": [ + "0.0.0.0/0", + "10.10.10.0/23", + "::/0" + ], + "advertisedRoutes": [ + "0.0.0.0/0", + "10.10.10.0/23", + "::/0" + ], + "clientConnectivity": { + "endpoints": [ + "10.10.10.1:41641", + "111.111.111.111:41641" + ], + "derp": "", + "mappingVariesByDestIP": false, + "latency": { + "Bangalore": { + "latencyMs": 143.42505599999998 + }, + "Chicago": { + "latencyMs": 101.123646 + }, + "Dallas": { + "latencyMs": 136.85886 + }, + "Frankfurt": { + "latencyMs": 18.968314 + }, + "London": { + "preferred": true, + "latencyMs": 14.314574 + }, + "New York City": { + "latencyMs": 83.078912 + }, + "San Francisco": { + "latencyMs": 148.215522 + }, + "Seattle": { + "latencyMs": 181.553595 + }, + "Singapore": { + "latencyMs": 164.566539 + }, + "São Paulo": { + "latencyMs": 207.250179 + }, + "Tokyo": { + "latencyMs": 226.90714300000002 + } + }, + "clientSupports": { + "hairPinning": true, + "ipv6": false, + "pcp": false, + "pmp": false, + "udp": true, + "upnp": false + } + } + } + ] +} \ No newline at end of file diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py new file mode 100644 index 00000000000..e3d0c9c9348 --- /dev/null +++ b/tests/components/tailscale/test_binary_sensor.py @@ -0,0 +1,41 @@ +"""Tests for the sensors provided by the Tailscale integration.""" +from homeassistant.components.binary_sensor import STATE_ON, BinarySensorDeviceClass +from homeassistant.components.tailscale.const import DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_tailscale_binary_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Tailscale binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("binary_sensor.frencks_iphone_client") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_client") + assert entry + assert state + assert entry.unique_id == "123456_update_available" + assert state.state == STATE_ON + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Client" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "123456")} + assert device_entry.manufacturer == "Tailscale Inc." + assert device_entry.model == "iOS" + assert device_entry.name == "Frencks-iPhone" + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE + assert device_entry.sw_version == "1.12.3-td91ea7286-ge1bbbd90c" + assert ( + device_entry.configuration_url + == "https://login.tailscale.com/admin/machines/100.11.11.111" + ) diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py new file mode 100644 index 00000000000..eb070cfdbb2 --- /dev/null +++ b/tests/components/tailscale/test_config_flow.py @@ -0,0 +1,257 @@ +"""Tests for the Tailscale config flow.""" + +from unittest.mock import AsyncMock, MagicMock + +from tailscale import TailscaleAuthenticationError, TailscaleConnectionError + +from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_tailscale_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-FAKE", + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "homeassistant.github" + assert result2.get("data") == { + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-FAKE", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 + + +async def test_full_flow_with_authentication_error( + hass: HomeAssistant, + mock_tailscale_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow with incorrect API key. + + This tests tests a full config flow, with a case the user enters an invalid + Tailscale API key, but recovers by entering the correct one. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-INVALID", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "invalid_auth"} + assert "flow_id" in result2 + + assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 + + mock_tailscale_config_flow.devices.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-VALID", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "homeassistant.github" + assert result3.get("data") == { + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-VALID", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 2 + + +async def test_connection_error( + hass: HomeAssistant, mock_tailscale_config_flow: MagicMock +) -> None: + """Test API connection error.""" + mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-FAKE", + }, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {"base": "cannot_connect"} + + assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tailscale_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the reauthentication configuration flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "reauth_confirm" + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "tskey-REAUTH"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-REAUTH", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 + + +async def test_reauth_with_authentication_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tailscale_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the reauthentication configuration flow with an authentication error. + + This tests tests a reauth flow, with a case the user enters an invalid + API key, but recover by entering the correct one. + """ + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "reauth_confirm" + assert "flow_id" in result + + mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "tskey-INVALID"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("errors") == {"base": "invalid_auth"} + assert "flow_id" in result2 + + assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 + + mock_tailscale_config_flow.devices.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_API_KEY: "tskey-VALID"}, + ) + await hass.async_block_till_done() + + assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_TAILNET: "homeassistant.github", + CONF_API_KEY: "tskey-VALID", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_tailscale_config_flow.devices.mock_calls) == 2 + + +async def test_reauth_api_error( + hass: HomeAssistant, + mock_tailscale_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test API error during reauthentication.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "reauth_confirm" + assert "flow_id" in result + + mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "tskey-VALID"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tailscale/test_init.py b/tests/components/tailscale/test_init.py new file mode 100644 index 00000000000..11ca8a910a6 --- /dev/null +++ b/tests/components/tailscale/test_init.py @@ -0,0 +1,72 @@ +"""Tests for the Tailscale integration.""" +from unittest.mock import MagicMock + +from tailscale import TailscaleAuthenticationError, TailscaleConnectionError + +from homeassistant.components.tailscale.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tailscale: MagicMock, +) -> None: + """Test the Tailscale configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tailscale: MagicMock, +) -> None: + """Test the Tailscale configuration entry not ready.""" + mock_tailscale.devices.side_effect = TailscaleConnectionError + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_tailscale.devices.mock_calls) == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_authentication_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tailscale: MagicMock, +) -> None: + """Test trigger reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + + mock_tailscale.devices.side_effect = TailscaleAuthenticationError + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id From 5d0cf4cb95c6d1c3c565a20dba7d5bd16eeeb741 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 Dec 2021 14:53:30 +0100 Subject: [PATCH 1116/1452] Fix tests for Hue integration (#60683) * fix tests make sure the migration code is not called in all other tests * only patch v2 check where needed --- tests/components/hue/conftest.py | 27 ++++------------ tests/components/hue/test_config_flow.py | 8 +++-- tests/components/hue/test_init.py | 41 ++++++++++++++++-------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 0ebeaca9b88..9e9ed9af31b 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -22,8 +22,6 @@ from tests.common import ( mock_device_registry, ) -# from tests.components.light.conftest import mock_light_profiles # noqa: F401 - @pytest.fixture(autouse=True) def no_request_delay(): @@ -248,9 +246,12 @@ async def setup_component(hass): async def setup_bridge(hass, mock_bridge, config_entry): """Load the Hue integration with the provided bridge.""" mock_bridge.config_entry = config_entry - config_entry.add_to_hass(hass) - with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): - await hass.config_entries.async_setup(config_entry.entry_id) + with patch.object( + hue.migration, "is_v2_bridge", return_value=mock_bridge.api_version == 2 + ): + config_entry.add_to_hass(hass) + with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): + await hass.config_entries.async_setup(config_entry.entry_id) async def setup_platform( @@ -283,22 +284,6 @@ async def setup_platform( await hass.async_block_till_done() -@pytest.fixture -def mock_bridge_setup(): - """Mock bridge setup.""" - with patch.object(hue, "HueBridge") as mock_bridge: - mock_bridge.return_value.async_initialize_bridge = AsyncMock(return_value=True) - mock_bridge.return_value.api_version = 1 - mock_bridge.return_value.api.config = Mock( - bridge_id="mock-id", - mac_address="00:00:00:00:00:00", - software_version="1.0.0", - model_id="BSB002", - ) - mock_bridge.return_value.api.config.name = "Mock Hue bridge" - yield mock_bridge.return_value - - @pytest.fixture(name="device_reg") def get_device_reg(hass): """Return an empty, loaded, registry.""" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index ca5be5e28e9..80e1a8909b9 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -723,7 +723,7 @@ async def test_bridge_zeroconf(hass, aioclient_mock): data=zeroconf.ZeroconfServiceInfo( host="192.168.1.217", port=443, - hostname="Philips-hue.local.", + hostname="Philips-hue.local", type="_hue._tcp.local.", name="Philips Hue - ABCABC._hue._tcp.local.", properties={ @@ -740,7 +740,9 @@ async def test_bridge_zeroconf(hass, aioclient_mock): async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): """Test a bridge being discovered by zeroconf already exists.""" - create_mock_api_discovery(aioclient_mock, [("192.168.1.217", "ecb5faabcabc")]) + create_mock_api_discovery( + aioclient_mock, [("0.0.0.0", "ecb5faabcabc"), ("192.168.1.217", "ecb5faabcabc")] + ) entry = MockConfigEntry( domain="hue", source=config_entries.SOURCE_SSDP, @@ -754,7 +756,7 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): data=zeroconf.ZeroconfServiceInfo( host="192.168.1.217", port=443, - hostname="Philips-hue.local.", + hostname="Philips-hue.local", type="_hue._tcp.local.", name="Philips Hue - ABCABC._hue._tcp.local.", properties={ diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index b44a638669e..3bce9f9ec83 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,6 +1,7 @@ """Test Hue setup process.""" from unittest.mock import AsyncMock, Mock, patch +import aiohue.v2 as aiohue_v2 import pytest from homeassistant import config_entries @@ -10,17 +11,22 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -@pytest.fixture(name="mock_bridge_setup") -def get_mock_bridge_setup(): +@pytest.fixture +def mock_bridge_setup(): """Mock bridge setup.""" with patch.object(hue, "HueBridge") as mock_bridge: + mock_bridge.return_value.api_version = 2 mock_bridge.return_value.async_initialize_bridge = AsyncMock(return_value=True) - mock_bridge.return_value.api_version = 1 mock_bridge.return_value.api.config = Mock( bridge_id="mock-id", mac_address="00:00:00:00:00:00", - software_version="1.0.0", model_id="BSB002", + software_version="1.0.0", + bridge_device=Mock( + id="4a507550-8742-4087-8bf5-c2334f29891c", + product_data=Mock(manufacturer_name="Mock"), + ), + spec=aiohue_v2.ConfigController, ) mock_bridge.return_value.api.config.name = "Mock Hue bridge" yield mock_bridge.return_value @@ -39,7 +45,9 @@ async def test_setup_with_no_config(hass): async def test_unload_entry(hass, mock_bridge_setup): """Test being able to unload an entry.""" - entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry = MockConfigEntry( + domain=hue.DOMAIN, data={"host": "0.0.0.0", "api_version": 2} + ) entry.add_to_hass(hass) assert await async_setup_component(hass, hue.DOMAIN, {}) is True @@ -58,7 +66,9 @@ async def test_unload_entry(hass, mock_bridge_setup): async def test_setting_unique_id(hass, mock_bridge_setup): """Test we set unique ID if not set yet.""" - entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry = MockConfigEntry( + domain=hue.DOMAIN, data={"host": "0.0.0.0", "api_version": 2} + ) entry.add_to_hass(hass) assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert entry.unique_id == "mock-id" @@ -67,7 +77,9 @@ async def test_setting_unique_id(hass, mock_bridge_setup): async def test_fixing_unique_id_no_other(hass, mock_bridge_setup): """Test we set unique ID if not set yet.""" entry = MockConfigEntry( - domain=hue.DOMAIN, data={"host": "0.0.0.0"}, unique_id="invalid-id" + domain=hue.DOMAIN, + data={"host": "0.0.0.0", "api_version": 2}, + unique_id="invalid-id", ) entry.add_to_hass(hass) assert await async_setup_component(hass, hue.DOMAIN, {}) is True @@ -78,13 +90,13 @@ async def test_fixing_unique_id_other_ignored(hass, mock_bridge_setup): """Test we set unique ID if not set yet.""" MockConfigEntry( domain=hue.DOMAIN, - data={"host": "0.0.0.0"}, + data={"host": "0.0.0.0", "api_version": 2}, unique_id="mock-id", source=config_entries.SOURCE_IGNORE, ).add_to_hass(hass) entry = MockConfigEntry( domain=hue.DOMAIN, - data={"host": "0.0.0.0"}, + data={"host": "0.0.0.0", "api_version": 2}, unique_id="invalid-id", ) entry.add_to_hass(hass) @@ -98,13 +110,13 @@ async def test_fixing_unique_id_other_correct(hass, mock_bridge_setup): """Test we remove config entry if another one has correct ID.""" correct_entry = MockConfigEntry( domain=hue.DOMAIN, - data={"host": "0.0.0.0"}, + data={"host": "0.0.0.0", "api_version": 2}, unique_id="mock-id", ) correct_entry.add_to_hass(hass) entry = MockConfigEntry( domain=hue.DOMAIN, - data={"host": "0.0.0.0"}, + data={"host": "0.0.0.0", "api_version": 2}, unique_id="invalid-id", ) entry.add_to_hass(hass) @@ -121,11 +133,14 @@ async def test_security_vuln_check(hass): entry.add_to_hass(hass) config = Mock( - bridge_id="", mac_address="", model_id="BSB002", software_version="1935144020" + bridge_id="", + mac_address="", + model_id="BSB002", + software_version="1935144020", ) config.name = "Hue" - with patch.object( + with patch.object(hue.migration, "is_v2_bridge", return_value=False), patch.object( hue, "HueBridge", Mock( From ecf1bc1b22394e184a5cbb0317c6482af340a84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 15:50:35 +0100 Subject: [PATCH 1117/1452] Use state class enum in Tibber (#60729) --- homeassistant/components/tibber/sensor.py | 40 +++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index e0657d26a0a..cd4d4994f97 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -9,12 +9,10 @@ from random import randrange import aiohttp from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -55,14 +53,14 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( key="power", name="power", device_class=SensorDeviceClass.POWER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( key="powerProduction", name="power production", device_class=SensorDeviceClass.POWER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), SensorEntityDescription( @@ -82,111 +80,111 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( name="accumulated consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="accumulatedProduction", name="accumulated production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", name="accumulated production current hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterConsumption", name="last meter consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterProduction", name="last meter production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="voltagePhase1", name="voltage phase1", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase2", name="voltage phase2", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase3", name="voltage phase3", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL1", name="current L1", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL2", name="current L2", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL3", name="current L3", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="signalStrength", name="signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="accumulatedReward", name="accumulated reward", device_class=SensorDeviceClass.MONETARY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="accumulatedCost", name="accumulated cost", device_class=SensorDeviceClass.MONETARY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="powerFactor", name="power factor", device_class=SensorDeviceClass.POWER_FACTOR, native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 00f71e5dfc4c26e9d2f465c9ee746991bd9e4fb1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 15:52:08 +0100 Subject: [PATCH 1118/1452] Use dataclass properties in screenlogic discovery (#60735) Co-authored-by: epenet --- homeassistant/components/screenlogic/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index ccb2dd38bb8..d9feec629e2 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -79,13 +79,13 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - mac = _extract_mac_from_name(discovery_info[dhcp.HOSTNAME]) + mac = _extract_mac_from_name(discovery_info.hostname) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: discovery_info[dhcp.IP_ADDRESS]} + updates={CONF_IP_ADDRESS: discovery_info.ip} ) - self.discovered_ip = discovery_info[dhcp.IP_ADDRESS] - self.context["title_placeholders"] = {"name": discovery_info[dhcp.HOSTNAME]} + self.discovered_ip = discovery_info.ip + self.context["title_placeholders"] = {"name": discovery_info.hostname} return await self.async_step_gateway_entry() async def async_step_gateway_select(self, user_input=None): From 76d906d17ee90c6bce687e80af20461c664292a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 15:53:02 +0100 Subject: [PATCH 1119/1452] Use dataclass properties in roomba discovery (#60734) Co-authored-by: epenet --- homeassistant/components/roomba/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index e1919941068..2dbecdf32f2 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -80,13 +80,13 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self._async_abort_entries_match({CONF_HOST: discovery_info[dhcp.IP_ADDRESS]}) + self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) - if not discovery_info[dhcp.HOSTNAME].startswith(("irobot-", "roomba-")): + if not discovery_info.hostname.startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - self.host = discovery_info[dhcp.IP_ADDRESS] - self.blid = _async_blid_from_hostname(discovery_info[dhcp.HOSTNAME]) + self.host = discovery_info.ip + self.blid = _async_blid_from_hostname(discovery_info.hostname) await self.async_set_unique_id(self.blid) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) From e7f00c2c4f25edfd68649a46699b49e635f9b6be Mon Sep 17 00:00:00 2001 From: einarhauks Date: Wed, 1 Dec 2021 15:07:24 +0000 Subject: [PATCH 1120/1452] Refactor Tesla wall connector config flow (#60755) --- .../tesla_wall_connector/config_flow.py | 55 +++---------------- .../tesla_wall_connector/test_config_flow.py | 31 +---------- 2 files changed, 9 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py index 8b4dc423fbb..7183a68d428 100644 --- a/homeassistant/components/tesla_wall_connector/config_flow.py +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -10,17 +10,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.dhcp import IP_ADDRESS -from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - DEFAULT_SCAN_INTERVAL, - DOMAIN, - WALLCONNECTOR_DEVICE_NAME, - WALLCONNECTOR_SERIAL_NUMBER, -) +from .const import DOMAIN, WALLCONNECTOR_DEVICE_NAME, WALLCONNECTOR_SERIAL_NUMBER _LOGGER = logging.getLogger(__name__) @@ -112,49 +107,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - existing_entry = await self.async_set_unique_id( - info[WALLCONNECTOR_SERIAL_NUMBER] + await self.async_set_unique_id( + unique_id=info[WALLCONNECTOR_SERIAL_NUMBER], raise_on_progress=True + ) + self._abort_if_unique_id_configured( + updates=user_input, reload_on_update=True ) - 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="already_configured") return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for Tesla Wall Connector.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ), - ): vol.All(vol.Coerce(int), vol.Clamp(min=1)) - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index e28f0749b5a..2038a130124 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -6,7 +6,7 @@ from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant import config_entries, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.tesla_wall_connector.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -176,32 +176,3 @@ async def test_dhcp_error_from_wall_connector(mock_wall_connector_version, hass) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" - - -async def test_option_flow(hass): - """Test option flow.""" - entry = MockConfigEntry( - domain=DOMAIN, unique_id="abc123", data={CONF_HOST: "1.2.3.4"} - ) - entry.add_to_hass(hass) - - assert not entry.options - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init( - entry.entry_id, - data=None, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_SCAN_INTERVAL: 30}, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {CONF_SCAN_INTERVAL: 30} From fc3c9b1b4e1ebaa26330aa82792ca178828692ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:42:42 +0100 Subject: [PATCH 1121/1452] Use dataclass properties in samsungtv discovery (#60595) Co-authored-by: epenet --- .../components/samsungtv/config_flow.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index ba464a17b82..fe9e4384884 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -12,12 +12,6 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, ssdp, zeroconf -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_UDN, -) from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -269,12 +263,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) - model_name: str = discovery_info.get(ATTR_UPNP_MODEL_NAME) or "" - self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) - if hostname := urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname: + model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" + self._udn = _strip_uuid(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + if hostname := urlparse(discovery_info.ssdp_location or "").hostname: self._host = hostname await self._async_set_unique_id_from_udn() - self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] + self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] self._abort_if_manufacturer_is_not_samsung() if not await self._async_get_and_check_device_info(): # If we cannot get device info for an SSDP discovery @@ -290,8 +284,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) - self._mac = discovery_info[dhcp.MAC_ADDRESS] - self._host = discovery_info[dhcp.IP_ADDRESS] + self._mac = discovery_info.macaddress + self._host = discovery_info.ip await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} From 7f355681a785944bc7f33f7d0e4fad635d8fc224 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:43:09 +0100 Subject: [PATCH 1122/1452] Add compatibility tests for DhcpServiceInfo (#60752) Co-authored-by: epenet --- tests/components/dhcp/test_init.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 40164375d66..41059722113 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -843,3 +843,34 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass): hostname="connect", macaddress="b8b7f16db533", ) + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) + + # Ensure first call get logged + assert discovery_info["ip"] == "192.168.210.56" + assert discovery_info.get("ip") == "192.168.210.56" + assert discovery_info.get("ip", "fallback_host") == "192.168.210.56" + assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" + assert "Detected code that accessed discovery_info['ip']" in caplog.text + assert "Detected code that accessed discovery_info.get('ip')" not in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["ip"] == "192.168.210.56" + assert discovery_info.get("ip") == "192.168.210.56" + assert "Detected code that accessed discovery_info['ip']" not in caplog.text + assert "Detected code that accessed discovery_info.get('ip')" not in caplog.text + + discovery_info._warning_logged = False # pylint: disable=[protected-access] + assert discovery_info.get("ip") == "192.168.210.56" + assert "Detected code that accessed discovery_info.get('ip')" in caplog.text From 2fe0382841b9e6009cadd7e140f9352ce051ed07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:43:31 +0100 Subject: [PATCH 1123/1452] Add compatibility tests for UsbServiceInfo (#60753) Co-authored-by: epenet --- tests/components/usb/test_init.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index 7d620b45984..cc34add6726 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -777,3 +777,27 @@ def test_human_readable_device_name(): assert "Silicon Labs" in name assert "10C4" in name assert "8A2A" in name + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = usb.UsbServiceInfo( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + + # Ensure first call get logged + assert discovery_info["vid"] == 12345 + assert "Detected code that accessed discovery_info['vid']" in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["vid"] == 12345 + assert "Detected code that accessed discovery_info['vid']" not in caplog.text From 35c40bcf85ad75c4ec60a9cf6447b32b0d834e52 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:44:01 +0100 Subject: [PATCH 1124/1452] Enable warnings for UsbServiceInfo (#60757) Co-authored-by: epenet --- homeassistant/components/usb/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index ba63e59e8f1..87216c65e9e 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -58,7 +58,6 @@ class UsbServiceInfo(BaseServiceInfo): f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={"usb"}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True return getattr(self, name) From 683eb10f0e400a587de96c73116d23ef99082b0a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:55:36 +0100 Subject: [PATCH 1125/1452] Enable warnings for ZeroconfServiceInfo (#60759) Co-authored-by: epenet --- homeassistant/components/zeroconf/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 5d029b56c0b..50db346451f 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -121,7 +121,6 @@ class ZeroconfServiceInfo(BaseServiceInfo): f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True return getattr(self, name) @@ -137,7 +136,6 @@ class ZeroconfServiceInfo(BaseServiceInfo): f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True if hasattr(self, name): From 38153b015f77355fd02a5be0b708128d150c0fcc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:56:54 +0100 Subject: [PATCH 1126/1452] Enable warnings for DhcpServiceInfo (#60750) Co-authored-by: epenet --- homeassistant/components/dhcp/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 47a202aa2dd..7da74fb66b0 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -82,7 +82,6 @@ class DhcpServiceInfo(BaseServiceInfo): f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True return getattr(self, name) @@ -98,7 +97,6 @@ class DhcpServiceInfo(BaseServiceInfo): f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True if hasattr(self, name): From 6caaa5f6bdf5a8d42750130097838334cea524b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:04:49 +0100 Subject: [PATCH 1127/1452] Add compatibility tests for MqttServiceInfo (#60754) Co-authored-by: epenet --- tests/components/mqtt/test_discovery.py | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index a97047195fb..faa9f714ae0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -11,7 +11,11 @@ from homeassistant.components.mqtt.abbreviations import ( ABBREVIATIONS, DEVICE_ABBREVIATIONS, ) -from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start +from homeassistant.components.mqtt.discovery import ( + ALREADY_DISCOVERED, + MqttServiceInfo, + async_start, +) from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -905,3 +909,27 @@ async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock await hass.async_block_till_done() await hass.async_block_till_done() mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#") + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/config", + payload="", + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) + + # Ensure first call get logged + assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" + assert "Detected code that accessed discovery_info['topic']" in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" + assert "Detected code that accessed discovery_info['topic']" not in caplog.text From c985bee1dd256fcb45a9defe2cfc22953b87ca9a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 17:05:44 +0100 Subject: [PATCH 1128/1452] Add sensor platform to Tailscale (#60751) Co-authored-by: Martin Hjelmare --- .../components/tailscale/__init__.py | 48 +++++++++++- .../components/tailscale/binary_sensor.py | 42 +---------- homeassistant/components/tailscale/sensor.py | 73 +++++++++++++++++++ .../tailscale/fixtures/devices.json | 2 +- tests/components/tailscale/test_sensor.py | 41 +++++++++++ 5 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/tailscale/sensor.py create mode 100644 tests/components/tailscale/test_sensor.py diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index f5e1c59b222..72a0ef49fc0 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -1,14 +1,23 @@ """The Tailscale integration.""" from __future__ import annotations +from tailscale import Device as TailscaleDevice + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN from .coordinator import TailscaleDataUpdateCoordinator -PLATFORMS = (BINARY_SENSOR_DOMAIN,) +PLATFORMS = (BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -28,3 +37,40 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: del hass.data[DOMAIN][entry.entry_id] return unload_ok + + +class TailscaleEntity(CoordinatorEntity): + """Defines a Tailscale base entity.""" + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + device: TailscaleDevice, + description: EntityDescription, + ) -> None: + """Initialize a Tailscale sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self.device_id = device.device_id + self._attr_name = f"{device.hostname} {description.name}" + self._attr_unique_id = f"{device.device_id}_{description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + device: TailscaleDevice = self.coordinator.data[self.device_id] + + configuration_url = "https://login.tailscale.com/admin/machines/" + if device.addresses: + configuration_url += device.addresses[0] + + return DeviceInfo( + configuration_url=configuration_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, device.device_id)}, + manufacturer="Tailscale Inc.", + model=device.os, + name=device.hostname, + sw_version=device.client_version, + ) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 37a25055bfc..17a40d04241 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -13,14 +13,9 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from . import TailscaleEntity from .const import DOMAIN @@ -66,44 +61,11 @@ async def async_setup_entry( ) -class TailscaleBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): +class TailscaleBinarySensorEntity(TailscaleEntity, BinarySensorEntity): """Defines a Tailscale binary sensor.""" entity_description: TailscaleBinarySensorEntityDescription - def __init__( - self, - *, - coordinator: DataUpdateCoordinator, - device: TailscaleDevice, - description: TailscaleBinarySensorEntityDescription, - ) -> None: - """Initialize a Tailscale binary sensor.""" - super().__init__(coordinator=coordinator) - self.entity_description = description - self.device_id = device.device_id - self._attr_name = f"{device.hostname} {description.name}" - self._attr_unique_id = f"{device.device_id}_{description.key}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - device: TailscaleDevice = self.coordinator.data[self.device_id] - - configuration_url = "https://login.tailscale.com/admin/machines/" - if device.addresses: - configuration_url += device.addresses[0] - - return DeviceInfo( - configuration_url=configuration_url, - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, device.device_id)}, - manufacturer="Tailscale Inc.", - model=device.os, - name=device.hostname, - sw_version=device.client_version, - ) - @property def is_on(self) -> bool: """Return the state of the sensor.""" diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py new file mode 100644 index 00000000000..d6ffdd177bf --- /dev/null +++ b/homeassistant/components/tailscale/sensor.py @@ -0,0 +1,73 @@ +"""Support for Tailscale sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime + +from tailscale import Device as TailscaleDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TailscaleEntity +from .const import DOMAIN + + +@dataclass +class TailscaleSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[TailscaleDevice], datetime | None] + + +@dataclass +class TailscaleSensorEntityDescription( + SensorEntityDescription, TailscaleSensorEntityDescriptionMixin +): + """Describes a Tailscale sensor entity.""" + + +SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( + TailscaleSensorEntityDescription( + key="expires", + name="Expires", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda device: device.expires, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Tailscale sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TailscaleSensorEntity( + coordinator=coordinator, + device=device, + description=description, + ) + for device in coordinator.data.values() + for description in SENSORS + ) + + +class TailscaleSensorEntity(TailscaleEntity, SensorEntity): + """Defines a Tailscale sensor.""" + + entity_description: TailscaleSensorEntityDescription + + @property + def native_value(self) -> datetime | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data[self.device_id]) diff --git a/tests/components/tailscale/fixtures/devices.json b/tests/components/tailscale/fixtures/devices.json index 1d7ea756399..776c32b5e40 100644 --- a/tests/components/tailscale/fixtures/devices.json +++ b/tests/components/tailscale/fixtures/devices.json @@ -42,7 +42,7 @@ }, { "addresses": [ - "100.11.11.111" + "100.11.11.112" ], "id": "123457", "user": "frenck", diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py new file mode 100644 index 00000000000..0a8918f04d1 --- /dev/null +++ b/tests/components/tailscale/test_sensor.py @@ -0,0 +1,41 @@ +"""Tests for the sensors provided by the Tailscale integration.""" +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.tailscale.const import DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_tailscale_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Tailscale sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.router_expires") + entry = entity_registry.async_get("sensor.router_expires") + assert entry + assert state + assert entry.unique_id == "123457_expires" + assert state.state == "2022-02-25T09:49:06+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Expires" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "123457")} + assert device_entry.manufacturer == "Tailscale Inc." + assert device_entry.model == "linux" + assert device_entry.name == "router" + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE + assert device_entry.sw_version == "1.14.0-t5cff36945-g809e87bba" + assert ( + device_entry.configuration_url + == "https://login.tailscale.com/admin/machines/100.11.11.112" + ) From 1268cefc38b1414ccfd56869b296b3c5eb148c36 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Wed, 1 Dec 2021 16:26:53 +0000 Subject: [PATCH 1129/1452] Use state class enum for Coinbase (#60764) --- homeassistant/components/coinbase/sensor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index f2d450463f2..011dd63b151 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,11 +1,7 @@ """Support for Coinbase sensors.""" import logging -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - SensorEntity, -) +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -110,7 +106,7 @@ class AccountSensor(SensorEntity): API_ACCOUNT_CURRENCY ] break - self._attr_state_class = STATE_CLASS_TOTAL + self._attr_state_class = SensorStateClass.TOTAL self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", entry_type=DeviceEntryType.SERVICE, @@ -183,7 +179,7 @@ class ExchangeRateSensor(SensorEntity): 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = exchange_base - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_device_info = DeviceInfo( configuration_url="https://www.coinbase.com/settings/api", entry_type=DeviceEntryType.SERVICE, From 7363033adaf9600e0ca11f87df4fa390b2fa819b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:33:26 +0100 Subject: [PATCH 1130/1452] Use device class enum in Onewire (#60766) Co-authored-by: epenet --- homeassistant/components/onewire/sensor.py | 60 ++++++++-------- tests/components/onewire/const.py | 82 ++++++++++------------ 2 files changed, 67 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 50d1a8e0c8e..823762edd2b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -18,17 +18,13 @@ from homeassistant.components.onewire.model import ( from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_TYPE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, @@ -64,7 +60,7 @@ class OneWireSensorEntityDescription(OneWireEntityDescription, SensorEntityDescr SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( key="temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, @@ -79,7 +75,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "12": ( OneWireSensorEntityDescription( key="TAI8570/temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, @@ -88,7 +84,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="TAI8570/pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, entity_registry_enabled_default=False, name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, @@ -101,7 +97,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION, OneWireSensorEntityDescription( key="humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, name="Humidity", native_unit_of_measurement=PERCENTAGE, @@ -110,7 +106,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="HIH3600/humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, name="Humidity HIH3600", native_unit_of_measurement=PERCENTAGE, @@ -119,7 +115,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="HIH4000/humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, name="Humidity HIH4000", native_unit_of_measurement=PERCENTAGE, @@ -128,7 +124,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="HIH5030/humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, name="Humidity HIH5030", native_unit_of_measurement=PERCENTAGE, @@ -137,7 +133,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="HTM1735/humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, name="Humidity HTM1735", native_unit_of_measurement=PERCENTAGE, @@ -146,7 +142,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="B1-R1-A/pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, entity_registry_enabled_default=False, name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, @@ -155,7 +151,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="S3-R1-A/illuminance", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, entity_registry_enabled_default=False, name="Illuminance", native_unit_of_measurement=LIGHT_LUX, @@ -164,7 +160,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="VAD", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, name="Voltage VAD", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -173,7 +169,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="VDD", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, name="Voltage VDD", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -182,7 +178,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="vis", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, name="vis", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -219,7 +215,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "HobbyBoards_EF": ( OneWireSensorEntityDescription( key="humidity/humidity_corrected", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, @@ -227,7 +223,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="humidity/humidity_raw", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, name="Humidity Raw", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, @@ -235,7 +231,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="humidity/temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, @@ -245,7 +241,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "HB_MOISTURE_METER": ( OneWireSensorEntityDescription( key="moisture/sensor.0", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Moisture 0", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, @@ -253,7 +249,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="moisture/sensor.1", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Moisture 1", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, @@ -261,7 +257,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="moisture/sensor.2", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Moisture 2", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, @@ -269,7 +265,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="moisture/sensor.3", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Moisture 3", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, @@ -284,7 +280,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "EDS0066": ( OneWireSensorEntityDescription( key="EDS0066/temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, @@ -292,7 +288,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="EDS0066/pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, @@ -302,7 +298,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "EDS0068": ( OneWireSensorEntityDescription( key="EDS0068/temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, @@ -310,7 +306,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="EDS0068/pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, @@ -318,7 +314,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="EDS0068/light", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, name="Illuminance", native_unit_of_measurement=LIGHT_LUX, read_mode=READ_MODE_FLOAT, @@ -326,7 +322,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), OneWireSensorEntityDescription( key="EDS0068/humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, @@ -404,7 +400,7 @@ def get_entities( ) if is_leaf: description = copy.deepcopy(description) - description.device_class = DEVICE_CLASS_HUMIDITY + description.device_class = SensorDeviceClass.HUMIDITY description.native_unit_of_measurement = PERCENTAGE description.name = f"Wetness {s_id}" device_file = os.path.join( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 537c93cbe11..3aea03e82f9 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -26,11 +27,6 @@ from homeassistant.const import ( ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, ATTR_VIA_DEVICE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, @@ -95,7 +91,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", @@ -134,7 +130,7 @@ MOCK_OWPROXY_DEVICES = { SENSOR_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.12_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", @@ -144,7 +140,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.12_111111111111_pressure", ATTR_INJECT_READS: b" 1025.123", ATTR_STATE: "1025.1", @@ -275,7 +271,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", ATTR_INJECT_READS: ProtocolError, ATTR_STATE: STATE_UNKNOWN, @@ -297,7 +293,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.26_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", @@ -307,7 +303,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.26_111111111111_humidity", ATTR_INJECT_READS: b" 72.7563", ATTR_STATE: "72.8", @@ -317,7 +313,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih3600", ATTR_INJECT_READS: b" 73.7563", ATTR_STATE: "73.8", @@ -327,7 +323,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih4000", ATTR_INJECT_READS: b" 74.7563", ATTR_STATE: "74.8", @@ -337,7 +333,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih5030", ATTR_INJECT_READS: b" 75.7563", ATTR_STATE: "75.8", @@ -347,7 +343,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_htm1735", ATTR_INJECT_READS: ProtocolError, ATTR_STATE: STATE_UNKNOWN, @@ -357,7 +353,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.26_111111111111_pressure", ATTR_INJECT_READS: b" 969.265", ATTR_STATE: "969.3", @@ -367,7 +363,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, + ATTR_DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, ATTR_ENTITY_ID: "sensor.26_111111111111_illuminance", ATTR_INJECT_READS: b" 65.8839", ATTR_STATE: "65.9", @@ -377,7 +373,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vad", ATTR_INJECT_READS: b" 2.97", ATTR_STATE: "3.0", @@ -387,7 +383,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vdd", ATTR_INJECT_READS: b" 4.74", ATTR_STATE: "4.7", @@ -397,7 +393,7 @@ MOCK_OWPROXY_DEVICES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_ENTITY_ID: "sensor.26_111111111111_vis", ATTR_INJECT_READS: b" 0.12", ATTR_STATE: "0.1", @@ -428,7 +424,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", ATTR_INJECT_READS: b" 26.984", ATTR_STATE: "27.0", @@ -676,7 +672,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", ATTR_INJECT_READS: b" 28.243", ATTR_STATE: "28.2", @@ -698,7 +694,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", ATTR_INJECT_READS: b" 29.123", ATTR_STATE: "29.1", @@ -720,7 +716,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity", ATTR_INJECT_READS: b" 67.745", ATTR_STATE: "67.7", @@ -729,7 +725,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity_raw", ATTR_INJECT_READS: b" 65.541", ATTR_STATE: "65.5", @@ -738,7 +734,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.ef_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", @@ -764,7 +760,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_0", ATTR_INJECT_READS: b" 41.745", ATTR_STATE: "41.7", @@ -773,7 +769,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_1", ATTR_INJECT_READS: b" 42.541", ATTR_STATE: "42.5", @@ -782,7 +778,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_2", ATTR_INJECT_READS: b" 43.123", ATTR_STATE: "43.1", @@ -791,7 +787,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_3", ATTR_INJECT_READS: b" 44.123", ATTR_STATE: "44.1", @@ -814,7 +810,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.7e_111111111111_temperature", ATTR_INJECT_READS: b" 13.9375", ATTR_STATE: "13.9", @@ -823,7 +819,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.7e_111111111111_pressure", ATTR_INJECT_READS: b" 1012.21", ATTR_STATE: "1012.2", @@ -832,7 +828,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, + ATTR_DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, ATTR_ENTITY_ID: "sensor.7e_111111111111_illuminance", ATTR_INJECT_READS: b" 65.8839", ATTR_STATE: "65.9", @@ -841,7 +837,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.7e_111111111111_humidity", ATTR_INJECT_READS: b" 41.375", ATTR_STATE: "41.4", @@ -864,7 +860,7 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.7e_222222222222_temperature", ATTR_INJECT_READS: b" 13.9375", ATTR_STATE: "13.9", @@ -873,7 +869,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_ENTITY_ID: "sensor.7e_222222222222_pressure", ATTR_INJECT_READS: b" 1012.21", ATTR_STATE: "1012.2", @@ -898,7 +894,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", ATTR_INJECT_READS: 25.123, ATTR_STATE: "25.1", @@ -917,7 +913,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", ATTR_INJECT_READS: FileNotFoundError, ATTR_STATE: STATE_UNKNOWN, @@ -936,7 +932,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", ATTR_INJECT_READS: InvalidCRCException, ATTR_STATE: STATE_UNKNOWN, @@ -955,7 +951,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", ATTR_INJECT_READS: 29.993, ATTR_STATE: "30.0", @@ -974,7 +970,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", ATTR_INJECT_READS: UnsupportResponseException, ATTR_STATE: STATE_UNKNOWN, @@ -993,7 +989,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111112_temperature", ATTR_INJECT_READS: [UnsupportResponseException] * 9 + [27.993], ATTR_STATE: "28.0", @@ -1012,7 +1008,7 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111113_temperature", ATTR_INJECT_READS: [UnsupportResponseException] * 10 + [27.993], ATTR_STATE: STATE_UNKNOWN, From b154f43657d502425929b16d5ba917930ea429d6 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 1 Dec 2021 17:34:30 +0100 Subject: [PATCH 1131/1452] Use device class enum in Rituals (#60767) --- .../rituals_perfume_genie/binary_sensor.py | 4 ++-- .../components/rituals_perfume_genie/sensor.py | 13 ++++--------- .../rituals_perfume_genie/test_binary_sensor.py | 6 ++++-- .../components/rituals_perfume_genie/test_sensor.py | 7 +++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index be251b86311..4c51f6c16bd 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from pyrituals import Diffuser from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -38,7 +38,7 @@ async def async_setup_entry( class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 5299539bcd9..f5cd2f144e5 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -3,14 +3,9 @@ from __future__ import annotations from pyrituals import Diffuser -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_SIGNAL_STRENGTH, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -91,7 +86,7 @@ class DiffuserFillSensor(DiffuserEntity, SensorEntity): class DiffuserBatterySensor(DiffuserEntity, SensorEntity): """Representation of a diffuser battery sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @@ -110,7 +105,7 @@ class DiffuserBatterySensor(DiffuserEntity, SensorEntity): class DiffuserWifiSensor(DiffuserEntity, SensorEntity): """Representation of a diffuser wifi sensor.""" - _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC diff --git a/tests/components/rituals_perfume_genie/test_binary_sensor.py b/tests/components/rituals_perfume_genie/test_binary_sensor.py index 769384dbbf8..cef9a8e85c4 100644 --- a/tests/components/rituals_perfume_genie/test_binary_sensor.py +++ b/tests/components/rituals_perfume_genie/test_binary_sensor.py @@ -1,5 +1,5 @@ """Tests for the Rituals Perfume Genie binary sensor platform.""" -from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.rituals_perfume_genie.binary_sensor import CHARGING_SUFFIX from homeassistant.const import ATTR_DEVICE_CLASS, ENTITY_CATEGORY_DIAGNOSTIC, STATE_ON from homeassistant.core import HomeAssistant @@ -23,7 +23,9 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.genie_battery_charging") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BATTERY_CHARGING + assert ( + state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.BATTERY_CHARGING + ) entry = registry.async_get("binary_sensor.genie_battery_charging") assert entry diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py index a76cf0d6c46..a6c89bd6620 100644 --- a/tests/components/rituals_perfume_genie/test_sensor.py +++ b/tests/components/rituals_perfume_genie/test_sensor.py @@ -4,13 +4,12 @@ from homeassistant.components.rituals_perfume_genie.sensor import ( FILL_SUFFIX, PERFUME_SUFFIX, WIFI_SUFFIX, + SensorDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_SIGNAL_STRENGTH, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) @@ -54,7 +53,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non state = hass.states.get("sensor.genie_battery") assert state assert state.state == str(diffuser.battery_percentage) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.genie_battery") @@ -65,7 +64,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non state = hass.states.get("sensor.genie_wifi") assert state assert state.state == str(diffuser.wifi_percentage) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.genie_wifi") From cd9962dfa0b0f08b7f1f7a78462dc24c0bae77c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:40:56 +0100 Subject: [PATCH 1132/1452] Use device and state class enum for Renault (#60768) Co-authored-by: epenet --- .../components/renault/binary_sensor.py | 7 +- homeassistant/components/renault/sensor.py | 48 ++++--- tests/components/renault/const.py | 117 ++++++++---------- 3 files changed, 79 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index a054cba2f12..c2ebdb5cb0f 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -7,8 +7,7 @@ from renault_api.kamereon.enums import ChargeState, PlugState from renault_api.kamereon.models import KamereonVehicleBatteryStatusData from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_PLUG, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -75,7 +74,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = ( RenaultBinarySensorEntityDescription( key="plugged_in", coordinator="battery", - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, name="Plugged In", on_key="plugStatus", on_value=PlugState.PLUGGED.value, @@ -83,7 +82,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = ( RenaultBinarySensorEntityDescription( key="charging", coordinator="battery", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, name="Charging", on_key="chargingStatus", on_value=ChargeState.CHARGE_IN_PROGRESS.value, diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3dddfafab07..d98b9864875 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -15,19 +15,13 @@ from renault_api.kamereon.models import ( ) from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, @@ -167,11 +161,11 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( key="battery_level", coordinator="battery", data_key="batteryLevel", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], name="Battery Level", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="charge_state", @@ -191,29 +185,29 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( icon="mdi:timer", name="Charging Remaining Time", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="charging_power", condition_lambda=lambda a: not a.details.reports_charging_power_in_watts(), coordinator="battery", data_key="chargingInstantaneousPower", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], name="Charging Power", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="charging_power", condition_lambda=lambda a: a.details.reports_charging_power_in_watts(), coordinator="battery", data_key="chargingInstantaneousPower", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], name="Charging Power", native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value_lambda=_get_charging_power, ), RenaultSensorEntityDescription( @@ -234,32 +228,32 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( icon="mdi:ev-station", name="Battery Autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="battery_available_energy", coordinator="battery", data_key="batteryAvailableEnergy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, name="Battery Available Energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="battery_temperature", coordinator="battery", data_key="batteryTemperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], name="Battery Temperature", native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="battery_last_activity", coordinator="battery", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, data_key="timestamp", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], entity_registry_enabled_default=False, @@ -274,7 +268,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( icon="mdi:sign-direction", name="Mileage", native_unit_of_measurement=LENGTH_KILOMETERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, value_lambda=_get_rounded_value, ), RenaultSensorEntityDescription( @@ -285,7 +279,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( icon="mdi:gas-station", name="Fuel Autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, value_lambda=_get_rounded_value, ), @@ -297,24 +291,24 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( icon="mdi:fuel", name="Fuel Quantity", native_unit_of_measurement=VOLUME_LITERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, value_lambda=_get_rounded_value, ), RenaultSensorEntityDescription( key="outside_temperature", coordinator="hvac_status", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, data_key="externalTemperature", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], name="Outside Temperature", native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( key="location_last_activity", coordinator="location", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleLocationData], entity_registry_enabled_default=False, diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index e0283867132..a3e7b521b23 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,8 +1,7 @@ """Constants for the Renault integration tests.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_PLUG, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, ) from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -19,8 +18,8 @@ from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -35,12 +34,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, @@ -106,13 +99,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777999_plugged_in", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.BATTERY_CHARGING, ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", @@ -148,38 +141,38 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", ATTR_STATE: "141", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, ATTR_ENTITY_ID: "sensor.reg_number_battery_level", ATTR_STATE: "60", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", ATTR_STATE: "2020-01-12T21:40:16+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_last_activity", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", ATTR_STATE: "20", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -191,10 +184,10 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENTITY_ID: "sensor.reg_number_charging_power", ATTR_STATE: "0.027", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, @@ -202,7 +195,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: "145", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -210,15 +203,15 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", ATTR_STATE: "49114", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.reg_number_outside_temperature", ATTR_STATE: "8.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -254,13 +247,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "vf1aaaaa555777999_plugged_in", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.BATTERY_CHARGING, ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", @@ -303,38 +296,38 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", ATTR_STATE: "128", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, ATTR_ENTITY_ID: "sensor.reg_number_battery_level", ATTR_STATE: "50", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", ATTR_STATE: "2020-11-17T08:06:48+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_last_activity", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -346,10 +339,10 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, ATTR_ENTITY_ID: "sensor.reg_number_charging_power", ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, @@ -357,7 +350,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -365,7 +358,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", ATTR_STATE: "49114", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -378,7 +371,7 @@ MOCK_VEHICLES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777999_location_last_activity", @@ -408,13 +401,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777123_plugged_in", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.BATTERY_CHARGING, ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging", @@ -457,38 +450,38 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", ATTR_STATE: "141", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, ATTR_ENTITY_ID: "sensor.reg_number_battery_level", ATTR_STATE: "60", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", ATTR_STATE: "2020-01-12T21:40:16+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_last_activity", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", ATTR_STATE: "20", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -500,10 +493,10 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_state", }, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, ATTR_ENTITY_ID: "sensor.reg_number_charging_power", ATTR_STATE: "27.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, @@ -511,7 +504,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: "145", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -519,7 +512,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_autonomy", ATTR_ICON: "mdi:gas-station", ATTR_STATE: "35", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -527,7 +520,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", ATTR_STATE: "3", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, @@ -535,7 +528,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", ATTR_STATE: "5567", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -548,7 +541,7 @@ MOCK_VEHICLES = { }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", @@ -597,7 +590,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_autonomy", ATTR_ICON: "mdi:gas-station", ATTR_STATE: "35", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -605,7 +598,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", ATTR_STATE: "3", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, @@ -613,13 +606,13 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", ATTR_STATE: "5567", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", From 3eba575fde7d55326360cddc8b6d7f82212020bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:42:08 +0100 Subject: [PATCH 1133/1452] Use state class enum for Onewire (#60770) Co-authored-by: epenet --- homeassistant/components/onewire/sensor.py | 59 ++++++++------- tests/components/onewire/const.py | 87 +++++++++++----------- 2 files changed, 72 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 823762edd2b..cbc616872ba 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -16,11 +16,10 @@ from homeassistant.components.onewire.model import ( OWServerDeviceDescription, ) from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -64,7 +63,7 @@ SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ) _LOGGER = logging.getLogger(__name__) @@ -80,7 +79,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="TAI8570/pressure", @@ -89,7 +88,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), "22": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), @@ -102,7 +101,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="HIH3600/humidity", @@ -111,7 +110,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity HIH3600", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="HIH4000/humidity", @@ -120,7 +119,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity HIH4000", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="HIH5030/humidity", @@ -129,7 +128,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity HIH5030", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="HTM1735/humidity", @@ -138,7 +137,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity HTM1735", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="B1-R1-A/pressure", @@ -147,7 +146,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="S3-R1-A/illuminance", @@ -156,7 +155,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Illuminance", native_unit_of_measurement=LIGHT_LUX, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="VAD", @@ -165,7 +164,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Voltage VAD", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="VDD", @@ -174,7 +173,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Voltage VDD", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="vis", @@ -183,7 +182,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="vis", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), "28": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), @@ -195,14 +194,14 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Counter A", native_unit_of_measurement="count", read_mode=READ_MODE_INT, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), OneWireSensorEntityDescription( key="counter.B", name="Counter B", native_unit_of_measurement="count", read_mode=READ_MODE_INT, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), ), } @@ -219,7 +218,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="humidity/humidity_raw", @@ -227,7 +226,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity Raw", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="humidity/temperature", @@ -235,7 +234,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), "HB_MOISTURE_METER": ( @@ -245,7 +244,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Moisture 0", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="moisture/sensor.1", @@ -253,7 +252,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Moisture 1", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="moisture/sensor.2", @@ -261,7 +260,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Moisture 2", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="moisture/sensor.3", @@ -269,7 +268,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Moisture 3", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), } @@ -284,7 +283,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="EDS0066/pressure", @@ -292,7 +291,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), "EDS0068": ( @@ -302,7 +301,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="EDS0068/pressure", @@ -310,7 +309,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Pressure", native_unit_of_measurement=PRESSURE_MBAR, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="EDS0068/light", @@ -318,7 +317,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Illuminance", native_unit_of_measurement=LIGHT_LUX, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( key="EDS0068/humidity", @@ -326,7 +325,7 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Humidity", native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ), } diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 3aea03e82f9..ebf52f2a25f 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -12,9 +12,8 @@ from homeassistant.components.onewire.const import ( from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, SensorDeviceClass, + SensorStateClass, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -95,7 +94,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/10.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -134,7 +133,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.12_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -144,7 +143,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.12_111111111111_pressure", ATTR_INJECT_READS: b" 1025.123", ATTR_STATE: "1025.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/pressure", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, @@ -195,7 +194,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", ATTR_INJECT_READS: b" 251123", ATTR_STATE: "251123", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "/1D.111111111111/counter.A", ATTR_UNIT_OF_MEASUREMENT: "count", }, @@ -203,7 +202,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", ATTR_INJECT_READS: b" 248125", ATTR_STATE: "248125", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "/1D.111111111111/counter.B", ATTR_UNIT_OF_MEASUREMENT: "count", }, @@ -241,7 +240,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", ATTR_INJECT_READS: b" 251123", ATTR_STATE: "251123", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "/1D.111111111111/counter.A", ATTR_UNIT_OF_MEASUREMENT: "count", }, @@ -250,7 +249,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", ATTR_INJECT_READS: b" 248125", ATTR_STATE: "248125", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "/1D.111111111111/counter.B", ATTR_UNIT_OF_MEASUREMENT: "count", }, @@ -275,7 +274,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", ATTR_INJECT_READS: ProtocolError, ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/22.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -297,7 +296,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -307,7 +306,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_humidity", ATTR_INJECT_READS: b" 72.7563", ATTR_STATE: "72.8", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -317,7 +316,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih3600", ATTR_INJECT_READS: b" 73.7563", ATTR_STATE: "73.8", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/HIH3600/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -327,7 +326,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih4000", ATTR_INJECT_READS: b" 74.7563", ATTR_STATE: "74.8", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/HIH4000/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -337,7 +336,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih5030", ATTR_INJECT_READS: b" 75.7563", ATTR_STATE: "75.8", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/HIH5030/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -347,7 +346,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_htm1735", ATTR_INJECT_READS: ProtocolError, ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/HTM1735/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -357,7 +356,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_pressure", ATTR_INJECT_READS: b" 969.265", ATTR_STATE: "969.3", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/B1-R1-A/pressure", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, @@ -367,7 +366,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_illuminance", ATTR_INJECT_READS: b" 65.8839", ATTR_STATE: "65.9", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/S3-R1-A/illuminance", ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, }, @@ -377,7 +376,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vad", ATTR_INJECT_READS: b" 2.97", ATTR_STATE: "3.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VAD", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, }, @@ -387,7 +386,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vdd", ATTR_INJECT_READS: b" 4.74", ATTR_STATE: "4.7", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VDD", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, }, @@ -397,7 +396,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.26_111111111111_vis", ATTR_INJECT_READS: b" 0.12", ATTR_STATE: "0.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/vis", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, }, @@ -428,7 +427,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", ATTR_INJECT_READS: b" 26.984", ATTR_STATE: "27.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -676,7 +675,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", ATTR_INJECT_READS: b" 28.243", ATTR_STATE: "28.2", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/3B.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -698,7 +697,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", ATTR_INJECT_READS: b" 29.123", ATTR_STATE: "29.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/42.111111111111/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -720,7 +719,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity", ATTR_INJECT_READS: b" 67.745", ATTR_STATE: "67.7", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111111/humidity/humidity_corrected", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -729,7 +728,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity_raw", ATTR_INJECT_READS: b" 65.541", ATTR_STATE: "65.5", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111111/humidity/humidity_raw", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -738,7 +737,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111111_temperature", ATTR_INJECT_READS: b" 25.123", ATTR_STATE: "25.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111111/humidity/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -764,7 +763,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_0", ATTR_INJECT_READS: b" 41.745", ATTR_STATE: "41.7", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.0", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -773,7 +772,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_1", ATTR_INJECT_READS: b" 42.541", ATTR_STATE: "42.5", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.1", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -782,7 +781,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_2", ATTR_INJECT_READS: b" 43.123", ATTR_STATE: "43.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.2", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, @@ -791,7 +790,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_3", ATTR_INJECT_READS: b" 44.123", ATTR_STATE: "44.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.3", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, @@ -814,7 +813,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_111111111111_temperature", ATTR_INJECT_READS: b" 13.9375", ATTR_STATE: "13.9", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -823,7 +822,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_111111111111_pressure", ATTR_INJECT_READS: b" 1012.21", ATTR_STATE: "1012.2", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/pressure", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, @@ -832,7 +831,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_111111111111_illuminance", ATTR_INJECT_READS: b" 65.8839", ATTR_STATE: "65.9", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/light", ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, }, @@ -841,7 +840,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_111111111111_humidity", ATTR_INJECT_READS: b" 41.375", ATTR_STATE: "41.4", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -864,7 +863,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_222222222222_temperature", ATTR_INJECT_READS: b" 13.9375", ATTR_STATE: "13.9", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -873,7 +872,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_ENTITY_ID: "sensor.7e_222222222222_pressure", ATTR_INJECT_READS: b" 1012.21", ATTR_STATE: "1012.2", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/pressure", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, @@ -898,7 +897,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", ATTR_INJECT_READS: 25.123, ATTR_STATE: "25.1", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/10-111111111111/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -917,7 +916,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", ATTR_INJECT_READS: FileNotFoundError, ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/22-111111111111/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -936,7 +935,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", ATTR_INJECT_READS: InvalidCRCException, ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/28-111111111111/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -955,7 +954,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", ATTR_INJECT_READS: 29.993, ATTR_STATE: "30.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/3B-111111111111/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -974,7 +973,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", ATTR_INJECT_READS: UnsupportResponseException, ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111111/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -993,7 +992,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.42_111111111112_temperature", ATTR_INJECT_READS: [UnsupportResponseException] * 9 + [27.993], ATTR_STATE: "28.0", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111112/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -1012,7 +1011,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_ENTITY_ID: "sensor.42_111111111113_temperature", ATTR_INJECT_READS: [UnsupportResponseException] * 10 + [27.993], ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111113/w1_slave", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, From 92b149fffe167f764b2344c62be38d4a0be16581 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:45:09 +0100 Subject: [PATCH 1134/1452] Use dataclass properties in isy994 discovery (#60715) Co-authored-by: epenet --- .../components/isy994/config_flow.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 60e69f73cef..7289f7d416e 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -187,15 +187,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: dhcp.DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a discovered isy994 via dhcp.""" - friendly_name = discovery_info[dhcp.HOSTNAME] - url = f"http://{discovery_info[dhcp.IP_ADDRESS]}" - mac = discovery_info[dhcp.MAC_ADDRESS] + friendly_name = discovery_info.hostname + url = f"http://{discovery_info.ip}" + mac = discovery_info.macaddress isy_mac = ( f"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}" ) - await self._async_set_unique_id_or_update( - isy_mac, discovery_info[dhcp.IP_ADDRESS], None - ) + await self._async_set_unique_id_or_update(isy_mac, discovery_info.ip, None) self.discovered_conf = { CONF_NAME: friendly_name, @@ -205,12 +203,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle a discovered isy994.""" - friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] - url = discovery_info[ssdp.ATTR_SSDP_LOCATION] + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + url = discovery_info.ssdp_location parsed_url = urlparse(url) - mac = discovery_info[ssdp.ATTR_UPNP_UDN] + mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] if mac.startswith(UDN_UUID_PREFIX): mac = mac[len(UDN_UUID_PREFIX) :] if url.endswith(ISY_URL_POSTFIX): From 2b4a1ee7ebb880197db05628ed9f5e58ea1efafb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:47:14 +0100 Subject: [PATCH 1135/1452] Use dataclass properties in flux_led discovery (#60696) Co-authored-by: epenet --- homeassistant/components/flux_led/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index f33a623faa2..7a161c56826 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -85,9 +85,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" self._discovered_device = { - ATTR_IPADDR: discovery_info[dhcp.IP_ADDRESS], - ATTR_MODEL: discovery_info[dhcp.HOSTNAME], - ATTR_ID: discovery_info[dhcp.MAC_ADDRESS].replace(":", ""), + ATTR_IPADDR: discovery_info.ip, + ATTR_MODEL: discovery_info.hostname, + ATTR_ID: discovery_info.macaddress.replace(":", ""), } return await self._async_handle_discovery() From fa95146aa0ec9cdc62f0f578e26c9a8f36681edc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:51:01 +0100 Subject: [PATCH 1136/1452] Use dataclass properties in songpal discovery (#60737) Co-authored-by: epenet --- homeassistant/components/songpal/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 1475e51afb5..1a0030e4904 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_ENDPOINT, DOMAIN @@ -92,15 +93,15 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_NAME: self.conf.name, CONF_ENDPOINT: self.conf.endpoint}, ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Songpal device.""" - await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() _LOGGER.debug("Discovered: %s", discovery_info) - friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] - parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + parsed_url = urlparse(discovery_info.ssdp_location) scalarweb_info = discovery_info["X_ScalarWebAPI_DeviceInfo"] endpoint = scalarweb_info["X_ScalarWebAPI_BaseURL"] service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ From 64a4218a244ebccc3eb21900fce7bd05138fc035 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:52:29 +0100 Subject: [PATCH 1137/1452] Use dataclass properties in squeezebox discovery (#60738) Co-authored-by: epenet --- homeassistant/components/squeezebox/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index ac5bf8b580e..03c5f45e357 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -192,7 +192,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug( "Reached dhcp discovery of a player with info: %s", discovery_info ) - await self.async_set_unique_id(format_mac(discovery_info[dhcp.MAC_ADDRESS])) + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured() _LOGGER.debug("Configuring dhcp player with unique id: %s", self.unique_id) From 30e573b6945c6f17f3f47a7f1574f177ba62e657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 17:53:04 +0100 Subject: [PATCH 1138/1452] Use state class enum in Mill (#60726) --- homeassistant/components/mill/sensor.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 6d53ae22a4e..f82d3dbcc34 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -4,11 +4,10 @@ from __future__ import annotations import mill from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, @@ -43,14 +42,14 @@ HEATER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=CONSUMPTION_YEAR, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, name="Year consumption", ), SensorEntityDescription( key=CONSUMPTION_TODAY, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, name="Day consumption", ), ) @@ -61,21 +60,21 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, name="Temperature", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, name="Humidity", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, name="Battery", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( @@ -88,7 +87,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=TVOC, native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, name="TVOC", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 4fa58b1ecbf7ba1db47290be1835dd3e267cf71f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:54:40 +0100 Subject: [PATCH 1139/1452] Use dataclass properties in unifi discovery (#60743) Co-authored-by: epenet --- homeassistant/components/unifi/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 210216cfe3c..4ab566eb5b4 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -215,11 +216,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered UniFi device.""" - parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) - model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] - mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL]) + parsed_url = urlparse(discovery_info.ssdp_location) + model_description = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] + mac_address = format_mac(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) self.config = { CONF_HOST: parsed_url.hostname, From aefd89d8f29ed145de69ee0267958d6f37e8960e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:01:51 +0100 Subject: [PATCH 1140/1452] Use dataclass properties in tplink discovery (#60742) Co-authored-by: epenet --- homeassistant/components/tplink/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index f07d8887b85..e2c03dd43f2 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -35,7 +35,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" return await self._async_handle_discovery( - discovery_info[dhcp.IP_ADDRESS], discovery_info[dhcp.MAC_ADDRESS] + discovery_info.ip, discovery_info.macaddress ) async def async_step_discovery( From 0c89c8a6b4f7ec0cd06a40a17f4b81ac99f30c6c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:04:00 +0100 Subject: [PATCH 1141/1452] Use dataclass properties in synology_dsm discovery (#60740) Co-authored-by: epenet --- homeassistant/components/synology_dsm/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 985b18d68a9..8b3a566d854 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -239,12 +239,12 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered synology_dsm.""" - parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + parsed_url = urlparse(discovery_info.ssdp_location) friendly_name = ( - discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() + discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() ) - discovered_mac = discovery_info[ssdp.ATTR_UPNP_SERIAL].upper() + discovered_mac = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL].upper() # Synology NAS can broadcast on multiple IP addresses, since they can be connected to multiple ethernets. # The serial of the NAS is actually its MAC address. From d60517d5f473e3e82ae4b6b92db354ab6b27066a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 18:06:13 +0100 Subject: [PATCH 1142/1452] Use state and device class enum in Rfxtrx (#60773) --- homeassistant/components/rfxtrx/sensor.py | 85 ++++++++++------------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 8ce9843076d..d4d37bcd07b 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -8,23 +8,14 @@ import logging from RFXtrx import ControlEvent, SensorEvent from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) 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, @@ -77,94 +68,94 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Barometer", - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, convert=_battery_convert, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Current", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), RfxtrxSensorEntityDescription( key="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, convert=_rssi_convert, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), RfxtrxSensorEntityDescription( @@ -175,33 +166,33 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Count", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Wind gust", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Rain total", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LENGTH_MILLIMETERS, ), RfxtrxSensorEntityDescription( @@ -215,7 +206,7 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="UV", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), ) From c7eaba45f4d388325e0a4657e933c608afaad98a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:07:12 +0100 Subject: [PATCH 1143/1452] Use dataclass properties in wilight discovery (#60748) Co-authored-by: epenet --- .../components/wilight/config_flow.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index a5e14ebfc6b..dc8e5fc39cc 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -6,6 +6,7 @@ import pywilight from homeassistant.components import ssdp from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult from . import DOMAIN @@ -49,24 +50,24 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): } return self.async_create_entry(title=self._title, data=data) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered WiLight.""" # Filter out basic information if ( - not discovery_info.get(ssdp.ATTR_SSDP_LOCATION) - or ssdp.ATTR_UPNP_MANUFACTURER not in discovery_info - or ssdp.ATTR_UPNP_SERIAL not in discovery_info - or ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info - or ssdp.ATTR_UPNP_MODEL_NUMBER not in discovery_info + not discovery_info.ssdp_location + or ssdp.ATTR_UPNP_MANUFACTURER not in discovery_info.upnp + or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp + or ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info.upnp + or ssdp.ATTR_UPNP_MODEL_NUMBER not in discovery_info.upnp ): return self.async_abort(reason="not_wilight_device") # Filter out non-WiLight devices - if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER] != WILIGHT_MANUFACTURER: + if discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] != WILIGHT_MANUFACTURER: return self.async_abort(reason="not_wilight_device") - host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL] - model_name = discovery_info[ssdp.ATTR_UPNP_MODEL_NAME] + host = urlparse(discovery_info.ssdp_location).hostname + serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + model_name = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] if not self._wilight_update(host, serial_number, model_name): return self.async_abort(reason="not_wilight_device") From a6ec646f987cfda9b356c9a4bec281c1f7f4cee0 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Wed, 1 Dec 2021 17:18:42 +0000 Subject: [PATCH 1144/1452] Tesla wall connector config flow refactor continued (#60774) --- .../tesla_wall_connector/strings.json | 10 ------- .../tesla_wall_connector/conftest.py | 10 +++++++ .../tesla_wall_connector/test_config_flow.py | 29 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/strings.json b/homeassistant/components/tesla_wall_connector/strings.json index 78d3556fbd1..6fd43f52f30 100644 --- a/homeassistant/components/tesla_wall_connector/strings.json +++ b/homeassistant/components/tesla_wall_connector/strings.json @@ -16,15 +16,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "options": { - "step": { - "init": { - "title": "Configure options for Tesla Wall Connector", - "data": { - "scan_interval": "Update frequency" - } - } - } } } \ No newline at end of file diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index 477926d4b66..e9f26a58eec 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -29,6 +29,16 @@ def mock_wall_connector_version(): yield +@pytest.fixture +async def mock_wall_connector_setup(): + """Mock component setup.""" + with patch( + "homeassistant.components.tesla_wall_connector.async_setup_entry", + return_value=True, + ): + yield + + def get_default_version_data(): """Return default version data object for a wall connector.""" return Version( diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 2038a130124..7ed3dfbb8ea 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from tesla_wall_connector.exceptions import WallConnectorConnectionError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST @@ -15,7 +15,6 @@ from tests.common import MockConfigEntry async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> 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} ) @@ -78,7 +77,9 @@ async def test_form_other_error( assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(mock_wall_connector_version, hass): +async def test_form_already_configured( + mock_wall_connector_setup, mock_wall_connector_version, hass +): """Test we get already configured.""" entry = MockConfigEntry( @@ -90,24 +91,22 @@ async def test_form_already_configured(mock_wall_connector_version, hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.tesla_wall_connector.async_setup_entry", - return_value=True, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1"}, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1"}, + ) + await hass.async_block_till_done() - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP assert entry.data[CONF_HOST] == "1.1.1.1" -async def test_dhcp_can_finish(mock_wall_connector_version, hass): +async def test_dhcp_can_finish( + mock_wall_connector_setup, mock_wall_connector_version, hass +): """Test DHCP discovery flow can finish right away.""" result = await hass.config_entries.flow.async_init( From b41e020f85e15fa829aae573ca9fc6b01605407f Mon Sep 17 00:00:00 2001 From: xpac1985 Date: Wed, 1 Dec 2021 18:18:58 +0100 Subject: [PATCH 1145/1452] Better warning if unit of sensor is unsupported for its device class (#60665) * Better warning if unit of sensor is unsupported for its device class * Prettify the code --- homeassistant/components/sensor/recorder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 8bddc74693e..25cb81ded12 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -266,7 +266,12 @@ def _normalize_states( hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id) - _LOGGER.warning("%s has unknown unit %s", entity_id, unit) + _LOGGER.warning( + "%s has unit %s which is unsupported for device_class %s", + entity_id, + unit, + device_class, + ) continue fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) From 037f4dbdb1d9d46bc88db9118ff349fb9d5588bb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 1 Dec 2021 18:30:47 +0100 Subject: [PATCH 1146/1452] Use device class enums in Netatmo (#60723) --- homeassistant/components/netatmo/sensor.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 5b93bdec27f..519e7d8b973 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -10,6 +10,7 @@ import pyatmo from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -19,12 +20,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONCENTRATION_PARTS_PER_MILLION, DEGREE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_MILLIMETERS, PERCENTAGE, @@ -93,7 +88,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Temperature", entity_registry_enabled_default=True, native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -109,7 +104,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -118,7 +113,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Pressure", entity_registry_enabled_default=True, native_unit_of_measurement=PRESSURE_MBAR, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -143,7 +138,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Humidity", entity_registry_enabled_default=True, native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -180,7 +175,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -256,7 +251,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( @@ -274,7 +269,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( From 3db3f264c29937603f13a43725da43c7861911d2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:31:51 +0100 Subject: [PATCH 1147/1452] Use dataclass properties in nuki discovery (#60731) Co-authored-by: epenet --- homeassistant/components/nuki/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index c3e0ed93372..bd2c5a0d750 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -69,13 +69,13 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Nuki bridge.""" - await self.async_set_unique_id(int(discovery_info[dhcp.HOSTNAME][12:], 16)) + await self.async_set_unique_id(int(discovery_info.hostname[12:], 16)) self._abort_if_unique_id_configured() self.discovery_schema = vol.Schema( { - vol.Required(CONF_HOST, default=discovery_info[dhcp.IP_ADDRESS]): str, + vol.Required(CONF_HOST, default=discovery_info.ip): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, vol.Required(CONF_TOKEN): str, } From e001cb3b17f9bcae2bfaf60b8c6e7a035916ecae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:32:15 +0100 Subject: [PATCH 1148/1452] Use dataclass properties in powerwall discovery (#60732) Co-authored-by: epenet --- homeassistant/components/powerwall/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index b8b2f6fdee0..cb9929a890e 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -60,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.ip_address = discovery_info[dhcp.IP_ADDRESS] + self.ip_address = discovery_info.ip self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() From 800ffc0decb3f9d6788caed3b71908feca3fb282 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:32:48 +0100 Subject: [PATCH 1149/1452] Use ssdp namespace in roku (#60733) Co-authored-by: epenet --- homeassistant/components/roku/config_flow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 688e243886b..dff0f7d53c6 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -8,7 +8,6 @@ from rokuecp import Roku, RokuError import voluptuous as vol from homeassistant.components import ssdp, zeroconf -from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback @@ -113,8 +112,8 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info.ssdp_location).hostname - name = discovery_info.upnp[ATTR_UPNP_FRIENDLY_NAME] - serial_number = discovery_info.upnp[ATTR_UPNP_SERIAL] + name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] await self.async_set_unique_id(serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) From ead4f745e3d4d0f3664d3e05c53bc0b8eaaaa6da Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:33:22 +0100 Subject: [PATCH 1150/1452] Use dataclass properties in somfy_mylink discovery (#60736) Co-authored-by: epenet --- homeassistant/components/somfy_mylink/config_flow.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 6f695d06cbe..768d12da45b 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -61,16 +61,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self._async_abort_entries_match({CONF_HOST: discovery_info[dhcp.IP_ADDRESS]}) + self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) - formatted_mac = format_mac(discovery_info[dhcp.MAC_ADDRESS]) + formatted_mac = format_mac(discovery_info.macaddress) await self.async_set_unique_id(format_mac(formatted_mac)) - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[dhcp.IP_ADDRESS]} - ) - self.host = discovery_info[dhcp.HOSTNAME] + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + self.host = discovery_info.hostname self.mac = formatted_mac - self.ip_address = discovery_info[dhcp.IP_ADDRESS] + self.ip_address = discovery_info.ip self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac} return await self.async_step_user() From 68011ee952dfc928fc13a7308870b8737177a5ae Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 1 Dec 2021 18:35:12 +0100 Subject: [PATCH 1151/1452] Clean up Netatmo climate platform (#60694) Co-authored-by: Franck Nijhof --- homeassistant/components/netatmo/climate.py | 101 ++++++-------------- 1 file changed, 31 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index db324cd1722..10c8ba182f0 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -173,6 +173,13 @@ async def async_setup_entry( class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" + _attr_hvac_mode = HVAC_MODE_AUTO + _attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + _attr_max_temp = DEFAULT_MAX_TEMP + _attr_preset_modes = SUPPORT_PRESET + _attr_target_temperature_step = PRECISION_HALVES + _attr_temperature_unit = TEMP_CELSIUS + def __init__( self, data_handler: NetatmoDataHandler, home_id: str, room_id: str ) -> None: @@ -213,13 +220,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._device_name = self._data.rooms[home_id][room_id]["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" - self._current_temperature: float | None = None - self._target_temperature: float | None = None - self._preset: str | None = None self._away: bool | None = None - self._operation_list = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] self._support_flags = SUPPORT_FLAGS - self._hvac_mode: str = HVAC_MODE_AUTO self._battery_level = None self._connected: bool | None = None @@ -230,9 +232,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._selected_schedule = None if self._model == NA_THERM: - self._operation_list.append(HVAC_MODE_OFF) + self._attr_hvac_modes.append(HVAC_MODE_OFF) - self._attr_max_temp = DEFAULT_MAX_TEMP self._attr_unique_id = f"{self._id}-{self._model}" async def async_added_to_hass(self) -> None: @@ -283,13 +284,13 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return if data["event_type"] == EVENT_TYPE_THERM_MODE: - self._preset = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] - self._hvac_mode = HVAC_MAP_NETATMO[self._preset] - if self._preset == PRESET_FROST_GUARD: - self._target_temperature = self._hg_temperature - elif self._preset == PRESET_AWAY: - self._target_temperature = self._away_temperature - elif self._preset == PRESET_SCHEDULE: + self._attr_preset_mode = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] + self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] + if self._attr_preset_mode == PRESET_FROST_GUARD: + self._attr_target_temperature = self._hg_temperature + elif self._attr_preset_mode == PRESET_AWAY: + self._attr_target_temperature = self._away_temperature + elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() self.data_handler.async_force_update(self._home_status_class) self.async_write_ha_state() @@ -298,20 +299,20 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): for room in home.get("rooms", []): if data["event_type"] == EVENT_TYPE_SET_POINT and self._id == room["id"]: if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: - self._hvac_mode = HVAC_MODE_OFF - self._preset = STATE_NETATMO_OFF - self._target_temperature = 0 + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_preset_mode = STATE_NETATMO_OFF + self._attr_target_temperature = 0 elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX: - self._hvac_mode = HVAC_MODE_HEAT - self._preset = PRESET_MAP_NETATMO[PRESET_BOOST] - self._target_temperature = DEFAULT_MAX_TEMP + self._attr_hvac_mode = HVAC_MODE_HEAT + self._attr_preset_mode = PRESET_MAP_NETATMO[PRESET_BOOST] + self._attr_target_temperature = DEFAULT_MAX_TEMP elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL: - self._hvac_mode = HVAC_MODE_HEAT - self._target_temperature = room["therm_setpoint_temperature"] + self._attr_hvac_mode = HVAC_MODE_HEAT + self._attr_target_temperature = room["therm_setpoint_temperature"] else: - self._target_temperature = room["therm_setpoint_temperature"] - if self._target_temperature == DEFAULT_MAX_TEMP: - self._hvac_mode = HVAC_MODE_HEAT + self._attr_target_temperature = room["therm_setpoint_temperature"] + if self._attr_target_temperature == DEFAULT_MAX_TEMP: + self._attr_hvac_mode = HVAC_MODE_HEAT self.async_write_ha_state() return @@ -335,36 +336,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Return the list of supported features.""" return self._support_flags - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self) -> float | None: - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def target_temperature_step(self) -> float | None: - """Return the supported step of target temperature.""" - return PRECISION_HALVES - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode.""" - return self._hvac_mode - - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return self._operation_list - @property def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" @@ -432,16 +403,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() - @property - def preset_mode(self) -> str | None: - """Return the current preset mode, e.g., home, away, temp.""" - return self._preset - - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return SUPPORT_PRESET - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: @@ -510,14 +471,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if "current_temperature" not in roomstatus: return - self._current_temperature = roomstatus["current_temperature"] - self._target_temperature = roomstatus["target_temperature"] - self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] - self._hvac_mode = HVAC_MAP_NETATMO[self._preset] + self._attr_current_temperature = roomstatus["current_temperature"] + self._attr_target_temperature = roomstatus["target_temperature"] + self._attr_preset_mode = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] + self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] self._battery_level = roomstatus.get("battery_state") self._connected = True - self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] + self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] if self._battery_level is not None: self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = self._battery_level From 824b31370547ff356ab480d2b40917c5f5ae1b4a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:36:07 +0100 Subject: [PATCH 1152/1452] Use dataclass properties in yamaha_musiccast discovery (#60749) Co-authored-by: epenet --- .../components/yamaha_musiccast/config_flow.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 6645af20d83..2d4e723d745 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -82,16 +82,18 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_ssdp(self, discovery_info) -> data_entry_flow.FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> data_entry_flow.FlowResult: """Handle ssdp discoveries.""" if not await MusicCastDevice.check_yamaha_ssdp( - discovery_info[ssdp.ATTR_SSDP_LOCATION], async_get_clientsession(self.hass) + discovery_info.ssdp_location, async_get_clientsession(self.hass) ): return self.async_abort(reason="yxc_control_url_missing") - self.serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL] - self.host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - self.upnp_description = discovery_info[ssdp.ATTR_SSDP_LOCATION] + self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + self.host = urlparse(discovery_info.ssdp_location or "").hostname or "" + self.upnp_description = discovery_info.ssdp_location await self.async_set_unique_id(self.serial_number) self._abort_if_unique_id_configured( { @@ -102,7 +104,9 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): self.context.update( { "title_placeholders": { - "name": discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host) + "name": discovery_info.upnp.get( + ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host + ) } } ) From 5c992ec2cc14462622e26290230c384b15583f08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:38:07 +0100 Subject: [PATCH 1153/1452] Remove cleanup_registry from onewire (#60546) Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 31 +------------ .../components/onewire/onewirehub.py | 11 ----- tests/components/onewire/test_init.py | 44 ------------------- 3 files changed, 1 insertion(+), 85 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 5981a654820..753d30e5958 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,11 +1,9 @@ """The 1-Wire component.""" -import asyncio import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -25,34 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = onewirehub - async def cleanup_registry(onewirehub: OneWireHub) -> None: - # Get registries - device_registry = dr.async_get(hass) - # Generate list of all device entries - registry_devices = list( - dr.async_entries_for_config_entry(device_registry, entry.entry_id) - ) - # Remove devices that don't belong to any entity - for device in registry_devices: - if not onewirehub.has_device_in_cache(device): - _LOGGER.debug( - "Removing device `%s` because it is no longer available", - device.id, - ) - device_registry.async_remove_device(device.id) - - async def start_platforms(onewirehub: OneWireHub) -> None: - """Start platforms and cleanup devices.""" - # wait until all required platforms are ready - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await cleanup_registry(onewirehub) - - hass.async_create_task(start_platforms(onewirehub)) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 65f49261f56..ed032eb4fde 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -22,7 +22,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity import DeviceInfo from .const import ( @@ -222,16 +221,6 @@ class OneWireHub: assert isinstance(device_type, str) return device_type - def has_device_in_cache(self, device: DeviceEntry) -> bool: - """Check if device was present in the cache.""" - if TYPE_CHECKING: - assert self.devices - for internal_device in self.devices: - for identifier in internal_device.device_info[ATTR_IDENTIFIERS]: - if identifier in device.identifiers: - return True - return False - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 5425802c6d7..e3a3fdcc564 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,18 +1,11 @@ """Tests for 1-Wire config flow.""" import logging -from unittest.mock import MagicMock, patch import pytest from homeassistant.components.onewire.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er - -from . import setup_owproxy_mock_devices - -from tests.common import mock_device_registry, mock_registry @pytest.mark.usefixtures("owproxy_with_connerror") @@ -71,40 +64,3 @@ async def test_unload_sysbus_entry( assert sysbus_config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) - - -@patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]) -async def test_registry_cleanup( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock -): - """Test for 1-Wire device. - - As they would be on a clean setup: all binary-sensors and switches disabled. - """ - entity_registry = mock_registry(hass) - device_registry = mock_device_registry(hass) - - # Initialise with two components - setup_owproxy_mock_devices( - owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"] - ) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 - assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 - - # Second item has disappeared from bus, and was removed manually from the front-end - setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"]) - entity_registry.async_remove("sensor.28_111111111111_temperature") - await hass.async_block_till_done() - - assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 - assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 - - # Second item has disappeared from bus, and was removed manually from the front-end - await hass.config_entries.async_reload("2") - await hass.async_block_till_done() - - assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 - assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1 From b65b2c4cd118ee358a5d40fd8636ebb8509e6de9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:40:27 +0100 Subject: [PATCH 1154/1452] Use dataclass properties in syncthru discovery (#60739) Co-authored-by: epenet --- homeassistant/components/syncthru/config_flow.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index db822f650dc..ac7b317faf8 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_NAME, CONF_URL +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN @@ -33,15 +34,15 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import initiated flow.""" return await self.async_step_user(user_input=user_input) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP initiated flow.""" - await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() self.url = url_normalize( - discovery_info.get( + discovery_info.upnp.get( ssdp.ATTR_UPNP_PRESENTATION_URL, - f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", + f"http://{urlparse(discovery_info.ssdp_location or '').hostname}/", ) ) @@ -50,12 +51,12 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): # Update unique id of entry with the same URL if not existing_entry.unique_id: - await self.hass.config_entries.async_update_entry( - existing_entry, unique_id=discovery_info[ssdp.ATTR_UPNP_UDN] + self.hass.config_entries.async_update_entry( + existing_entry, unique_id=discovery_info.upnp[ssdp.ATTR_UPNP_UDN] ) return self.async_abort(reason="already_configured") - self.name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + self.name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") if self.name: # Remove trailing " (ip)" if present for consistency with user driven config self.name = re.sub(r"\s+\([\d.]+\)\s*$", "", self.name) From a053c0a106f47dd8427547665e3572957f4af36d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 1 Dec 2021 10:52:33 -0700 Subject: [PATCH 1155/1452] Bump py17track to 2021.12.1 (#60762) --- .../components/seventeentrack/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/seventeentrack/test_sensor.py | 182 +++++++++--------- 4 files changed, 94 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 15a94a4230f..05f240043a9 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -2,7 +2,7 @@ "domain": "seventeentrack", "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": ["py17track==3.2.1"], + "requirements": ["py17track==2021.12.1"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0a65c414efd..c9c32cb4520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ py-synologydsm-api==1.0.4 py-zabbix==1.1.7 # homeassistant.components.seventeentrack -py17track==3.2.1 +py17track==2021.12.1 # homeassistant.components.hdmi_cec pyCEC==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cca40724fa4..a7e50494b6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -792,7 +792,7 @@ py-nightscout==1.2.2 py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack -py17track==3.2.1 +py17track==2021.12.1 # homeassistant.components.control4 pyControl4==0.0.6 diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index cef92496561..4e22822150b 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -172,14 +172,14 @@ async def test_invalid_config(hass): async def test_add_package(hass): """Ensure package is added correctly when user add a new package.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package] @@ -188,14 +188,14 @@ async def test_add_package(hass): assert len(hass.states.async_entity_ids()) == 1 package2 = Package( - "789", - 206, - "friendly name 2", - "info text 2", - "location 2", - "2020-08-10 14:25", - 206, - 2, + tracking_number="789", + destination_country=206, + friendly_name="friendly name 2", + info_text="info text 2", + location="location 2", + timestamp="2020-08-10 14:25", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package, package2] @@ -208,24 +208,24 @@ async def test_add_package(hass): async def test_remove_package(hass): """Ensure entity is not there anymore if package is not there.""" package1 = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, ) package2 = Package( - "789", - 206, - "friendly name 2", - "info text 2", - "location 2", - "2020-08-10 14:25", - 206, - 2, + tracking_number="789", + destination_country=206, + friendly_name="friendly name 2", + info_text="info text 2", + location="location 2", + timestamp="2020-08-10 14:25", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package1, package2] @@ -248,14 +248,14 @@ async def test_remove_package(hass): async def test_friendly_name_changed(hass): """Test friendly name change.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package] @@ -265,14 +265,14 @@ async def test_friendly_name_changed(hass): assert len(hass.states.async_entity_ids()) == 1 package = Package( - "456", - 206, - "friendly name 2", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 2", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package] @@ -289,15 +289,15 @@ async def test_friendly_name_changed(hass): async def test_delivered_not_shown(hass): """Ensure delivered packages are not shown.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, - 40, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, + status=40, ) ProfileMock.package_list = [package] @@ -312,15 +312,15 @@ async def test_delivered_not_shown(hass): async def test_delivered_shown(hass): """Ensure delivered packages are show when user choose to show them.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, - 40, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, + status=40, ) ProfileMock.package_list = [package] @@ -335,14 +335,14 @@ async def test_delivered_shown(hass): async def test_becomes_delivered_not_shown_notification(hass): """Ensure notification is triggered when package becomes delivered.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, ) ProfileMock.package_list = [package] @@ -352,15 +352,15 @@ async def test_becomes_delivered_not_shown_notification(hass): assert len(hass.states.async_entity_ids()) == 1 package_delivered = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, - 40, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, + status=40, ) ProfileMock.package_list = [package_delivered] @@ -391,14 +391,14 @@ async def test_summary_correctly_updated(hass): async def test_utc_timestamp(hass): """Ensure package timestamp is converted correctly from HA-defined time zone to UTC.""" package = Package( - "456", - 206, - "friendly name 1", - "info text 1", - "location 1", - "2020-08-10 10:32", - 206, - 2, + tracking_number="456", + destination_country=206, + friendly_name="friendly name 1", + info_text="info text 1", + location="location 1", + timestamp="2020-08-10 10:32", + origin_country=206, + package_type=2, tz="Asia/Jakarta", ) ProfileMock.package_list = [package] From 8ddfa424c00724bcc6eb7616b54ba726d523176b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 1 Dec 2021 18:59:52 +0100 Subject: [PATCH 1156/1452] Add typing to deCONZ init and config flow (#59999) --- homeassistant/components/deconz/__init__.py | 38 ++++++--- .../components/deconz/config_flow.py | 84 ++++++++++++------- homeassistant/components/deconz/services.py | 7 +- tests/components/deconz/test_services.py | 21 +++++ 4 files changed, 106 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 47a70a43ae2..f069605d438 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,12 +1,18 @@ """Support for deCONZ devices.""" + +from __future__ import annotations + +from typing import cast + +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import async_migrate_entries +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.entity_registry as er from .config_flow import get_master_gateway from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN @@ -14,7 +20,7 @@ from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a deCONZ bridge for a config entry. Load config, group, light and sensor data for server information. @@ -28,7 +34,6 @@ async def async_setup_entry(hass, config_entry): await async_update_master_gateway(hass, config_entry) gateway = DeconzGateway(hass, config_entry) - if not await gateway.async_setup(): return False @@ -46,7 +51,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload deCONZ config entry.""" gateway = hass.data[DOMAIN].pop(config_entry.entry_id) @@ -61,27 +66,36 @@ async def async_unload_entry(hass, config_entry): return await gateway.async_reset() -async def async_update_master_gateway(hass, config_entry): +async def async_update_master_gateway( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Update master gateway boolean. Called by setup_entry and unload_entry. Makes sure there is always one master available. """ - master = not get_master_gateway(hass) + try: + master_gateway = get_master_gateway(hass) + master = master_gateway.config_entry == config_entry + except ValueError: + master = True + options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) -async def async_update_group_unique_id(hass, config_entry) -> None: +async def async_update_group_unique_id( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Update unique ID entities based on deCONZ groups.""" - if not (old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE)): + if not isinstance(old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE), str): return - new_unique_id: str = config_entry.unique_id + new_unique_id = cast(str, config_entry.unique_id) @callback - def update_unique_id(entity_entry): + def update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if f"{old_unique_id}-" not in entity_entry.unique_id: return None @@ -91,7 +105,7 @@ async def async_update_group_unique_id(hass, config_entry) -> None: ) } - await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) data = { CONF_API_KEY: config_entry.data[CONF_API_KEY], CONF_HOST: config_entry.data[CONF_HOST], diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 42541123b4f..e4011f6a4a6 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,6 +1,10 @@ """Config flow to configure deCONZ component.""" + +from __future__ import annotations + import asyncio from pprint import pformat +from typing import Any, cast from urllib.parse import urlparse import async_timeout @@ -15,8 +19,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.components.deconz.gateway import DeconzGateway +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import ( @@ -36,33 +43,36 @@ CONF_MANUAL_INPUT = "Manually define gateway" @callback -def get_master_gateway(hass): +def get_master_gateway(hass: HomeAssistant) -> DeconzGateway: """Return the gateway which is marked as master.""" for gateway in hass.data[DOMAIN].values(): if gateway.master: - return gateway + return cast(DeconzGateway, gateway) + raise ValueError -class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a deCONZ config flow.""" VERSION = 1 - _hassio_discovery = None + _hassio_discovery: dict[str, Any] @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return DeconzOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize the deCONZ config flow.""" - self.bridge_id = None - self.bridges = [] - self.deconz_config = {} + self.bridge_id = "" + self.bridges: list[dict[str, int | str]] = [] + self.deconz_config: dict[str, int | str] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a deCONZ config flow start. Let user choose between discovered bridges and manual configuration. @@ -75,7 +85,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.bridge_id = bridge[CONF_BRIDGE_ID] + self.bridge_id = cast(str, bridge[CONF_BRIDGE_ID]) self.deconz_config = { CONF_HOST: bridge[CONF_HOST], CONF_PORT: bridge[CONF_PORT], @@ -108,7 +118,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual_input() - async def async_step_manual_input(self, user_input=None): + async def async_step_manual_input( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manual configuration.""" if user_input: self.deconz_config = user_input @@ -124,9 +136,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_link(self, user_input=None): + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Attempt to link with the deCONZ bridge.""" - errors = {} + errors: dict[str, str] = {} LOGGER.debug( "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config) @@ -153,7 +167,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="link", errors=errors) - async def _create_entry(self): + async def _create_entry(self) -> FlowResult: """Create entry for gateway.""" if not self.bridge_id: session = aiohttp_client.async_get_clientsession(self.hass) @@ -178,7 +192,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) - async def async_step_reauth(self, config: dict): + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} @@ -189,7 +203,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_link() - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: dict[str, str]) -> FlowResult: """Handle a discovered deCONZ bridge.""" if ( discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) @@ -206,20 +220,20 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if entry and entry.source == config_entries.SOURCE_HASSIO: return self.async_abort(reason="already_configured") + hostname = cast(str, parsed_url.hostname) + port = cast(int, parsed_url.port) + self._abort_if_unique_id_configured( - updates={CONF_HOST: parsed_url.hostname, CONF_PORT: parsed_url.port} + updates={CONF_HOST: hostname, CONF_PORT: port} ) - self.context["title_placeholders"] = {"host": parsed_url.hostname} + self.context["title_placeholders"] = {"host": hostname} - self.deconz_config = { - CONF_HOST: parsed_url.hostname, - CONF_PORT: parsed_url.port, - } + self.deconz_config = {CONF_HOST: hostname, CONF_PORT: port} return await self.async_step_link() - async def async_step_hassio(self, discovery_info): + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: """Prepare configuration for a Hass.io deCONZ bridge. This flow is triggered by the discovery component. @@ -241,8 +255,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_hassio_confirm() - async def async_step_hassio_confirm(self, user_input=None): + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a Hass.io discovery.""" + if user_input is not None: self.deconz_config = { CONF_HOST: self._hassio_discovery[CONF_HOST], @@ -258,21 +275,26 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class DeconzOptionsFlowHandler(config_entries.OptionsFlow): +class DeconzOptionsFlowHandler(OptionsFlow): """Handle deCONZ options.""" - def __init__(self, config_entry): + gateway: DeconzGateway + + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize deCONZ options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.gateway = None - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the deCONZ options.""" self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry) return await self.async_step_deconz_devices() - async def async_step_deconz_devices(self, user_input=None): + async def async_step_deconz_devices( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the deconz devices options.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 63252aa7787..529616138a2 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -66,7 +66,6 @@ def async_setup_services(hass: HomeAssistant) -> None: service = service_call.service service_data = service_call.data - gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in service_data: found_gateway = False bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID]) @@ -80,6 +79,12 @@ def async_setup_services(hass: HomeAssistant) -> None: if not found_gateway: LOGGER.error("Could not find the gateway %s", bridge_id) return + else: + try: + gateway = get_master_gateway(hass) + except ValueError: + LOGGER.error("No master gateway available") + return if service == SERVICE_CONFIGURE_DEVICE: await async_configure_service(gateway, service_data) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index c27530b012e..5cdd36440ea 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components.deconz.const import ( CONF_BRIDGE_ID, + CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT @@ -187,6 +188,26 @@ async def test_configure_service_with_faulty_entity(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 0 +async def test_calling_service_with_no_master_gateway_fails(hass, aioclient_mock): + """Test that service call fails when no master gateway exist.""" + await setup_deconz_integration( + hass, aioclient_mock, options={CONF_MASTER_GATEWAY: False} + ) + aioclient_mock.clear_requests() + + data = { + SERVICE_FIELD: "/lights/1", + SERVICE_DATA: {"on": True}, + } + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 0 + + async def test_service_refresh_devices(hass, aioclient_mock): """Test that service can refresh devices.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) From 4dfdb3b96fd9e2d448f0b540580f079132ac0241 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 19:08:35 +0100 Subject: [PATCH 1157/1452] Upgrade vehicle to 0.2.2 (#60763) * Upgrade vehicle to 0.2.1 * Upgrade vehicle to 0.2.2 --- homeassistant/components/rdw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index cf4e457c0ac..c4f80f812ca 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -3,7 +3,7 @@ "name": "RDW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", - "requirements": ["vehicle==0.2.0"], + "requirements": ["vehicle==0.2.2"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index c9c32cb4520..29534df1657 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.rdw -vehicle==0.2.0 +vehicle==0.2.2 # homeassistant.components.velbus velbus-aio==2021.11.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7e50494b6f..682aa6b7f18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1404,7 +1404,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.rdw -vehicle==0.2.0 +vehicle==0.2.2 # homeassistant.components.velbus velbus-aio==2021.11.7 From 5c4422dc720681c510080087ff9857f1a0dcf721 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Wed, 1 Dec 2021 18:21:56 +0000 Subject: [PATCH 1158/1452] Remove power sensor from Tesla Wall Connector (#60775) Add voltage and current sensors for each phase --- .../components/tesla_wall_connector/sensor.py | 70 ++++++++++++------- .../tesla_wall_connector/test_sensor.py | 38 ++++++---- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index db4064c8c31..b2353681291 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -10,14 +10,13 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, - POWER_KILO_WATT, TEMP_CELSIUS, ) @@ -73,29 +72,52 @@ WALL_CONNECTOR_SENSORS = [ entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), WallConnectorSensorDescription( - key="power", - name=prefix_entity_name("Power"), - native_unit_of_measurement=POWER_KILO_WATT, - value_fn=lambda data: round( - ( - ( - data[WALLCONNECTOR_DATA_VITALS].currentA_a - * data[WALLCONNECTOR_DATA_VITALS].voltageA_v - ) - + ( - data[WALLCONNECTOR_DATA_VITALS].currentB_a - * data[WALLCONNECTOR_DATA_VITALS].voltageB_v - ) - + ( - data[WALLCONNECTOR_DATA_VITALS].currentC_a - * data[WALLCONNECTOR_DATA_VITALS].voltageC_v - ) - ) - / 1000.0, - 1, - ), - device_class=DEVICE_CLASS_POWER, + key="current_a_a", + name=prefix_entity_name("Phase A Current"), + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentA_a, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="current_b_a", + name=prefix_entity_name("Phase B Current"), + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentB_a, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="current_c_a", + name=prefix_entity_name("Phase C Current"), + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentC_a, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="voltage_a_v", + name=prefix_entity_name("Phase A Voltage"), + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageA_v, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="voltage_b_v", + name=prefix_entity_name("Phase B Voltage"), + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageB_v, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + WallConnectorSensorDescription( + key="voltage_c_v", + name=prefix_entity_name("Phase C Voltage"), + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageC_v, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), WallConnectorSensorDescription( key="total_energy_kWh", diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 37d6c7d9cd1..6763f685441 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -23,10 +23,27 @@ async def test_sensors(hass: HomeAssistant) -> None: EntityAndExpectedValues( "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" ), - EntityAndExpectedValues("sensor.tesla_wall_connector_power", "7.6", "7.6"), EntityAndExpectedValues( "sensor.tesla_wall_connector_total_energy", "988.022", "989.0" ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_a_current", "10", "7" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_b_current", "11.1", "8" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_c_current", "12", "9" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_a_voltage", "230.1", "228.1" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_b_voltage", "231", "229.1" + ), + EntityAndExpectedValues( + "sensor.tesla_wall_connector_phase_c_voltage", "232.1", "230" + ), ] mock_vitals_first_update = get_vitals_mock() @@ -34,13 +51,11 @@ async def test_sensors(hass: HomeAssistant) -> None: mock_vitals_first_update.handle_temp_c = 25.51 mock_vitals_first_update.grid_v = 230.15 mock_vitals_first_update.grid_hz = 50.021 - # to calculate power, we calculate power of each phase and sum up - # (230.1*10) + (231.1*11) + (232.1*12) = 7628.3 W mock_vitals_first_update.voltageA_v = 230.1 - mock_vitals_first_update.voltageB_v = 231.1 + mock_vitals_first_update.voltageB_v = 231 mock_vitals_first_update.voltageC_v = 232.1 mock_vitals_first_update.currentA_a = 10 - mock_vitals_first_update.currentB_a = 11 + mock_vitals_first_update.currentB_a = 11.1 mock_vitals_first_update.currentC_a = 12 mock_vitals_second_update = get_vitals_mock() @@ -48,13 +63,12 @@ async def test_sensors(hass: HomeAssistant) -> None: mock_vitals_second_update.handle_temp_c = -1.42 mock_vitals_second_update.grid_v = 229.21 mock_vitals_second_update.grid_hz = 49.981 - # (228.1*10) + (229.1*11) + (230.1*12) = 7562.3 W - mock_vitals_second_update.voltageB_v = 228.1 - mock_vitals_second_update.voltageC_v = 229.1 - mock_vitals_second_update.voltageA_v = 230.1 - mock_vitals_second_update.currentA_a = 10 - mock_vitals_second_update.currentB_a = 11 - mock_vitals_second_update.currentC_a = 12 + mock_vitals_second_update.voltageA_v = 228.1 + mock_vitals_second_update.voltageB_v = 229.1 + mock_vitals_second_update.voltageC_v = 230 + mock_vitals_second_update.currentA_a = 7 + mock_vitals_second_update.currentB_a = 8 + mock_vitals_second_update.currentC_a = 9 lifetime_mock_first_update = get_lifetime_mock() lifetime_mock_first_update.energy_wh = 988022 From 6a926b41f27869d7679058e2552d2462247cf15f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 19:35:39 +0100 Subject: [PATCH 1159/1452] Enable warnings for SsdpServiceInfo (#60756) Co-authored-by: epenet --- homeassistant/components/ssdp/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c5ff03264a0..57948000701 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -117,7 +117,6 @@ class SsdpServiceInfo( f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True # Use a property if it is available, fallback to upnp data @@ -138,7 +137,6 @@ class SsdpServiceInfo( f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True if hasattr(self, name): @@ -157,7 +155,6 @@ class SsdpServiceInfo( "or discovery_info.ssdp_headers.__contains__(); this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, - level=logging.DEBUG, ) self._warning_logged = True if hasattr(self, name): From 4411d51d6f0236f0a2fc3efe71a4fb7d73eb4a1c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 19:38:20 +0100 Subject: [PATCH 1160/1452] Use dataclass properties in deconz discovery (#60690) Co-authored-by: epenet --- homeassistant/components/deconz/config_flow.py | 8 ++++---- tests/components/deconz/test_gateway.py | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index e4011f6a4a6..ca8e95f70ce 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -203,18 +203,18 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_link() - async def async_step_ssdp(self, discovery_info: dict[str, str]) -> FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered deCONZ bridge.""" if ( - discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) + discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != DECONZ_MANUFACTURERURL ): return self.async_abort(reason="not_deconz_bridge") LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) - self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) - parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + self.bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) + parsed_url = urlparse(discovery_info.ssdp_location) entry = await self.async_set_unique_id(self.bridge_id) if entry and entry.source == config_entries.SOURCE_HASSIO: diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 9457c2e988b..30473814f26 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -7,6 +7,7 @@ import pydeconz from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING import pytest +from homeassistant.components import ssdp from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) @@ -28,7 +29,6 @@ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, ATTR_UPNP_UDN, @@ -262,12 +262,16 @@ async def test_update_address(hass, aioclient_mock): ) as mock_setup_entry: await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, - ATTR_UPNP_UDN: "uuid:456DEF", - }, + data=ssdp.SsdpServiceInfo( + ssdp_st="mock_st", + ssdp_usn="mock_usn", + ssdp_location="http://2.3.4.5:80/", + upnp={ + ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_UDN: "uuid:456DEF", + }, + ), context={"source": SOURCE_SSDP}, ) await hass.async_block_till_done() From 3d5f4e54ea35923524a0bcc01a6f1538b2242531 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 19:39:03 +0100 Subject: [PATCH 1161/1452] Use dataclass properties in vicare discovery (#60746) Co-authored-by: epenet --- homeassistant/components/vicare/config_flow.py | 7 ++++--- tests/components/vicare/test_config_flow.py | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 659b90b6dad..fcd9a1553ca 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -8,7 +8,7 @@ from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import ( CONF_CLIENT_ID, CONF_NAME, @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -74,9 +75,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Invoke when a Viessmann MAC address is discovered on the network.""" - formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) + formatted_mac = format_mac(discovery_info.macaddress) _LOGGER.info("Found device with mac %s", formatted_mac) await self.async_set_unique_id(formatted_mac) diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 6d977934de3..c816f535077 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -167,9 +167,11 @@ async def test_form_dhcp(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - dhcp.MAC_ADDRESS: MOCK_MAC, - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + hostname="mock_hostname", + macaddress=MOCK_MAC, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -249,9 +251,11 @@ async def test_dhcp_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - dhcp.MAC_ADDRESS: MOCK_MAC, - }, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + hostname="mock_hostname", + macaddress=MOCK_MAC, + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" From a3cccb50c7ebc5c7fb1349c7a42166b6dec1af0f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 19:39:43 +0100 Subject: [PATCH 1162/1452] Use dataclass properties in tesla_wall_connector discovery (#60741) Co-authored-by: epenet --- .../tesla_wall_connector/config_flow.py | 8 ++--- .../tesla_wall_connector/test_config_flow.py | 32 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py index 7183a68d428..5b3cf9bd835 100644 --- a/homeassistant/components/tesla_wall_connector/config_flow.py +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -9,7 +9,7 @@ from tesla_wall_connector.exceptions import WallConnectorError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -45,12 +45,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" super().__init__() - self.ip_address = None + self.ip_address: str | None = None self.serial_number = None - async def async_step_dhcp(self, discovery_info) -> FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - self.ip_address = discovery_info[IP_ADDRESS] + self.ip_address = discovery_info.ip _LOGGER.debug("Discovered Tesla Wall Connector at [%s]", self.ip_address) self._async_abort_entries_match({CONF_HOST: self.ip_address}) diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 7ed3dfbb8ea..2e286f75b61 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant import config_entries -from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -112,11 +112,11 @@ async def test_dhcp_can_finish( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "teslawallconnector_abc", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "DC:44:27:12:12", - }, + data=dhcp.DhcpServiceInfo( + hostname="teslawallconnector_abc", + ip="1.2.3.4", + macaddress="DC:44:27:12:12", + ), ) await hass.async_block_till_done() assert result["type"] == "form" @@ -143,11 +143,11 @@ async def test_dhcp_already_exists(mock_wall_connector_version, hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "teslawallconnector_aabbcc", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, + data=dhcp.DhcpServiceInfo( + hostname="teslawallconnector_aabbcc", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), ) await hass.async_block_till_done() @@ -165,11 +165,11 @@ async def test_dhcp_error_from_wall_connector(mock_wall_connector_version, hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={ - HOSTNAME: "teslawallconnector_aabbcc", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, + data=dhcp.DhcpServiceInfo( + hostname="teslawallconnector_aabbcc", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), ) await hass.async_block_till_done() From a1aaecb3bf1fb47ed6ab5e288c2b4da750eddac7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 1 Dec 2021 19:40:51 +0100 Subject: [PATCH 1163/1452] Use state class enums in Netatmo (#60725) --- homeassistant/components/netatmo/sensor.py | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 519e7d8b973..d06c96b3c6a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -8,11 +8,10 @@ from typing import NamedTuple, cast import pyatmo from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -88,8 +87,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Temperature", entity_registry_enabled_default=True, native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="temp_trend", @@ -104,8 +103,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CO2, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="pressure", @@ -113,8 +112,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Pressure", entity_registry_enabled_default=True, native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="pressure_trend", @@ -130,7 +129,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, native_unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="humidity", @@ -138,8 +137,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Humidity", entity_registry_enabled_default=True, native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="rain", @@ -147,7 +146,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Rain", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -156,7 +155,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -165,7 +164,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_24", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -175,8 +174,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.BATTERY, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="windangle", @@ -192,7 +191,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="windstrength", @@ -201,7 +200,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="gustangle", @@ -217,7 +216,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="guststrength", @@ -226,7 +225,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="reachable", @@ -251,8 +250,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="wifi_status", @@ -269,8 +268,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, - state_class=STATE_CLASS_MEASUREMENT, ), NetatmoSensorEntityDescription( key="health_idx", From b32e1d9339ae3305bdf8b2ed56296a29317d73ae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 19:44:42 +0100 Subject: [PATCH 1164/1452] Upgrade hole to 0.7.0 (#60779) --- homeassistant/components/pi_hole/__init__.py | 1 - homeassistant/components/pi_hole/config_flow.py | 2 +- homeassistant/components/pi_hole/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 420cc51373c..930ded4aa42 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -102,7 +102,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass, verify_tls) api = Hole( host, - hass.loop, session, location=location, tls=use_tls, diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index cccd80472e3..5acaffd13b1 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -170,5 +170,5 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, host: str, location: str, tls: bool, verify_tls: bool ) -> None: session = async_get_clientsession(self.hass, verify_tls) - pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls) + pi_hole = Hole(host, session, location=location, tls=tls) await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 415075549bb..28ceb8e6c45 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -2,7 +2,7 @@ "domain": "pi_hole", "name": "Pi-hole", "documentation": "https://www.home-assistant.io/integrations/pi_hole", - "requirements": ["hole==0.6.0"], + "requirements": ["hole==0.7.0"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 29534df1657..2ece7eab270 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hkavr==0.0.5 hlk-sw16==0.0.9 # homeassistant.components.pi_hole -hole==0.6.0 +hole==0.7.0 # homeassistant.components.workday holidays==0.11.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 682aa6b7f18..e5029c44100 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -509,7 +509,7 @@ herepy==2.0.0 hlk-sw16==0.0.9 # homeassistant.components.pi_hole -hole==0.6.0 +hole==0.7.0 # homeassistant.components.workday holidays==0.11.3.1 From d7bf8a7ac3938c544612effe9a77e17f58242674 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 19:45:16 +0100 Subject: [PATCH 1165/1452] Upgrade aiohttp to 3.8.1 (#60778) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 199bca41619..7b1d842bdc9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ PyJWT==2.1.0 PyNaCl==1.4.0 aiodiscover==1.4.5 -aiohttp==3.8.0 +aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.22.12 diff --git a/requirements.txt b/requirements.txt index 8009c50a7e9..5832d0ea2d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.0 +aiohttp==3.8.1 astral==2.2 async_timeout==4.0.0 attrs==21.2.0 diff --git a/setup.py b/setup.py index a616102018d..ee163bc79f4 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.8.0", + "aiohttp==3.8.1", "astral==2.2", "async_timeout==4.0.0", "attrs==21.2.0", From e95914cf60c148512c0dea642337669a488f9801 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 19:59:12 +0100 Subject: [PATCH 1166/1452] Use dataclass properties in dlna_dmr discovery (#60693) Co-authored-by: epenet --- .../components/dlna_dmr/config_flow.py | 29 +-- .../components/dlna_dmr/media_player.py | 10 +- .../components/dlna_dmr/test_media_player.py | 222 +++++++++++------- 3 files changed, 153 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 5370b1395fd..141ff159bea 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -216,7 +216,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Abort if the device doesn't support all services required for a DmrDevice. # Use the discovery_info instead of DmrDevice.is_profile_device to avoid # contacting the device again. - discovery_service_list = discovery_info.get(ssdp.ATTR_UPNP_SERVICE_LIST) + discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST) if not discovery_service_list: return self.async_abort(reason="not_dmr") discovery_service_ids = { @@ -334,15 +334,15 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Set information required for a config entry from the SSDP discovery.""" LOGGER.debug( "_async_set_info_from_discovery: location: %s, UDN: %s", - discovery_info[ssdp.ATTR_SSDP_LOCATION], - discovery_info[ssdp.ATTR_SSDP_UDN], + discovery_info.ssdp_location, + discovery_info.ssdp_udn, ) if not self._location: - self._location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + self._location = discovery_info.ssdp_location assert isinstance(self._location, str) - self._udn = discovery_info[ssdp.ATTR_SSDP_UDN] + self._udn = discovery_info.ssdp_udn await self.async_set_unique_id(self._udn) if abort_if_configured: @@ -351,11 +351,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): updates={CONF_URL: self._location}, reload_on_update=False ) - self._device_type = ( - discovery_info.get(ssdp.ATTR_SSDP_NT) or discovery_info[ssdp.ATTR_SSDP_ST] - ) + self._device_type = discovery_info.ssdp_nt or discovery_info.ssdp_st self._name = ( - discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or urlparse(self._location).hostname or DEFAULT_NAME ) @@ -461,22 +459,25 @@ def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: flow, which will list all discovered but unconfigured devices. """ # Did the discovery trigger more than just this flow? - if len(discovery_info.get(ssdp.ATTR_HA_MATCHING_DOMAINS, set())) > 1: + if len(discovery_info.x_homeassistant_matching_domains) > 1: LOGGER.debug( "Ignoring device supported by multiple integrations: %s", - discovery_info[ssdp.ATTR_HA_MATCHING_DOMAINS], + discovery_info.x_homeassistant_matching_domains, ) return True # Is the root device not a DMR? - if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: + if ( + discovery_info.upnp.get(ssdp.ATTR_UPNP_DEVICE_TYPE) + not in DmrDevice.DEVICE_TYPES + ): return True # Special cases for devices with other discovery methods (e.g. mDNS), or # that advertise multiple unrelated (sent in separate discovery packets) # UPnP devices. - manufacturer = discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() - model = discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() + manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() + model = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() if manufacturer.startswith("xbmc") or model == "kodi": # kodi diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index d7afe92d1c9..bd39b070228 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -247,12 +247,12 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.debug( "SSDP %s notification of device %s at %s", change, - info[ssdp.ATTR_SSDP_USN], - info.get(ssdp.ATTR_SSDP_LOCATION), + info.ssdp_usn, + info.ssdp_location, ) try: - bootid_str = info[ssdp.ATTR_SSDP_BOOTID] + bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_BOOTID] bootid: int | None = int(bootid_str, 10) except (KeyError, ValueError): bootid = None @@ -263,7 +263,7 @@ class DlnaDmrEntity(MediaPlayerEntity): # Store the new value (because our old value matches) so that we # can ignore subsequent ssdp:alive messages with contextlib.suppress(KeyError, ValueError): - next_bootid_str = info[ssdp.ATTR_SSDP_NEXTBOOTID] + next_bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_NEXTBOOTID] self._bootid = int(next_bootid_str, 10) # Nothing left to do until ssdp:alive comes through return @@ -278,7 +278,7 @@ class DlnaDmrEntity(MediaPlayerEntity): await self._device_disconnect() if change == ssdp.SsdpChange.ALIVE and not self._device: - location = info[ssdp.ATTR_SSDP_LOCATION] + location = info.ssdp_location or "" try: await self._device_connect(location) except UpnpError as err: diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index b9bbdbffc92..fe2a916fdcc 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -51,6 +51,8 @@ from .conftest import ( from tests.common import MockConfigEntry +MOCK_DEVICE_ST = "mock_st" + # Auto-use the domain_data_mock fixture for every test in this module pytestmark = pytest.mark.usefixtures("domain_data_mock") @@ -1052,10 +1054,12 @@ async def test_become_available( # Send an SSDP notification from the now alive device ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1114,10 +1118,12 @@ async def test_alive_but_gone( # Send an SSDP notification from the still missing device ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1153,17 +1159,21 @@ async def test_multiple_ssdp_alive( # Send two SSDP notifications with the new device URL ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: NEW_DEVICE_LOCATION, - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1189,11 +1199,13 @@ async def test_ssdp_byebye( # First byebye will cause a disconnect ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:byebye", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={"NTS": "ssdp:byebye"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.BYEBYE, ) @@ -1206,11 +1218,13 @@ async def test_ssdp_byebye( # Second byebye will do nothing await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:byebye", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={"NTS": "ssdp:byebye"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.BYEBYE, ) @@ -1237,24 +1251,30 @@ async def test_ssdp_update_seen_bootid( # Send SSDP alive with boot ID ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "1", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() # Send SSDP update with next boot ID await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:update", - ssdp.ATTR_SSDP_BOOTID: "1", - ssdp.ATTR_SSDP_NEXTBOOTID: "2", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "1", + ssdp.ATTR_SSDP_NEXTBOOTID: "2", + }, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.UPDATE, ) await hass.async_block_till_done() @@ -1269,13 +1289,17 @@ async def test_ssdp_update_seen_bootid( # Send SSDP update with same next boot ID, again await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:update", - ssdp.ATTR_SSDP_BOOTID: "1", - ssdp.ATTR_SSDP_NEXTBOOTID: "2", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "1", + ssdp.ATTR_SSDP_NEXTBOOTID: "2", + }, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.UPDATE, ) await hass.async_block_till_done() @@ -1290,13 +1314,17 @@ async def test_ssdp_update_seen_bootid( # Send SSDP update with bad next boot ID await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:update", - ssdp.ATTR_SSDP_BOOTID: "2", - ssdp.ATTR_SSDP_NEXTBOOTID: "7c848375-a106-4bd1-ac3c-8e50427c8e4f", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "2", + ssdp.ATTR_SSDP_NEXTBOOTID: "7c848375-a106-4bd1-ac3c-8e50427c8e4f", + }, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.UPDATE, ) await hass.async_block_till_done() @@ -1311,11 +1339,13 @@ async def test_ssdp_update_seen_bootid( # Send a new SSDP alive with the new boot ID, device should not reconnect await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "2", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1348,24 +1378,30 @@ async def test_ssdp_update_missed_bootid( # Send SSDP alive with boot ID ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "1", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() # Send SSDP update with skipped boot ID (not previously seen) await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - "_udn": MOCK_DEVICE_UDN, - "NTS": "ssdp:update", - ssdp.ATTR_SSDP_BOOTID: "2", - ssdp.ATTR_SSDP_NEXTBOOTID: "3", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "2", + ssdp.ATTR_SSDP_NEXTBOOTID: "3", + }, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.UPDATE, ) await hass.async_block_till_done() @@ -1380,11 +1416,13 @@ async def test_ssdp_update_missed_bootid( # Send a new SSDP alive with the new boot ID, device should reconnect await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "3", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "3"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1417,11 +1455,13 @@ async def test_ssdp_bootid( # Send SSDP alive with boot ID ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "1", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1435,11 +1475,13 @@ async def test_ssdp_bootid( # Send SSDP alive with same boot ID, nothing should happen await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "1", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() @@ -1453,11 +1495,13 @@ async def test_ssdp_bootid( # Send a new SSDP alive with an incremented boot ID, device should be dis/reconnected await ssdp_callback( - { - ssdp.ATTR_SSDP_USN: MOCK_DEVICE_USN, - ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, - ssdp.ATTR_SSDP_BOOTID: "2", - }, + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + ssdp_st=MOCK_DEVICE_ST, + upnp={}, + ), ssdp.SsdpChange.ALIVE, ) await hass.async_block_till_done() From 4437926e065eb772824952da4507ff6c5bc70d26 Mon Sep 17 00:00:00 2001 From: Eric Mai Date: Wed, 1 Dec 2021 10:59:27 -0800 Subject: [PATCH 1167/1452] Map OpenWeatherMap weather condition `721` to `Fog` instead of `Exceptional` (#60518) --- homeassistant/components/openweathermap/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 607f223167f..74aa767e44d 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -139,7 +139,7 @@ LANGUAGES = [ WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT = 800 CONDITION_CLASSES = { ATTR_CONDITION_CLOUDY: [803, 804], - ATTR_CONDITION_FOG: [701, 741], + ATTR_CONDITION_FOG: [701, 721, 741], ATTR_CONDITION_HAIL: [906], ATTR_CONDITION_LIGHTNING: [210, 211, 212, 221], ATTR_CONDITION_LIGHTNING_RAINY: [200, 201, 202, 230, 231, 232], @@ -153,7 +153,6 @@ CONDITION_CLASSES = { ATTR_CONDITION_WINDY_VARIANT: [958, 959, 960, 961], ATTR_CONDITION_EXCEPTIONAL: [ 711, - 721, 731, 751, 761, From ed8794de1c96b5a261805788b69e61f55a163e94 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Dec 2021 20:13:27 +0100 Subject: [PATCH 1168/1452] Upgrade tailscale to 0.1.3 (#60780) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index 973ae420d40..e1b2435b989 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.2"], + "requirements": ["tailscale==0.1.3"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 2ece7eab270..a4e828a0ddb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.2 +tailscale==0.1.3 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5029c44100..320e5dd19ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.2 +tailscale==0.1.3 # homeassistant.components.tellduslive tellduslive==0.10.11 From 7a098cff1ca64c41a79fabce699ac147982ffb06 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:23:38 +0100 Subject: [PATCH 1169/1452] Use dataclass properties in upnp discovery (#60744) Co-authored-by: epenet --- homeassistant/components/upnp/config_flow.py | 29 +++++++++++--------- tests/components/upnp/conftest.py | 4 ++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 58dee0c7021..fd4ed9d4051 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import ssdp from homeassistant.components.ssdp import SsdpChange from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from .const import ( CONFIG_ENTRY_HOSTNAME, @@ -28,22 +29,22 @@ from .const import ( ) -def _friendly_name_from_discovery(discovery_info: Mapping[str, Any]) -> str: +def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str: """Extract user-friendly name from discovery.""" return ( - discovery_info.get("friendlyName") - or discovery_info.get("modeName") - or discovery_info.get("_host", "") + discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) + or discovery_info.ssdp_headers.get("_host", "") ) -def _is_complete_discovery(discovery_info: Mapping[str, Any]) -> bool: +def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: """Test if discovery is complete and usable.""" return ( - ssdp.ATTR_UPNP_UDN in discovery_info - and discovery_info.get(ssdp.ATTR_SSDP_ST) - and discovery_info.get(ssdp.ATTR_SSDP_LOCATION) - and discovery_info.get(ssdp.ATTR_SSDP_USN) + ssdp.ATTR_UPNP_UDN in discovery_info.upnp + and discovery_info.ssdp_st + and discovery_info.ssdp_location + and discovery_info.ssdp_usn ) @@ -90,7 +91,9 @@ async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: return True -async def _async_discover_igd_devices(hass: HomeAssistant) -> list[Mapping[str, Any]]: +async def _async_discover_igd_devices( + hass: HomeAssistant, +) -> list[ssdp.SsdpServiceInfo]: """Discovery IGD devices.""" return await ssdp.async_get_discovery_info_by_st( hass, ST_IGD_V1 @@ -206,7 +209,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry_from_discovery(discovery) - async def async_step_ssdp(self, discovery_info: Mapping) -> Mapping[str, Any]: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered UPnP/IGD device. This flow is triggered by the SSDP component. It will check if the @@ -220,9 +223,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. - unique_id = discovery_info[ssdp.ATTR_SSDP_USN] + unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) - hostname = discovery_info["_host"] + hostname = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured(updates={CONFIG_ENTRY_HOSTNAME: hostname}) # Handle devices changing their UDN, only allow a single host. diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 6f6f2905869..d2b1a849ba5 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -38,10 +38,12 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ssdp.ATTR_UPNP_UDN: TEST_UDN, "usn": TEST_USN, "location": TEST_LOCATION, - "_host": TEST_HOSTNAME, "_udn": TEST_UDN, "friendlyName": TEST_FRIENDLY_NAME, }, + ssdp_headers={ + "_host": TEST_HOSTNAME, + }, ) From 1fa035144787d0d1f2ee0b11f01d5cd47418d782 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:43:33 +0100 Subject: [PATCH 1170/1452] Use dataclass properties in tolo discovery (#60784) --- homeassistant/components/tolo/config_flow.py | 11 +++++------ tests/components/tolo/test_config_flow.py | 6 ++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tolo/config_flow.py b/homeassistant/components/tolo/config_flow.py index 24708580d20..14304f6653e 100644 --- a/homeassistant/components/tolo/config_flow.py +++ b/homeassistant/components/tolo/config_flow.py @@ -10,7 +10,6 @@ from tololib.errors import ResponseTimedOutError import voluptuous as vol from homeassistant.components import dhcp -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult @@ -67,16 +66,16 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by discovery.""" - await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) - self._abort_if_unique_id_configured({CONF_HOST: discovery_info[IP_ADDRESS]}) - self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ip}) + self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) device_available = await self.hass.async_add_executor_job( - self._check_device_availability, discovery_info[IP_ADDRESS] + self._check_device_availability, discovery_info.ip ) if device_available: - self._discovered_host = discovery_info[IP_ADDRESS] + self._discovered_host = discovery_info.ip return await self.async_step_confirm() return self.async_abort(reason="not_tolo_device") diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index df9134b4dbd..9991decc511 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch import pytest from tololib.errors import ResponseTimedOutError -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.components import dhcp from homeassistant.components.tolo.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST @@ -15,7 +15,9 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -MOCK_DHCP_DATA = {IP_ADDRESS: "127.0.0.2", MAC_ADDRESS: "00:11:22:33:44:55"} +MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( + ip="127.0.0.2", macaddress="00:11:22:33:44:55", hostname="mock_hostname" +) @pytest.fixture(name="toloclient") From fbaec76b8a20bc02993335eccfbb6e19610b79af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Dec 2021 20:44:48 +0100 Subject: [PATCH 1171/1452] Add more Tractive sensors (#55170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tractive, add more sensors Signed-off-by: Daniel Hjelseth Høyer * source Signed-off-by: Daniel Hjelseth Høyer * Fix unit for sensor Signed-off-by: Daniel Hjelseth Høyer * Device state Signed-off-by: Daniel Hjelseth Høyer * Device state Signed-off-by: Daniel Hjelseth Høyer * Tractive Signed-off-by: Daniel Hjelseth Høyer * Tractive Signed-off-by: Daniel Hjelseth Høyer * unit Signed-off-by: Daniel Hjelseth Høyer * Handle unavailable Signed-off-by: Daniel Hjelseth Høyer * time Signed-off-by: Daniel Hjelseth Høyer * continue Signed-off-by: Daniel Hjelseth Høyer * remove sensor Signed-off-by: Daniel Hjelseth Høyer * style Signed-off-by: Daniel Hjelseth Høyer * tractive states Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tractive/__init__.py | 26 ++++++++++++++----- homeassistant/components/tractive/const.py | 1 + .../components/tractive/device_tracker.py | 9 ++++++- homeassistant/components/tractive/sensor.py | 13 +++++++++- .../components/tractive/strings.sensor.json | 10 +++++++ 5 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/tractive/strings.sensor.json diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 1d51ab66585..66a2afccb43 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -27,6 +27,7 @@ from .const import ( ATTR_LED, ATTR_LIVE_TRACKING, ATTR_MINUTES_ACTIVE, + ATTR_TRACKER_STATE, CLIENT, DOMAIN, RECONNECT_INTERVAL, @@ -143,6 +144,8 @@ class TractiveClient: self._hass = hass self._client = client self._user_id = user_id + self._last_hw_time = 0 + self._last_pos_time = 0 self._listen_task: asyncio.Task | None = None @property @@ -181,20 +184,29 @@ class TractiveClient: if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False - if event["message"] == "activity_update": self._send_activity_update(event) - else: - if "hardware" in event: - self._send_hardware_update(event) + continue + if ( + "hardware" in event + and self._last_hw_time != event["hardware"]["time"] + ): + self._last_hw_time = event["hardware"]["time"] + self._send_hardware_update(event) - if "position" in event: - self._send_position_update(event) + if ( + "position" in event + and self._last_pos_time != event["position"]["time"] + ): + self._last_pos_time = event["position"]["time"] + self._send_position_update(event) except aiotractive.exceptions.TractiveError: _LOGGER.debug( "Tractive is not available. Internet connection is down? Sleeping %i seconds and retrying", RECONNECT_INTERVAL.total_seconds(), ) + self._last_hw_time = 0 + self._last_pos_time = 0 async_dispatcher_send( self._hass, f"{SERVER_UNAVAILABLE}-{self._user_id}" ) @@ -206,6 +218,7 @@ class TractiveClient: # Sometimes hardware event doesn't contain complete data. payload = { ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"], + ATTR_TRACKER_STATE: event["tracker_state"].lower(), ATTR_BATTERY_CHARGING: event["charging_state"] == "CHARGING", ATTR_LIVE_TRACKING: event.get("live_tracking", {}).get("active"), ATTR_BUZZER: event.get("buzzer_control", {}).get("active"), @@ -229,6 +242,7 @@ class TractiveClient: "latitude": event["position"]["latlong"][0], "longitude": event["position"]["latlong"][1], "accuracy": event["position"]["accuracy"], + "sensor_used": event["position"]["sensor_used"], } self._dispatch_tracker_event( TRACKER_POSITION_UPDATED, event["tracker_id"], payload diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 6a61024cd51..0d7d62ccae7 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -11,6 +11,7 @@ ATTR_BUZZER = "buzzer" ATTR_LED = "led" ATTR_LIVE_TRACKING = "live_tracking" ATTR_MINUTES_ACTIVE = "minutes_active" +ATTR_TRACKER_STATE = "tracker_state" CLIENT = "client" TRACKABLES = "trackables" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index a4109eee71c..218151ae769 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -3,7 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import ( + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_GPS, +) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -47,6 +50,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._latitude: float = item.pos_report["latlong"][0] self._longitude: float = item.pos_report["latlong"][1] self._accuracy: int = item.pos_report["pos_uncertainty"] + self._source_type: str = item.pos_report["sensor_used"] self._attr_name = f"{self._tracker_id} {item.trackable['details']['name']}" self._attr_unique_id = item.trackable["_id"] @@ -54,6 +58,8 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): @property def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" + if self._source_type == "PHONE": + return SOURCE_TYPE_BLUETOOTH return SOURCE_TYPE_GPS @property @@ -87,6 +93,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._latitude = event["latitude"] self._longitude = event["longitude"] self._accuracy = event["accuracy"] + self._source_type = event["sensor_used"] self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 2bbd755da2c..3f6c18fa07f 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -24,6 +24,7 @@ from . import Trackables from .const import ( ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, + ATTR_TRACKER_STATE, CLIENT, DOMAIN, SERVER_UNAVAILABLE, @@ -77,7 +78,9 @@ class TractiveHardwareSensor(TractiveSensor): @callback def handle_hardware_status_update(self, event: dict[str, Any]) -> None: """Handle hardware status update.""" - self._attr_native_value = event[self.entity_description.key] + if (_state := event[self.entity_description.key]) is None: + return + self._attr_native_value = _state self._attr_available = True self.async_write_ha_state() @@ -140,6 +143,14 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( entity_class=TractiveHardwareSensor, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + TractiveSensorEntityDescription( + # Currently, only state operational and not_reporting are used + # More states are available by polling the data + key=ATTR_TRACKER_STATE, + name="Tracker state", + device_class="tractive__tracker_state", + entity_class=TractiveHardwareSensor, + ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, name="Minutes Active", diff --git a/homeassistant/components/tractive/strings.sensor.json b/homeassistant/components/tractive/strings.sensor.json new file mode 100644 index 00000000000..b9c2cd603da --- /dev/null +++ b/homeassistant/components/tractive/strings.sensor.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Not reporting", + "operational": "Operational", + "system_shutdown_user": "System shutdown user", + "system_startup": "System startup" + } + } +} From 2b49694a4c5264eee51848557ef51dcc767ed155 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:46:10 +0100 Subject: [PATCH 1172/1452] Use dataclass properties in songpal discovery (#60786) --- homeassistant/components/songpal/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 1a0030e4904..10737127e0b 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -102,7 +102,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] parsed_url = urlparse(discovery_info.ssdp_location) - scalarweb_info = discovery_info["X_ScalarWebAPI_DeviceInfo"] + scalarweb_info = discovery_info.upnp["X_ScalarWebAPI_DeviceInfo"] endpoint = scalarweb_info["X_ScalarWebAPI_BaseURL"] service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ "X_ScalarWebAPI_ServiceType" From 4b8a8dda8da880eecbe08cb1a181a0a3e57801cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Dec 2021 10:13:59 -1000 Subject: [PATCH 1173/1452] Fix yeelight discovery (#60783) Regressed in #60640 --- homeassistant/components/yeelight/scanner.py | 9 +++++++-- tests/components/yeelight/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index a331db8d4ed..4b372df3744 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -10,7 +10,7 @@ from urllib.parse import urlparse from async_upnp_client.search import SsdpSearchListener from homeassistant import config_entries -from homeassistant.components import network +from homeassistant.components import network, ssdp from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later, async_track_time_interval @@ -161,7 +161,12 @@ class YeelightScanner: self._hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=response, + data=ssdp.SsdpServiceInfo( + ssdp_usn="", + ssdp_st=SSDP_ST, + ssdp_headers=response, + upnp={}, + ), ) ) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 6f7ae807c9d..b6bf0b10d67 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -182,8 +182,8 @@ def _patch_discovery(no_device=False, capabilities=None): info = None if not no_device: info = ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", + ssdp_usn="", + ssdp_st=scanner.SSDP_ST, upnp={}, ssdp_headers=capabilities or CAPABILITIES, ) From 737dd6fc261cc1f3b4e793f2fe7c3d321c8e9707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 1 Dec 2021 21:41:31 +0100 Subject: [PATCH 1174/1452] Add tests to hassio binary_sensor platform (#60609) --- .coveragerc | 3 - tests/components/hassio/test_binary_sensor.py | 156 ++++++++++++++++++ tests/components/hassio/test_init.py | 2 +- tests/components/hassio/test_sensor.py | 56 +++---- 4 files changed, 183 insertions(+), 34 deletions(-) create mode 100644 tests/components/hassio/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 6d042b3f46e..bf26b41649d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,9 +420,6 @@ omit = homeassistant/components/harmony/data.py homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py - homeassistant/components/hassio/binary_sensor.py - homeassistant/components/hassio/entity.py - homeassistant/components/hassio/sensor.py homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py new file mode 100644 index 00000000000..e4263eb5529 --- /dev/null +++ b/tests/components/hassio/test_binary_sensor.py @@ -0,0 +1,156 @@ +"""The tests for the hassio binary sensors.""" + +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.hassio import DOMAIN +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock, request): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version_latest": "1.0.0", + "addons": [ + { + "name": "test", + "state": "started", + "slug": "test", + "installed": True, + "update_available": True, + "version": "2.0.0", + "version_latest": "2.0.1", + "repository": "core", + "url": "https://github.com/home-assistant/addons/test", + }, + { + "name": "test2", + "state": "stopped", + "slug": "test2", + "installed": True, + "update_available": False, + "version": "3.1.0", + "version_latest": "3.1.0", + "repository": "core", + "url": "https://github.com", + }, + ], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + + +@pytest.mark.parametrize( + "entity_id,expected", + [ + ("binary_sensor.home_assistant_operating_system_update_available", "off"), + ("binary_sensor.test_update_available", "on"), + ("binary_sensor.test2_update_available", "off"), + ("binary_sensor.test_running", "on"), + ("binary_sensor.test2_running", "off"), + ], +) +async def test_binary_sensor(hass, entity_id, expected, aioclient_mock): + """Test hassio OS and addons binary sensor.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + # Verify that the entity is disabled by default. + assert hass.states.get(entity_id) is None + + # Enable the entity. + ent_reg = entity_registry.async_get(hass) + ent_reg.async_update_entity(entity_id, disabled_by=None) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that the entity have the expected state. + state = hass.states.get(entity_id) + assert state.state == expected diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f5214b563b3..417fc77b527 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -59,7 +59,7 @@ def mock_all(aioclient_mock, request): ) aioclient_mock.get( "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, ) aioclient_mock.get( "http://127.0.0.1/supervisor/info", diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 00d2c32c520..481ba1b578f 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -111,8 +111,25 @@ def mock_all(aioclient_mock, request): ) -async def test_sensors(hass, aioclient_mock): - """Test hassio OS and addons sensors.""" +@pytest.mark.parametrize( + "entity_id,expected", + [ + ("sensor.home_assistant_operating_system_version", "1.0.0"), + ("sensor.home_assistant_operating_system_newest_version", "1.0.0"), + ("sensor.test_version", "2.0.0"), + ("sensor.test_newest_version", "2.0.1"), + ("sensor.test2_version", "3.1.0"), + ("sensor.test2_newest_version", "3.2.0"), + ("sensor.test_cpu_percent", "0.99"), + ("sensor.test2_cpu_percent", "unavailable"), + ("sensor.test_memory_percent", "4.59"), + ("sensor.test2_memory_percent", "unavailable"), + ], +) +async def test_sensor(hass, entity_id, expected, aioclient_mock): + """Test hassio OS and addons sensor.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component( @@ -121,38 +138,17 @@ async def test_sensors(hass, aioclient_mock): {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, ) assert result - - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - await hass.async_block_till_done() - sensors = { - "sensor.home_assistant_operating_system_version": "1.0.0", - "sensor.home_assistant_operating_system_newest_version": "1.0.0", - "sensor.test_version": "2.0.0", - "sensor.test_newest_version": "2.0.1", - "sensor.test2_version": "3.1.0", - "sensor.test2_newest_version": "3.2.0", - "sensor.test_cpu_percent": "0.99", - "sensor.test2_cpu_percent": "unavailable", - "sensor.test_memory_percent": "4.59", - "sensor.test2_memory_percent": "unavailable", - } + # Verify that the entity is disabled by default. + assert hass.states.get(entity_id) is None - """Check that entities are disabled by default.""" - for sensor in sensors: - assert hass.states.get(sensor) is None - - """Enable sensors.""" + # Enable the entity. ent_reg = entity_registry.async_get(hass) - for sensor in sensors: - ent_reg.async_update_entity(sensor, disabled_by=None) + ent_reg.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - """Check sensor values.""" - for sensor, value in sensors.items(): - state = hass.states.get(sensor) - assert state.state == value + # Verify that the entity have the expected state. + state = hass.states.get(entity_id) + assert state.state == expected From 0cf228d5a1a8d73489607173497fdf7f8847b405 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Dec 2021 21:46:55 +0100 Subject: [PATCH 1175/1452] Use dataclass properties in hyperion (#60792) --- homeassistant/components/hyperion/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 3b5785dd533..4d6253cb161 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -198,14 +198,14 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): try: self._data[CONF_PORT] = int( - discovery_info.get("ports", {}).get( + discovery_info.upnp.get("ports", {}).get( "jsonServer", const.DEFAULT_PORT_JSON ) ) except ValueError: self._data[CONF_PORT] = const.DEFAULT_PORT_JSON - if not (hyperion_id := discovery_info.get(ssdp.ATTR_UPNP_SERIAL)): + if not (hyperion_id := discovery_info.upnp.get(ssdp.ATTR_UPNP_SERIAL)): return self.async_abort(reason="no_id") # For discovery mechanisms, we set the unique_id as early as possible to From cb7e7e9bd1710c59a5d225768966f303fee53f10 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 1 Dec 2021 14:49:21 -0600 Subject: [PATCH 1176/1452] Improve Sonos activity tracking (#60642) --- homeassistant/components/sonos/speaker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e8cd729bf6c..f45a81060f8 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -64,6 +64,7 @@ from .const import ( from .favorites import SonosFavorites from .helpers import soco_error +NEVER_TIME = -1200.0 EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, @@ -159,7 +160,6 @@ class SonosSpeaker: self.available = True # Synchronization helpers - self._is_ready: bool = False self._platforms_ready: set[str] = set() # Subscriptions and events @@ -167,7 +167,7 @@ class SonosSpeaker: self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} - self._last_activity: datetime.datetime | None = None + self._last_activity: float = NEVER_TIME # Scheduled callback handles self._poll_timer: Callable | None = None @@ -280,7 +280,6 @@ class SonosSpeaker: if self._platforms_ready == PLATFORMS: self._resubscription_lock = asyncio.Lock() await self.async_subscribe() - self._is_ready = True def write_entity_states(self) -> None: """Write states for associated SonosEntity instances.""" @@ -965,6 +964,7 @@ class SonosSpeaker: # # Media and playback state handlers # + @soco_error() def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -979,6 +979,7 @@ class SonosSpeaker: except SoCoSlaveException: pass + @soco_error() def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables From d1962f6e5186390b810f4fe711b25b029e1a37b5 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 1 Dec 2021 21:52:44 +0100 Subject: [PATCH 1177/1452] Cleanup here_travel_time tests (#60529) --- .../components/here_travel_time/sensor.py | 12 +- tests/components/here_travel_time/__init__.py | 2 +- tests/components/here_travel_time/conftest.py | 23 + tests/components/here_travel_time/const.py | 8 + .../fixtures/attribution_response.json | 276 ---- .../fixtures/bike_response.json | 274 ---- .../fixtures/car_enabled_response.json | 298 ----- .../fixtures/car_response.json | 11 +- .../fixtures/car_shortest_response.json | 231 ---- .../fixtures/pedestrian_response.json | 308 ----- .../fixtures/public_response.json | 294 ----- .../fixtures/public_time_table_response.json | 308 ----- .../routing_error_invalid_credentials.json | 15 - .../routing_error_no_route_found.json | 21 - .../fixtures/truck_response.json | 187 --- .../here_travel_time/test_sensor.py | 1117 ++--------------- 16 files changed, 158 insertions(+), 3227 deletions(-) create mode 100644 tests/components/here_travel_time/conftest.py create mode 100644 tests/components/here_travel_time/const.py delete mode 100644 tests/components/here_travel_time/fixtures/attribution_response.json delete mode 100644 tests/components/here_travel_time/fixtures/bike_response.json delete mode 100644 tests/components/here_travel_time/fixtures/car_enabled_response.json delete mode 100644 tests/components/here_travel_time/fixtures/car_shortest_response.json delete mode 100644 tests/components/here_travel_time/fixtures/pedestrian_response.json delete mode 100644 tests/components/here_travel_time/fixtures/public_response.json delete mode 100644 tests/components/here_travel_time/fixtures/public_time_table_response.json delete mode 100644 tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json delete mode 100644 tests/components/here_travel_time/fixtures/routing_error_no_route_found.json delete mode 100644 tests/components/here_travel_time/fixtures/truck_response.json diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 3277d737a8e..e29c6682d8f 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta import logging import herepy +from herepy.here_enum import RouteMode import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -198,14 +199,17 @@ def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: known_working_origin = [38.9, -77.04833] known_working_destination = [39.0, -77.1] try: - here_client.car_route( + here_client.public_transport_timetable( known_working_origin, known_working_destination, + True, [ - herepy.RouteMode[ROUTE_MODE_FASTEST], - herepy.RouteMode[TRAVEL_MODE_CAR], - herepy.RouteMode[TRAFFIC_MODE_DISABLED], + RouteMode[ROUTE_MODE_FASTEST], + RouteMode[TRAVEL_MODE_CAR], + RouteMode[TRAFFIC_MODE_ENABLED], ], + arrival=None, + departure="now", ) except herepy.InvalidCredentialsError: return False diff --git a/tests/components/here_travel_time/__init__.py b/tests/components/here_travel_time/__init__.py index ac0ec709654..b46865c8157 100644 --- a/tests/components/here_travel_time/__init__.py +++ b/tests/components/here_travel_time/__init__.py @@ -1 +1 @@ -"""Tests for here_travel_time component.""" +"""Tests for HERE Travel Time.""" diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py new file mode 100644 index 00000000000..2d5af2b0186 --- /dev/null +++ b/tests/components/here_travel_time/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for HERE Travel Time tests.""" +import json +from unittest.mock import patch + +from herepy.models import RoutingResponse +import pytest + +from tests.common import load_fixture + +RESPONSE = RoutingResponse.new_from_jsondict( + json.loads(load_fixture("here_travel_time/car_response.json")) +) +RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" + + +@pytest.fixture(name="valid_response") +def valid_response_fixture(): + """Return valid api response.""" + with patch( + "herepy.RoutingApi.public_transport_timetable", + return_value=RESPONSE, + ): + yield diff --git a/tests/components/here_travel_time/const.py b/tests/components/here_travel_time/const.py new file mode 100644 index 00000000000..0cc3143bc0b --- /dev/null +++ b/tests/components/here_travel_time/const.py @@ -0,0 +1,8 @@ +"""Constants for HERE Travel Time tests.""" + +API_KEY = "test" + +CAR_ORIGIN_LATITUDE = "38.9" +CAR_ORIGIN_LONGITUDE = "-77.04833" +CAR_DESTINATION_LATITUDE = "39.0" +CAR_DESTINATION_LONGITUDE = "-77.1" diff --git a/tests/components/here_travel_time/fixtures/attribution_response.json b/tests/components/here_travel_time/fixtures/attribution_response.json deleted file mode 100644 index 9b682f6c51f..00000000000 --- a/tests/components/here_travel_time/fixtures/attribution_response.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-09-21T15:17:31Z", - "mapVersion": "8.30.100.154", - "moduleVersion": "7.2.201937-5251", - "interfaceVersion": "2.6.70", - "availableMapVersion": [ - "8.30.100.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "+565790671", - "mappedPosition": { - "latitude": 50.0378591, - "longitude": 14.3924721 - }, - "originalPosition": { - "latitude": 50.0377513, - "longitude": 14.3923344 - }, - "type": "stopOver", - "spot": 0.3, - "sideOfStreet": "left", - "mappedRoadName": "V Bokách III", - "label": "V Bokách III", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+748931502", - "mappedPosition": { - "latitude": 50.0798786, - "longitude": 14.4260037 - }, - "originalPosition": { - "latitude": 50.0799383, - "longitude": 14.4258216 - }, - "type": "stopOver", - "spot": 1.0, - "sideOfStreet": "left", - "mappedRoadName": "Štěpánská", - "label": "Štěpánská", - "shapeIndex": 116, - "source": "user" - } - ], - "mode": { - "type": "shortest", - "transportModes": [ - "publicTransportTimeTable" - ], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "+565790671", - "mappedPosition": { - "latitude": 50.0378591, - "longitude": 14.3924721 - }, - "originalPosition": { - "latitude": 50.0377513, - "longitude": 14.3923344 - }, - "type": "stopOver", - "spot": 0.3, - "sideOfStreet": "left", - "mappedRoadName": "V Bokách III", - "label": "V Bokách III", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+748931502", - "mappedPosition": { - "latitude": 50.0798786, - "longitude": 14.4260037 - }, - "originalPosition": { - "latitude": 50.0799383, - "longitude": 14.4258216 - }, - "type": "stopOver", - "spot": 1.0, - "sideOfStreet": "left", - "mappedRoadName": "Štěpánská", - "label": "Štěpánská", - "shapeIndex": 116, - "source": "user" - }, - "length": 7835, - "travelTime": 2413, - "maneuver": [ - { - "position": { - "latitude": 50.0378591, - "longitude": 14.3924721 - }, - "instruction": "Head northwest on Kosořská. Go for 28 m.", - "travelTime": 32, - "length": 28, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0380039, - "longitude": 14.3921542 - }, - "instruction": "Turn left onto Kosořská. Go for 24 m.", - "travelTime": 24, - "length": 24, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0380039, - "longitude": 14.3918109 - }, - "instruction": "Take the street on the left, Slivenecká. Go for 343 m.", - "travelTime": 354, - "length": 343, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0376499, - "longitude": 14.3871975 - }, - "instruction": "Turn left onto Slivenecká. Go for 64 m.", - "travelTime": 72, - "length": 64, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0373602, - "longitude": 14.3879807 - }, - "instruction": "Turn right onto Slivenecká. Go for 91 m.", - "travelTime": 95, - "length": 91, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0365448, - "longitude": 14.3878305 - }, - "instruction": "Turn left onto K Barrandovu. Go for 124 m.", - "travelTime": 126, - "length": 124, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0363168, - "longitude": 14.3894618 - }, - "instruction": "Go to the Tram station Geologicka and take the rail 5 toward Ústřední dílny DP. Follow for 13 stations.", - "travelTime": 1440, - "length": 6911, - "id": "M7", - "stopName": "Geologicka", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 50.0800508, - "longitude": 14.423403 - }, - "instruction": "Get off at Vodickova.", - "travelTime": 0, - "length": 0, - "id": "M8", - "stopName": "Vodickova", - "nextRoadName": "Vodičkova", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 50.0800508, - "longitude": 14.423403 - }, - "instruction": "Head northeast on Vodičkova. Go for 65 m.", - "travelTime": 74, - "length": 65, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0804901, - "longitude": 14.4239759 - }, - "instruction": "Turn right onto V Jámě. Go for 163 m.", - "travelTime": 174, - "length": 163, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0796962, - "longitude": 14.4258857 - }, - "instruction": "Turn left onto Štěpánská. Go for 22 m.", - "travelTime": 22, - "length": 22, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 50.0798786, - "longitude": 14.4260037 - }, - "instruction": "Arrive at Štěpánská. Your destination is on the left.", - "travelTime": 0, - "length": 0, - "id": "M12", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "publicTransportLine": [ - { - "lineName": "5", - "lineForeground": "#F5ADCE", - "lineBackground": "#F5ADCE", - "companyName": "HERE Technologies", - "destination": "Ústřední dílny DP", - "type": "railLight", - "id": "L1" - } - ], - "summary": { - "distance": 7835, - "baseTime": 2413, - "flags": [ - "noThroughRoad", - "builtUpArea" - ], - "text": "The trip takes 7.8 km and 40 mins.", - "travelTime": 2413, - "departure": "2019-09-21T17:16:17+02:00", - "timetableExpiration": "2019-09-21T00:00:00Z", - "_type": "PublicTransportRouteSummaryType" - } - } - ], - "language": "en-us", - "sourceAttribution": { - "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", - "supplier": [ - { - "title": "HERE Technologies", - "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" - } - ] - } - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/bike_response.json b/tests/components/here_travel_time/fixtures/bike_response.json deleted file mode 100644 index a3af39129d0..00000000000 --- a/tests/components/here_travel_time/fixtures/bike_response.json +++ /dev/null @@ -1,274 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-24T10:17:40Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201929-4522", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 87, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "bicycle" - ], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 87, - "source": "user" - }, - "length": 12613, - "travelTime": 3292, - "maneuver": [ - { - "position": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "instruction": "Head south on Mannheim Rd (US-12/US-45). Go for 2.6 km.", - "travelTime": 646, - "length": 2648, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9579244, - "longitude": -87.8838551 - }, - "instruction": "Keep left onto Mannheim Rd (US-12/US-45). Go for 2.4 km.", - "travelTime": 621, - "length": 2427, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9364238, - "longitude": -87.8849387 - }, - "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", - "travelTime": 158, - "length": 595, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9362521, - "longitude": -87.8921163 - }, - "instruction": "Turn left onto Cullerton St. Go for 669 m.", - "travelTime": 180, - "length": 669, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9305658, - "longitude": -87.8932428 - }, - "instruction": "Continue on N Landen Dr. Go for 976 m.", - "travelTime": 246, - "length": 976, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9217896, - "longitude": -87.8928781 - }, - "instruction": "Turn right onto E Fullerton Ave. Go for 904 m.", - "travelTime": 238, - "length": 904, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.921618, - "longitude": -87.9038107 - }, - "instruction": "Turn left onto N Wolf Rd. Go for 1.6 km.", - "travelTime": 417, - "length": 1604, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.907177, - "longitude": -87.9032314 - }, - "instruction": "Turn right onto W North Ave (IL-64). Go for 2.0 km.", - "travelTime": 574, - "length": 2031, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065225, - "longitude": -87.9277039 - }, - "instruction": "Turn left onto N Clinton Ave. Go for 275 m.", - "travelTime": 78, - "length": 275, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9040549, - "longitude": -87.9277253 - }, - "instruction": "Turn left onto E Third St. Go for 249 m.", - "travelTime": 63, - "length": 249, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9040334, - "longitude": -87.9247105 - }, - "instruction": "Continue on N Caroline Ave. Go for 96 m.", - "travelTime": 37, - "length": 96, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9038832, - "longitude": -87.9236054 - }, - "instruction": "Turn slightly left. Go for 113 m.", - "travelTime": 28, - "length": 113, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9039047, - "longitude": -87.9222536 - }, - "instruction": "Turn left. Go for 26 m.", - "travelTime": 6, - "length": 26, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "instruction": "Arrive at your destination on the right.", - "travelTime": 0, - "length": 0, - "id": "M14", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 12613, - "baseTime": 3292, - "flags": [ - "noThroughRoad", - "builtUpArea", - "park" - ], - "text": "The trip takes 12.6 km and 55 mins.", - "travelTime": 3292, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/car_enabled_response.json b/tests/components/here_travel_time/fixtures/car_enabled_response.json deleted file mode 100644 index 08da738f046..00000000000 --- a/tests/components/here_travel_time/fixtures/car_enabled_response.json +++ /dev/null @@ -1,298 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-21T21:21:31Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4478", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1128310200", - "mappedPosition": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "originalPosition": { - "latitude": 38.9029809, - "longitude": -77.048338 - }, - "type": "stopOver", - "spot": 0.3538462, - "sideOfStreet": "right", - "mappedRoadName": "K St NW", - "label": "K St NW", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "-18459081", - "mappedPosition": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "originalPosition": { - "latitude": 39.042158, - "longitude": -77.119116 - }, - "type": "stopOver", - "spot": 0.7253521, - "sideOfStreet": "left", - "mappedRoadName": "Commonwealth Dr", - "label": "Commonwealth Dr", - "shapeIndex": 283, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "car" - ], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1128310200", - "mappedPosition": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "originalPosition": { - "latitude": 38.9029809, - "longitude": -77.048338 - }, - "type": "stopOver", - "spot": 0.3538462, - "sideOfStreet": "right", - "mappedRoadName": "K St NW", - "label": "K St NW", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "-18459081", - "mappedPosition": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "originalPosition": { - "latitude": 39.042158, - "longitude": -77.119116 - }, - "type": "stopOver", - "spot": 0.7253521, - "sideOfStreet": "left", - "mappedRoadName": "Commonwealth Dr", - "label": "Commonwealth Dr", - "shapeIndex": 283, - "source": "user" - }, - "length": 23381, - "travelTime": 1817, - "maneuver": [ - { - "position": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "instruction": "Head toward 22nd St NW on K St NW. Go for 140 m.", - "travelTime": 36, - "length": 140, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9027703, - "longitude": -77.0494902 - }, - "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 325 m.", - "travelTime": 81, - "length": 325, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9026523, - "longitude": -77.0529449 - }, - "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", - "travelTime": 29, - "length": 201, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9025235, - "longitude": -77.0552516 - }, - "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", - "travelTime": 143, - "length": 1381, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9050448, - "longitude": -77.0701969 - }, - "instruction": "Turn left onto M St NW. Go for 784 m.", - "travelTime": 80, - "length": 784, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9060318, - "longitude": -77.0790696 - }, - "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", - "travelTime": 287, - "length": 4230, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9303219, - "longitude": -77.1117926 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", - "travelTime": 55, - "length": 844, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9368558, - "longitude": -77.1166742 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", - "travelTime": 294, - "length": 4652, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9706838, - "longitude": -77.1461463 - }, - "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", - "travelTime": 90, - "length": 2069, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9858222, - "longitude": -77.1571326 - }, - "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 2.9 km.", - "travelTime": 129, - "length": 2890, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0104449, - "longitude": -77.1508026 - }, - "instruction": "Keep left onto I-270-SPUR toward I-270/Rockville/Frederick. Go for 1.1 km.", - "travelTime": 48, - "length": 1136, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0192747, - "longitude": -77.144773 - }, - "instruction": "Take exit 1 toward Democracy Blvd/Old Georgetown Rd/MD-187 onto Democracy Blvd. Go for 1.8 km.", - "travelTime": 205, - "length": 1818, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0247464, - "longitude": -77.1253431 - }, - "instruction": "Turn left onto Old Georgetown Rd (MD-187). Go for 2.3 km.", - "travelTime": 230, - "length": 2340, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0447772, - "longitude": -77.1203649 - }, - "instruction": "Turn right onto Nicholson Ln. Go for 208 m.", - "travelTime": 31, - "length": 208, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0448952, - "longitude": -77.1179724 - }, - "instruction": "Turn right onto Commonwealth Dr. Go for 341 m.", - "travelTime": 75, - "length": 341, - "id": "M15", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", - "travelTime": 4, - "length": 22, - "id": "M16", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 23381, - "trafficTime": 1782, - "baseTime": 1712, - "flags": [ - "noThroughRoad", - "motorway", - "builtUpArea", - "park" - ], - "text": "The trip takes 23.4 km and 30 mins.", - "travelTime": 1782, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/car_response.json b/tests/components/here_travel_time/fixtures/car_response.json index bda8454f3f3..ef050b78362 100644 --- a/tests/components/here_travel_time/fixtures/car_response.json +++ b/tests/components/here_travel_time/fixtures/car_response.json @@ -294,6 +294,15 @@ } } ], - "language": "en-us" + "language": "en-us", + "sourceAttribution": { + "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", + "supplier": [ + { + "title": "HERE Technologies", + "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + } + ] + } } } \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/car_shortest_response.json b/tests/components/here_travel_time/fixtures/car_shortest_response.json deleted file mode 100644 index 765c438c1cd..00000000000 --- a/tests/components/here_travel_time/fixtures/car_shortest_response.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-21T21:05:28Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4478", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1128310200", - "mappedPosition": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "originalPosition": { - "latitude": 38.9029809, - "longitude": -77.048338 - }, - "type": "stopOver", - "spot": 0.3538462, - "sideOfStreet": "right", - "mappedRoadName": "K St NW", - "label": "K St NW", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "-18459081", - "mappedPosition": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "originalPosition": { - "latitude": 39.042158, - "longitude": -77.119116 - }, - "type": "stopOver", - "spot": 0.7253521, - "sideOfStreet": "left", - "mappedRoadName": "Commonwealth Dr", - "label": "Commonwealth Dr", - "shapeIndex": 162, - "source": "user" - } - ], - "mode": { - "type": "shortest", - "transportModes": [ - "car" - ], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1128310200", - "mappedPosition": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "originalPosition": { - "latitude": 38.9029809, - "longitude": -77.048338 - }, - "type": "stopOver", - "spot": 0.3538462, - "sideOfStreet": "right", - "mappedRoadName": "K St NW", - "label": "K St NW", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "-18459081", - "mappedPosition": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "originalPosition": { - "latitude": 39.042158, - "longitude": -77.119116 - }, - "type": "stopOver", - "spot": 0.7253521, - "sideOfStreet": "left", - "mappedRoadName": "Commonwealth Dr", - "label": "Commonwealth Dr", - "shapeIndex": 162, - "source": "user" - }, - "length": 18388, - "travelTime": 2493, - "maneuver": [ - { - "position": { - "latitude": 38.9026523, - "longitude": -77.048338 - }, - "instruction": "Head west on K St NW. Go for 79 m.", - "travelTime": 22, - "length": 79, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9026523, - "longitude": -77.048825 - }, - "instruction": "Turn right onto 22nd St NW. Go for 141 m.", - "travelTime": 79, - "length": 141, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9039075, - "longitude": -77.048825 - }, - "instruction": "Keep left onto 22nd St NW. Go for 841 m.", - "travelTime": 256, - "length": 841, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9114928, - "longitude": -77.0487821 - }, - "instruction": "Turn left onto Massachusetts Ave NW. Go for 145 m.", - "travelTime": 22, - "length": 145, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9120293, - "longitude": -77.0502949 - }, - "instruction": "Take the 1st exit from Massachusetts Ave NW roundabout onto Massachusetts Ave NW. Go for 2.8 km.", - "travelTime": 301, - "length": 2773, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9286053, - "longitude": -77.073158 - }, - "instruction": "Turn right onto Wisconsin Ave NW. Go for 3.8 km.", - "travelTime": 610, - "length": 3801, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9607918, - "longitude": -77.0857322 - }, - "instruction": "Continue on Wisconsin Ave (MD-355). Go for 9.7 km.", - "travelTime": 1013, - "length": 9686, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0447664, - "longitude": -77.1116638 - }, - "instruction": "Turn left onto Nicholson Ln. Go for 559 m.", - "travelTime": 111, - "length": 559, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0448952, - "longitude": -77.1179724 - }, - "instruction": "Turn left onto Commonwealth Dr. Go for 341 m.", - "travelTime": 75, - "length": 341, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0422511, - "longitude": -77.1193526 - }, - "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", - "travelTime": 4, - "length": 22, - "id": "M10", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 18388, - "trafficTime": 2427, - "baseTime": 2150, - "flags": [ - "noThroughRoad", - "builtUpArea", - "park" - ], - "text": "The trip takes 18.4 km and 40 mins.", - "travelTime": 2427, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/pedestrian_response.json b/tests/components/here_travel_time/fixtures/pedestrian_response.json deleted file mode 100644 index 07881e8bd3d..00000000000 --- a/tests/components/here_travel_time/fixtures/pedestrian_response.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-21T18:40:10Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4478", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 122, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "pedestrian" - ], - "trafficMode": "disabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 122, - "source": "user" - }, - "length": 12533, - "travelTime": 12631, - "maneuver": [ - { - "position": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "instruction": "Head south on Mannheim Rd. Go for 848 m.", - "travelTime": 848, - "length": 848, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9722581, - "longitude": -87.8776109 - }, - "instruction": "Take the street on the left, Mannheim Rd. Go for 4.2 km.", - "travelTime": 4239, - "length": 4227, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9364238, - "longitude": -87.8849387 - }, - "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", - "travelTime": 605, - "length": 595, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9362521, - "longitude": -87.8921163 - }, - "instruction": "Turn left onto Cullerton St. Go for 406 m.", - "travelTime": 411, - "length": 406, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9326043, - "longitude": -87.8919983 - }, - "instruction": "Turn right onto Cullerton St. Go for 1.2 km.", - "travelTime": 1249, - "length": 1239, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9217896, - "longitude": -87.8928781 - }, - "instruction": "Turn right onto E Fullerton Ave. Go for 786 m.", - "travelTime": 796, - "length": 786, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9216394, - "longitude": -87.9023838 - }, - "instruction": "Turn left onto La Porte Ave. Go for 424 m.", - "travelTime": 430, - "length": 424, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9180024, - "longitude": -87.9028559 - }, - "instruction": "Turn right onto E Palmer Ave. Go for 864 m.", - "travelTime": 875, - "length": 864, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9175196, - "longitude": -87.9132199 - }, - "instruction": "Turn left onto N Railroad Ave. Go for 1.2 km.", - "travelTime": 1180, - "length": 1170, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9070268, - "longitude": -87.9130161 - }, - "instruction": "Turn right onto W North Ave. Go for 638 m.", - "travelTime": 638, - "length": 638, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9068551, - "longitude": -87.9207087 - }, - "instruction": "Take the street on the left, E North Ave. Go for 354 m.", - "travelTime": 354, - "length": 354, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065869, - "longitude": -87.9249573 - }, - "instruction": "Take the street on the left, E North Ave. Go for 228 m.", - "travelTime": 242, - "length": 228, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065225, - "longitude": -87.9277039 - }, - "instruction": "Turn left. Go for 409 m.", - "travelTime": 419, - "length": 409, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9040334, - "longitude": -87.9260409 - }, - "instruction": "Turn left onto E Third St. Go for 206 m.", - "travelTime": 206, - "length": 206, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9038832, - "longitude": -87.9236054 - }, - "instruction": "Turn left. Go for 113 m.", - "travelTime": 113, - "length": 113, - "id": "M15", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9039047, - "longitude": -87.9222536 - }, - "instruction": "Turn left. Go for 26 m.", - "travelTime": 26, - "length": 26, - "id": "M16", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "instruction": "Arrive at your destination on the right.", - "travelTime": 0, - "length": 0, - "id": "M17", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 12533, - "baseTime": 12631, - "flags": [ - "noThroughRoad", - "builtUpArea", - "park", - "privateRoad" - ], - "text": "The trip takes 12.5 km and 3:31 h.", - "travelTime": 12631, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/public_response.json b/tests/components/here_travel_time/fixtures/public_response.json deleted file mode 100644 index 149b4d06c39..00000000000 --- a/tests/components/here_travel_time/fixtures/public_response.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-21T18:40:37Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4478", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 191, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "publicTransport" - ], - "trafficMode": "disabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 191, - "source": "user" - }, - "length": 22325, - "travelTime": 5350, - "maneuver": [ - { - "position": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "instruction": "Head south on Mannheim Rd. Go for 848 m.", - "travelTime": 848, - "length": 848, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9722581, - "longitude": -87.8776109 - }, - "instruction": "Take the street on the left, Mannheim Rd. Go for 825 m.", - "travelTime": 825, - "length": 825, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9650483, - "longitude": -87.8769565 - }, - "instruction": "Go to the stop Mannheim/Lawrence and take the bus 332 toward Palmer/Schiller. Follow for 7 stops.", - "travelTime": 475, - "length": 4360, - "id": "M3", - "stopName": "Mannheim/Lawrence", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9541478, - "longitude": -87.9133594 - }, - "instruction": "Get off at Irving Park/Taft.", - "travelTime": 0, - "length": 0, - "id": "M4", - "stopName": "Irving Park/Taft", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9541478, - "longitude": -87.9133594 - }, - "instruction": "Take the bus 332 toward Cargo Rd./Delta Cargo. Follow for 1 stop.", - "travelTime": 155, - "length": 3505, - "id": "M5", - "stopName": "Irving Park/Taft", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9599199, - "longitude": -87.9162776 - }, - "instruction": "Get off at Cargo Rd./S. Access Rd./Lufthansa.", - "travelTime": 0, - "length": 0, - "id": "M6", - "stopName": "Cargo Rd./S. Access Rd./Lufthansa", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9599199, - "longitude": -87.9162776 - }, - "instruction": "Take the bus 332 toward Palmer/Schiller. Follow for 41 stops.", - "travelTime": 1510, - "length": 11261, - "id": "M7", - "stopName": "Cargo Rd./S. Access Rd./Lufthansa", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9041729, - "longitude": -87.9399669 - }, - "instruction": "Get off at York/Third.", - "travelTime": 0, - "length": 0, - "id": "M8", - "stopName": "York/Third", - "nextRoadName": "N York St", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9041729, - "longitude": -87.9399669 - }, - "instruction": "Head east on N York St. Go for 33 m.", - "travelTime": 43, - "length": 33, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9039476, - "longitude": -87.9398811 - }, - "instruction": "Turn left onto E Third St. Go for 1.4 km.", - "travelTime": 1355, - "length": 1354, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9038832, - "longitude": -87.9236054 - }, - "instruction": "Turn left. Go for 113 m.", - "travelTime": 113, - "length": 113, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9039047, - "longitude": -87.9222536 - }, - "instruction": "Turn left. Go for 26 m.", - "travelTime": 26, - "length": 26, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "instruction": "Arrive at your destination on the right.", - "travelTime": 0, - "length": 0, - "id": "M13", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "publicTransportLine": [ - { - "lineName": "332", - "companyName": "", - "destination": "Palmer/Schiller", - "type": "busPublic", - "id": "L1" - }, - { - "lineName": "332", - "companyName": "", - "destination": "Cargo Rd./Delta Cargo", - "type": "busPublic", - "id": "L2" - }, - { - "lineName": "332", - "companyName": "", - "destination": "Palmer/Schiller", - "type": "busPublic", - "id": "L3" - } - ], - "summary": { - "distance": 22325, - "baseTime": 5350, - "flags": [ - "noThroughRoad", - "builtUpArea", - "park" - ], - "text": "The trip takes 22.3 km and 1:29 h.", - "travelTime": 5350, - "departure": "1970-01-01T00:00:00Z", - "_type": "PublicTransportRouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/public_time_table_response.json b/tests/components/here_travel_time/fixtures/public_time_table_response.json deleted file mode 100644 index 52df0d4eb35..00000000000 --- a/tests/components/here_travel_time/fixtures/public_time_table_response.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-08-06T06:43:24Z", - "mapVersion": "8.30.99.152", - "moduleVersion": "7.2.201931-4739", - "interfaceVersion": "2.6.66", - "availableMapVersion": [ - "8.30.99.152" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 111, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "publicTransportTimeTable" - ], - "trafficMode": "disabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "-1230414527", - "mappedPosition": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5079365, - "sideOfStreet": "right", - "mappedRoadName": "Mannheim Rd", - "label": "Mannheim Rd - US-12", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+924115108", - "mappedPosition": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0.1925926, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 111, - "source": "user" - }, - "length": 14775, - "travelTime": 4784, - "maneuver": [ - { - "position": { - "latitude": 41.9797859, - "longitude": -87.8790879 - }, - "instruction": "Head south on Mannheim Rd. Go for 848 m.", - "travelTime": 848, - "length": 848, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9722581, - "longitude": -87.8776109 - }, - "instruction": "Take the street on the left, Mannheim Rd. Go for 812 m.", - "travelTime": 812, - "length": 812, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.965051, - "longitude": -87.8769591 - }, - "instruction": "Go to the Bus stop Mannheim/Lawrence and take the bus 330 toward Archer/Harlem (Terminal). Follow for 33 stops.", - "travelTime": 900, - "length": 7815, - "id": "M3", - "stopName": "Mannheim/Lawrence", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.896836, - "longitude": -87.883771 - }, - "instruction": "Get off at Mannheim/Lake.", - "travelTime": 0, - "length": 0, - "id": "M4", - "stopName": "Mannheim/Lake", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.896836, - "longitude": -87.883771 - }, - "instruction": "Walk to Bus Lake/Mannheim.", - "travelTime": 300, - "length": 72, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.897263, - "longitude": -87.8842648 - }, - "instruction": "Take the bus 309 toward Elmhurst Metra Station. Follow for 18 stops.", - "travelTime": 1020, - "length": 4362, - "id": "M6", - "stopName": "Lake/Mannheim", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9066347, - "longitude": -87.928671 - }, - "instruction": "Get off at North/Berteau.", - "travelTime": 0, - "length": 0, - "id": "M7", - "stopName": "North/Berteau", - "nextRoadName": "E Berteau Ave", - "_type": "PublicTransportManeuverType" - }, - { - "position": { - "latitude": 41.9066347, - "longitude": -87.928671 - }, - "instruction": "Head north on E Berteau Ave. Go for 23 m.", - "travelTime": 40, - "length": 23, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9067693, - "longitude": -87.9284549 - }, - "instruction": "Turn right onto E Berteau Ave. Go for 40 m.", - "travelTime": 44, - "length": 40, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065011, - "longitude": -87.9282939 - }, - "instruction": "Turn left onto E North Ave. Go for 49 m.", - "travelTime": 56, - "length": 49, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065225, - "longitude": -87.9277039 - }, - "instruction": "Turn slightly right. Go for 409 m.", - "travelTime": 419, - "length": 409, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9040334, - "longitude": -87.9260409 - }, - "instruction": "Turn left onto E Third St. Go for 206 m.", - "travelTime": 206, - "length": 206, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9038832, - "longitude": -87.9236054 - }, - "instruction": "Turn left. Go for 113 m.", - "travelTime": 113, - "length": 113, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9039047, - "longitude": -87.9222536 - }, - "instruction": "Turn left. Go for 26 m.", - "travelTime": 26, - "length": 26, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.90413, - "longitude": -87.9223502 - }, - "instruction": "Arrive at your destination on the right.", - "travelTime": 0, - "length": 0, - "id": "M15", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "publicTransportLine": [ - { - "lineName": "330", - "companyName": "PACE", - "destination": "Archer/Harlem (Terminal)", - "type": "busPublic", - "id": "L1" - }, - { - "lineName": "309", - "companyName": "PACE", - "destination": "Elmhurst Metra Station", - "type": "busPublic", - "id": "L2" - } - ], - "summary": { - "distance": 14775, - "baseTime": 4784, - "flags": [ - "noThroughRoad", - "builtUpArea", - "park" - ], - "text": "The trip takes 14.8 km and 1:20 h.", - "travelTime": 4784, - "departure": "2019-08-06T05:09:20-05:00", - "timetableExpiration": "2019-08-04T00:00:00Z", - "_type": "PublicTransportRouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json b/tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json deleted file mode 100644 index 81fb246178c..00000000000 --- a/tests/components/here_travel_time/fixtures/routing_error_invalid_credentials.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "_type": "ns2:RoutingServiceErrorType", - "type": "PermissionError", - "subtype": "InvalidCredentials", - "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.", - "metaInfo": { - "timestamp": "2019-07-10T09:43:14Z", - "mapVersion": "8.30.98.152", - "moduleVersion": "7.2.201927-4307", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.152" - ] - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/routing_error_no_route_found.json b/tests/components/here_travel_time/fixtures/routing_error_no_route_found.json deleted file mode 100644 index a776fa91c43..00000000000 --- a/tests/components/here_travel_time/fixtures/routing_error_no_route_found.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "_type": "ns2:RoutingServiceErrorType", - "type": "ApplicationError", - "subtype": "NoRouteFound", - "details": "Error is NGEO_ERROR_ROUTE_NO_END_POINT", - "additionalData": [ - { - "key": "error_code", - "value": "NGEO_ERROR_ROUTE_NO_END_POINT" - } - ], - "metaInfo": { - "timestamp": "2019-07-10T09:51:04Z", - "mapVersion": "8.30.98.152", - "moduleVersion": "7.2.201927-4307", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.152" - ] - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/fixtures/truck_response.json b/tests/components/here_travel_time/fixtures/truck_response.json deleted file mode 100644 index a302d564902..00000000000 --- a/tests/components/here_travel_time/fixtures/truck_response.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-21T14:25:00Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4478", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "+930461269", - "mappedPosition": { - "latitude": 41.9800687, - "longitude": -87.8805614 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5555556, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "-1035319462", - "mappedPosition": { - "latitude": 41.9042909, - "longitude": -87.9216528 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0, - "sideOfStreet": "left", - "mappedRoadName": "Eisenhower Expy E", - "label": "Eisenhower Expy E - I-290", - "shapeIndex": 135, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "truck" - ], - "trafficMode": "disabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "+930461269", - "mappedPosition": { - "latitude": 41.9800687, - "longitude": -87.8805614 - }, - "originalPosition": { - "latitude": 41.9798, - "longitude": -87.8801 - }, - "type": "stopOver", - "spot": 0.5555556, - "sideOfStreet": "right", - "mappedRoadName": "", - "label": "", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "-1035319462", - "mappedPosition": { - "latitude": 41.9042909, - "longitude": -87.9216528 - }, - "originalPosition": { - "latitude": 41.9043, - "longitude": -87.9216001 - }, - "type": "stopOver", - "spot": 0, - "sideOfStreet": "left", - "mappedRoadName": "Eisenhower Expy E", - "label": "Eisenhower Expy E - I-290", - "shapeIndex": 135, - "source": "user" - }, - "length": 13049, - "travelTime": 812, - "maneuver": [ - { - "position": { - "latitude": 41.9800687, - "longitude": -87.8805614 - }, - "instruction": "Take ramp onto I-190. Go for 631 m.", - "travelTime": 53, - "length": 631, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.98259, - "longitude": -87.8744352 - }, - "instruction": "Take exit 1D toward Indiana onto I-294 S (Tri-State Tollway). Go for 10.9 km.", - "travelTime": 573, - "length": 10872, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9059324, - "longitude": -87.9199362 - }, - "instruction": "Take exit 33 toward Rockford/US-20/IL-64 onto I-290 W (Eisenhower Expy W). Go for 475 m.", - "travelTime": 54, - "length": 475, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9067156, - "longitude": -87.9237771 - }, - "instruction": "Take exit 13B toward North Ave onto IL-64 W (E North Ave). Go for 435 m.", - "travelTime": 51, - "length": 435, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9065869, - "longitude": -87.9249573 - }, - "instruction": "Take ramp onto I-290 E (Eisenhower Expy E) toward Chicago/I-294 S. Go for 636 m.", - "travelTime": 81, - "length": 636, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 41.9042909, - "longitude": -87.9216528 - }, - "instruction": "Arrive at Eisenhower Expy E (I-290). Your destination is on the left.", - "travelTime": 0, - "length": 0, - "id": "M6", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 13049, - "trafficTime": 812, - "baseTime": 812, - "flags": [ - "tollroad", - "motorway", - "builtUpArea" - ], - "text": "The trip takes 13.0 km and 14 mins.", - "travelTime": 812, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us" - } -} \ No newline at end of file diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index b67e24dfc18..03d2313da2e 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,9 +1,7 @@ -"""The test for the here_travel_time sensor platform.""" -import logging +"""The test for the HERE Travel Time sensor platform.""" from unittest.mock import patch -import urllib -import herepy +from herepy.routing_api import InvalidCredentialsError, NoRouteFoundError import pytest from homeassistant.components.here_travel_time.sensor import ( @@ -25,143 +23,43 @@ from homeassistant.components.here_travel_time.sensor import ( ICON_PUBLIC, ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, - ROUTE_MODE_FASTEST, - ROUTE_MODE_SHORTEST, - SCAN_INTERVAL, TIME_MINUTES, - TRAFFIC_MODE_DISABLED, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, - TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, - convert_time_to_isodate, + TRAVEL_MODES_VEHICLE, ) from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, load_fixture +from tests.components.here_travel_time.const import ( + API_KEY, + CAR_DESTINATION_LATITUDE, + CAR_DESTINATION_LONGITUDE, + CAR_ORIGIN_LATITUDE, + CAR_ORIGIN_LONGITUDE, +) DOMAIN = "sensor" PLATFORM = "here_travel_time" -API_KEY = "test" -TRUCK_ORIGIN_LATITUDE = "41.9798" -TRUCK_ORIGIN_LONGITUDE = "-87.8801" -TRUCK_DESTINATION_LATITUDE = "41.9043" -TRUCK_DESTINATION_LONGITUDE = "-87.9216" - -BIKE_ORIGIN_LATITUDE = "41.9798" -BIKE_ORIGIN_LONGITUDE = "-87.8801" -BIKE_DESTINATION_LATITUDE = "41.9043" -BIKE_DESTINATION_LONGITUDE = "-87.9216" - -CAR_ORIGIN_LATITUDE = "38.9" -CAR_ORIGIN_LONGITUDE = "-77.04833" -CAR_DESTINATION_LATITUDE = "39.0" -CAR_DESTINATION_LONGITUDE = "-77.1" - - -def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival=None): - """Construct a url for HERE.""" - base_url = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json?" - parameters = { - "waypoint0": f"geo!{origin}", - "waypoint1": f"geo!{destination}", - "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), - "apikey": api_key, - } - if arrival is not None: - parameters["arrival"] = arrival - if departure is not None: - parameters["departure"] = departure - if departure is None and arrival is None: - parameters["departure"] = "now" - url = base_url + urllib.parse.urlencode(parameters) - return url - - -def _assert_truck_sensor(sensor): - """Assert that states and attributes are correct for truck_response.""" - assert sensor.state == "14" - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None - assert sensor.attributes.get(ATTR_DURATION) == 13.533333333333333 - assert sensor.attributes.get(ATTR_DISTANCE) == 13.049 - assert sensor.attributes.get(ATTR_ROUTE) == ( - "I-190; I-294 S - Tri-State Tollway; I-290 W - Eisenhower Expy W; " - "IL-64 W - E North Ave; I-290 E - Eisenhower Expy E; I-290" - ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 13.533333333333333 - assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( - [TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( - [TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Eisenhower Expy E" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_TRUCK - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False - - assert sensor.attributes.get(ATTR_ICON) == ICON_TRUCK - - -@pytest.fixture -def requests_mock_credentials_check(requests_mock): - """Add the url used in the api validation to all requests mock.""" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), - ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), - modes, - API_KEY, - ) - requests_mock.get( - response_url, text=load_fixture("here_travel_time/car_response.json") - ) - return requests_mock - - -@pytest.fixture -def requests_mock_truck_response(requests_mock_credentials_check): - """Return a requests_mock for truck response.""" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), - ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), - modes, - API_KEY, - ) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/truck_response.json") - ) - - -@pytest.fixture -def requests_mock_car_disabled_response(requests_mock_credentials_check): - """Return a requests_mock for truck response.""" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), - ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), - modes, - API_KEY, - ) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/car_response.json") - ) - - -async def test_car(hass, requests_mock_car_disabled_response): - """Test that car works.""" +@pytest.mark.parametrize( + "mode,icon,traffic_mode,unit_system", + [ + (TRAVEL_MODE_CAR, ICON_CAR, True, "metric"), + (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric"), + (TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, False, "imperial"), + (TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, False, "imperial"), + (TRAVEL_MODE_TRUCK, ICON_TRUCK, True, "metric"), + ], +) +async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_response): + """Test that sensor works.""" config = { DOMAIN: { "platform": PLATFORM, @@ -171,26 +69,41 @@ async def test_car(hass, requests_mock_car_disabled_response): "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, + "traffic_mode": traffic_mode, + "unit_system": unit_system, + "mode": mode, } } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() sensor = hass.states.get("sensor.test") - assert sensor.state == "30" assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert ( + sensor.attributes.get(ATTR_ATTRIBUTION) + == "With the support of HERE Technologies. All information is provided without warranty of any kind." + ) + if traffic_mode: + assert sensor.state == "31" + else: + assert sensor.state == "30" + assert sensor.attributes.get(ATTR_DURATION) == 30.05 - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + if unit_system == "metric": + assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + else: + assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 assert sensor.attributes.get(ATTR_ROUTE) == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system + if mode in TRAVEL_MODES_VEHICLE: + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 + else: + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 30.05 assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] ) @@ -199,374 +112,19 @@ async def test_car(hass, requests_mock_car_disabled_response): ) assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_CAR - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + assert sensor.attributes.get(CONF_MODE) == mode + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is traffic_mode - assert sensor.attributes.get(ATTR_ICON) == ICON_CAR + assert sensor.attributes.get(ATTR_ICON) == icon - # Test traffic mode disabled - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) + # Test traffic mode disabled for vehicles + if mode in TRAVEL_MODES_VEHICLE: + assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( + ATTR_DURATION_IN_TRAFFIC + ) -async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): - """Test that traffic mode enabled works.""" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url( - ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), - ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), - modes, - API_KEY, - ) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/car_enabled_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "traffic_mode": True, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - # Test traffic mode enabled - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) - - -async def test_imperial(hass, requests_mock_car_disabled_response): - """Test that imperial units work.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "unit_system": "imperial", - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 - - -async def test_route_mode_shortest(hass, requests_mock_credentials_check): - """Test that route mode shortest works.""" - origin = "38.902981,-77.048338" - destination = "39.042158,-77.119116" - modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/car_shortest_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "route_mode": ROUTE_MODE_SHORTEST, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 18.388 - - -async def test_route_mode_fastest(hass, requests_mock_credentials_check): - """Test that route mode fastest works.""" - origin = "38.902981,-77.048338" - destination = "39.042158,-77.119116" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/car_enabled_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "traffic_mode": True, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.381 - - -async def test_truck(hass, requests_mock_truck_response): - """Test that truck works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": TRUCK_ORIGIN_LATITUDE, - "origin_longitude": TRUCK_ORIGIN_LONGITUDE, - "destination_latitude": TRUCK_DESTINATION_LATITUDE, - "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - -async def test_public_transport(hass, requests_mock_credentials_check): - """Test that publicTransport works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/public_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "mode": TRAVEL_MODE_PUBLIC, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "89" - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None - assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667 - assert sensor.attributes.get(ATTR_DISTANCE) == 22.325 - assert sensor.attributes.get(ATTR_ROUTE) == ( - "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; 332 - Palmer/Schiller" - ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667 - assert sensor.attributes.get(ATTR_ORIGIN) == origin - assert sensor.attributes.get(ATTR_DESTINATION) == destination - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False - - assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC - - -async def test_public_transport_time_table(hass, requests_mock_credentials_check): - """Test that publicTransportTimeTable works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_travel_time/public_time_table_response.json"), - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "80" - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None - assert sensor.attributes.get(ATTR_DURATION) == 79.73333333333333 - assert sensor.attributes.get(ATTR_DISTANCE) == 14.775 - assert sensor.attributes.get(ATTR_ROUTE) == ( - "330 - Archer/Harlem (Terminal); 309 - Elmhurst Metra Station" - ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 79.73333333333333 - assert sensor.attributes.get(ATTR_ORIGIN) == origin - assert sensor.attributes.get(ATTR_DESTINATION) == destination - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC_TIME_TABLE - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False - - assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC - - -async def test_pedestrian(hass, requests_mock_credentials_check): - """Test that pedestrian works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/pedestrian_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "mode": TRAVEL_MODE_PEDESTRIAN, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "211" - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None - assert sensor.attributes.get(ATTR_DURATION) == 210.51666666666668 - assert sensor.attributes.get(ATTR_DISTANCE) == 12.533 - assert sensor.attributes.get(ATTR_ROUTE) == ( - "Mannheim Rd; W Belmont Ave; Cullerton St; E Fullerton Ave; " - "La Porte Ave; E Palmer Ave; N Railroad Ave; W North Ave; " - "E North Ave; E Third St" - ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 210.51666666666668 - assert sensor.attributes.get(ATTR_ORIGIN) == origin - assert sensor.attributes.get(ATTR_DESTINATION) == destination - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PEDESTRIAN - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False - - assert sensor.attributes.get(ATTR_ICON) == ICON_PEDESTRIAN - - -async def test_bicycle(hass, requests_mock_credentials_check): - """Test that bicycle works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/bike_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "mode": TRAVEL_MODE_BICYCLE, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "55" - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES - - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None - assert sensor.attributes.get(ATTR_DURATION) == 54.86666666666667 - assert sensor.attributes.get(ATTR_DISTANCE) == 12.613 - assert sensor.attributes.get(ATTR_ROUTE) == ( - "Mannheim Rd; W Belmont Ave; Cullerton St; N Landen Dr; " - "E Fullerton Ave; N Wolf Rd; W North Ave; N Clinton Ave; " - "E Third St; N Caroline Ave" - ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 54.86666666666667 - assert sensor.attributes.get(ATTR_ORIGIN) == origin - assert sensor.attributes.get(ATTR_DESTINATION) == destination - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" - assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_BICYCLE - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False - - assert sensor.attributes.get(ATTR_ICON) == ICON_BICYCLE - - -async def test_location_zone(hass, requests_mock_truck_response, legacy_patchable_time): +async def test_entity_ids(hass, valid_response): """Test that origin/destination supplied by a zone works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -575,15 +133,15 @@ async def test_location_zone(hass, requests_mock_truck_response, legacy_patchabl "zone": [ { "name": "Destination", - "latitude": TRUCK_DESTINATION_LATITUDE, - "longitude": TRUCK_DESTINATION_LONGITUDE, + "latitude": CAR_DESTINATION_LATITUDE, + "longitude": CAR_DESTINATION_LONGITUDE, "radius": 250, "passive": False, }, { "name": "Origin", - "latitude": TRUCK_ORIGIN_LATITUDE, - "longitude": TRUCK_ORIGIN_LONGITUDE, + "latitude": CAR_ORIGIN_LATITUDE, + "longitude": CAR_ORIGIN_LONGITUDE, "radius": 250, "passive": False, }, @@ -607,299 +165,17 @@ async def test_location_zone(hass, requests_mock_truck_response, legacy_patchabl await hass.async_block_till_done() sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - # Test that update works more than once - async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) + assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 -async def test_location_sensor( - hass, requests_mock_truck_response, legacy_patchable_time -): - """Test that origin/destination supplied by a sensor works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - hass.states.async_set( - "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) - ) - hass.states.async_set( - "sensor.destination", - ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "sensor.origin", - "destination_entity_id": "sensor.destination", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - # Test that update works more than once - async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - -async def test_location_person( - hass, requests_mock_truck_response, legacy_patchable_time -): - """Test that origin/destination supplied by a person works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - hass.states.async_set( - "person.origin", - "unknown", - { - "latitude": float(TRUCK_ORIGIN_LATITUDE), - "longitude": float(TRUCK_ORIGIN_LONGITUDE), - }, - ) - hass.states.async_set( - "person.destination", - "unknown", - { - "latitude": float(TRUCK_DESTINATION_LATITUDE), - "longitude": float(TRUCK_DESTINATION_LONGITUDE), - }, - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "person.origin", - "destination_entity_id": "person.destination", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - # Test that update works more than once - async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - -async def test_location_device_tracker( - hass, requests_mock_truck_response, legacy_patchable_time -): - """Test that origin/destination supplied by a device_tracker works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - hass.states.async_set( - "device_tracker.origin", - "unknown", - { - "latitude": float(TRUCK_ORIGIN_LATITUDE), - "longitude": float(TRUCK_ORIGIN_LONGITUDE), - }, - ) - hass.states.async_set( - "device_tracker.destination", - "unknown", - { - "latitude": float(TRUCK_DESTINATION_LATITUDE), - "longitude": float(TRUCK_DESTINATION_LONGITUDE), - }, - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "device_tracker.origin", - "destination_entity_id": "device_tracker.destination", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - # Test that update works more than once - async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - - -async def test_location_device_tracker_added_after_update( - hass, requests_mock_truck_response, legacy_patchable_time, caplog -): - """Test that device_tracker added after first update works.""" - caplog.set_level(logging.ERROR) - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "device_tracker.origin", - "destination_entity_id": "device_tracker.destination", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert "Unable to find entity" in caplog.text - caplog.clear() - - # Device tracker appear after first update - hass.states.async_set( - "device_tracker.origin", - "unknown", - { - "latitude": float(TRUCK_ORIGIN_LATITUDE), - "longitude": float(TRUCK_ORIGIN_LONGITUDE), - }, - ) - hass.states.async_set( - "device_tracker.destination", - "unknown", - { - "latitude": float(TRUCK_DESTINATION_LATITUDE), - "longitude": float(TRUCK_DESTINATION_LONGITUDE), - }, - ) - - # Test that update works more than once - async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - assert len(caplog.records) == 0 - - -async def test_location_device_tracker_in_zone( - hass, requests_mock_truck_response, caplog -): - """Test that device_tracker in zone uses device_tracker state works.""" - caplog.set_level(logging.DEBUG) - zone_config = { - "zone": [ - { - "name": "Origin", - "latitude": TRUCK_ORIGIN_LATITUDE, - "longitude": TRUCK_ORIGIN_LONGITUDE, - "radius": 250, - "passive": False, - } - ] - } - assert await async_setup_component(hass, "zone", zone_config) - hass.states.async_set( - "device_tracker.origin", "origin", {"latitude": None, "longitude": None} - ) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "device_tracker.origin", - "destination_latitude": TRUCK_DESTINATION_LATITUDE, - "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - _assert_truck_sensor(sensor) - assert ", getting zone location" in caplog.text - - -async def test_route_not_found(hass, requests_mock_credentials_check, caplog): +async def test_route_not_found(hass, caplog, valid_response): """Test that route not found error is correctly handled.""" - caplog.set_level(logging.ERROR) - origin = "52.516,13.3779" - destination = "47.013399,-10.171986" - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_travel_time/routing_error_no_route_found.json"), - ) - config = { DOMAIN: { "platform": PLATFORM, "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - assert len(caplog.records) == 1 - assert NO_ROUTE_ERROR_MESSAGE in caplog.text - - -async def test_pattern_origin(hass, caplog): - """Test that pattern matching the origin works.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": "138.90", - "origin_longitude": "-77.04833", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, @@ -907,43 +183,40 @@ async def test_pattern_origin(hass, caplog): } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - assert "invalid latitude" in caplog.text + with patch( + "herepy.RoutingApi.public_transport_timetable", + side_effect=NoRouteFoundError, + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert NO_ROUTE_ERROR_MESSAGE in caplog.text -async def test_pattern_destination(hass, caplog): - """Test that pattern matching the destination works.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": "139.0", - "destination_longitude": "-77.1", - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert "invalid latitude" in caplog.text - - -async def test_invalid_credentials(hass, requests_mock, caplog): +async def test_invalid_credentials(hass, caplog): """Test that invalid credentials error is correctly handled.""" - caplog.set_level(logging.ERROR) - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), - ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), - modes, - API_KEY, - ) - requests_mock.get( - response_url, - text=load_fixture("here_travel_time/routing_error_invalid_credentials.json"), - ) + with patch( + "herepy.RoutingApi.public_transport_timetable", + side_effect=InvalidCredentialsError, + ): + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "api_key": API_KEY, + } + } + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + assert "Invalid credentials" in caplog.text + +async def test_arrival(hass, valid_response): + """Test that arrival works.""" config = { DOMAIN: { "platform": PLATFORM, @@ -953,36 +226,8 @@ async def test_invalid_credentials(hass, requests_mock, caplog): "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert len(caplog.records) == 1 - assert "Invalid credentials" in caplog.text - - -async def test_attribution(hass, requests_mock_credentials_check): - """Test that attributions are correctly displayed.""" - origin = "50.037751372637686,14.39233448220898" - destination = "50.07993838201255,14.42582157361062" - modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, API_KEY) - requests_mock_credentials_check.get( - response_url, text=load_fixture("here_travel_time/attribution_response.json") - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "traffic_mode": True, - "route_mode": ROUTE_MODE_SHORTEST, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + "arrival": "01:00:00", } } assert await async_setup_component(hass, DOMAIN, config) @@ -992,124 +237,22 @@ async def test_attribution(hass, requests_mock_credentials_check): await hass.async_block_till_done() sensor = hass.states.get("sensor.test") - assert ( - sensor.attributes.get(ATTR_ATTRIBUTION) - == "With the support of HERE Technologies. All information is provided without warranty of any kind." - ) + assert sensor.state == "30" -async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): - """Test that pattern matching the state of an entity works.""" - caplog.set_level(logging.ERROR) - hass.states.async_set("sensor.origin", "invalid") - +async def test_departure(hass, valid_response): + """Test that departure works.""" config = { DOMAIN: { "platform": PLATFORM, "name": "test", - "origin_entity_id": "sensor.origin", - "destination_latitude": TRUCK_DESTINATION_LATITUDE, - "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - assert len(caplog.records) == 1 - assert ( - "Entity sensor.origin does not contain a location and does not point at an entity that does: invalid" - in caplog.text - ) - - -async def test_pattern_entity_state_with_space(hass, requests_mock_truck_response): - """Test that pattern matching the state including a space of an entity works.""" - hass.states.async_set( - "sensor.origin", ", ".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "sensor.origin", - "destination_latitude": TRUCK_DESTINATION_LATITUDE, - "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - -async def test_delayed_update(hass, requests_mock_truck_response, caplog): - """Test that delayed update does not complain about missing entities.""" - caplog.set_level(logging.WARNING) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "sensor.origin", - "destination_latitude": TRUCK_DESTINATION_LATITUDE, - "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - sensor_config = { - "sensor": { - "platform": "template", - "sensors": [ - {"template_sensor": {"value_template": "{{states('sensor.origin')}}"}} - ], - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert await async_setup_component(hass, "sensor", sensor_config) - hass.states.async_set( - "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) - ) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - assert "Unable to find entity" not in caplog.text - - -async def test_arrival(hass, requests_mock_credentials_check): - """Test that arrival works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - arrival = "01:00:00" - arrival_isodate = convert_time_to_isodate(arrival) - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - origin, destination, modes, API_KEY, arrival=arrival_isodate - ) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_travel_time/public_time_table_response.json"), - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - "arrival": arrival, + "departure": "23:00:00", } } assert await async_setup_component(hass, DOMAIN, config) @@ -1119,60 +262,19 @@ async def test_arrival(hass, requests_mock_credentials_check): await hass.async_block_till_done() sensor = hass.states.get("sensor.test") - assert sensor.state == "80" - - -async def test_departure(hass, requests_mock_credentials_check): - """Test that arrival works.""" - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" - departure = "23:00:00" - departure_isodate = convert_time_to_isodate(departure) - modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url( - origin, destination, modes, API_KEY, departure=departure_isodate - ) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_travel_time/public_time_table_response.json"), - ) - - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], - "api_key": API_KEY, - "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - "departure": departure, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "80" + assert sensor.state == "30" async def test_arrival_only_allowed_for_timetable(hass, caplog): """Test that arrival is only allowed when mode is publicTransportTimeTable.""" - caplog.set_level(logging.ERROR) - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" config = { DOMAIN: { "platform": PLATFORM, "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, "arrival": "01:00:00", } @@ -1184,17 +286,14 @@ async def test_arrival_only_allowed_for_timetable(hass, caplog): async def test_exclusive_arrival_and_departure(hass, caplog): """Test that arrival and departure are exclusive.""" - caplog.set_level(logging.ERROR) - origin = "41.9798,-87.8801" - destination = "41.9043,-87.9216" config = { DOMAIN: { "platform": PLATFORM, "name": "test", - "origin_latitude": origin.split(",")[0], - "origin_longitude": origin.split(",")[1], - "destination_latitude": destination.split(",")[0], - "destination_longitude": destination.split(",")[1], + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, "api_key": API_KEY, "arrival": "01:00:00", "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, From c54ca7941f11fd59b3465bad3ecab28d4a9a893f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 1 Dec 2021 22:59:57 +0100 Subject: [PATCH 1178/1452] Make sure entity inherit disabled device (#60469) --- homeassistant/helpers/entity_platform.py | 8 ++++- tests/helpers/test_entity_platform.py | 42 ++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 8b78e985ff9..d3eea95f545 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -42,7 +42,7 @@ from . import ( service, ) from .device_registry import DeviceRegistry -from .entity_registry import DISABLED_INTEGRATION, EntityRegistry +from .entity_registry import DISABLED_DEVICE, DISABLED_INTEGRATION, EntityRegistry from .event import async_call_later, async_track_time_interval from .typing import ConfigType, DiscoveryInfoType @@ -456,6 +456,7 @@ class EntityPlatform: device_info = entity.device_info device_id = None + device = None if config_entry_id is not None and device_info is not None: processed_dev_info: dict[str, str | None] = { @@ -523,6 +524,11 @@ class EntityPlatform: unit_of_measurement=entity.unit_of_measurement, ) + if device and device.disabled and not entry.disabled: + entry = entity_registry.async_update_entity( + entry.entity_id, disabled_by=DISABLED_DEVICE + ) + entity.registry_entry = entry entity.entity_id = entry.entity_id diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index e367fa248d5..baaea35b62c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -7,14 +7,14 @@ from unittest.mock import ANY, Mock, patch import pytest from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( device_registry as dr, entity_platform, entity_registry as er, ) -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -1080,6 +1080,44 @@ async def test_entity_disabled_by_integration(hass): assert entry_disabled.disabled_by == er.DISABLED_INTEGRATION +async def test_entity_disabled_by_device(hass: HomeAssistant): + """Test entity disabled by device.""" + + connections = {(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")} + entity_disabled = MockEntity( + unique_id="disabled", device_info=DeviceInfo(connections=connections) + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([entity_disabled]) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id", domain=DOMAIN) + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections=connections, + disabled_by=dr.DeviceEntryDisabler.USER, + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert entity_disabled.hass is None + assert entity_disabled.platform is None + + registry = er.async_get(hass) + + entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") + assert entry_disabled.disabled_by == er.DISABLED_DEVICE + + async def test_entity_info_added_to_entity_registry(hass): """Test entity info is written to entity registry.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) From d211dc6e6e2e4b2977720ef0eb07ba004f18e194 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 2 Dec 2021 00:14:42 +0100 Subject: [PATCH 1179/1452] Fix trafikverket_weatherstation (#60772) * First commit * Modify test according to fixes * Review changes * Clean up Co-authored-by: Martin Hjelmare --- .../trafikverket_weatherstation/__init__.py | 12 +-- .../config_flow.py | 53 +++++---- .../trafikverket_weatherstation/sensor.py | 4 +- .../trafikverket_weatherstation/strings.json | 10 +- .../translations/en.json | 8 +- .../test_config_flow.py | 101 ++++++++++++++++-- 6 files changed, 140 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index 535ef304557..854b5d54d70 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -6,19 +6,17 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, PLATFORMS +from .const import PLATFORMS _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Weatherstation from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - title = entry.title hass.config_entries.async_setup_platforms(entry, PLATFORMS) - _LOGGER.debug("Loaded entry for %s", title) + _LOGGER.debug("Loaded entry for %s", entry.title) return True @@ -28,12 +26,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - title = entry.title if unload_ok: - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - _LOGGER.debug("Unloaded entry for %s", title) + _LOGGER.debug("Unloaded entry for %s", entry.title) return unload_ok return False diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py index f079852be93..103af1c7eb4 100644 --- a/homeassistant/components/trafikverket_weatherstation/config_flow.py +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -1,22 +1,18 @@ """Adds config flow for Trafikverket Weather integration.""" from __future__ import annotations -import json - +from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_STATION, DOMAIN -from .sensor import SENSOR_TYPES - -SENSOR_LIST: set[str] = {description.key for (description) in SENSOR_TYPES} DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_STATION): cv.string, } @@ -30,14 +26,24 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry + async def validate_input(self, sensor_api: str, station: str) -> str: + """Validate input from user input.""" + web_session = async_get_clientsession(self.hass) + weather_api = TrafikverketWeather(web_session, sensor_api) + try: + await weather_api.async_get_weather(station) + except ValueError as err: + return str(err) + return "connected" + async def async_step_import(self, config: dict): """Import a configuration from config.yaml.""" self.context.update( - {"title_placeholders": {CONF_NAME: f"YAML import {DOMAIN}"}} + {"title_placeholders": {CONF_STATION: f"YAML import {DOMAIN}"}} ) - self._async_abort_entries_match({CONF_NAME: config[CONF_NAME]}) + self._async_abort_entries_match({CONF_STATION: config[CONF_STATION]}) return await self.async_step_user(user_input=config) async def async_step_user(self, user_input=None): @@ -45,20 +51,27 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - name = user_input[CONF_NAME] + name = user_input[CONF_STATION] api_key = user_input[CONF_API_KEY] station = user_input[CONF_STATION] - conditions = json.dumps(list(SENSOR_LIST)) - return self.async_create_entry( - title=name, - data={ - CONF_NAME: name, - CONF_API_KEY: api_key, - CONF_STATION: station, - CONF_MONITORED_CONDITIONS: conditions, - }, - ) + validate = await self.validate_input(api_key, station) + if validate == "connected": + return self.async_create_entry( + title=name, + data={ + CONF_API_KEY: api_key, + CONF_STATION: station, + }, + ) + if validate == "Source: Security, message: Invalid authentication": + errors["base"] = "invalid_auth" + elif validate == "Could not find a weather station with the specified name": + errors["base"] = "invalid_station" + elif validate == "Found multiple weather stations with the specified name": + errors["base"] = "more_stations" + else: + errors["base"] = "cannot_connect" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 32703aef4cc..46fa3d9a5bd 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -172,7 +172,7 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - sensor_name = entry.data[CONF_NAME] + sensor_name = entry.data[CONF_STATION] sensor_api = entry.data[CONF_API_KEY] sensor_station = entry.data[CONF_STATION] @@ -180,13 +180,11 @@ async def async_setup_entry( weather_api = TrafikverketWeather(web_session, sensor_api) - monitored_conditions = entry.data[CONF_MONITORED_CONDITIONS] entities = [ TrafikverketWeatherStation( weather_api, sensor_name, sensor_station, description ) for description in SENSOR_TYPES - if description.key in monitored_conditions ] async_add_entities(entities, True) diff --git a/homeassistant/components/trafikverket_weatherstation/strings.json b/homeassistant/components/trafikverket_weatherstation/strings.json index 7fa8071e389..d4a1eb69f1d 100644 --- a/homeassistant/components/trafikverket_weatherstation/strings.json +++ b/homeassistant/components/trafikverket_weatherstation/strings.json @@ -3,13 +3,17 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_station": "Could not find a weather station with the specified name", + "more_stations": "Found multiple weather stations with the specified name" + }, "step": { "user": { "data": { - "name": "[%key:common::config_flow::data::username%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "station": "Station", - "conditions": "Monitored conditions" + "station": "Station" } } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/en.json b/homeassistant/components/trafikverket_weatherstation/translations/en.json index 883e67e9bfa..73f02145899 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/en.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/en.json @@ -3,12 +3,16 @@ "abort": { "already_configured": "Account is already configured" }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_station": "Could not find a weather station with the specified name", + "more_stations": "Found multiple weather stations with the specified name" + }, "step": { "user": { "data": { "api_key": "API Key", - "conditions": "Monitored conditions", - "name": "Username", "station": "Station" } } diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index ecbd84f0eb4..b8a18181a11 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -1,15 +1,16 @@ """Test the Trafikverket weatherstation config flow.""" from __future__ import annotations -import json from unittest.mock import patch +import pytest + from homeassistant import config_entries -from homeassistant.components.trafikverket_weatherstation.sensor import SENSOR_TYPES from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_FORM -SENSOR_LIST: list[str | None] = {description.key for (description) in SENSOR_TYPES} +from tests.common import MockConfigEntry DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -25,13 +26,14 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + ), patch( "homeassistant.components.trafikverket_weatherstation.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_NAME: "Vallby Vasteras", CONF_API_KEY: "1234567890", CONF_STATION: "Vallby", }, @@ -39,12 +41,10 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "Vallby Vasteras" + assert result2["title"] == "Vallby" assert result2["data"] == { - "name": "Vallby Vasteras", "api_key": "1234567890", "station": "Vallby", - "monitored_conditions": json.dumps(list(SENSOR_LIST)), } assert len(mock_setup_entry.mock_calls) == 1 @@ -53,6 +53,8 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: """Test a successful import of yaml.""" with patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + ), patch( "homeassistant.components.trafikverket_weatherstation.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -60,7 +62,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - CONF_NAME: "Vallby Vasteras", + CONF_NAME: "Vallby", CONF_API_KEY: "1234567890", CONF_STATION: "Vallby", }, @@ -68,11 +70,88 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "Vallby Vasteras" + assert result2["title"] == "Vallby" assert result2["data"] == { - "name": "Vallby Vasteras", "api_key": "1234567890", "station": "Vallby", - "monitored_conditions": json.dumps(list(SENSOR_LIST)), } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_STATION: "Vallby", + }, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_weatherstation.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Vallby", + CONF_API_KEY: "1234567890", + CONF_STATION: "Vallby", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "error_message,base_error", + [ + ( + "Source: Security, message: Invalid authentication", + "invalid_auth", + ), + ( + "Could not find a weather station with the specified name", + "invalid_station", + ), + ( + "Found multiple weather stations with the specified name", + "more_stations", + ), + ( + "Unknown", + "cannot_connect", + ), + ], +) +async def test_flow_fails( + hass: HomeAssistant, error_message: str, base_error: str +) -> None: + """Test config flow errors.""" + result4 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result4["type"] == RESULT_TYPE_FORM + assert result4["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + side_effect=ValueError(error_message), + ): + result4 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_STATION: "Vallby", + }, + ) + + assert result4["errors"] == {"base": base_error} From 12cd87d230247d53475c074af74005a3c8ebbe81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Dec 2021 15:20:08 -0800 Subject: [PATCH 1180/1452] Bump frontend to 20211201.0 (#60801) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f42b28df92f..be8766c2608 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211130.0" + "home-assistant-frontend==20211201.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b1d842bdc9..43425199d99 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211130.0 +home-assistant-frontend==20211201.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index a4e828a0ddb..1166abee943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211130.0 +home-assistant-frontend==20211201.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 320e5dd19ae..598533be930 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211130.0 +home-assistant-frontend==20211201.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 344cd0d71fb947e57b0c74aec23a6caaf5b3dd51 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 1 Dec 2021 16:59:33 -0700 Subject: [PATCH 1181/1452] Only unload RainMachine services if the last config entry is loaded (#60805) --- homeassistant/components/rainmachine/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 76a02fa94a8..9f7f014f13d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -11,7 +11,7 @@ from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_DEVICE_ID, CONF_IP_ADDRESS, @@ -313,9 +313,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.config_entries.async_entries(DOMAIN)) == 1: - # If this is the last instance of RainMachine, deregister any services defined - # during integration setup: + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of RainMachine, deregister any services + # defined during integration setup: for service_name in ( SERVICE_NAME_PAUSE_WATERING, SERVICE_NAME_PUSH_WEATHER_DATA, From de792e3af23e6f0d335a18593e577b070d1cfcad Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 2 Dec 2021 00:13:02 +0000 Subject: [PATCH 1182/1452] [ci skip] Translation update --- .../components/arcam_fmj/translations/ja.json | 3 ++ .../components/awair/translations/ca.json | 2 +- .../binary_sensor/translations/ja.json | 19 +++---- .../components/blink/translations/ca.json | 2 +- .../components/brunt/translations/nl.json | 6 ++- .../components/climate/translations/ja.json | 4 ++ .../components/cover/translations/ja.json | 16 +++--- .../components/deconz/translations/ja.json | 6 ++- .../components/group/translations/ja.json | 4 +- .../components/hue/translations/ja.json | 2 + .../components/hyperion/translations/ca.json | 2 +- .../components/knx/translations/nl.json | 50 +++++++++++++++++++ .../lutron_caseta/translations/ja.json | 10 ++-- .../components/mill/translations/nl.json | 16 +++++- .../components/mqtt/translations/ja.json | 6 ++- .../components/nanoleaf/translations/ca.json | 2 +- .../components/nest/translations/ca.json | 2 +- .../components/nest/translations/en.json | 2 +- .../components/nest/translations/et.json | 2 +- .../components/nest/translations/ja.json | 6 ++- .../components/nest/translations/nl.json | 13 ++++- .../components/point/translations/ca.json | 2 +- .../components/shelly/translations/ja.json | 4 +- .../components/tailscale/translations/ca.json | 26 ++++++++++ .../components/tailscale/translations/de.json | 26 ++++++++++ .../components/tailscale/translations/hu.json | 26 ++++++++++ .../components/tailscale/translations/nl.json | 26 ++++++++++ .../components/tailscale/translations/ru.json | 26 ++++++++++ .../components/toon/translations/ja.json | 7 +++ .../tractive/translations/sensor.en.json | 10 ++++ .../translations/de.json | 17 +++++++ .../translations/en.json | 10 ++-- .../translations/et.json | 17 +++++++ .../translations/hu.json | 17 +++++++ .../translations/nl.json | 17 +++++++ .../translations/no.json | 17 +++++++ .../translations/ru.json | 17 +++++++ .../translations/zh-Hant.json | 17 +++++++ .../tuya/translations/select.nl.json | 3 ++ .../yale_smart_alarm/translations/ca.json | 3 +- .../yale_smart_alarm/translations/de.json | 3 +- .../yale_smart_alarm/translations/et.json | 3 +- .../yale_smart_alarm/translations/hu.json | 3 +- .../yale_smart_alarm/translations/ja.json | 3 +- .../yale_smart_alarm/translations/nl.json | 3 +- .../yale_smart_alarm/translations/no.json | 3 +- .../yale_smart_alarm/translations/ru.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/zha/translations/ja.json | 12 ++++- 49 files changed, 445 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/tailscale/translations/ca.json create mode 100644 homeassistant/components/tailscale/translations/de.json create mode 100644 homeassistant/components/tailscale/translations/hu.json create mode 100644 homeassistant/components/tailscale/translations/nl.json create mode 100644 homeassistant/components/tailscale/translations/ru.json create mode 100644 homeassistant/components/tractive/translations/sensor.en.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/de.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/et.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/hu.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/nl.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/no.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/ru.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json diff --git a/homeassistant/components/arcam_fmj/translations/ja.json b/homeassistant/components/arcam_fmj/translations/ja.json index 053954d79b9..2ba5cd17aa0 100644 --- a/homeassistant/components/arcam_fmj/translations/ja.json +++ b/homeassistant/components/arcam_fmj/translations/ja.json @@ -7,6 +7,9 @@ }, "flow_title": "{host}", "step": { + "confirm": { + "description": "Home Assistant\u306bArcam FMJ on `{host}` \u3092\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index ac69e06df1e..12384b088bb 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -6,7 +6,7 @@ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index ee695425407..979d2cf966a 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -60,6 +60,7 @@ "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "light": "{entity_name} \u306f\u3001\u5149\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", + "moist": "{entity_name} \u304c\u6e7f\u3063\u305f", "motion": "{entity_name} \u306f\u3001\u52d5\u304d\u3092\u691c\u51fa\u3057\u59cb\u3081\u307e\u3057\u305f", "moving": "{entity_name} \u306f\u3001\u79fb\u52d5\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "no_gas": "{entity_name} \u306f\u3001\u30ac\u30b9\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", @@ -78,7 +79,7 @@ "not_moist": "{entity_name} \u306f\u4e7e\u3044\u3066\u3044\u307e\u305b\u3093", "not_moving": "{entity_name} \u304c\u52d5\u304d\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "not_occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f", - "not_opened": "{entity_name} \u9589\u3058\u307e\u3057\u305f", + "not_opened": "{entity_name} \u30af\u30ed\u30fc\u30ba\u30c9", "not_plugged_in": "{entity_name} \u306e\u30d7\u30e9\u30b0\u304c\u629c\u304b\u308c\u307e\u3057\u305f", "not_powered": "{entity_name} \u306f\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u305b\u3093", "not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", @@ -135,12 +136,12 @@ "on": "\u63a5\u7d9a\u6e08" }, "door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "on": "\u30aa\u30fc\u30d7\u30f3" }, "garage_door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "on": "\u30aa\u30fc\u30d7\u30f3" }, "gas": { "off": "\u30af\u30ea\u30a2", @@ -175,8 +176,8 @@ "on": "\u691c\u51fa" }, "opening": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "on": "\u30aa\u30fc\u30d7\u30f3" }, "plug": { "off": "\u30a2\u30f3\u30d7\u30e9\u30b0\u30c9", @@ -215,8 +216,8 @@ "on": "\u691c\u51fa" }, "window": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "on": "\u30aa\u30fc\u30d7\u30f3" } }, "title": "\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc" diff --git a/homeassistant/components/blink/translations/ca.json b/homeassistant/components/blink/translations/ca.json index bd5079d2a16..695db588b0d 100644 --- a/homeassistant/components/blink/translations/ca.json +++ b/homeassistant/components/blink/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/brunt/translations/nl.json b/homeassistant/components/brunt/translations/nl.json index 6cf4f7e2f94..86a7df6a585 100644 --- a/homeassistant/components/brunt/translations/nl.json +++ b/homeassistant/components/brunt/translations/nl.json @@ -19,8 +19,10 @@ }, "user": { "data": { - "password": "Wachtwoord" - } + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Stel uw Brunt-integratie in" } } } diff --git a/homeassistant/components/climate/translations/ja.json b/homeassistant/components/climate/translations/ja.json index 146d928a483..0c89bac48d8 100644 --- a/homeassistant/components/climate/translations/ja.json +++ b/homeassistant/components/climate/translations/ja.json @@ -4,6 +4,10 @@ "set_hvac_mode": "{entity_name} \u306eHVAC\u30e2\u30fc\u30c9\u3092\u5909\u66f4", "set_preset_mode": "{entity_name} \u306e\u30d7\u30ea\u30bb\u30c3\u30c8\u3092\u5909\u66f4" }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u306f\u7279\u5b9a\u306eHVAC\u30e2\u30fc\u30c9\u306b\u30bb\u30c3\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "is_preset_mode": "{entity_name} \u306f\u7279\u5b9a\u306e\u30d7\u30ea\u30bb\u30c3\u30c8\u30e2\u30fc\u30c9\u306b\u30bb\u30c3\u30c8\u3055\u308c\u3066\u3044\u307e\u3059" + }, "trigger_type": { "current_humidity_changed": "{entity_name} \u6e2c\u5b9a\u6e7f\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", "current_temperature_changed": "{entity_name} \u6e2c\u5b9a\u6e29\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index c8ec1f5bbed..2b2f8cdf284 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "close": "\u30af\u30ed\u30fc\u30ba {entity_name}", + "close_tilt": "\u30af\u30ed\u30fc\u30ba {entity_name} \u50be\u304d", + "open": "\u30aa\u30fc\u30d7\u30f3 {entity_name}", + "open_tilt": "\u30aa\u30fc\u30d7\u30f3 {entity_name} \u50be\u304d", "set_position": "{entity_name} \u4f4d\u7f6e\u306e\u8a2d\u5b9a", "set_tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u8a2d\u5b9a", "stop": "\u505c\u6b62 {entity_name}" @@ -9,25 +13,25 @@ "is_closed": "{entity_name} \u306f\u9589\u3058\u3066\u3044\u307e\u3059", "is_closing": "{entity_name} \u304c\u7d42\u4e86\u3057\u3066\u3044\u307e\u3059", "is_open": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059", - "is_opening": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059", + "is_opening": "{entity_name} \u304c\u958b\u3044\u3066\u3044\u307e\u3059(is opening)", "is_position": "\u73fe\u5728\u306e {entity_name} \u4f4d\u7f6e", "is_tilt_position": "\u73fe\u5728\u306e {entity_name} \u50be\u659c\u4f4d\u7f6e" }, "trigger_type": { - "closed": "{entity_name} \u9589\u3058\u307e\u3057\u305f", + "closed": "{entity_name} \u30af\u30ed\u30fc\u30ba\u30c9", "closing": "{entity_name} \u304c\u7d42\u4e86", "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", - "opening": "{entity_name} \u304c\u958b\u304f", + "opening": "{entity_name} \u304c\u958b\u304f(Opening)", "position": "{entity_name} \u4f4d\u7f6e\u306e\u5909\u5316", "tilt_position": "{entity_name} \u50be\u659c\u4f4d\u7f6e\u306e\u5909\u5316" } }, "state": { "_": { - "closed": "\u9589\u9396", + "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", "closing": "\u9589\u3058\u3066\u3044\u307e\u3059", - "open": "\u958b\u653e", - "opening": "\u6249", + "open": "\u30aa\u30fc\u30d7\u30f3", + "opening": "\u6249(Opening)", "stopped": "\u505c\u6b62" } }, diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 71c3caa6ad0..4b33589ddfc 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -46,7 +46,7 @@ "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_7": "7\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_8": "8\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "close": "\u9589\u3058\u308b", + "close": "\u30af\u30ed\u30fc\u30ba", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", @@ -64,9 +64,13 @@ }, "trigger_type": { "remote_awakened": "\u30c7\u30d0\u30a4\u30b9\u304c\u76ee\u899a\u3081\u305f", + "remote_button_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", + "remote_button_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af", + "remote_button_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af", "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3059\u308b \"{subtype}\"", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af", "remote_double_tap": "\u30c7\u30d0\u30a4\u30b9 \"{subtype}\" \u304c\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f", "remote_double_tap_any_side": "\u30c7\u30d0\u30a4\u30b9\u306e\u3044\u305a\u308c\u304b\u306e\u9762\u3092\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3057\u305f", "remote_falling": "\u81ea\u7531\u843d\u4e0b\u6642\u306e\u30c7\u30d0\u30a4\u30b9(Device in free fall)", diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index c49045fedd6..1faf42c68d4 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -1,14 +1,14 @@ { "state": { "_": { - "closed": "\u9589\u9396", + "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", "home": "\u5728\u5b85", "locked": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", "not_home": "\u96e2\u5e2d(away)", "off": "\u30aa\u30d5", "ok": "OK", "on": "\u30aa\u30f3", - "open": "\u958b\u653e", + "open": "\u30aa\u30fc\u30d7\u30f3", "problem": "\u554f\u984c", "unlocked": "\u30ed\u30c3\u30af\u89e3\u9664" } diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 68fff69f518..d1bcad85ca9 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -56,6 +56,7 @@ "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", + "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" @@ -65,6 +66,7 @@ "step": { "init": { "data": { + "allow_hue_groups": "Hue(\u8272\u76f8)\u30b0\u30eb\u30fc\u30d7\u306e\u8a31\u53ef", "allow_hue_scenes": "Hue\u30b7\u30fc\u30f3\u3092\u8a31\u53ef", "allow_unreachable": "\u5230\u9054\u3067\u304d\u306a\u304b\u3063\u305f\u96fb\u7403(bulbs)\u304c\u305d\u306e\u72b6\u614b\u3092\u6b63\u3057\u304f\u5831\u544a\u3067\u304d\u308b\u3088\u3046\u306b\u3059\u308b" } diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json index 3a1de53102b..c0021e911cb 100644 --- a/homeassistant/components/hyperion/translations/ca.json +++ b/homeassistant/components/hyperion/translations/ca.json @@ -12,7 +12,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid" + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid" }, "step": { "auth": { diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index ac8a07a747a..3a5e3dc5d70 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -1,6 +1,56 @@ { + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "individual_address": "Individueel adres voor de verbinding", + "port": "Poort", + "route_back": "Route Back / NAT Mode" + }, + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." + }, + "routing": { + "data": { + "individual_address": "Individueel adres voor de routing verbinding", + "multicast_group": "De multicast groep gebruikt voor de routing", + "multicast_port": "De multicast-poort gebruikt voor de routing" + }, + "description": "Configureer de routing opties" + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Connection" + }, + "description": "Selecteer een gateway uit de lijst." + }, + "type": { + "data": { + "connection_type": "KNX-verbindingstype" + }, + "description": "Voer het verbindingstype in dat we moeten gebruiken voor uw KNX-verbinding.\n AUTOMATISCH - De integratie zorgt voor de connectiviteit met uw KNX-bus door een gateway-scan uit te voeren.\n TUNNELING - De integratie maakt verbinding met uw KNX-bus via tunneling.\n ROUTING - De integratie maakt via routing verbinding met uw KNX-bus." + } + } + }, "options": { "step": { + "init": { + "data": { + "connection_type": "KNX-verbindingstype", + "individual_address": "Standaard individueel adres", + "multicast_group": "Multicast groep gebruikt voor routing en ontdekking", + "multicast_port": "Multicast poort gebruikt voor routing en ontdekking", + "rate_limit": "Maximaal aantal uitgaande telegrammen per seconde", + "state_updater": "Globaal vrijgeven van het lezen van de KNX bus" + } + }, "tunnel": { "data": { "host": "Host", diff --git a/homeassistant/components/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json index 6815b5e33dc..f32e47e9942 100644 --- a/homeassistant/components/lutron_caseta/translations/ja.json +++ b/homeassistant/components/lutron_caseta/translations/ja.json @@ -33,11 +33,11 @@ "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "close_1": "\u9589\u3058\u308b1", - "close_2": "\u9589\u3058\u308b2", - "close_3": "\u9589\u3058\u308b3", - "close_4": "\u9589\u3058\u308b4", - "close_all": "\u3059\u3079\u3066\u9589\u3058\u308b", + "close_1": "\u30af\u30ed\u30fc\u30ba1", + "close_2": "\u30af\u30ed\u30fc\u30ba2", + "close_3": "\u30af\u30ed\u30fc\u30ba3", + "close_4": "\u30af\u30ed\u30fc\u30ba4", + "close_all": "\u3059\u3079\u3066\u30af\u30ed\u30fc\u30ba", "group_1_button_1": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", "group_1_button_2": "\u6700\u521d\u306e\u30b0\u30eb\u30fc\u30d7\u306e2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "group_2_button_1": "2\u756a\u76ee\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u6700\u521d\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json index fff0a8232e4..f37a5cf0758 100644 --- a/homeassistant/components/mill/translations/nl.json +++ b/homeassistant/components/mill/translations/nl.json @@ -7,11 +7,25 @@ "cannot_connect": "Kan geen verbinding maken" }, "step": { - "user": { + "cloud": { "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" } + }, + "local": { + "data": { + "ip_address": "IP-adres" + }, + "description": "Lokaal IP-adres van het apparaat." + }, + "user": { + "data": { + "connection_type": "Selecteer verbindingstype", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Selecteer verbindingstype. Lokaal vereist generatie 3 kachels" } } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 48165f64111..a7de3f583b2 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -39,8 +39,12 @@ "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { + "button_double_press": "\"{subtype}\" \u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "button_long_press": "\"{subtype}\" \u304c\u3001\u7d99\u7d9a\u7684\u306b\u62bc\u3055\u308c\u305f", - "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" + "button_quadruple_press": "\"{subtype}\" 4\u56de(quadruple)\u30af\u30ea\u30c3\u30af", + "button_quintuple_press": "\"{subtype}\" 5\u56de(quintuple)\u30af\u30ea\u30c3\u30af", + "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f", + "button_triple_press": "\"{subtype}\" 3\u56de\u30af\u30ea\u30c3\u30af" } }, "options": { diff --git a/homeassistant/components/nanoleaf/translations/ca.json b/homeassistant/components/nanoleaf/translations/ca.json index 6c966627f94..d040dac3e6b 100644 --- a/homeassistant/components/nanoleaf/translations/ca.json +++ b/homeassistant/components/nanoleaf/translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_token": "Token d'acc\u00e9s no v\u00e0lid", + "invalid_token": "Token d'acc\u00e9s inv\u00e0lid", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index ce7d4da99e6..888b7ed5b44 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5b0b0e84afc..6376807302b 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", - "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", + "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 90a78abbd63..898c9e9f3f3 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", - "invalid_access_token": "Vigane juurdep\u00e4\u00e4su t\u00f5end", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", "reauth_successful": "Taastuvastamine \u00f5nnestus", diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index c306b78bc51..752e847336f 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", @@ -12,10 +13,13 @@ "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" }, "error": { + "bad_project_id": "\u6709\u52b9\u306aCloud Project ID\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044(\u30af\u30e9\u30a6\u30c9 \u30b3\u30f3\u30bd\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044)", "internal_error": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u4e2d\u306b\u5185\u90e8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", "invalid_pin": "\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9", + "subscriber_error": "\u4e0d\u660e\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d0\u30fc\u30a8\u30e9\u30fc\u3001\u30ed\u30b0\u3092\u53c2\u7167", "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "wrong_project_id": "\u6709\u52b9\u306aCloud Project ID\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044(\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f)" }, "step": { "auth": { diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 7709ab400f2..0e2984972af 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol", @@ -12,10 +13,13 @@ "default": "Succesvol geauthenticeerd" }, "error": { + "bad_project_id": "Voer een geldige Cloud Project ID in (controleer Cloud Console)", "internal_error": "Interne foutvalidatiecode", "invalid_pin": "Ongeldige PIN-code", + "subscriber_error": "Onbekende abonneefout, zie logs", "timeout": "Time-out validatie van code", - "unknown": "Onverwachte fout" + "unknown": "Onverwachte fout", + "wrong_project_id": "Voer een geldig Cloud Project ID in (found Device Acces Project ID)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Kies een authenticatie methode" }, + "pubsub": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "Bezoek de [Cloud Console]({url}) om uw Google Cloud Project ID te vinden.", + "title": "Google Cloud configureren" + }, "reauth_confirm": { "description": "De Nest-integratie moet uw account opnieuw verifi\u00ebren", "title": "Verifieer de integratie opnieuw" diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 39269e3740d..3bd746f9388 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -12,7 +12,7 @@ }, "error": { "follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia", - "no_token": "Token d'acc\u00e9s no v\u00e0lid" + "no_token": "Token d'acc\u00e9s inv\u00e0lid" }, "step": { "auth": { diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 9d3053f92bd..12a97c8508a 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -40,13 +40,13 @@ "btn_down": "{subtype} button down", "btn_up": "{subtype} button up", "double": "{subtype} \u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", - "double_push": "{subtype} double push", + "double_push": "{subtype} \u30c0\u30d6\u30eb\u30d7\u30c3\u30b7\u30e5", "long": "{subtype} \u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af", "long_push": "{subtype} long push", "long_single": "{subtype} \u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089\u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af", "single": "{subtype} \u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af", "single_long": "{subtype} \u30b7\u30f3\u30b0\u30eb\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089\u30ed\u30f3\u30b0\u30af\u30ea\u30c3\u30af", - "single_push": "{subtype} single push", + "single_push": "{subtype} \u30b7\u30f3\u30b0\u30eb\u30d7\u30c3\u30b7\u30e5", "triple": "{subtype} 3\u56de\u30af\u30ea\u30c3\u30af" } } diff --git a/homeassistant/components/tailscale/translations/ca.json b/homeassistant/components/tailscale/translations/ca.json new file mode 100644 index 00000000000..d111da767f2 --- /dev/null +++ b/homeassistant/components/tailscale/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Els tokens API de Tailscale s\u00f3n v\u00e0lids durant 90 dies. Pots crear una nova clau API de Tailscale a https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "Clau API", + "tailnet": "Tailnet" + }, + "description": "Per autenticar-te amb Tailscale, has de crear una clau API a https://login.tailscale.com/admin/settings/authkeys. \n\nLa Tailnet \u00e9s el nom de la teva xarxa Tailscale. La pots trobar a l'extrem superior esquerre del tauler d'administraci\u00f3 de Tailscale (al costat del logotip de Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/de.json b/homeassistant/components/tailscale/translations/de.json new file mode 100644 index 00000000000..9fbcceaa674 --- /dev/null +++ b/homeassistant/components/tailscale/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Tailscale-API-Token sind 90 Tage lang g\u00fcltig. Du kannst unter https://login.tailscale.com/admin/settings/authkeys einen neuen Tailscale-API-Schl\u00fcssel erstellen." + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "tailnet": "Tailnet" + }, + "description": "Um sich bei Tailscale zu authentifizieren, musst du einen API-Schl\u00fcssel unter https://login.tailscale.com/admin/settings/authkeys erstellen.\n\nEin Tailnet ist der Name Ihres Tailscale-Netzwerks. Sie finden ihn in der linken oberen Ecke des Tailscale Admin Panels (neben dem Tailscale Logo)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/hu.json b/homeassistant/components/tailscale/translations/hu.json new file mode 100644 index 00000000000..ec727cbc00f --- /dev/null +++ b/homeassistant/components/tailscale/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "A Tailscale API kulcsok 90 napig \u00e9rv\u00e9nyesek. \u00daj Tailscale API kulcsot a https://login.tailscale.com/admin/settings/authkeys oldalon hozhat l\u00e9tre." + }, + "user": { + "data": { + "api_key": "API kulcs", + "tailnet": "Tailnet" + }, + "description": "A Tailscale-rel val\u00f3 hiteles\u00edt\u00e9shez l\u00e9tre kell hoznia egy API-kulcsot a https://login.tailscale.com/admin/settings/authkeys oldalon.\n\nTailnet az \u00f6n tailscale h\u00e1l\u00f3zat\u00e1nak neve. Megtal\u00e1lhat\u00f3 a bal fels\u0151 sarokban a Tailscale Admin panelen (a Tailscale log\u00f3 mellett)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/nl.json b/homeassistant/components/tailscale/translations/nl.json new file mode 100644 index 00000000000..5e46f4f0511 --- /dev/null +++ b/homeassistant/components/tailscale/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Tailscale API tokens zijn 90 dagen geldig. U kunt een nieuwe Tailscale API sleutel aanmaken op https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "API-sleutel", + "tailnet": "Tailnet" + }, + "description": "Om te authenticeren met Tailscale moet je een API-sleutel maken op https://login.tailscale.com/admin/settings/authkeys. \n\n Een Tailnet is de naam van uw Tailscale-netwerk. Je vindt het in de linkerbovenhoek in het Tailscale Admin Panel (naast het Tailscale-logo)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/ru.json b/homeassistant/components/tailscale/translations/ru.json new file mode 100644 index 00000000000..1b97b0998e7 --- /dev/null +++ b/homeassistant/components/tailscale/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0422\u043e\u043a\u0435\u043d\u044b API Tailscale \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 90 \u0434\u043d\u0435\u0439. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API Tailscale \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "tailnet": "Tailnet" + }, + "description": "\u0414\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet \u2014 \u044d\u0442\u043e \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438 Tailscale. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0435\u0433\u043e \u0432 \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u043b\u0435\u0432\u043e\u043c \u0443\u0433\u043b\u0443 \u043f\u0430\u043d\u0435\u043b\u0438 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 Tailscale (\u0440\u044f\u0434\u043e\u043c \u0441 \u043b\u043e\u0433\u043e\u0442\u0438\u043f\u043e\u043c Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ja.json b/homeassistant/components/toon/translations/ja.json index 9d956d5808c..26889353f92 100644 --- a/homeassistant/components/toon/translations/ja.json +++ b/homeassistant/components/toon/translations/ja.json @@ -8,6 +8,13 @@ "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "step": { + "agreement": { + "data": { + "agreement": "\u5408\u610f(Agreement)" + }, + "description": "\u8ffd\u52a0\u3057\u305f\u3044\u5951\u7d04(Agreement)\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "title": "\u5951\u7d04(Agreement)\u306e\u9078\u629e" + }, "pick_implementation": { "title": "\u8a8d\u8a3c\u3059\u308b\u30c6\u30ca\u30f3\u30c8\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/tractive/translations/sensor.en.json b/homeassistant/components/tractive/translations/sensor.en.json new file mode 100644 index 00000000000..13232951cd9 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.en.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Not reporting", + "operational": "Operational", + "system_shutdown_user": "System shutdown user", + "system_startup": "System startup" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/de.json b/homeassistant/components/trafikverket_weatherstation/translations/de.json new file mode 100644 index 00000000000..51f66aed22e --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "conditions": "\u00dcberwachte Bedingungen", + "name": "Benutzername", + "station": "Station" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/en.json b/homeassistant/components/trafikverket_weatherstation/translations/en.json index 73f02145899..0c0c15f5bb9 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/en.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/en.json @@ -4,15 +4,17 @@ "already_configured": "Account is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "invalid_station": "Could not find a weather station with the specified name", - "more_stations": "Found multiple weather stations with the specified name" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_station": "Could not find a weather station with the specified name", + "more_stations": "Found multiple weather stations with the specified name" }, "step": { "user": { "data": { "api_key": "API Key", + "conditions": "Monitored conditions", + "name": "Username", "station": "Station" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/et.json b/homeassistant/components/trafikverket_weatherstation/translations/et.json new file mode 100644 index 00000000000..c5c5f76ae9d --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "conditions": "J\u00e4lgitavad elemendid", + "name": "Kasutajanimi", + "station": "Seirejaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/hu.json b/homeassistant/components/trafikverket_weatherstation/translations/hu.json new file mode 100644 index 00000000000..27c8d290af4 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "conditions": "Megfigyelt k\u00f6r\u00fclm\u00e9nyek", + "name": "Felhaszn\u00e1l\u00f3n\u00e9v", + "station": "\u00c1llom\u00e1s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/nl.json b/homeassistant/components/trafikverket_weatherstation/translations/nl.json new file mode 100644 index 00000000000..39970fa00b6 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "conditions": "Gemonitorde condities", + "name": "Gebruikersnaam", + "station": "Station" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/no.json b/homeassistant/components/trafikverket_weatherstation/translations/no.json new file mode 100644 index 00000000000..7c3b2c3a183 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "conditions": "Overv\u00e5kede forhold", + "name": "Brukernavn", + "station": "Stasjon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ru.json b/homeassistant/components/trafikverket_weatherstation/translations/ru.json new file mode 100644 index 00000000000..948b43fb80e --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "conditions": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0443\u0441\u043b\u043e\u0432\u0438\u044f", + "name": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "station": "\u0421\u0442\u0430\u043d\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json new file mode 100644 index 00000000000..ecf0be95000 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "conditions": "\u5df2\u76e3\u63a7\u72c0\u614b", + "name": "\u4f7f\u7528\u8005\u540d\u7a31", + "station": "\u76e3\u63a7\u7ad9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index 169d81d0be4..97a971043d9 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -14,6 +14,9 @@ "0": "Lage gevoeligheid", "1": "Hoge gevoeligheid" }, + "tuya__fingerbot_mode": { + "switch": "Schakelaar" + }, "tuya__ipc_work_mode": { "0": "Energiezuinige modus", "1": "Continue werkmodus:" diff --git a/homeassistant/components/yale_smart_alarm/translations/ca.json b/homeassistant/components/yale_smart_alarm/translations/ca.json index 04e894afe1b..6e14f2d6e20 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ca.json +++ b/homeassistant/components/yale_smart_alarm/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" diff --git a/homeassistant/components/yale_smart_alarm/translations/de.json b/homeassistant/components/yale_smart_alarm/translations/de.json index b3434a70b7e..6050bafa645 100644 --- a/homeassistant/components/yale_smart_alarm/translations/de.json +++ b/homeassistant/components/yale_smart_alarm/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" diff --git a/homeassistant/components/yale_smart_alarm/translations/et.json b/homeassistant/components/yale_smart_alarm/translations/et.json index e773e628d1e..dd55b1ebd7d 100644 --- a/homeassistant/components/yale_smart_alarm/translations/et.json +++ b/homeassistant/components/yale_smart_alarm/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamine nurjus" diff --git a/homeassistant/components/yale_smart_alarm/translations/hu.json b/homeassistant/components/yale_smart_alarm/translations/hu.json index 8c60574227d..6845e245f2d 100644 --- a/homeassistant/components/yale_smart_alarm/translations/hu.json +++ b/homeassistant/components/yale_smart_alarm/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/yale_smart_alarm/translations/ja.json b/homeassistant/components/yale_smart_alarm/translations/ja.json index 7c85312543f..53d868fe351 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ja.json +++ b/homeassistant/components/yale_smart_alarm/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json index 53c1b8fb086..bf2f3409e42 100644 --- a/homeassistant/components/yale_smart_alarm/translations/nl.json +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/yale_smart_alarm/translations/no.json b/homeassistant/components/yale_smart_alarm/translations/no.json index eba8861fa46..4d306dc3cad 100644 --- a/homeassistant/components/yale_smart_alarm/translations/no.json +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/yale_smart_alarm/translations/ru.json b/homeassistant/components/yale_smart_alarm/translations/ru.json index 1f2410be1dc..4a9132c7546 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ru.json +++ b/homeassistant/components/yale_smart_alarm/translations/ru.json @@ -1,7 +1,8 @@ { "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." + "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_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." diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json index e02b74f27a1..5d7c14b07b2 100644 --- a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 7974d0722da..35e8933220a 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -63,7 +63,7 @@ "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3", - "close": "\u9589\u3058\u308b", + "close": "\u30af\u30ed\u30fc\u30ba", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "left": "\u5de6", @@ -80,10 +80,18 @@ "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", "device_slid": "\u30c7\u30d0\u30a4\u30b9 \u30b9\u30e9\u30a4\u30c9 \"{subtype}\"", "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b", + "remote_button_alt_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_short_press": "\"{subtype}\" \u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", - "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002" + "remote_button_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af", + "remote_button_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af", + "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af" } } } \ No newline at end of file From 7dc2a11ea5d1c892b8b5ad1c5335bb3b5c317440 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 2 Dec 2021 03:10:54 +0100 Subject: [PATCH 1183/1452] Use state class enums in AsusWrt (#60808) --- homeassistant/components/asuswrt/sensor.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 8beb5d3d9ee..3475473b21b 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -5,10 +5,9 @@ from dataclasses import dataclass from numbers import Real from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -49,14 +48,14 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_CONNECTED_DEVICE[0], name="Devices Connected", icon="mdi:router-network", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UNIT_DEVICES, ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[0], name="Download Speed", icon="mdi:download-network", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, @@ -65,7 +64,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_RATES[1], name="Upload Speed", icon="mdi:upload-network", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, @@ -74,7 +73,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[0], name="Download", icon="mdi:download", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, @@ -83,7 +82,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[1], name="Upload", icon="mdi:upload", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, @@ -92,7 +91,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[0], name="Load Avg (1m)", icon="mdi:cpu-32-bit", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, @@ -102,7 +101,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[1], name="Load Avg (5m)", icon="mdi:cpu-32-bit", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, @@ -112,7 +111,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[2], name="Load Avg (15m)", icon="mdi:cpu-32-bit", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, From 43d8c8fc2d7ccdbd8ed65cd53be70e43090f3f8c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 2 Dec 2021 03:11:05 +0100 Subject: [PATCH 1184/1452] Use entity category enum in Rituals (#60809) --- .../components/rituals_perfume_genie/binary_sensor.py | 4 ++-- homeassistant/components/rituals_perfume_genie/select.py | 5 +++-- homeassistant/components/rituals_perfume_genie/sensor.py | 7 ++++--- .../components/rituals_perfume_genie/test_binary_sensor.py | 5 +++-- tests/components/rituals_perfume_genie/test_select.py | 4 ++-- tests/components/rituals_perfume_genie/test_sensor.py | 6 +++--- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index 4c51f6c16bd..8592c1fcfb8 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -8,8 +8,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator @@ -39,7 +39,7 @@ class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py index 9907e1d227f..265ed292c2a 100644 --- a/homeassistant/components/rituals_perfume_genie/select.py +++ b/homeassistant/components/rituals_perfume_genie/select.py @@ -5,8 +5,9 @@ from pyrituals import Diffuser from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import AREA_SQUARE_METERS, ENTITY_CATEGORY_CONFIG +from homeassistant.const import AREA_SQUARE_METERS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator @@ -36,7 +37,7 @@ class DiffuserRoomSize(DiffuserEntity, SelectEntity): _attr_icon = "mdi:ruler-square" _attr_unit_of_measurement = AREA_SQUARE_METERS _attr_options = ["15", "30", "60", "100"] - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index f5cd2f144e5..0172df4b1c1 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -5,8 +5,9 @@ from pyrituals import Diffuser from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator @@ -88,7 +89,7 @@ class DiffuserBatterySensor(DiffuserEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -107,7 +108,7 @@ class DiffuserWifiSensor(DiffuserEntity, SensorEntity): _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH _attr_native_unit_of_measurement = PERCENTAGE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator diff --git a/tests/components/rituals_perfume_genie/test_binary_sensor.py b/tests/components/rituals_perfume_genie/test_binary_sensor.py index cef9a8e85c4..c0ba64f281c 100644 --- a/tests/components/rituals_perfume_genie/test_binary_sensor.py +++ b/tests/components/rituals_perfume_genie/test_binary_sensor.py @@ -1,9 +1,10 @@ """Tests for the Rituals Perfume Genie binary sensor platform.""" from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.rituals_perfume_genie.binary_sensor import CHARGING_SUFFIX -from homeassistant.const import ATTR_DEVICE_CLASS, ENTITY_CATEGORY_DIAGNOSTIC, STATE_ON +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from .common import ( init_integration, @@ -30,4 +31,4 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: entry = registry.async_get("binary_sensor.genie_battery_charging") assert entry assert entry.unique_id == f"{hublot}{CHARGING_SUFFIX}" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category == EntityCategory.DIAGNOSTIC diff --git a/tests/components/rituals_perfume_genie/test_select.py b/tests/components/rituals_perfume_genie/test_select.py index 5c6d344adca..883e00b8a59 100644 --- a/tests/components/rituals_perfume_genie/test_select.py +++ b/tests/components/rituals_perfume_genie/test_select.py @@ -9,11 +9,11 @@ from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ENTITY_ID, ATTR_ICON, - ENTITY_CATEGORY_CONFIG, SERVICE_SELECT_OPTION, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component from .common import init_integration, mock_config_entry, mock_diffuser @@ -37,7 +37,7 @@ async def test_select_entity(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == f"{diffuser.hublot}{ROOM_SIZE_SUFFIX}" assert entry.unit_of_measurement == AREA_SQUARE_METERS - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category == EntityCategory.CONFIG async def test_select_option(hass: HomeAssistant) -> None: diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py index a6c89bd6620..e7b8daec27f 100644 --- a/tests/components/rituals_perfume_genie/test_sensor.py +++ b/tests/components/rituals_perfume_genie/test_sensor.py @@ -10,11 +10,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from .common import ( init_integration, @@ -59,7 +59,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non entry = registry.async_get("sensor.genie_battery") assert entry assert entry.unique_id == f"{hublot}{BATTERY_SUFFIX}" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category == EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.genie_wifi") assert state @@ -70,7 +70,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non entry = registry.async_get("sensor.genie_wifi") assert entry assert entry.unique_id == f"{hublot}{WIFI_SUFFIX}" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category == EntityCategory.DIAGNOSTIC async def test_sensors_diffuser_v2_no_battery_no_cartridge(hass: HomeAssistant) -> None: From c875d726b1b9202d7d0b37abd595decc08ddeaf8 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 2 Dec 2021 03:11:19 +0100 Subject: [PATCH 1185/1452] Use state and device class enums in Nut (#60810) --- homeassistant/components/nut/const.py | 78 +++++++++++++-------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 12306b63dc1..11b07f04b78 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -5,13 +5,9 @@ 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, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -64,8 +60,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.temperature", name="UPS Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -74,7 +70,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "ups.load.high": SensorEntityDescription( key="ups.load.high", @@ -180,7 +176,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Efficiency", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -189,7 +185,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Current Apparent Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -205,8 +201,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.realpower", name="Current Real Power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -214,7 +210,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.realpower.nominal", name="Nominal Real Power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -271,8 +267,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.charge", name="Battery Charge", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), "battery.charge.low": SensorEntityDescription( key="battery.charge.low", @@ -307,8 +303,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.voltage", name="Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -316,7 +312,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.voltage.nominal", name="Nominal Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -324,7 +320,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.voltage.low", name="Low Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -332,7 +328,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.voltage.high", name="High Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -349,7 +345,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Battery Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -365,8 +361,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.temperature", name="Battery Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -447,7 +443,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="input.transfer.low", name="Low Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -455,7 +451,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="input.transfer.high", name="High Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -470,14 +466,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="input.voltage", name="Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.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, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -486,7 +482,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Input Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -510,7 +506,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -526,14 +522,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="output.voltage", name="Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.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, + device_class=SensorDeviceClass.VOLTAGE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -542,7 +538,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Output Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -558,8 +554,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ambient.humidity", name="Ambient Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -567,8 +563,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ambient.temperature", name="Ambient Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -576,8 +572,8 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="watts", name="Watts", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), } From 563b48873974024e824c548e5de50a60152bf185 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 03:19:24 +0100 Subject: [PATCH 1186/1452] Use state/device/entity category enums in Tuya (#60788) --- .../components/tuya/binary_sensor.py | 53 ++-- homeassistant/components/tuya/button.py | 12 +- homeassistant/components/tuya/const.py | 130 ++++---- homeassistant/components/tuya/cover.py | 24 +- homeassistant/components/tuya/humidifier.py | 7 +- homeassistant/components/tuya/number.py | 60 ++-- homeassistant/components/tuya/select.py | 46 +-- homeassistant/components/tuya/sensor.py | 279 +++++++++--------- homeassistant/components/tuya/switch.py | 104 +++---- 9 files changed, 332 insertions(+), 383 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 12fbc963942..c9a1ab598c3 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -6,21 +6,14 @@ from dataclasses import dataclass from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_TAMPER, - DEVICE_CLASS_VIBRATION, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData @@ -43,8 +36,8 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, ) @@ -58,7 +51,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "co2bj": ( TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -68,12 +61,12 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "cobj": ( TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="1", ), TuyaBinarySensorEntityDescription( key=DPCode.CO_STATUS, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -83,7 +76,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "hps": ( TuyaBinarySensorEntityDescription( key=DPCode.PRESENCE_STATE, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, on_value="presence", ), ), @@ -92,7 +85,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "jqbj": ( TuyaBinarySensorEntityDescription( key=DPCode.CH2O_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -102,7 +95,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "jwbj": ( TuyaBinarySensorEntityDescription( key=DPCode.CH4_SENSOR_STATE, - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -112,7 +105,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "mcs": ( TuyaBinarySensorEntityDescription( key=DPCode.DOORCONTACT_STATE, - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), TAMPER_BINARY_SENSOR, ), @@ -122,8 +115,8 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, ), TAMPER_BINARY_SENSOR, ), @@ -132,7 +125,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "pir": ( TuyaBinarySensorEntityDescription( key=DPCode.PIR, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, on_value="pir", ), TAMPER_BINARY_SENSOR, @@ -142,7 +135,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "pm2.5": ( TuyaBinarySensorEntityDescription( key=DPCode.PM25_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -152,12 +145,12 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "rqbj": ( TuyaBinarySensorEntityDescription( key=DPCode.GAS_SENSOR_STATUS, - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.GAS_SENSOR_STATE, - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, on_value="1", ), TAMPER_BINARY_SENSOR, @@ -167,7 +160,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "sj": ( TuyaBinarySensorEntityDescription( key=DPCode.WATERSENSOR_STATE, - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -177,7 +170,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "sos": ( TuyaBinarySensorEntityDescription( key=DPCode.SOS_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), TAMPER_BINARY_SENSOR, ), @@ -186,7 +179,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "voc": ( TuyaBinarySensorEntityDescription( key=DPCode.VOC_STATE, - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TAMPER_BINARY_SENSOR, @@ -208,12 +201,12 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "ywbj": ( TuyaBinarySensorEntityDescription( key=DPCode.SMOKE_SENSOR_STATUS, - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.SMOKE_SENSOR_STATE, - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, on_value="1", ), TAMPER_BINARY_SENSOR, @@ -225,7 +218,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { key=f"{DPCode.SHOCK_STATE}_vibration", dpcode=DPCode.SHOCK_STATE, name="Vibration", - device_class=DEVICE_CLASS_VIBRATION, + device_class=BinarySensorDeviceClass.VIBRATION, on_value="vibration", ), TuyaBinarySensorEntityDescription( diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index d07d6947272..cd410db24cc 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -7,9 +7,9 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData @@ -26,31 +26,31 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { key=DPCode.RESET_DUSTER_CLOTH, name="Reset Duster Cloth", icon="mdi:restart", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, name="Reset Edge Brush", icon="mdi:restart", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, name="Reset Filter", icon="mdi:air-filter", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, name="Reset Map", icon="mdi:map-marker-remove", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, name="Reset Roll Brush", icon="mdi:restart", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), } diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index dd229011c2a..66142998691 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -7,38 +7,12 @@ from enum import Enum from tuya_iot import TuyaCloudOpenAPIEndpoint +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_AQI, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_DATE, - 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_VOLATILE_ORGANIC_COMPOUNDS, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, @@ -355,33 +329,33 @@ UNITS = ( unit="", aliases={" "}, device_classes={ - DEVICE_CLASS_AQI, - DEVICE_CLASS_DATE, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.AQI, + SensorDeviceClass.DATE, + SensorDeviceClass.MONETARY, + SensorDeviceClass.TIMESTAMP, }, ), UnitOfMeasurement( unit=PERCENTAGE, aliases={"pct", "percent"}, device_classes={ - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER_FACTOR, + SensorDeviceClass.BATTERY, + SensorDeviceClass.HUMIDITY, + SensorDeviceClass.POWER_FACTOR, }, ), UnitOfMeasurement( unit=CONCENTRATION_PARTS_PER_MILLION, device_classes={ - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, + SensorDeviceClass.CO, + SensorDeviceClass.CO2, }, ), UnitOfMeasurement( unit=CONCENTRATION_PARTS_PER_BILLION, device_classes={ - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, + SensorDeviceClass.CO, + SensorDeviceClass.CO2, }, conversion_unit=CONCENTRATION_PARTS_PER_MILLION, conversion_fn=lambda x: x / 1000, @@ -389,73 +363,73 @@ UNITS = ( UnitOfMeasurement( unit=ELECTRIC_CURRENT_AMPERE, aliases={"a", "ampere"}, - device_classes={DEVICE_CLASS_CURRENT}, + device_classes={SensorDeviceClass.CURRENT}, ), UnitOfMeasurement( unit=ELECTRIC_CURRENT_MILLIAMPERE, aliases={"ma", "milliampere"}, - device_classes={DEVICE_CLASS_CURRENT}, + device_classes={SensorDeviceClass.CURRENT}, conversion_unit=ELECTRIC_CURRENT_AMPERE, conversion_fn=lambda x: x / 1000, ), UnitOfMeasurement( unit=ENERGY_WATT_HOUR, aliases={"wh", "watthour"}, - device_classes={DEVICE_CLASS_ENERGY}, + device_classes={SensorDeviceClass.ENERGY}, ), UnitOfMeasurement( unit=ENERGY_KILO_WATT_HOUR, aliases={"kwh", "kilowatt-hour", "kW·h"}, - device_classes={DEVICE_CLASS_ENERGY}, + device_classes={SensorDeviceClass.ENERGY}, ), UnitOfMeasurement( unit=VOLUME_CUBIC_FEET, aliases={"ft3"}, - device_classes={DEVICE_CLASS_GAS}, + device_classes={SensorDeviceClass.GAS}, ), UnitOfMeasurement( unit=VOLUME_CUBIC_METERS, aliases={"m3"}, - device_classes={DEVICE_CLASS_GAS}, + device_classes={SensorDeviceClass.GAS}, ), UnitOfMeasurement( unit=LIGHT_LUX, aliases={"lux"}, - device_classes={DEVICE_CLASS_ILLUMINANCE}, + device_classes={SensorDeviceClass.ILLUMINANCE}, ), UnitOfMeasurement( unit="lm", aliases={"lum", "lumen"}, - device_classes={DEVICE_CLASS_ILLUMINANCE}, + device_classes={SensorDeviceClass.ILLUMINANCE}, ), UnitOfMeasurement( unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, aliases={"ug/m3", "µg/m3", "ug/m³"}, device_classes={ - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_NITROGEN_MONOXIDE, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PM10, - DEVICE_CLASS_SULPHUR_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + SensorDeviceClass.NITROGEN_DIOXIDE, + SensorDeviceClass.NITROGEN_MONOXIDE, + SensorDeviceClass.NITROUS_OXIDE, + SensorDeviceClass.OZONE, + SensorDeviceClass.PM1, + SensorDeviceClass.PM25, + SensorDeviceClass.PM10, + SensorDeviceClass.SULPHUR_DIOXIDE, + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, }, ), UnitOfMeasurement( unit=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, aliases={"mg/m3"}, device_classes={ - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_NITROGEN_MONOXIDE, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PM10, - DEVICE_CLASS_SULPHUR_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + SensorDeviceClass.NITROGEN_DIOXIDE, + SensorDeviceClass.NITROGEN_MONOXIDE, + SensorDeviceClass.NITROUS_OXIDE, + SensorDeviceClass.OZONE, + SensorDeviceClass.PM1, + SensorDeviceClass.PM25, + SensorDeviceClass.PM10, + SensorDeviceClass.SULPHUR_DIOXIDE, + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, }, conversion_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, conversion_fn=lambda x: x * 1000, @@ -463,69 +437,69 @@ UNITS = ( UnitOfMeasurement( unit=POWER_WATT, aliases={"watt"}, - device_classes={DEVICE_CLASS_POWER}, + device_classes={SensorDeviceClass.POWER}, ), UnitOfMeasurement( unit=POWER_KILO_WATT, aliases={"kilowatt"}, - device_classes={DEVICE_CLASS_POWER}, + device_classes={SensorDeviceClass.POWER}, ), UnitOfMeasurement( unit=PRESSURE_BAR, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=PRESSURE_MBAR, aliases={"millibar"}, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=PRESSURE_HPA, aliases={"hpa", "hectopascal"}, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=PRESSURE_INHG, aliases={"inhg"}, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=PRESSURE_PSI, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=PRESSURE_PA, - device_classes={DEVICE_CLASS_PRESSURE}, + device_classes={SensorDeviceClass.PRESSURE}, ), UnitOfMeasurement( unit=SIGNAL_STRENGTH_DECIBELS, aliases={"db"}, - device_classes={DEVICE_CLASS_SIGNAL_STRENGTH}, + device_classes={SensorDeviceClass.SIGNAL_STRENGTH}, ), UnitOfMeasurement( unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, aliases={"dbm"}, - device_classes={DEVICE_CLASS_SIGNAL_STRENGTH}, + device_classes={SensorDeviceClass.SIGNAL_STRENGTH}, ), UnitOfMeasurement( unit=TEMP_CELSIUS, aliases={"°c", "c", "celsius"}, - device_classes={DEVICE_CLASS_TEMPERATURE}, + device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( unit=TEMP_FAHRENHEIT, aliases={"°f", "f", "fahrenheit"}, - device_classes={DEVICE_CLASS_TEMPERATURE}, + device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( unit=ELECTRIC_POTENTIAL_VOLT, aliases={"volt"}, - device_classes={DEVICE_CLASS_VOLTAGE}, + device_classes={SensorDeviceClass.VOLTAGE}, ), UnitOfMeasurement( unit=ELECTRIC_POTENTIAL_MILLIVOLT, aliases={"mv", "millivolt"}, - device_classes={DEVICE_CLASS_VOLTAGE}, + device_classes={SensorDeviceClass.VOLTAGE}, conversion_unit=ELECTRIC_POTENTIAL_VOLT, conversion_fn=lambda x: x / 1000, ), diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 2598429dda3..09ae829f382 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -10,14 +10,12 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_GARAGE, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, CoverEntityDescription, ) @@ -53,21 +51,21 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { current_state=DPCode.SITUATION_SET, current_position=DPCode.PERCENT_STATE, set_position=DPCode.PERCENT_CONTROL, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.CONTROL_2, name="Curtain 2", current_position=DPCode.PERCENT_STATE_2, set_position=DPCode.PERCENT_CONTROL_2, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.CONTROL_3, name="Curtain 3", current_position=DPCode.PERCENT_STATE_3, set_position=DPCode.PERCENT_CONTROL_3, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), # switch_1 is an undocumented code that behaves identically to control # It is used by the Kogan Smart Blinds Driver @@ -76,7 +74,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { name="Blind", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, - device_class=DEVICE_CLASS_BLIND, + device_class=CoverDeviceClass.BLIND, ), ), # Garage Door Opener @@ -87,21 +85,21 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { name="Door", current_state=DPCode.DOORCONTACT_STATE, current_state_inverse=True, - device_class=DEVICE_CLASS_GARAGE, + device_class=CoverDeviceClass.GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_2, name="Door 2", current_state=DPCode.DOORCONTACT_STATE_2, current_state_inverse=True, - device_class=DEVICE_CLASS_GARAGE, + device_class=CoverDeviceClass.GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_3, name="Door 3", current_state=DPCode.DOORCONTACT_STATE_3, current_state_inverse=True, - device_class=DEVICE_CLASS_GARAGE, + device_class=CoverDeviceClass.GARAGE, ), ), # Curtain Switch @@ -112,14 +110,14 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { name="Curtain", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.CONTROL_2, name="Curtain 2", current_position=DPCode.PERCENT_CONTROL_2, set_position=DPCode.PERCENT_CONTROL_2, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), ), # Curtain Robot @@ -129,7 +127,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { key=DPCode.CONTROL, current_position=DPCode.PERCENT_STATE, set_position=DPCode.PERCENT_CONTROL, - device_class=DEVICE_CLASS_CURTAIN, + device_class=CoverDeviceClass.CURTAIN, ), ), } diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 3169000fcba..c6cddad2759 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -6,9 +6,8 @@ from dataclasses import dataclass from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.humidifier import ( - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, SUPPORT_MODES, + HumidifierDeviceClass, HumidifierEntity, HumidifierEntityDescription, ) @@ -39,7 +38,7 @@ HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = { key=DPCode.SWITCH, dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY), humidity=DPCode.DEHUMIDITY_SET_VALUE, - device_class=DEVICE_CLASS_DEHUMIDIFIER, + device_class=HumidifierDeviceClass.DEHUMIDIFIER, ), # Humidifier # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b @@ -47,7 +46,7 @@ HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = { key=DPCode.SWITCH, dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY), humidity=DPCode.HUMIDITY_SET, - device_class=DEVICE_CLASS_HUMIDIFIER, + device_class=HumidifierDeviceClass.HUMIDIFIER, ), } diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index a3ef644fa16..f75f21dc57f 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -8,9 +8,9 @@ from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData @@ -28,31 +28,31 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.TEMP_SET, name="Temperature", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET_F, name="Temperature", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, name="Temperature After Boiling", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, name="Temperature After Boiling", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, name="Heat Preservation Time", icon="mdi:timer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Human Presence Sensor @@ -61,19 +61,19 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.SENSITIVITY, name="Sensitivity", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, name="Near Detection", icon="mdi:signal-distance-variant", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, name="Far Detection", icon="mdi:signal-distance-variant", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Coffee maker @@ -83,24 +83,24 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.WATER_SET, name="Water Level", icon="mdi:cup-water", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, name="Heat Preservation Time", icon="mdi:timer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.POWDER_SET, name="Powder", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Robot Vacuum @@ -110,7 +110,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.VOLUME_SET, name="Volume", icon="mdi:volume-high", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Siren Alarm @@ -119,7 +119,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.ALARM_TIME, name="Time", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Smart Camera @@ -129,7 +129,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.BASIC_DEVICE_VOLUME, name="Volume", icon="mdi:volume-high", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch @@ -139,37 +139,37 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.BRIGHTNESS_MIN_1, name="Minimum Brightness", icon="mdi:lightbulb-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, name="Maximum Brightness", icon="mdi:lightbulb-on-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, name="Minimum Brightness 2", icon="mdi:lightbulb-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, name="Maximum Brightness 2", icon="mdi:lightbulb-on-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, name="Minimum Brightness 3", icon="mdi:lightbulb-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, name="Maximum Brightness 3", icon="mdi:lightbulb-on-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch @@ -179,25 +179,25 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.BRIGHTNESS_MIN_1, name="Minimum Brightness", icon="mdi:lightbulb-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, name="Maximum Brightness", icon="mdi:lightbulb-on-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, name="Minimum Brightness 2", icon="mdi:lightbulb-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, name="Maximum Brightness 2", icon="mdi:lightbulb-on-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Vibration Sensor @@ -206,7 +206,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.SENSITIVITY, name="Sensitivity", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Fingerbot @@ -215,19 +215,19 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.ARM_DOWN_PERCENT, name="Move Down %", icon="mdi:arrow-down-bold", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, name="Move Up %", icon="mdi:arrow-up-bold", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, name="Down Delay", icon="mdi:timer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), } diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 4eee162f814..9d5012c6578 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -8,9 +8,9 @@ from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData @@ -47,12 +47,12 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.CONCENTRATION_SET, name="Concentration", icon="mdi:altimeter", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.MATERIAL, name="Material", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.MODE, @@ -67,13 +67,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.RELAY_STATUS, name="Power on Behavior", device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, name="Indicator Light Mode", device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Heater @@ -91,12 +91,12 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.ALARM_VOLUME, name="Volume", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BRIGHT_STATE, name="Brightness", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Smart Camera @@ -106,42 +106,42 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.IPC_WORK_MODE, name="IPC Mode", device_class=DEVICE_CLASS_TUYA_IPC_WORK_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, name="Sound Detection Sensitivity", icon="mdi:volume-vibrate", device_class=DEVICE_CLASS_TUYA_DECIBEL_SENSITIVITY, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.RECORD_MODE, name="Record Mode", icon="mdi:record-rec", device_class=DEVICE_CLASS_TUYA_RECORD_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, name="Night Vision", icon="mdi:theme-light-dark", device_class=DEVICE_CLASS_TUYA_BASIC_NIGHTVISION, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BASIC_ANTI_FLICKER, name="Anti-flicker", icon="mdi:image-outline", device_class=DEVICE_CLASS_TUYA_BASIC_ANTI_FLICKR, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, name="Motion Detection Sensitivity", icon="mdi:motion-sensor", device_class=DEVICE_CLASS_TUYA_MOTION_SENSITIVITY, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # IoT Switch? @@ -151,13 +151,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.RELAY_STATUS, name="Power on Behavior", device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, name="Indicator Light Mode", device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch @@ -167,31 +167,31 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.RELAY_STATUS, name="Power on Behavior", device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, name="Indicator Light Mode", device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_1, name="Light Source Type", device_class=DEVICE_CLASS_TUYA_LED_TYPE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, name="Light 2 Source Type", device_class=DEVICE_CLASS_TUYA_LED_TYPE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_3, name="Light 3 Source Type", device_class=DEVICE_CLASS_TUYA_LED_TYPE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Dimmer @@ -201,13 +201,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.LED_TYPE_1, name="Light Source Type", device_class=DEVICE_CLASS_TUYA_LED_TYPE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, name="Light 2 Source Type", device_class=DEVICE_CLASS_TUYA_LED_TYPE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Fingerbot @@ -216,7 +216,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.MODE, name="Mode", device_class=DEVICE_CLASS_TUYA_FINGERBOT_MODE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), } diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 4ba7fb461c3..62a330cbefb 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -8,36 +8,21 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_KILO_WATT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -66,29 +51,29 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( key=DPCode.BATTERY_PERCENTAGE, name="Battery", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, name="Battery State", icon="mdi:battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.BATTERY_VALUE, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_BATTERY, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), ) @@ -103,14 +88,14 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Current Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, name="Current Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.STATUS, @@ -124,20 +109,20 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -147,8 +132,8 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CO_VALUE, name="Carbon Monoxide", - device_class=DEVICE_CLASS_CO, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -158,37 +143,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), ), # Formaldehyde Detector @@ -197,37 +182,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -237,7 +222,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, name="Methane", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -247,22 +232,22 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CUR_CURRENT, name="Current", - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_POWER, name="Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_VOLTAGE, name="Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), ), @@ -277,26 +262,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, name="Luminosity", - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -312,49 +297,49 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM1, name="Particulate Matter 1.0 µm", - device_class=DEVICE_CLASS_PM1, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM10, name="Particulate Matter 10.0 µm", - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -364,8 +349,8 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.WORK_POWER, name="Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ), # Gas Detector @@ -374,7 +359,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, icon="mdi:gas-cylinder", - device_class=STATE_CLASS_MEASUREMENT, + device_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -390,21 +375,21 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.SENSOR_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.SENSOR_HUMIDITY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.WIRELESS_ELECTRICITY, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), ), # Fingerbot @@ -418,37 +403,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, name="Carbon Dioxide", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, name="Particulate Matter 2.5 µm", - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, name="Formaldehyde", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, name="Volatile Organic Compound", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -458,32 +443,32 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, name="Luminosity", - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -492,8 +477,8 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "ylcg": ( TuyaSensorEntityDescription( key=DPCode.PRESSURE_VALUE, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -504,8 +489,8 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.SMOKE_SENSOR_VALUE, name="Smoke Amount", icon="mdi:smoke-detector", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - device_class=STATE_CLASS_MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -518,78 +503,78 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.FORWARD_ENERGY_TOTAL, name="Total Energy", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, name="Phase A Current", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, subkey="electriccurrent", ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, name="Phase A Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, subkey="power", ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, name="Phase A Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, subkey="voltage", ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, name="Phase B Current", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, subkey="electriccurrent", ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, name="Phase B Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, subkey="power", ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, name="Phase B Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, subkey="voltage", ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, name="Phase C Current", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, subkey="electriccurrent", ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, name="Phase C Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, subkey="power", ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, name="Phase C Voltage", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, subkey="voltage", ), diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index acc6e2e57e0..17b0feb70a7 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -6,14 +6,14 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, + SwitchDeviceClass, SwitchEntity, SwitchEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData @@ -35,7 +35,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.WARM, name="Heat Preservation", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Pet Water Feeder @@ -45,13 +45,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.FILTER_RESET, name="Filter reset", icon="mdi:filter", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.PUMP_RESET, name="Water pump reset", icon="mdi:pump", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -61,7 +61,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.WATER_RESET, name="Reset of water usage days", icon="mdi:water-sync", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Light @@ -81,7 +81,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, @@ -95,37 +95,37 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, name="Switch 1", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, name="Switch 2", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, name="Switch 3", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, name="Switch 4", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, name="Switch 5", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, name="Switch 6", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, @@ -154,7 +154,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, name="Switch", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), ), # Air Purifier @@ -164,19 +164,19 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.ANION, name="Ionizer", icon="mdi:minus-circle-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FILTER_RESET, name="Filter cartridge reset", icon="mdi:filter", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, name="Child lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -186,7 +186,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.WET, name="Humidification", icon="mdi:water-percent", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Air conditioner @@ -196,13 +196,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.ANION, name="Ionizer", icon="mdi:minus-circle-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Power Socket @@ -212,37 +212,37 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, name="Socket 1", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, name="Socket 2", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, name="Socket 3", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, name="Socket 4", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, name="Socket 5", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, name="Socket 6", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, @@ -271,7 +271,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, name="Socket", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), ), # Heater @@ -281,13 +281,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.ANION, name="Ionizer", icon="mdi:minus-circle-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Robot Vacuum @@ -297,13 +297,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.SWITCH_DISTURB, name="Do Not Disturb", icon="mdi:minus-circle", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.VOICE_SWITCH, name="Voice", icon="mdi:account-voice", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Siren Alarm @@ -312,7 +312,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.MUFFLING, name="Mute", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Smart Camera @@ -322,67 +322,67 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.WIRELESS_BATTERYLOCK, name="Battery Lock", icon="mdi:battery-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.CRY_DETECTION_SWITCH, icon="mdi:emoticon-cry", name="Cry Detection", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.DECIBEL_SWITCH, icon="mdi:microphone-outline", name="Sound Detection", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.RECORD_SWITCH, icon="mdi:record-rec", name="Video Recording", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_RECORD, icon="mdi:record-rec", name="Motion Recording", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_PRIVATE, icon="mdi:eye-off", name="Privacy Mode", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_FLIP, icon="mdi:flip-horizontal", name="Flip", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_OSD, icon="mdi:watermark", name="Time Watermark", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_WDR, icon="mdi:watermark", name="Wide Dynamic Range", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_TRACKING, icon="mdi:motion-sensor", name="Motion Tracking", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_SWITCH, icon="mdi:motion-sensor", name="Motion Alarm", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Fingerbot @@ -399,23 +399,23 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_1, name="Switch 1", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, name="Switch 2", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, name="Switch 3", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Solar Light @@ -425,7 +425,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.SWITCH_SAVE_ENERGY, name="Energy Saving", icon="mdi:leaf", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Ceiling Light @@ -435,7 +435,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.DO_NOT_DISTURB, name="Do not disturb", icon="mdi:minus-circle-outline", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Diffuser @@ -454,7 +454,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.SWITCH_VOICE, name="Voice", icon="mdi:account-voice", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Smart Electricity Meter From ed106f203f280ff8eb47e0162ba6c96706ded93f Mon Sep 17 00:00:00 2001 From: einarhauks Date: Thu, 2 Dec 2021 02:29:29 +0000 Subject: [PATCH 1187/1452] Update tesla_wall_connector lib to version 1.0.0 (#60776) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index bf03f51b13f..08d52b3016b 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Wall Connector", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", - "requirements": ["tesla-wall-connector==0.2.0"], + "requirements": ["tesla-wall-connector==1.0.0"], "dhcp": [ { "hostname": "teslawallconnector_*", diff --git a/requirements_all.txt b/requirements_all.txt index 1166abee943..64508edfdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2305,7 +2305,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==0.2.0 +tesla-wall-connector==1.0.0 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 598533be930..73ab9a2a4fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1361,7 +1361,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==0.2.0 +tesla-wall-connector==1.0.0 # homeassistant.components.tolo tololib==0.1.0b3 From d066864158e7b406f539b9cc5de8baedc7bea38f Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Thu, 2 Dec 2021 02:41:28 +0000 Subject: [PATCH 1188/1452] Use precipitation probability in MetOffice forecasts (#58677) --- homeassistant/components/metoffice/weather.py | 4 ++-- tests/components/metoffice/test_weather.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 6ce1a49e6f1..79363db3667 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,7 +1,7 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, @@ -52,7 +52,7 @@ def _build_forecast_data(timestep): if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: - data[ATTR_FORECAST_PRECIPITATION] = timestep.precipitation.value + data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: data[ATTR_FORECAST_TEMP] = timestep.temperature.value if timestep.wind_direction: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 4fdd80f8277..158e44ca15b 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -154,6 +154,7 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti == "2020-04-28T21:00:00+00:00" ) assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[26]["temperature"] == 10 assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" @@ -176,6 +177,7 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" ) assert weather.attributes.get("forecast")[7]["condition"] == "rainy" + assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" @@ -250,6 +252,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t == "2020-04-27T21:00:00+00:00" ) assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" + assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" @@ -272,6 +275,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" ) assert weather.attributes.get("forecast")[7]["condition"] == "rainy" + assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" @@ -295,6 +299,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t == "2020-04-27T21:00:00+00:00" ) assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[18]["temperature"] == 10 assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" @@ -317,6 +322,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t weather.attributes.get("forecast")[5]["datetime"] == "2020-04-28T12:00:00+00:00" ) assert weather.attributes.get("forecast")[5]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[5]["precipitation_probability"] == 14 assert weather.attributes.get("forecast")[5]["temperature"] == 11 assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" From d0da0eef3608e961babe419c848ba1b488899064 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Thu, 2 Dec 2021 02:42:59 +0000 Subject: [PATCH 1189/1452] Add day/night markers to MetOffice daily forecast (#58679) --- homeassistant/components/metoffice/const.py | 2 ++ homeassistant/components/metoffice/weather.py | 9 +++++++-- tests/components/metoffice/test_weather.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e413b102898..d1f48eb4f2c 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -24,6 +24,8 @@ DOMAIN = "metoffice" DEFAULT_NAME = "Met Office" ATTRIBUTION = "Data provided by the Met Office" +ATTR_FORECAST_DAYTIME = "daytime" + DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) METOFFICE_COORDINATES = "metoffice_coordinates" diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 79363db3667..d25df1d2654 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_device_info from .const import ( + ATTR_FORECAST_DAYTIME, ATTRIBUTION, CONDITION_CLASSES, DEFAULT_NAME, @@ -46,7 +47,7 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep): +def _build_forecast_data(timestep, use_3hourly): data = {} data[ATTR_FORECAST_TIME] = timestep.date.isoformat() if timestep.weather: @@ -59,6 +60,9 @@ def _build_forecast_data(timestep): data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + if not use_3hourly: + # if it's close to noon, mark as Day, otherwise as Night + data[ATTR_FORECAST_DAYTIME] = abs(timestep.date.hour - 12) < 6 return data @@ -82,6 +86,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): ) self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] + self._use_3hourly = use_3hourly if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @@ -155,7 +160,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): if self.coordinator.data.forecast is None: return None return [ - _build_forecast_data(timestep) + _build_forecast_data(timestep, self._use_3hourly) for timestep in self.coordinator.data.forecast ] diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 158e44ca15b..1970217db5b 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -181,6 +181,13 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[7]["daytime"] is True + + # Check that night entry is correctly marked as Night + assert ( + weather.attributes.get("forecast")[6]["datetime"] == "2020-04-29T00:00:00+00:00" + ) + assert weather.attributes.get("forecast")[6]["daytime"] is False @patch( @@ -256,6 +263,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" + assert "daytime" not in weather.attributes.get("forecast")[18] # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") @@ -279,6 +287,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[7]["daytime"] is True # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -303,6 +312,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 10 assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" + assert "daytime" not in weather.attributes.get("forecast")[18] # King's Lynn daily weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_daily") @@ -326,3 +336,4 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[5]["temperature"] == 11 assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" + assert weather.attributes.get("forecast")[5]["daytime"] is True From 82798730185dfef0e5c11b5ba006372872982cc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 03:47:10 +0100 Subject: [PATCH 1190/1452] Extend entities provided by Tailscale (#60785) --- .../components/tailscale/binary_sensor.py | 44 +++++++++++ homeassistant/components/tailscale/sensor.py | 19 ++++- .../tailscale/test_binary_sensor.py | 79 ++++++++++++++++++- tests/components/tailscale/test_sensor.py | 24 ++++++ 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 17a40d04241..fff4cfbf908 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TailscaleEntity @@ -38,8 +39,51 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( key="update_available", name="Client", device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.update_available, ), + TailscaleBinarySensorEntityDescription( + key="client_supports_hair_pinning", + name="Supports Hairpinning", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_ipv6", + name="Supports IPv6", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.ipv6, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_pcp", + name="Supports PCP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.pcp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_pmp", + name="Supports NAT-PMP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.pmp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_udp", + name="Supports UDP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.udp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_upnp", + name="Supports UPnP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.upnp, + ), ) diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index d6ffdd177bf..07f7dbe91cc 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TailscaleEntity @@ -24,7 +25,7 @@ from .const import DOMAIN class TailscaleSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[TailscaleDevice], datetime | None] + value_fn: Callable[[TailscaleDevice], datetime | str | None] @dataclass @@ -39,8 +40,22 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( key="expires", name="Expires", device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.expires, ), + TailscaleSensorEntityDescription( + key="ip", + name="IP Address", + icon="mdi:ip-network", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.addresses[0] if device.addresses else None, + ), + TailscaleSensorEntityDescription( + key="last_seen", + name="Last Seen", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda device: device.last_seen, + ), ) @@ -68,6 +83,6 @@ class TailscaleSensorEntity(TailscaleEntity, SensorEntity): entity_description: TailscaleSensorEntityDescription @property - def native_value(self) -> datetime | None: + def native_value(self) -> datetime | str | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data[self.device_id]) diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index e3d0c9c9348..9caeb7b8eba 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -1,9 +1,14 @@ """Tests for the sensors provided by the Tailscale integration.""" -from homeassistant.components.binary_sensor import STATE_ON, BinarySensorDeviceClass +from homeassistant.components.binary_sensor import ( + STATE_OFF, + STATE_ON, + BinarySensorDeviceClass, +) from homeassistant.components.tailscale.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -21,11 +26,83 @@ async def test_tailscale_binary_sensors( assert entry assert state assert entry.unique_id == "123456_update_available" + assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Client" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE assert ATTR_ICON not in state.attributes + state = hass.states.get("binary_sensor.frencks_iphone_supports_hairpinning") + entry = entity_registry.async_get( + "binary_sensor.frencks_iphone_supports_hairpinning" + ) + assert entry + assert state + assert entry.unique_id == "123456_client_supports_hair_pinning" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frencks-iPhone Supports Hairpinning" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_ipv6") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_ipv6") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_ipv6" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports IPv6" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_pcp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_pcp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_pcp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports PCP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_nat_pmp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_nat_pmp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_pmp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports NAT-PMP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_udp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_udp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_udp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_ON + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports UDP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_upnp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_upnp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_upnp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports UPnP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 0a8918f04d1..911de0eb64a 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.tailscale.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -21,11 +22,34 @@ async def test_tailscale_sensors( assert entry assert state assert entry.unique_id == "123457_expires" + assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2022-02-25T09:49:06+00:00" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Expires" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes + state = hass.states.get("sensor.router_last_seen") + entry = entity_registry.async_get("sensor.router_last_seen") + assert entry + assert state + assert entry.unique_id == "123457_last_seen" + assert entry.entity_category is None + assert state.state == "2021-11-15T20:37:03+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last Seen" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.router_ip_address") + entry = entity_registry.async_get("sensor.router_ip_address") + assert entry + assert state + assert entry.unique_id == "123457_ip" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "100.11.11.112" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP Address" + assert state.attributes.get(ATTR_ICON) == "mdi:ip-network" + assert ATTR_DEVICE_CLASS not in state.attributes + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry From 8e715064ccfb984d2a573e984a9f561fc3188351 Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Wed, 1 Dec 2021 22:11:55 -0500 Subject: [PATCH 1191/1452] Bump pydexcom version to 0.2.1 (#60812) --- homeassistant/components/dexcom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 1321f38a0d7..1bf15776cad 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.0"], + "requirements": ["pydexcom==0.2.1"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 64508edfdd2..abc2664cb15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1435,7 +1435,7 @@ pydeconz==85 pydelijn==0.6.1 # homeassistant.components.dexcom -pydexcom==0.2.0 +pydexcom==0.2.1 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73ab9a2a4fc..9fc74d49cb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pydaikin==2.6.0 pydeconz==85 # homeassistant.components.dexcom -pydexcom==0.2.0 +pydexcom==0.2.1 # homeassistant.components.zwave pydispatcher==2.0.5 From 58fdcfb6b8c8a0257929bff4a14fe4b14123d1c9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 04:15:17 +0100 Subject: [PATCH 1192/1452] Use device/state class enums in DSMR (#60791) --- homeassistant/components/dsmr/const.py | 116 +++++++++++-------------- tests/components/dsmr/test_sensor.py | 60 ++++++++----- 2 files changed, 91 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index ba90fa9b697..baf9f036a35 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -5,17 +5,7 @@ import logging from dsmr_parser import obis_references -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.components.sensor import SensorDeviceClass, SensorStateClass from .models import DSMRSensorEntityDescription @@ -50,16 +40,16 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_USAGE, name="Power Consumption", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, force_update=True, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_DELIVERY, name="Power Production", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, force_update=True, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_ACTIVE_TARIFF, @@ -71,75 +61,75 @@ SENSORS: tuple[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, + device_class=SensorDeviceClass.ENERGY, force_update=True, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.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, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.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, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.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, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, name="Power Consumption Phase L1", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, name="Power Consumption Phase L2", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, name="Power Consumption Phase L3", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, name="Power Production Phase L1", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, name="Power Production Phase L2", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, name="Power Production Phase L3", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.SHORT_POWER_FAILURE_COUNT, @@ -197,84 +187,84 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L1, name="Voltage Phase L1", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L2, name="Voltage Phase L2", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L3, name="Voltage Phase L3", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L1, name="Current Phase L1", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L2, name="Current Phase L2", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L3, name="Current Phase L3", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, name="Energy Consumption (total)", dsmr_versions={"5L"}, force_update=True, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, name="Energy Production (total)", dsmr_versions={"5L"}, force_update=True, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.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, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.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, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_IMPORTED_TOTAL, name="Energy Consumption (total)", dsmr_versions={"2.2", "4", "5", "5B"}, force_update=True, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.HOURLY_GAS_METER_READING, @@ -282,8 +272,8 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.BELGIUM_HOURLY_GAS_METER_READING, @@ -291,8 +281,8 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5B"}, is_gas=True, force_update=True, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.GAS_METER_READING, @@ -300,7 +290,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2"}, is_gas=True, force_update=True, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 6accf7c40da..19ac6dc5d1c 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -16,16 +16,13 @@ from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) 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, VOLUME_CUBIC_METERS, @@ -134,9 +131,14 @@ async def test_default_setup(hass, dsmr_connection_fixture): # make sure entities have been created and return 'unknown' state power_consumption = hass.states.get("sensor.power_consumption") assert power_consumption.state == STATE_UNKNOWN - assert power_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ( + power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + ) assert power_consumption.attributes.get(ATTR_ICON) is None - assert power_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + power_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.MEASUREMENT + ) assert power_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser @@ -163,9 +165,10 @@ async def test_default_setup(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) == DEVICE_CLASS_GAS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( - gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -261,10 +264,11 @@ async def test_v4_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) == DEVICE_CLASS_GAS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS assert ( - gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -331,9 +335,10 @@ async def test_v5_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) == DEVICE_CLASS_GAS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( - gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -397,9 +402,12 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): 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_DEVICE_CLASS) == SensorDeviceClass.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_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) assert ( power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -411,9 +419,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) == DEVICE_CLASS_GAS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( - gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -480,9 +489,10 @@ async def test_belgian_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 DEVICE_CLASS_GAS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( - gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -586,16 +596,22 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): 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_DEVICE_CLASS) == SensorDeviceClass.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_STATE_CLASS) + == SensorStateClass.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_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) assert ( power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) From 0c4b308e03ccd605d08b1aa22326308c71db7f31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 04:16:09 +0100 Subject: [PATCH 1193/1452] Use device class enum in Stookalert (#60789) --- homeassistant/components/stookalert/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index d41e24f7c94..b606e3f3d3b 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -7,8 +7,8 @@ import stookalert import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SAFETY, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -72,7 +72,7 @@ class StookalertBinarySensor(BinarySensorEntity): """Defines a Stookalert binary sensor.""" _attr_attribution = ATTRIBUTION - _attr_device_class = DEVICE_CLASS_SAFETY + _attr_device_class = BinarySensorDeviceClass.SAFETY def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" From caa04c186699fef953e8fb5840f5cac8e7bb016e Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 2 Dec 2021 04:50:17 +0100 Subject: [PATCH 1194/1452] Add support for Nanoleaf push updates (#60708) --- homeassistant/components/nanoleaf/__init__.py | 39 ++++++++++++++++++- homeassistant/components/nanoleaf/button.py | 5 ++- homeassistant/components/nanoleaf/light.py | 27 ++++++++++++- .../components/nanoleaf/manifest.json | 2 +- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 79bb2577bb7..3ed82ec2146 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -1,17 +1,31 @@ """The Nanoleaf integration.""" -from aionanoleaf import InvalidToken, Nanoleaf, Unavailable +from __future__ import annotations + +import asyncio +from dataclasses import dataclass + +from aionanoleaf import EffectsEvent, InvalidToken, Nanoleaf, StateEvent, Unavailable from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN PLATFORMS = ["button", "light"] +@dataclass +class NanoleafEntryData: + """Class for sharing data within the Nanoleaf integration.""" + + device: Nanoleaf + event_listener: asyncio.Task + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nanoleaf from a config entry.""" nanoleaf = Nanoleaf( @@ -24,8 +38,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except InvalidToken as err: raise ConfigEntryAuthFailed from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = nanoleaf + async def _callback_update_light_state(event: StateEvent | EffectsEvent) -> None: + """Receive state and effect event.""" + async_dispatcher_send(hass, f"{DOMAIN}_update_light_{nanoleaf.serial_no}") + + event_listener = asyncio.create_task( + nanoleaf.listen_events( + state_callback=_callback_update_light_state, + effects_callback=_callback_update_light_state, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = NanoleafEntryData( + nanoleaf, event_listener + ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + entry_data: NanoleafEntryData = hass.data[DOMAIN].pop(entry.entry_id) + entry_data.event_listener.cancel() + return True diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index d85e61fc09c..bf7f9fba9f7 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -8,6 +8,7 @@ from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NanoleafEntryData from .const import DOMAIN from .entity import NanoleafEntity @@ -16,8 +17,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Nanoleaf button.""" - nanoleaf: Nanoleaf = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafIdentifyButton(nanoleaf)]) + entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([NanoleafIdentifyButton(entry_data.device)]) class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index e6a602b51f3..f4312b704e2 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations +from datetime import timedelta import logging import math from typing import Any @@ -26,6 +27,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.color import ( @@ -33,6 +35,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) +from . import NanoleafEntryData from .const import DOMAIN from .entity import NanoleafEntity @@ -49,6 +52,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=5) + async def async_setup_platform( hass: HomeAssistant, @@ -70,8 +75,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Nanoleaf light.""" - nanoleaf: Nanoleaf = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafLight(nanoleaf)]) + entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([NanoleafLight(entry_data.device)]) class NanoleafLight(NanoleafEntity, LightEntity): @@ -188,3 +193,21 @@ class NanoleafLight(NanoleafEntity, LightEntity): if not self.available: _LOGGER.info("Fetching %s data recovered", self.name) self._attr_available = True + + async def async_handle_update(self) -> None: + """Handle state update.""" + self.async_write_ha_state() + if not self.available: + _LOGGER.info("Connection to %s recovered", self.name) + self._attr_available = True + + async def async_added_to_hass(self) -> None: + """Handle entity being added to Home Assistant.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_update_light_{self._nanoleaf.serial_no}", + self.async_handle_update, + ) + ) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index d2c2d1ff643..3550b56d352 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -25,5 +25,5 @@ } ], "codeowners": ["@milanmeu"], - "iot_class": "local_polling" + "iot_class": "local_push" } \ No newline at end of file From d619a86b4e740962cac629b08f13525d33babded Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 08:24:59 +0100 Subject: [PATCH 1195/1452] Use state class enums in AccuWeather (#60813) --- homeassistant/components/accuweather/const.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 5802695afef..deab90de706 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -220,7 +220,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="Ceiling", @@ -228,7 +228,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( name="Cloud Ceiling", unit_metric=LENGTH_METERS, unit_imperial=LENGTH_FEET, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="CloudCover", @@ -237,7 +237,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="DewPoint", @@ -246,7 +246,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="RealFeelTemperature", @@ -254,7 +254,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( name="RealFeel Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="RealFeelTemperatureShade", @@ -263,7 +263,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="Precipitation", @@ -271,7 +271,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( name="Precipitation", unit_metric=LENGTH_MILLIMETERS, unit_imperial=LENGTH_INCHES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="PressureTendency", @@ -287,7 +287,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( name="UV Index", unit_metric=UV_INDEX, unit_imperial=UV_INDEX, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="WetBulbTemperature", @@ -296,7 +296,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="WindChillTemperature", @@ -305,7 +305,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="Wind", @@ -313,7 +313,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( name="Wind", unit_metric=SPEED_KILOMETERS_PER_HOUR, unit_imperial=SPEED_MILES_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AccuWeatherSensorDescription( key="WindGust", @@ -322,6 +322,6 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( unit_metric=SPEED_KILOMETERS_PER_HOUR, unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From c0333483231affa9ad6b0355ccec195238c94cc9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 08:45:16 +0100 Subject: [PATCH 1196/1452] Use state class enums in Advantage Air (#60815) --- homeassistant/components/advantage_air/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index d879693fdb5..8b83de2b923 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -3,8 +3,8 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform @@ -84,7 +84,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" _attr_native_unit_of_measurement = PERCENTAGE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): @@ -114,7 +114,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" _attr_native_unit_of_measurement = PERCENTAGE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): @@ -149,7 +149,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_registry_enabled_default = False _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC From 9128dc214cd933a9beeb8e38ed05d86dd3e5d33b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 08:54:52 +0100 Subject: [PATCH 1197/1452] Upgrade apprise to 0.9.6 (#60816) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index e92c826faaa..4e0209cc337 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.5.1"], + "requirements": ["apprise==0.9.6"], "codeowners": ["@caronc"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index abc2664cb15..db8bb7d59af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -312,7 +312,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.9.5.1 +apprise==0.9.6 # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fc74d49cb0..b3be6d21543 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ androidtv[async]==0.0.60 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.9.5.1 +apprise==0.9.6 # homeassistant.components.aprs aprslib==0.6.46 From bee3c9102c01d945686be088d9c04c45f7af5b06 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Thu, 2 Dec 2021 09:03:24 +0100 Subject: [PATCH 1198/1452] Add binary characteristics, add deprecation warning for optional state_characteristic parameter (#60402) * Add binary source sensor statistics * Make state_characteristic a required parameter * Move binary unitless testcase * Add testcases for binary characteristics * Revert charact. to optional with deprecation warning * Correctly check for binary supported characteristic --- homeassistant/components/statistics/sensor.py | 96 +++++- .../statistics/fixtures/configuration.yaml | 1 + tests/components/statistics/test_sensor.py | 308 ++++++++++++++---- 3 files changed, 330 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index e32ce804cb2..23a4a31d936 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -60,12 +60,30 @@ STAT_VALUE_MAX = "value_max" STAT_VALUE_MIN = "value_min" STAT_VARIANCE = "variance" +STAT_DEFAULT = "default" +DEPRECATION_WARNING = ( + "The configuration parameter 'state_characteristics' will become " + "mandatory in a future release of the statistics integration. " + "Please add 'state_characteristics: %s' to the configuration of " + 'sensor "%s" to keep the current behavior. Read the documentation ' + "for further details: " + "https://www.home-assistant.io/integrations/statistics/" +) + STATS_NOT_A_NUMBER = ( STAT_DATETIME_OLDEST, STAT_DATETIME_NEWEST, STAT_QUANTILES, ) +STATS_BINARY_SUPPORT = ( + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_COUNT, + STAT_MEAN, + STAT_DEFAULT, +) + CONF_STATE_CHARACTERISTIC = "state_characteristic" CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" @@ -80,11 +98,24 @@ DEFAULT_QUANTILE_INTERVALS = 4 DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + +def valid_binary_characteristic_configuration(config): + """Validate that the characteristic selected is valid for the source sensor type, throw if it isn't.""" + if config.get(CONF_ENTITY_ID).split(".")[0] == "binary_sensor": + if config.get(CONF_STATE_CHARACTERISTIC) not in STATS_BINARY_SUPPORT: + raise ValueError( + "The configured characteristic '" + + config.get(CONF_STATE_CHARACTERISTIC) + + "' is not supported for a binary source sensor." + ) + return config + + +_PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_MEAN): vol.In( + vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_DEFAULT): vol.In( [ STAT_AVERAGE_LINEAR, STAT_AVERAGE_STEP, @@ -107,6 +138,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( STAT_VALUE_MAX, STAT_VALUE_MIN, STAT_VARIANCE, + STAT_DEFAULT, ] ), vol.Optional( @@ -122,6 +154,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), } ) +PLATFORM_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE, + valid_binary_characteristic_configuration, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -166,6 +202,9 @@ class StatisticsSensor(SensorEntity): self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name self._state_characteristic = state_characteristic + if self._state_characteristic == STAT_DEFAULT: + self._state_characteristic = STAT_COUNT if self.is_binary else STAT_MEAN + _LOGGER.warning(DEPRECATION_WARNING, self._state_characteristic, name) self._samples_max_buffer_size = samples_max_buffer_size self._samples_max_age = samples_max_age self._precision = precision @@ -181,9 +220,15 @@ class StatisticsSensor(SensorEntity): STAT_BUFFER_USAGE_RATIO: None, STAT_SOURCE_VALUE_VALID: None, } - self._state_characteristic_fn = getattr( - self, f"_stat_{self._state_characteristic}" - ) + + if self.is_binary: + self._state_characteristic_fn = getattr( + self, f"_stat_binary_{self._state_characteristic}" + ) + else: + self._state_characteristic_fn = getattr( + self, f"_stat_{self._state_characteristic}" + ) self._update_listener = None @@ -246,9 +291,13 @@ class StatisticsSensor(SensorEntity): def _derive_unit_of_measurement(self, new_state): base_unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if not base_unit: - unit = None - elif self.is_binary: + if self.is_binary and self._state_characteristic in ( + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_MEAN, + ): + unit = "%" + elif not base_unit: unit = None elif self._state_characteristic in ( STAT_AVERAGE_LINEAR, @@ -290,8 +339,6 @@ class StatisticsSensor(SensorEntity): @property def state_class(self): """Return the state class of this entity.""" - if self.is_binary: - return STATE_CLASS_MEASUREMENT if self._state_characteristic in STATS_NOT_A_NUMBER: return None return STATE_CLASS_MEASUREMENT @@ -450,10 +497,6 @@ class StatisticsSensor(SensorEntity): One of the _stat_*() functions is represented by self._state_characteristic_fn(). """ - if self.is_binary: - self._value = len(self.states) - return - value = self._state_characteristic_fn() if self._state_characteristic not in STATS_NOT_A_NUMBER: @@ -463,6 +506,8 @@ class StatisticsSensor(SensorEntity): value = int(value) self._value = value + # Statistics for numeric sensor + def _stat_average_linear(self): if len(self.states) >= 2: area = 0 @@ -590,3 +635,26 @@ class StatisticsSensor(SensorEntity): if len(self.states) >= 2: return statistics.variance(self.states) return None + + # Statistics for binary sensor + + def _stat_binary_average_step(self): + if len(self.states) >= 2: + on_seconds = 0 + for i in range(1, len(self.states)): + if self.states[i - 1] == "on": + on_seconds += (self.ages[i] - self.ages[i - 1]).total_seconds() + age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() + return 100 / age_range_seconds * on_seconds + return None + + def _stat_binary_average_timeless(self): + return self._stat_binary_mean() + + def _stat_binary_count(self): + return len(self.states) + + def _stat_binary_mean(self): + if len(self.states) > 0: + return 100.0 / len(self.states) * self.states.count("on") + return None diff --git a/tests/components/statistics/fixtures/configuration.yaml b/tests/components/statistics/fixtures/configuration.yaml index a6ce34377a0..4708910b53e 100644 --- a/tests/components/statistics/fixtures/configuration.yaml +++ b/tests/components/statistics/fixtures/configuration.yaml @@ -2,3 +2,4 @@ sensor: - platform: statistics entity_id: sensor.cpu name: cputest + state_characteristic: mean diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index f1f895b2b1f..658d6a089e7 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,56 +41,14 @@ class TestStatisticsSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.values_binary = ["on", "off", "on", "off", "on", "off", "on"] + self.values_binary = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] + self.mean_binary = round( + 100 / len(self.values_binary) * self.values_binary.count("on"), 2 + ) self.values = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] self.mean = round(sum(self.values) / len(self.values), 2) self.addCleanup(self.hass.stop) - def test_sensor_defaults_binary(self): - """Test the general behavior of the sensor, with binary source sensor.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "binary_sensor.test_monitored", - }, - { - "platform": "statistics", - "name": "test_unitless", - "entity_id": "binary_sensor.test_monitored_unitless", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values_binary: - self.hass.states.set( - "binary_sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set("binary_sensor.test_monitored_unitless", value) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(len(self.values_binary)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - assert state.attributes.get("buffer_usage_ratio") == round(7 / 20, 2) - assert state.attributes.get("source_value_valid") is True - assert "age_coverage_ratio" not in state.attributes - - state = self.hass.states.get("sensor.test_unitless") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - def test_sensor_defaults_numeric(self): """Test the general behavior of the sensor, with numeric source sensor.""" assert setup_component( @@ -178,6 +136,90 @@ class TestStatisticsSensor(unittest.TestCase): assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("source_value_valid") is False + def test_sensor_defaults_binary(self): + """Test the general behavior of the sensor, with binary source sensor.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "binary_sensor.test_monitored", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + for value in self.values_binary: + self.hass.states.set( + "binary_sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == str(len(self.values_binary)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes + + def test_sensor_source_with_force_update(self): + """Test the behavior of the sensor when the source sensor force-updates with same value.""" + repeating_values = [18, 0, 0, 0, 0, 0, 0, 0, 9] + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_normal", + "entity_id": "sensor.test_monitored_normal", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_force", + "entity_id": "sensor.test_monitored_force", + "state_characteristic": "mean", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in repeating_values: + self.hass.states.set( + "sensor.test_monitored_normal", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set( + "sensor.test_monitored_force", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + force_update=True, + ) + self.hass.block_till_done() + + state_normal = self.hass.states.get("sensor.test_normal") + state_force = self.hass.states.get("sensor.test_force") + assert state_normal.state == str(round(sum(repeating_values) / 3, 2)) + assert state_force.state == str(round(sum(repeating_values) / 9, 2)) + assert state_normal.attributes.get("buffer_usage_ratio") == round(3 / 20, 2) + assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + def test_sampling_size_non_default(self): """Test rotation.""" assert setup_component( @@ -189,6 +231,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 5, }, ] @@ -223,6 +266,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 1, }, ] @@ -268,6 +312,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "max_age": {"minutes": 4}, }, ] @@ -341,6 +386,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "precision": 0, }, ] @@ -373,6 +419,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "precision": 1, }, ] @@ -459,6 +506,17 @@ class TestStatisticsSensor(unittest.TestCase): "entity_id": "sensor.test_monitored_unitless", "state_characteristic": "change_second", }, + { + "platform": "statistics", + "name": "test_unitless_4", + "entity_id": "binary_sensor.test_monitored_unitless", + }, + { + "platform": "statistics", + "name": "test_unitless_5", + "entity_id": "binary_sensor.test_monitored_unitless", + "state_characteristic": "mean", + }, ] }, ) @@ -473,6 +531,12 @@ class TestStatisticsSensor(unittest.TestCase): value, ) self.hass.block_till_done() + for value in self.values_binary: + self.hass.states.set( + "binary_sensor.test_monitored_unitless", + value, + ) + self.hass.block_till_done() state = self.hass.states.get("sensor.test_unitless_1") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None @@ -480,6 +544,10 @@ class TestStatisticsSensor(unittest.TestCase): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = self.hass.states.get("sensor.test_unitless_3") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_4") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_5") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" def test_state_characteristics(self): """Test configured state characteristic for value and unit.""" @@ -495,6 +563,7 @@ class TestStatisticsSensor(unittest.TestCase): characteristics = ( { + "source_sensor_domain": "sensor", "name": "average_linear", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -502,6 +571,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "average_step", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -509,6 +579,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "average_timeless", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -516,6 +587,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "change", "value_0": STATE_UNKNOWN, "value_1": float(0), @@ -523,6 +595,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "change_sample", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -534,6 +607,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C/sample", }, { + "source_sensor_domain": "sensor", "name": "change_second", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -547,6 +621,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C/s", }, { + "source_sensor_domain": "sensor", "name": "count", "value_0": 0, "value_1": 1, @@ -554,6 +629,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "datetime_newest", "value_0": STATE_UNKNOWN, "value_1": datetime( @@ -577,6 +653,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "datetime_oldest", "value_0": STATE_UNKNOWN, "value_1": datetime( @@ -592,6 +669,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "distance_95_percent_of_values", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -599,6 +677,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "distance_99_percent_of_values", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -606,6 +685,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "distance_absolute", "value_0": STATE_UNKNOWN, "value_1": float(0), @@ -613,6 +693,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "mean", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -620,6 +701,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "median", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -627,6 +709,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "noisiness", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -636,6 +719,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "quantiles", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -645,6 +729,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "standard_deviation", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -652,6 +737,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "total", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -659,6 +745,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "value_max", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -666,6 +753,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "value_min", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -673,20 +761,53 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "variance", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, "value_9": float(round(statistics.variance(self.values), 2)), "unit": "°C²", }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_step", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 50.0, + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_timeless", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float(self.mean_binary), + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "count", + "value_0": 0, + "value_1": 1, + "value_9": len(self.values_binary), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "mean", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float(self.mean_binary), + "unit": "%", + }, ) sensors_config = [] for characteristic in characteristics: sensors_config.append( { "platform": "statistics", - "name": "test_" + characteristic["name"], - "entity_id": "sensor.test_monitored", + "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", + "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", "state_characteristic": characteristic["name"], "max_age": {"minutes": 10}, } @@ -707,20 +828,29 @@ class TestStatisticsSensor(unittest.TestCase): # With all values in buffer - for value in self.values: + for i in range(len(self.values)): self.hass.states.set( "sensor.test_monitored", - value, + self.values[i], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set( + "binary_sensor.test_monitored", + self.values_binary[i], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() mock_data["return_time"] += timedelta(minutes=value_spacing_minutes) for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) assert state.state == str(characteristic["value_9"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (buffer filled) " - f"- assert {state.state} == {str(characteristic['value_9'])}" + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer filled) - " + f"assert {state.state} == {str(characteristic['value_9'])}" ) assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -734,10 +864,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) assert state.state == str(characteristic["value_0"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (buffer empty) " - f"- assert {state.state} == {str(characteristic['value_0'])}" + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer empty) - " + f"assert {state.state} == {str(characteristic['value_0'])}" ) # With single value in buffer @@ -747,15 +881,65 @@ class TestStatisticsSensor(unittest.TestCase): self.values[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) - self.hass.block_till_done() + self.hass.states.set( + "binary_sensor.test_monitored", + self.values_binary[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + force_update=True, + ) mock_data["return_time"] += timedelta(minutes=1) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) - assert state.state == str(characteristic["value_1"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (one stored value) " - f"- assert {state.state} == {str(characteristic['value_1'])}" + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" ) + assert state.state == str(characteristic["value_1"]), ( + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(one stored value) - " + f"assert {state.state} == {str(characteristic['value_1'])}" + ) + + def test_invalid_state_characteristic(self): + """Test the detection of wrong state_characteristics selected.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_numeric", + "entity_id": "sensor.test_monitored", + "state_characteristic": "invalid", + }, + { + "platform": "statistics", + "name": "test_binary", + "entity_id": "binary_sensor.test_monitored", + "state_characteristic": "variance", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set( + "sensor.test_monitored", + self.values[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_numeric") + assert state is None + state = self.hass.states.get("sensor.test_binary") + assert state is None def test_initialize_from_database(self): """Test initializing the statistics from the database.""" @@ -784,6 +968,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 100, }, ] @@ -886,6 +1071,7 @@ async def test_reload(hass): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 100, }, ] From a9b2036de1f49beb2fecdf4b274b9b75dbd6da11 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 09:26:39 +0100 Subject: [PATCH 1199/1452] Use state class enums in aemet (#60817) --- homeassistant/components/aemet/const.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index ba37c66da64..0074a5be6d8 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -1,10 +1,7 @@ """Constant values for the AEMET OpenData component.""" from __future__ import annotations -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -255,14 +252,14 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_RAIN, @@ -273,7 +270,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_RAIN_PROB, name="Rain probability", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_SNOW, @@ -284,7 +281,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_SNOW_PROB, name="Snow probability", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_STATION_ID, @@ -303,21 +300,21 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_STORM_PROB, name="Storm probability", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE_FEELING, name="Temperature feeling", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TOWN_ID, @@ -336,7 +333,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_MAX_SPEED, @@ -347,7 +344,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 831e69d07c16df11de7c0cc7de7166fcf0c1395c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 2 Dec 2021 09:52:19 +0100 Subject: [PATCH 1200/1452] Use callback instead of coroutine function (#60821) --- homeassistant/components/nanoleaf/light.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index f4312b704e2..e8bb994a06b 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -25,7 +25,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -194,7 +194,8 @@ class NanoleafLight(NanoleafEntity, LightEntity): _LOGGER.info("Fetching %s data recovered", self.name) self._attr_available = True - async def async_handle_update(self) -> None: + @callback + def async_handle_update(self) -> None: """Handle state update.""" self.async_write_ha_state() if not self.available: From 63c377a2398df0b35616c4bb06839522bf6f8329 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 09:53:02 +0100 Subject: [PATCH 1201/1452] Use state class enums in airvisual (#60819) --- homeassistant/components/airvisual/sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 08896e13557..f5dfc4f1b11 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -81,7 +81,7 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( name="Air Quality Index", device_class=DEVICE_CLASS_AQI, native_unit_of_measurement="AQI", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_POLLUTANT, @@ -98,7 +98,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( name="Air Quality Index", device_class=DEVICE_CLASS_AQI, native_unit_of_measurement="AQI", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_BATTERY_LEVEL, @@ -112,7 +112,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( name="C02", device_class=DEVICE_CLASS_CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_HUMIDITY, @@ -125,35 +125,35 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( name="PM 0.1", device_class=DEVICE_CLASS_PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_PM_1_0, name="PM 1.0", device_class=DEVICE_CLASS_PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_PM_2_5, name="PM 2.5", device_class=DEVICE_CLASS_PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_TEMPERATURE, name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_VOC, name="VOC", device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 00605c1e3519272fab5e6252fefede91dff0195f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 2 Dec 2021 09:59:07 +0100 Subject: [PATCH 1202/1452] Fix old model network suffix for Fritz (#60802) --- homeassistant/components/fritz/switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 969f8cf8f9e..4a740a26c33 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -246,6 +246,9 @@ def wifi_entities_list( """Get list of wifi entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK) std_table = {"ax": "Wifi6", "ac": "5Ghz", "n": "2.4Ghz"} + if fritzbox_tools.model == "FRITZ!Box 7390": + std_table = {"n": "5Ghz"} + networks: dict = {} for i in range(4): if not ("WLANConfiguration" + str(i)) in fritzbox_tools.connection.services: From 4c158e8168686394f08c53f4b9c01f6b59b8eab6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 10:00:16 +0100 Subject: [PATCH 1203/1452] Upgrade twentemilieu to 0.5.0 (#60820) --- homeassistant/components/twentemilieu/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index b4f0a3730a9..2a9a7915e76 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,7 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.4.2"], + "requirements": ["twentemilieu==0.5.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index db8bb7d59af..3c4fcfc431b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2344,7 +2344,7 @@ transmissionrpc==0.11 tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu -twentemilieu==0.4.2 +twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3be6d21543..4b63f53d759 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ transmissionrpc==0.11 tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu -twentemilieu==0.4.2 +twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 From 42bae5439ba469a558e2bcd6587f33620bd50b14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 10:21:19 +0100 Subject: [PATCH 1204/1452] Add support for Christmas Tree pickup to TwenteMilieu (#60822) --- homeassistant/components/twentemilieu/sensor.py | 7 +++++++ tests/components/twentemilieu/conftest.py | 1 + tests/components/twentemilieu/test_sensor.py | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 43bfba2b017..f25b84ace15 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -40,6 +40,13 @@ class TwenteMilieuSensorDescription( SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( + TwenteMilieuSensorDescription( + key="tree", + waste_type=WasteType.TREE, + name="Christmas Tree Pickup", + icon="mdi:pine-tree", + device_class=SensorDeviceClass.DATE, + ), TwenteMilieuSensorDescription( key="Non-recyclable", waste_type=WasteType.NON_RECYCLABLE, diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py index d540658787b..530e9723251 100644 --- a/tests/components/twentemilieu/conftest.py +++ b/tests/components/twentemilieu/conftest.py @@ -69,6 +69,7 @@ def mock_twentemilieu() -> Generator[None, MagicMock, None]: WasteType.ORGANIC: date(2021, 11, 2), WasteType.PACKAGES: date(2021, 11, 3), WasteType.PAPER: None, + WasteType.TREE: date(2022, 1, 6), } yield twentemilieu diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 502bc2e3089..5f09018358e 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -22,6 +22,17 @@ async def test_waste_pickup_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) + state = hass.states.get("sensor.christmas_tree_pickup") + entry = entity_registry.async_get("sensor.christmas_tree_pickup") + assert entry + assert state + assert entry.unique_id == "twentemilieu_12345_tree" + assert state.state == "2022-01-06" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Christmas Tree Pickup" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE + assert state.attributes.get(ATTR_ICON) == "mdi:pine-tree" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + state = hass.states.get("sensor.non_recyclable_waste_pickup") entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup") assert entry From 3307e54363766bfef07f616ed05605632e78dacf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Dec 2021 10:21:31 +0100 Subject: [PATCH 1205/1452] Add MQTT availability template and encoding (#60470) * Add MQTT availability template and encoding * use generic encoding field * pylint and cleanup * remove additional topic check --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 26 ++++- tests/components/mqtt/test_discovery.py | 94 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c47b512b727..c5c70ad33a4 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -10,6 +10,7 @@ ABBREVIATIONS = { "avty": "availability", "avty_mode": "availability_mode", "avty_t": "availability_topic", + "avty_tpl": "availability_template", "away_mode_cmd_t": "away_mode_command_topic", "away_mode_stat_tpl": "away_mode_state_template", "away_mode_stat_t": "away_mode_state_topic", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 3a374070f2f..713f0e5c030 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -42,6 +43,7 @@ from .const import ( ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, + CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_PAYLOAD_AVAILABLE, @@ -71,6 +73,7 @@ AVAILABILITY_LATEST = "latest" AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] CONF_AVAILABILITY_MODE = "availability_mode" +CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_ENABLED_BY_DEFAULT = "enabled_by_default" CONF_PAYLOAD_AVAILABLE = "payload_available" @@ -112,6 +115,7 @@ MQTT_ATTRIBUTES_BLOCKED = { MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( { vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional( CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE ): cv.string, @@ -138,6 +142,7 @@ MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE, ): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ], ), @@ -335,6 +340,7 @@ class MqttAvailability(Entity): self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], + CONF_AVAILABILITY_TEMPLATE: config.get(CONF_AVAILABILITY_TEMPLATE), } if CONF_AVAILABILITY in config: @@ -342,8 +348,22 @@ class MqttAvailability(Entity): self._avail_topics[avail[CONF_TOPIC]] = { CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], + CONF_AVAILABILITY_TEMPLATE: avail.get(CONF_VALUE_TEMPLATE), } + for ( + topic, # pylint: disable=unused-variable + avail_topic_conf, + ) in self._avail_topics.items(): + tpl = avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] + if tpl is None: + avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = lambda value: value + else: + tpl.hass = self.hass + avail_topic_conf[ + CONF_AVAILABILITY_TEMPLATE + ] = tpl.async_render_with_possible_json_value + self._avail_config = config async def _availability_subscribe_topics(self): @@ -354,10 +374,11 @@ class MqttAvailability(Entity): def availability_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT availability message.""" topic = msg.topic - if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: + payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload) + if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: self._available[topic] = True self._available_latest = True - elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: + elif payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: self._available[topic] = False self._available_latest = False @@ -372,6 +393,7 @@ class MqttAvailability(Entity): "topic": topic, "msg_callback": availability_message_received, "qos": self._avail_config[CONF_QOS], + "encoding": self._avail_config[CONF_ENCODING] or None, } for topic in self._avail_topics } diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index faa9f714ae0..6293d99aff4 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -737,6 +737,100 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): ) +async def test_discovery_expansion_without_encoding_and_value_template_1( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template as list.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "encoding":"",' + ' "availability": [{' + ' "topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "value_template":"{{ value | bitwise_and(1) }}"' + " }]," + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_expansion_without_encoding_and_value_template_2( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template directly.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability_topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "encoding":"",' + ' "availability_template":"{{ value | bitwise_and(1) }}",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE", From 653fb5b637a55aa383683004bde1c7c12408fb20 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 10:31:54 +0100 Subject: [PATCH 1206/1452] Update Netatmo climate platform (#59974) --- homeassistant/components/netatmo/climate.py | 327 +++---- .../components/netatmo/data_handler.py | 12 +- homeassistant/components/netatmo/helper.py | 24 - .../components/netatmo/netatmo_entity_base.py | 2 +- homeassistant/components/netatmo/select.py | 106 ++- tests/components/netatmo/common.py | 6 +- .../netatmo/fixtures/homesdata.json | 833 +++++++++--------- ... homestatus_91763b24c43d3e344f424e8b.json} | 0 .../homestatus_91763b24c43d3e344f424e8c.json | 12 + tests/components/netatmo/test_climate.py | 51 +- tests/components/netatmo/test_init.py | 3 +- tests/components/netatmo/test_select.py | 4 +- 12 files changed, 626 insertions(+), 754 deletions(-) rename tests/components/netatmo/fixtures/{homestatus.json => homestatus_91763b24c43d3e344f424e8b.json} (100%) create mode 100644 tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 10c8ba182f0..726c0ed43c8 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any import pyatmo import voluptuous as vol @@ -22,7 +22,6 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_SUGGESTED_AREA, ATTR_TEMPERATURE, PRECISION_HALVES, @@ -32,7 +31,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,7 +39,6 @@ from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, - DATA_DEVICE_IDS, DATA_HANDLER, DATA_HOMES, DATA_SCHEDULES, @@ -50,17 +47,15 @@ from .const import ( EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, - MANUFACTURER, SERVICE_SET_SCHEDULE, SIGNAL_NAME, TYPE_ENERGY, ) from .data_handler import ( - HOMEDATA_DATA_CLASS_NAME, - HOMESTATUS_DATA_CLASS_NAME, + CLIMATE_STATE_CLASS_NAME, + CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler, ) -from .helper import get_all_home_ids, update_climate_schedules from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -125,44 +120,42 @@ async def async_setup_entry( data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( - HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None ) - home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) - if not home_data or home_data.raw_data == {}: + if not climate_topology or climate_topology.raw_data == {}: raise PlatformNotReady entities = [] - for home_id in get_all_home_ids(home_data): - for room_id in home_data.rooms[home_id]: - signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id - ) - home_status = data_handler.data.get(signal_name) - if home_status and room_id in home_status.rooms: - entities.append(NetatmoThermostat(data_handler, home_id, room_id)) - - hass.data[DOMAIN][DATA_SCHEDULES].update( - update_climate_schedules( - home_ids=get_all_home_ids(home_data), - schedules=data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules, + for home_id in climate_topology.home_ids: + signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - ) + climate_state = data_handler.data[signal_name] + climate_topology.register_handler(home_id, climate_state.process_topology) - hass.data[DOMAIN][DATA_HOMES] = { - home_id: home_data.get("name") - for home_id, home_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() - ) - } + for room in climate_state.homes[home_id].rooms.values(): + if room.device_type is None or room.device_type.value not in [ + NA_THERM, + NA_VALVE, + ]: + continue + entities.append(NetatmoThermostat(data_handler, room)) + + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ + home_id + ].schedules + + hass.data[DOMAIN][DATA_HOMES][home_id] = climate_state.homes[home_id].name _LOGGER.debug("Adding climate devices %s", entities) async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() - if home_data is not None: + if climate_topology is not None: platform.async_register_entity_service( SERVICE_SET_SCHEDULE, {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, @@ -174,67 +167,61 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" _attr_hvac_mode = HVAC_MODE_AUTO - _attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] _attr_max_temp = DEFAULT_MAX_TEMP _attr_preset_modes = SUPPORT_PRESET + _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS def __init__( - self, data_handler: NetatmoDataHandler, home_id: str, room_id: str + self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom ) -> None: """Initialize the sensor.""" ClimateEntity.__init__(self) super().__init__(data_handler) - self._id = room_id - self._home_id = home_id + self._room = room + self._id = self._room.entity_id - self._home_status_class = f"{HOMESTATUS_DATA_CLASS_NAME}-{self._home_id}" + self._climate_state_class = ( + f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" + ) + self._climate_state: pyatmo.AsyncClimate = data_handler.data[ + self._climate_state_class + ] self._data_classes.extend( [ { - "name": HOMEDATA_DATA_CLASS_NAME, - SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + "name": CLIMATE_TOPOLOGY_CLASS_NAME, + SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, }, { - "name": HOMESTATUS_DATA_CLASS_NAME, - "home_id": self._home_id, - SIGNAL_NAME: self._home_status_class, + "name": CLIMATE_STATE_CLASS_NAME, + "home_id": self._room.home.entity_id, + SIGNAL_NAME: self._climate_state_class, }, ] ) - self._home_status = self.data_handler.data[self._home_status_class] - self._room_status = self._home_status.rooms[room_id] - self._room_data: dict = self._data.rooms[home_id][room_id] - - self._model: str = NA_VALVE - for module in self._room_data.get("module_ids", []): - if self._home_status.thermostats.get(module): - self._model = NA_THERM - break + self._model: str = getattr(room.device_type, "value") self._netatmo_type = TYPE_ENERGY - self._device_name = self._data.rooms[home_id][room_id]["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = self._room.name self._away: bool | None = None - self._support_flags = SUPPORT_FLAGS - self._battery_level = None self._connected: bool | None = None self._away_temperature: float | None = None self._hg_temperature: float | None = None self._boilerstatus: bool | None = None - self._setpoint_duration = None self._selected_schedule = None + self._attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] if self._model == NA_THERM: self._attr_hvac_modes.append(HVAC_MODE_OFF) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self._room.entity_id}-{self._model}" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -254,33 +241,32 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) - registry = await async_get_registry(self.hass) - device = registry.async_get_device({(DOMAIN, self._id)}, set()) - assert device - self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id - @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" data = event["data"] - if self._home_id != data["home_id"]: + if self._room.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].get(data["schedule_id"]) - self._attr_extra_state_attributes.update( - {"selected_schedule": self._selected_schedule} + self._selected_schedule = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._room.home.entity_id].get( + data["schedule_id"] + ), + "name", + None, ) + self._attr_extra_state_attributes[ + ATTR_SELECTED_SCHEDULE + ] = self._selected_schedule self.async_write_ha_state() - self.data_handler.async_force_update(self._home_status_class) + self.data_handler.async_force_update(self._climate_state_class) return home = data["home"] - if self._home_id != home["id"]: + if self._room.home.entity_id != home["id"]: return if data["event_type"] == EVENT_TYPE_THERM_MODE: @@ -292,12 +278,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._away_temperature elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() - self.data_handler.async_force_update(self._home_status_class) + self.data_handler.async_force_update(self._climate_state_class) self.async_write_ha_state() return for room in home.get("rooms", []): - if data["event_type"] == EVENT_TYPE_SET_POINT and self._id == room["id"]: + if ( + data["event_type"] == EVENT_TYPE_SET_POINT + and self._room.entity_id == room["id"] + ): if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: self._attr_hvac_mode = HVAC_MODE_OFF self._attr_preset_mode = STATE_NETATMO_OFF @@ -318,31 +307,21 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if ( data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT - and self._id == room["id"] + and self._room.entity_id == room["id"] ): self.async_update_callback() self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncHomeData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncHomeData, self.data_handler.data[self._data_classes[0]["name"]] - ) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return self._support_flags - @property def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if self._model == NA_THERM and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve - if self._room_status and self._room_status.get("heating_power_request", 0) > 0: + if ( + heating_req := getattr(self._room, "heating_power_request", 0) + ) is not None and heating_req > 0: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -363,8 +342,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): await self.async_turn_on() if self.target_temperature == 0: - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME, ) @@ -373,15 +352,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): and self._model == NA_VALVE and self.hvac_mode == HVAC_MODE_HEAT ): - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE ): - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) @@ -389,15 +368,17 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self.hvac_mode == HVAC_MODE_HEAT ): - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_HOME + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME ) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._home_status.async_set_room_thermpoint( - self._id, PRESET_MAP_NETATMO[preset_mode] + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, PRESET_MAP_NETATMO[preset_mode] ) elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): - await self._home_status.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self._climate_state.async_set_thermmode( + PRESET_MAP_NETATMO[preset_mode] + ) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -407,8 +388,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Set new target temperature for 2 hours.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) ) self.async_write_ha_state() @@ -416,20 +397,22 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): async def async_turn_off(self) -> None: """Turn the entity off.""" if self._model == NA_VALVE: - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self.hvac_mode != HVAC_MODE_OFF: - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_OFF + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_OFF ) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME + ) self.async_write_ha_state() @property @@ -440,135 +423,57 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._home_status = self.data_handler.data[self._home_status_class] - if self._home_status is None: + if not self._room.reachable: if self.available: self._connected = False return - self._room_status = self._home_status.rooms.get(self._id) - self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id, {}) - - if not self._room_status or not self._room_data: - if self._connected: - _LOGGER.info( - "The thermostat in room %s seems to be out of reach", - self._device_name, - ) - - self._connected = False - return - - roomstatus = {"roomID": self._room_status.get("id", {})} - if self._room_status.get("reachable"): - roomstatus.update(self._build_room_status()) - - self._away_temperature = self._data.get_away_temp(self._home_id) - self._hg_temperature = self._data.get_hg_temp(self._home_id) - self._setpoint_duration = self._data.setpoint_duration[self._home_id] - self._selected_schedule = roomstatus.get("selected_schedule") - - if "current_temperature" not in roomstatus: - return - - self._attr_current_temperature = roomstatus["current_temperature"] - self._attr_target_temperature = roomstatus["target_temperature"] - self._attr_preset_mode = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] - self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] - self._battery_level = roomstatus.get("battery_state") self._connected = True + self._away_temperature = self._room.home.get_away_temp() + self._hg_temperature = self._room.home.get_hg_temp() + self._attr_current_temperature = self._room.therm_measured_temperature + self._attr_target_temperature = self._room.therm_setpoint_temperature + self._attr_preset_mode = NETATMO_MAP_PRESET[ + getattr(self._room, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) + ] + self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] - if self._battery_level is not None: - self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = self._battery_level + self._selected_schedule = getattr( + self._room.home.get_selected_schedule(), "name", None + ) + self._attr_extra_state_attributes[ + ATTR_SELECTED_SCHEDULE + ] = self._selected_schedule if self._model == NA_VALVE: self._attr_extra_state_attributes[ ATTR_HEATING_POWER_REQUEST - ] = self._room_status.get("heating_power_request", 0) - - if self._selected_schedule is not None: - self._attr_extra_state_attributes[ - ATTR_SELECTED_SCHEDULE - ] = self._selected_schedule - - def _build_room_status(self) -> dict: - """Construct room status.""" - try: - roomstatus = { - "roomname": self._room_data["name"], - "target_temperature": self._room_status["therm_setpoint_temperature"], - "setpoint_mode": self._room_status["therm_setpoint_mode"], - "current_temperature": self._room_status["therm_measured_temperature"], - "module_type": self._data.get_thermostat_type( - home_id=self._home_id, room_id=self._id - ), - "module_id": None, - "heating_status": None, - "heating_power_request": None, - "selected_schedule": self._data._get_selected_schedule( # pylint: disable=protected-access - home_id=self._home_id - ).get( - "name" - ), - } - - batterylevel = None - for module_id in self._room_data["module_ids"]: - if ( - self._data.modules[self._home_id][module_id]["type"] == NA_THERM - or roomstatus["module_id"] is None - ): - roomstatus["module_id"] = module_id - if roomstatus["module_type"] == NA_THERM: - self._boilerstatus = self._home_status.boiler_status( - roomstatus["module_id"] - ) - roomstatus["heating_status"] = self._boilerstatus - batterylevel = self._home_status.thermostats[ - roomstatus["module_id"] - ].get("battery_state") - elif roomstatus["module_type"] == NA_VALVE: - roomstatus["heating_power_request"] = self._room_status[ - "heating_power_request" - ] - roomstatus["heating_status"] = roomstatus["heating_power_request"] > 0 - if self._boilerstatus is not None: - roomstatus["heating_status"] = ( - self._boilerstatus and roomstatus["heating_status"] - ) - batterylevel = self._home_status.valves[roomstatus["module_id"]].get( - "battery_state" - ) - - if batterylevel: - roomstatus["battery_state"] = batterylevel - - return roomstatus - - except KeyError as err: - _LOGGER.error("Update of room %s failed. Error: %s", self._id, err) - - return {} + ] = self._room.heating_power_request + else: + for module in self._room.modules.values(): + self._boilerstatus = module.boiler_status + break async def _async_service_set_schedule(self, **kwargs: Any) -> None: schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None - for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): - if name == schedule_name: + for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._room.home.entity_id + ].items(): + if schedule.name == schedule_name: schedule_id = sid + break if not schedule_id: _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._data.async_switch_home_schedule( - home_id=self._home_id, schedule_id=schedule_id - ) + await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._home_id, + self._room.home.entity_id, kwargs.get(ATTR_SCHEDULE_NAME), schedule_id, ) @@ -577,5 +482,5 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def device_info(self) -> DeviceInfo: """Return the device info for the thermostat.""" device_info: DeviceInfo = super().device_info - device_info[ATTR_SUGGESTED_AREA] = self._room_data["name"] + device_info[ATTR_SUGGESTED_AREA] = self._room.name return device_info diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 8cd0f2047ed..97321b0da53 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -32,23 +32,23 @@ _LOGGER = logging.getLogger(__name__) CAMERA_DATA_CLASS_NAME = "AsyncCameraData" WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" -HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" -HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" +CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology" +CLIMATE_STATE_CLASS_NAME = "AsyncClimate" PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" DATA_CLASSES = { WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, - HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData, - HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus, + CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology, + CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate, PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, } BATCH_SIZE = 3 DEFAULT_INTERVALS = { - HOMEDATA_DATA_CLASS_NAME: 900, - HOMESTATUS_DATA_CLASS_NAME: 300, + CLIMATE_TOPOLOGY_CLASS_NAME: 3600, + CLIMATE_STATE_CLASS_NAME: 300, CAMERA_DATA_CLASS_NAME: 900, WEATHERSTATION_DATA_CLASS_NAME: 600, HOMECOACH_DATA_CLASS_NAME: 300, diff --git a/homeassistant/components/netatmo/helper.py b/homeassistant/components/netatmo/helper.py index d824013ed27..ea30e059d3a 100644 --- a/homeassistant/components/netatmo/helper.py +++ b/homeassistant/components/netatmo/helper.py @@ -4,8 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from uuid import UUID, uuid4 -import pyatmo - @dataclass class NetatmoArea: @@ -19,25 +17,3 @@ class NetatmoArea: mode: str show_on_map: bool uuid: UUID = uuid4() - - -def get_all_home_ids(home_data: pyatmo.HomeData | None) -> list[str]: - """Get all the home ids returned by NetAtmo API.""" - if home_data is None: - return [] - return [ - home_data.homes[home_id]["id"] - for home_id in home_data.homes - if "modules" in home_data.homes[home_id] - ] - - -def update_climate_schedules(home_ids: list[str], schedules: dict) -> dict: - """Get updated list of all climate schedules.""" - return { - home_id: { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in schedules[home_id].items() - } - for home_id in home_ids - } diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 5a497275eaf..56f25e04906 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -66,7 +66,7 @@ class NetatmoBase(Entity): await self.data_handler.unregister_data_class(signal_name, None) registry = await self.hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, self._id)}, set()) + device = registry.async_get_device({(DOMAIN, self._id)}) self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id self.async_update_callback() diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index f5ab43bbd12..9902155be73 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import cast import pyatmo @@ -22,8 +21,11 @@ from .const import ( SIGNAL_NAME, TYPE_ENERGY, ) -from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler -from .helper import get_all_home_ids, update_climate_schedules +from .data_handler import ( + CLIMATE_STATE_CLASS_NAME, + CLIMATE_TOPOLOGY_CLASS_NAME, + NetatmoDataHandler, +) from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -36,25 +38,34 @@ async def async_setup_entry( data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( - HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None ) - home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) - if not home_data or home_data.raw_data == {}: + if not climate_topology or climate_topology.raw_data == {}: raise PlatformNotReady - hass.data[DOMAIN][DATA_SCHEDULES].update( - update_climate_schedules( - home_ids=get_all_home_ids(home_data), - schedules=data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules, + entities = [] + for home_id in climate_topology.home_ids: + signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - ) + climate_state = data_handler.data.get(signal_name) + climate_topology.register_handler(home_id, climate_state.process_topology) + + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ + home_id + ].schedules entities = [ NetatmoScheduleSelect( data_handler, home_id, - list(hass.data[DOMAIN][DATA_SCHEDULES][home_id].values()), + [ + schedule.name + for schedule in hass.data[DOMAIN][DATA_SCHEDULES][home_id].values() + ], ) for home_id in hass.data[DOMAIN][DATA_SCHEDULES] ] @@ -75,16 +86,28 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._home_id = home_id + self._climate_state_class = f"{CLIMATE_STATE_CLASS_NAME}-{self._home_id}" + self._climate_state: pyatmo.AsyncClimate = data_handler.data[ + self._climate_state_class + ] + + self._home = self._climate_state.homes[self._home_id] + self._data_classes.extend( [ { - "name": HOMEDATA_DATA_CLASS_NAME, - SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + "name": CLIMATE_TOPOLOGY_CLASS_NAME, + SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, + }, + { + "name": CLIMATE_STATE_CLASS_NAME, + "home_id": self._home_id, + SIGNAL_NAME: self._climate_state_class, }, ] ) - self._device_name = self._data.homes[home_id]["name"] + self._device_name = self._home.name self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model: str = "NATherm1" @@ -92,9 +115,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_unique_id = f"{self._home_id}-schedule-select" - self._attr_current_option = self._data._get_selected_schedule( - home_id=self._home_id - ).get("name") + self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") self._attr_options = options async def async_added_to_hass(self) -> None: @@ -119,23 +140,20 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._attr_current_option = self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].get(data["schedule_id"]) + self._attr_current_option = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].get( + data["schedule_id"] + ), + "name", + ) self.async_write_ha_state() - @property - def _data(self) -> pyatmo.AsyncHomeData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncHomeData, - self.data_handler.data[self._data_classes[0]["name"]], - ) - async def async_select_option(self, option: str) -> None: """Change the selected option.""" - for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): - if name != option: + for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].items(): + if schedule.name != option: continue _LOGGER.debug( "Setting %s schedule to %s (%s)", @@ -143,25 +161,17 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): option, sid, ) - await self._data.async_switch_home_schedule( - home_id=self._home_id, schedule_id=sid - ) + await self._climate_state.async_switch_home_schedule(schedule_id=sid) break @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_current_option = ( - self._data._get_selected_schedule( # pylint: disable=protected-access - home_id=self._home_id - ).get("name") - ) - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in ( - self._data.schedules[self._home_id].items() - ) - } - self._attr_options = list( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].values() - ) + self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules + self._attr_options = [ + schedule.name + for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].values() + ] diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index f2c03ac7de1..55083171a1a 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -51,7 +51,7 @@ async def fake_post_request(*args, **kwargs): if endpoint in "snapshot_720.jpg": return b"test stream image bytes" - elif endpoint in [ + if endpoint in [ "setpersonsaway", "setpersonshome", "setstate", @@ -61,6 +61,10 @@ async def fake_post_request(*args, **kwargs): ]: payload = f'{{"{endpoint}": true}}' + elif endpoint == "homestatus": + home_id = kwargs.get("params", {}).get("home_id") + payload = json.loads(load_fixture(f"netatmo/{endpoint}_{home_id}.json")) + else: payload = json.loads(load_fixture(f"netatmo/{endpoint}.json")) diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 9c5e985218f..fd63a0c200f 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -1,430 +1,407 @@ { - "body": { - "homes": [ - { - "id": "91763b24c43d3e344f424e8b", - "name": "MYHOME", - "altitude": 112, - "coordinates": [ - 52.516263, - 13.377726 - ], - "country": "DE", - "timezone": "Europe/Berlin", - "rooms": [ - { - "id": "2746182631", - "name": "Livingroom", - "type": "livingroom", - "module_ids": [ - "12:34:56:00:01:ae" - ] - }, - { - "id": "3688132631", - "name": "Hall", - "type": "custom", - "module_ids": [ - "12:34:56:00:f1:62" - ] - }, - { - "id": "2833524037", - "name": "Entrada", - "type": "lobby", - "module_ids": [ - "12:34:56:03:a5:54" - ] - }, - { - "id": "2940411577", - "name": "Cocina", - "type": "kitchen", - "module_ids": [ - "12:34:56:03:a0:ac" - ] - } - ], - "modules": [ - { - "id": "12:34:56:00:fa:d0", - "type": "NAPlug", - "name": "Thermostat", - "setup_date": 1494963356, - "modules_bridged": [ - "12:34:56:00:01:ae", - "12:34:56:03:a0:ac", - "12:34:56:03:a5:54" - ] - }, - { - "id": "12:34:56:00:01:ae", - "type": "NATherm1", - "name": "Livingroom", - "setup_date": 1494963356, - "room_id": "2746182631", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:03:a5:54", - "type": "NRV", - "name": "Valve1", - "setup_date": 1554549767, - "room_id": "2833524037", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:03:a0:ac", - "type": "NRV", - "name": "Valve2", - "setup_date": 1554554444, - "room_id": "2940411577", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "name": "Hall", - "setup_date": 1544828430, - "room_id": "3688132631" - } - ], - "schedules": [ - { - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0 - }, - { - "type": 1, - "name": "Night", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1 - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4 - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Default", - "selected": true, - "id": "591b54a2764ff4d50d8b5795", - "type": "therm" - }, - { - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0 - }, - { - "type": 1, - "name": "Night", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1 - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4 - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Winter", - "id": "b1b54a2f45795764f59d50d8", - "type": "therm" - } - ], - "therm_setpoint_default_duration": 120, - "persons": [ - { - "id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "pseudo": "John Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" - }, - { - "id": "91827375-7e04-5298-83ae-a0cb8372dff2", - "pseudo": "Jane Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" - }, - { - "id": "91827376-7e04-5298-83af-a0cb8372dff3", - "pseudo": "Richard Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" - } - ], - "therm_mode": "schedule" - }, - { - "id": "91763b24c43d3e344f424e8c", - "altitude": 112, - "coordinates": [ - 52.516263, - 13.377726 - ], - "country": "DE", - "timezone": "Europe/Berlin", - "therm_setpoint_default_duration": 180, - "therm_mode": "schedule" - } + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "altitude": 112, + "coordinates": [52.516263, 13.377726], + "country": "DE", + "timezone": "Europe/Berlin", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "module_ids": ["12:34:56:00:01:ae"] + }, + { + "id": "3688132631", + "name": "Hall", + "type": "custom", + "module_ids": ["12:34:56:00:f1:62"] + }, + { + "id": "2833524037", + "name": "Entrada", + "type": "lobby", + "module_ids": ["12:34:56:03:a5:54"] + }, + { + "id": "2940411577", + "name": "Cocina", + "type": "kitchen", + "module_ids": ["12:34:56:03:a0:ac"] + } ], - "user": { - "email": "john@doe.com", - "language": "de-DE", - "locale": "de-DE", - "feel_like_algorithm": 0, - "unit_pressure": 0, - "unit_system": 0, - "unit_wind": 0, - "id": "91763b24c43d3e344f424e8b" - } - }, - "status": "ok", - "time_exec": 0.056135892868042, - "time_server": 1559171003 -} \ No newline at end of file + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Thermostat", + "setup_date": 1494963356, + "modules_bridged": [ + "12:34:56:00:01:ae", + "12:34:56:03:a0:ac", + "12:34:56:03:a5:54" + ] + }, + { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a5:54", + "type": "NRV", + "name": "Valve1", + "setup_date": 1554549767, + "room_id": "2833524037", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a0:ac", + "type": "NRV", + "name": "Valve2", + "setup_date": 1554554444, + "room_id": "2940411577", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631" + } + ], + "schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "selected": true, + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + }, + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Winter", + "id": "b1b54a2f45795764f59d50d8", + "type": "therm" + } + ], + "therm_setpoint_default_duration": 120, + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "pseudo": "John Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "pseudo": "Jane Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "pseudo": "Richard Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + } + ], + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} diff --git a/tests/components/netatmo/fixtures/homestatus.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json similarity index 100% rename from tests/components/netatmo/fixtures/homestatus.json rename to tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json new file mode 100644 index 00000000000..d950c82a6a5 --- /dev/null +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json @@ -0,0 +1,12 @@ +{ + "status": "ok", + "time_server": 1559292041, + "body": { + "home": { + "modules": [], + "rooms": [], + "id": "91763b24c43d3e344f424e8c", + "persons": [] + } + } +} diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index ef7f8884e2e..b61081252a0 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -1,5 +1,5 @@ """The tests for the Netatmo climate platform.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, @@ -18,7 +18,6 @@ from homeassistant.components.climate.const import ( PRESET_AWAY, PRESET_BOOST, ) -from homeassistant.components.netatmo import climate from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE from homeassistant.components.netatmo.const import ( ATTR_SCHEDULE_NAME, @@ -37,7 +36,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -214,7 +213,7 @@ async def test_service_preset_mode_frost_guard_thermostat( await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -287,7 +286,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -415,11 +414,11 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" # Test setting a valid schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -429,7 +428,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ ) await hass.async_block_till_done() mock_switch_home_schedule.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", schedule_id="b1b54a2f45795764f59d50d8" + schedule_id="b1b54a2f45795764f59d50d8" ) # Fake backend response for valve being turned on @@ -448,7 +447,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ # Test setting an invalid schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -472,7 +471,7 @@ async def test_service_preset_mode_already_boost_valves( await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" assert hass.states.get(climate_entity_entrada).state == "auto" assert ( @@ -550,7 +549,7 @@ async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Test service setting the preset mode to "boost" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -602,7 +601,7 @@ async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_a await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.netatmo_cocina", ATTR_PRESET_MODE: "invalid"}, + {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, blocking=True, ) await hass.async_block_till_done() @@ -618,7 +617,12 @@ async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" + + assert hass.states.get(climate_entity_entrada).attributes["hvac_modes"] == [ + "auto", + "heat", + ] # Test turning valve off await hass.services.async_call( @@ -663,7 +667,7 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Test turning valve on await hass.services.async_call( @@ -700,21 +704,6 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): assert hass.states.get(climate_entity_entrada).state == "auto" -async def test_get_all_home_ids(): - """Test extracting all home ids returned by NetAtmo API.""" - # Test with backend returning no data - assert climate.get_all_home_ids(None) == [] - - # Test with fake data - home_data = Mock() - home_data.homes = { - "123": {"id": "123", "name": "Home 1", "modules": [], "therm_schedules": []}, - "987": {"id": "987", "name": "Home 2", "modules": [], "therm_schedules": []}, - } - expected = ["123", "987"] - assert climate.get_all_home_ids(home_data) == expected - - async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): @@ -723,7 +712,7 @@ async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -761,7 +750,7 @@ async def test_webhook_set_point(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Fake backend response for valve being turned on response = { diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2d0c43ac3f6..418854a61a2 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -136,7 +136,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) # Assert webhook is established successfully - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) assert hass.states.get(climate_entity_livingroom).state == "heat" @@ -440,7 +440,6 @@ async def test_setup_component_invalid_token(hass, config_entry): """Test handling of invalid token.""" async def fake_ensure_valid_token(*args, **kwargs): - print("fake_ensure_valid_token") raise aiohttp.ClientResponseError( request_info=aiohttp.client.RequestInfo( url="http://example.com", diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index f0e7cde7359..de357ffda89 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -38,7 +38,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a # Test setting a different schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( SELECT_DOMAIN, @@ -51,7 +51,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a ) await hass.async_block_till_done() mock_switch_home_schedule.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", schedule_id="591b54a2764ff4d50d8b5795" + schedule_id="591b54a2764ff4d50d8b5795" ) # Fake backend response changing schedule From da2fb17d9469a4b3914f94a11270aed899b5e225 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Dec 2021 23:55:06 -1000 Subject: [PATCH 1207/1452] Update flux_led for upstream strict typing (#60800) - Bump library to 0.25.10 - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.25.2...0.25.10 - This is a squashed version of #60554 since that one keeps failing to restore the python env on 3.9 --- homeassistant/components/flux_led/__init__.py | 32 ++++---- .../components/flux_led/config_flow.py | 78 +++++++++++++------ homeassistant/components/flux_led/entity.py | 4 +- homeassistant/components/flux_led/light.py | 21 ++--- .../components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/util.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 48 +++++++----- tests/components/flux_led/test_config_flow.py | 5 +- tests/components/flux_led/test_init.py | 3 +- tests/components/flux_led/test_light.py | 66 ++++++++++++---- tests/components/flux_led/test_number.py | 14 ++-- tests/components/flux_led/test_switch.py | 2 +- 14 files changed, 181 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index b37a3640db3..7ea86af2e9d 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -4,12 +4,13 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging -from typing import Any, Final, cast +from typing import Any, Final from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.aioscanner import AIOBulbScanner from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION +from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry @@ -50,33 +51,36 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: @callback -def async_name_from_discovery(device: dict[str, Any]) -> str: +def async_name_from_discovery(device: FluxLEDDiscovery) -> str: """Convert a flux_led discovery to a human readable name.""" - if (mac := device.get(ATTR_ID)) is None: - return cast(str, device[ATTR_IPADDR]) - short_mac = mac[-6:] - if device.get(ATTR_MODEL_DESCRIPTION): + mac_address = device[ATTR_ID] + if mac_address is None: + return device[ATTR_IPADDR] + short_mac = mac_address[-6:] + if device[ATTR_MODEL_DESCRIPTION]: return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" return f"{device[ATTR_MODEL]} {short_mac}" @callback def async_update_entry_from_discovery( - hass: HomeAssistant, entry: config_entries.ConfigEntry, device: dict[str, Any] + hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery ) -> None: """Update a config entry from a flux_led discovery.""" name = async_name_from_discovery(device) + mac_address = device[ATTR_ID] + assert mac_address is not None hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_NAME: name}, title=name, - unique_id=dr.format_mac(device[ATTR_ID]), + unique_id=dr.format_mac(mac_address), ) async def async_discover_devices( hass: HomeAssistant, timeout: int, address: str | None = None -) -> list[dict[str, str]]: +) -> list[FluxLEDDiscovery]: """Discover flux led devices.""" domain_data = hass.data.setdefault(DOMAIN, {}) if FLUX_LED_DISCOVERY_LOCK not in domain_data: @@ -84,9 +88,7 @@ async def async_discover_devices( async with domain_data[FLUX_LED_DISCOVERY_LOCK]: scanner = AIOBulbScanner() try: - discovered: list[dict[str, str]] = await scanner.async_scan( - timeout=timeout, address=address - ) + discovered = await scanner.async_scan(timeout=timeout, address=address) except OSError as ex: _LOGGER.debug("Scanning failed with error: %s", ex) return [] @@ -96,7 +98,7 @@ async def async_discover_devices( async def async_discover_device( hass: HomeAssistant, host: str -) -> dict[str, str] | None: +) -> FluxLEDDiscovery | None: """Direct discovery at a single ip instead of broadcast.""" # If we are missing the unique_id we should be able to fetch it # from the device by doing a directed discovery at the host only @@ -109,7 +111,7 @@ async def async_discover_device( @callback def async_trigger_discovery( hass: HomeAssistant, - discovered_devices: list[dict[str, Any]], + discovered_devices: list[FluxLEDDiscovery], ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: @@ -117,7 +119,7 @@ def async_trigger_discovery( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DISCOVERY}, - data=device, + data={**device}, ) ) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 7a161c56826..ab39e5b8ace 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -2,9 +2,10 @@ from __future__ import annotations import logging -from typing import Any, Final +from typing import Any, Final, cast from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION +from flux_led.scanner import FluxLEDDiscovery import voluptuous as vol from homeassistant import config_entries @@ -48,8 +49,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovered_devices: dict[str, dict[str, Any]] = {} - self._discovered_device: dict[str, Any] = {} + self._discovered_devices: dict[str, FluxLEDDiscovery] = {} + self._discovered_device: FluxLEDDiscovery | None = None @staticmethod @callback @@ -84,24 +85,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" - self._discovered_device = { - ATTR_IPADDR: discovery_info.ip, - ATTR_MODEL: discovery_info.hostname, - ATTR_ID: discovery_info.macaddress.replace(":", ""), - } + self._discovered_device = FluxLEDDiscovery( + ipaddr=discovery_info.ip, + model=discovery_info.hostname, + id=discovery_info.macaddress.replace(":", ""), + model_num=None, + version_num=None, + firmware_date=None, + model_info=None, + model_description=None, + ) return await self._async_handle_discovery() async def async_step_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle discovery.""" - self._discovered_device = discovery_info + self._discovered_device = cast(FluxLEDDiscovery, discovery_info) return await self._async_handle_discovery() async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device - mac = dr.format_mac(device[ATTR_ID]) + assert device is not None + mac_address = device[ATTR_ID] + assert mac_address is not None + mac = dr.format_mac(mac_address) host = device[ATTR_IPADDR] await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) @@ -113,13 +122,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == host: return self.async_abort(reason="already_in_progress") - if not device.get(ATTR_MODEL_DESCRIPTION): + if not device[ATTR_MODEL_DESCRIPTION]: try: - device = await self._async_try_connect(host) + device = await self._async_try_connect( + host, device[ATTR_ID], device[ATTR_MODEL] + ) except FLUX_LED_EXCEPTIONS: return self.async_abort(reason="cannot_connect") else: - if device.get(ATTR_MODEL_DESCRIPTION): + if device[ATTR_MODEL_DESCRIPTION]: self._discovered_device = device return await self.async_step_discovery_confirm() @@ -127,14 +138,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + mac_address = device[ATTR_ID] + assert mac_address is not None if user_input is not None: return self._async_create_entry_from_device(self._discovered_device) self._set_confirm_only() - device = self._discovered_device placeholders = { - "model": device.get(ATTR_MODEL_DESCRIPTION, device[ATTR_MODEL]), - "id": device[ATTR_ID][-6:], + "model": device[ATTR_MODEL_DESCRIPTION] or device[ATTR_MODEL], + "id": mac_address[-6:], "ipaddr": device[ATTR_IPADDR], } self.context["title_placeholders"] = placeholders @@ -143,7 +157,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry_from_device(self, device: dict[str, Any]) -> FlowResult: + def _async_create_entry_from_device(self, device: FluxLEDDiscovery) -> FlowResult: """Create a config entry from a device.""" self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]}) name = async_name_from_discovery(device) @@ -164,13 +178,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not (host := user_input[CONF_HOST]): return await self.async_step_pick_device() try: - device = await self._async_try_connect(host) + device = await self._async_try_connect(host, None, None) except FLUX_LED_EXCEPTIONS: errors["base"] = "cannot_connect" else: - if device[ATTR_ID]: + mac_address = device[ATTR_ID] + if mac_address is not None: await self.async_set_unique_id( - dr.format_mac(device[ATTR_ID]), raise_on_progress=False + dr.format_mac(mac_address), raise_on_progress=False ) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self._async_create_entry_from_device(device) @@ -198,9 +213,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovered_devices = await async_discover_devices( self.hass, DISCOVER_SCAN_TIMEOUT ) - self._discovered_devices = { - dr.format_mac(device[ATTR_ID]): device for device in discovered_devices - } + self._discovered_devices = {} + for device in discovered_devices: + mac_address = device[ATTR_ID] + assert mac_address is not None + self._discovered_devices[dr.format_mac(mac_address)] = device devices_name = { mac: f"{async_name_from_discovery(device)} ({device[ATTR_IPADDR]})" for mac, device in self._discovered_devices.items() @@ -215,7 +232,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), ) - async def _async_try_connect(self, host: str) -> dict[str, Any]: + async def _async_try_connect( + self, host: str, mac_address: str | None, model: str | None + ) -> FluxLEDDiscovery: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: host}) if device := await async_discover_device(self.hass, host): @@ -225,7 +244,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await bulb.async_setup(lambda: None) finally: await bulb.async_stop() - return {ATTR_ID: None, ATTR_MODEL: None, ATTR_IPADDR: host} + return FluxLEDDiscovery( + ipaddr=host, + model=model, + id=mac_address, + model_num=bulb.model_num, + version_num=bulb.version_num, + firmware_date=None, + model_info=None, + model_description=bulb.model_data.description, + ) class OptionsFlow(config_entries.OptionsFlow): diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index bab9a24bfe5..f4425f3b2a2 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any, cast +from typing import Any from flux_led.aiodevice import AIOWifiLedBulb @@ -72,7 +72,7 @@ class FluxOnOffEntity(FluxEntity): @property def is_on(self) -> bool: """Return true if device is on.""" - return cast(bool, self._device.is_on) + return self._device.is_on async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified device on.""" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index cf43a90bf22..fafa6a0b22e 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -3,7 +3,7 @@ from __future__ import annotations import ast import logging -from typing import Any, Final, cast +from typing import Any, Final from flux_led.const import ATTR_ID, ATTR_IPADDR from flux_led.utils import ( @@ -244,7 +244,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return cast(int, self._device.brightness) + return self._device.brightness @property def color_temp(self) -> int: @@ -254,20 +254,17 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value.""" - rgb: tuple[int, int, int] = self._device.rgb_unscaled - return rgb + return self._device.rgb_unscaled @property def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value.""" - rgbw: tuple[int, int, int, int] = self._device.rgbw - return rgbw + return self._device.rgbw @property def rgbww_color(self) -> tuple[int, int, int, int, int]: """Return the rgbww aka rgbcw color value.""" - rgbcw: tuple[int, int, int, int, int] = self._device.rgbcw - return rgbcw + return self._device.rgbcw @property def color_mode(self) -> str: @@ -279,10 +276,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def effect(self) -> str | None: """Return the current effect.""" - effect = self._device.effect - if effect is None: - return None - return cast(str, effect) + return self._device.effect async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" @@ -353,7 +347,8 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): return # Handle switch to RGB Color Mode if rgb := kwargs.get(ATTR_RGB_COLOR): - await self._device.async_set_levels(*rgb, brightness=brightness) + red, green, blue = rgb + await self._device.async_set_levels(red, green, blue, brightness=brightness) return # Handle switch to RGBW Color Mode if rgbw := kwargs.get(ATTR_RGBW_COLOR): diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index fcd66521153..91de8cb5b7d 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.2"], + "requirements": ["flux_led==0.25.10"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 8a089c9ebb6..774ae1aaa53 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -18,8 +18,12 @@ def _hass_color_modes(device: AIOWifiLedBulb) -> set[str]: return {_flux_color_mode_to_hass(mode, color_modes) for mode in color_modes} -def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) -> str: +def _flux_color_mode_to_hass( + flux_color_mode: str | None, flux_color_modes: set[str] +) -> str: """Map the flux color mode to Home Assistant color mode.""" + if flux_color_mode is None: + return COLOR_MODE_ONOFF if flux_color_mode == FLUX_COLOR_MODE_DIM: if len(flux_color_modes) > 1: return COLOR_MODE_WHITE diff --git a/requirements_all.txt b/requirements_all.txt index 3c4fcfc431b..8e5bfdb2d43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.2 +flux_led==0.25.10 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b63f53d759..4e41729ebd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.2 +flux_led==0.25.10 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 47aeb6be491..5b39c5656f6 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -2,20 +2,19 @@ from __future__ import annotations import asyncio +import datetime from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.const import ( - ATTR_ID, - ATTR_IPADDR, - ATTR_MODEL, - ATTR_MODEL_DESCRIPTION, COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, ) +from flux_led.models_db import MODEL_MAP from flux_led.protocol import LEDENETRawState +from flux_led.scanner import FluxLEDDiscovery from homeassistant.components import dhcp from homeassistant.core import HomeAssistant @@ -23,14 +22,14 @@ from homeassistant.core import HomeAssistant MODULE = "homeassistant.components.flux_led" MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" IP_ADDRESS = "127.0.0.1" +MODEL_NUM_HEX = "0x35" MODEL = "AZ120444" -MODEL_DESCRIPTION = "RGBW Controller" +MODEL_DESCRIPTION = "Bulb RGBCW" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" FLUX_MAC_ADDRESS = "aabbccddeeff" SHORT_MAC_ADDRESS = "ddeeff" DEFAULT_ENTRY_TITLE = f"{MODEL_DESCRIPTION} {SHORT_MAC_ADDRESS}" -DEFAULT_ENTRY_TITLE_PARTIAL = f"{MODEL} {SHORT_MAC_ADDRESS}" DHCP_DISCOVERY = dhcp.DhcpServiceInfo( @@ -38,17 +37,26 @@ DHCP_DISCOVERY = dhcp.DhcpServiceInfo( ip=IP_ADDRESS, macaddress=MAC_ADDRESS, ) -FLUX_DISCOVERY_PARTIAL = { - ATTR_IPADDR: IP_ADDRESS, - ATTR_MODEL: MODEL, - ATTR_ID: FLUX_MAC_ADDRESS, -} -FLUX_DISCOVERY = { - ATTR_IPADDR: IP_ADDRESS, - ATTR_MODEL: MODEL, - ATTR_ID: FLUX_MAC_ADDRESS, - ATTR_MODEL_DESCRIPTION: MODEL_DESCRIPTION, -} +FLUX_DISCOVERY_PARTIAL = FluxLEDDiscovery( + ipaddr=IP_ADDRESS, + model=MODEL, + id=FLUX_MAC_ADDRESS, + model_num=None, + version_num=None, + firmware_date=None, + model_info=None, + model_description=None, +) +FLUX_DISCOVERY = FluxLEDDiscovery( + ipaddr=IP_ADDRESS, + model=MODEL, + id=FLUX_MAC_ADDRESS, + model_num=0x25, + version_num=0x04, + firmware_date=datetime.date(2021, 5, 5), + model_info=MODEL, + model_description=MODEL_DESCRIPTION, +) def _mocked_bulb() -> AIOWifiLedBulb: @@ -85,9 +93,10 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.brightness = 128 bulb.model_num = 0x35 + bulb.model_data = MODEL_MAP[0x35] bulb.effect = None bulb.speed = 50 - bulb.model = "Smart Bulb (0x35)" + bulb.model = "Bulb RGBCW (0x35)" bulb.version_num = 8 bulb.speed_adjust_off = True bulb.rgbwcapable = True @@ -112,7 +121,8 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.async_turn_off = AsyncMock() switch.async_turn_on = AsyncMock() switch.model_num = 0x97 - switch.model = "Smart Switch (0x97)" + switch.model_data = MODEL_MAP[0x97] + switch.model = "Switch (0x97)" switch.version_num = 0x97 switch.raw_state = LEDENETRawState( 0, 0x97, 0, 0x61, 0x97, 50, 255, 0, 0, 50, 8, 0, 0, 0 diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 06b47dd2788..af84d3561f7 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -29,7 +29,6 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( DEFAULT_ENTRY_TITLE, - DEFAULT_ENTRY_TITLE_PARTIAL, DHCP_DISCOVERY, FLUX_DISCOVERY, IP_ADDRESS, @@ -428,7 +427,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert result2["type"] == "create_entry" assert result2["data"] == { CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE_PARTIAL, + CONF_NAME: DEFAULT_ENTRY_TITLE, } assert mock_async_setup.called assert mock_async_setup_entry.called @@ -509,4 +508,4 @@ async def test_options(hass: HomeAssistant): assert result2["type"] == "create_entry" assert result2["data"] == user_input assert result2["data"] == config_entry.options - assert hass.states.get("light.rgbw_controller_ddeeff") is not None + assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index abb671da9c2..23a238fa812 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -15,7 +15,6 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, - DEFAULT_ENTRY_TITLE_PARTIAL, FLUX_DISCOVERY, FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, @@ -75,7 +74,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: "discovery,title", [ (FLUX_DISCOVERY, DEFAULT_ENTRY_TITLE), - (FLUX_DISCOVERY_PARTIAL, DEFAULT_ENTRY_TITLE_PARTIAL), + (FLUX_DISCOVERY_PARTIAL, "AZ120444 ddeeff"), ], ) async def test_config_entry_fills_unique_id_with_directed_discovery( diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index e66492d2f43..4f401197173 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -84,7 +84,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS state = hass.states.get(entity_id) @@ -104,7 +104,7 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS state = hass.states.get(entity_id) @@ -136,7 +136,7 @@ async def test_light_no_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id) is None state = hass.states.get(entity_id) @@ -194,7 +194,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -300,7 +300,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -419,7 +419,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -521,7 +521,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -632,7 +632,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -772,7 +772,7 @@ async def test_white_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -806,6 +806,46 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb.async_set_brightness.reset_mock() +async def test_no_color_modes(hass: HomeAssistant) -> None: + """Test a light that has no color modes defined in the database.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.mode = "ww" + bulb.protocol = None + bulb.color_modes = set() + bulb.color_mode = None + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_COLOR_MODE] == "onoff" + assert ATTR_EFFECT_LIST in attributes # single channel now supports effects + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + await async_mock_device_turn_off(hass, bulb) + + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() + + async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: """Test an rgb light with a custom effect.""" config_entry = MockConfigEntry( @@ -827,7 +867,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -909,7 +949,7 @@ async def test_rgb_light_custom_effects_invalid_colors( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -938,7 +978,7 @@ async def test_rgb_light_custom_effect_via_service( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -1083,7 +1123,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.rgbw_controller_ddeeff" + entity_id = "light.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index c6167ebcedc..11df6daae4a 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -45,7 +45,7 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "number.rgbw_controller_ddeeff_effect_speed" + entity_id = "number.bulb_rgbcw_ddeeff_effect_speed" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS @@ -70,8 +70,8 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.rgbw_controller_ddeeff" - number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" + light_entity_id = "light.bulb_rgbcw_ddeeff" + number_entity_id = "number.bulb_rgbcw_ddeeff_effect_speed" with pytest.raises(HomeAssistantError): await hass.services.async_call( NUMBER_DOMAIN, @@ -135,8 +135,8 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.rgbw_controller_ddeeff" - number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" + light_entity_id = "light.bulb_rgbcw_ddeeff" + number_entity_id = "number.bulb_rgbcw_ddeeff_effect_speed" state = hass.states.get(light_entity_id) assert state.state == STATE_ON @@ -192,8 +192,8 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: await async_mock_device_turn_on(hass, bulb) - light_entity_id = "light.rgbw_controller_ddeeff" - number_entity_id = "number.rgbw_controller_ddeeff_effect_speed" + light_entity_id = "light.bulb_rgbcw_ddeeff" + number_entity_id = "number.bulb_rgbcw_ddeeff_effect_speed" state = hass.states.get(light_entity_id) assert state.state == STATE_ON diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index b3f27c28b5e..852e1efd49e 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -39,7 +39,7 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "switch.rgbw_controller_ddeeff" + entity_id = "switch.bulb_rgbcw_ddeeff" state = hass.states.get(entity_id) assert state.state == STATE_ON From f2f660289072e0ee1138f20ab1900c6cd729cf69 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 2 Dec 2021 11:05:22 +0100 Subject: [PATCH 1208/1452] Add support for Ohmpilots to Fronius integration (#60765) * add support for Fronius Ohmpilot * disable the debugger --- homeassistant/components/fronius/__init__.py | 11 +++ .../components/fronius/coordinator.py | 14 ++++ homeassistant/components/fronius/sensor.py | 71 +++++++++++++++++++ tests/components/fronius/test_sensor.py | 14 +++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 2b863fbb505..cf648d3b613 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -22,6 +22,7 @@ from .coordinator import ( FroniusInverterUpdateCoordinator, FroniusLoggerUpdateCoordinator, FroniusMeterUpdateCoordinator, + FroniusOhmpilotUpdateCoordinator, FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) @@ -83,6 +84,7 @@ class FroniusSolarNet: self.inverter_coordinators: list[FroniusInverterUpdateCoordinator] = [] self.logger_coordinator: FroniusLoggerUpdateCoordinator | None = None self.meter_coordinator: FroniusMeterUpdateCoordinator | None = None + self.ohmpilot_coordinator: FroniusOhmpilotUpdateCoordinator | None = None self.power_flow_coordinator: FroniusPowerFlowUpdateCoordinator | None = None self.storage_coordinator: FroniusStorageUpdateCoordinator | None = None @@ -121,6 +123,15 @@ class FroniusSolarNet: ) ) + self.ohmpilot_coordinator = await self._init_optional_coordinator( + FroniusOhmpilotUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_ohmpilot_{self.host}", + ) + ) + self.power_flow_coordinator = await self._init_optional_coordinator( FroniusPowerFlowUpdateCoordinator( hass=self.hass, diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 7a8d156dd65..e89f828f47d 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -22,6 +22,7 @@ from .sensor import ( INVERTER_ENTITY_DESCRIPTIONS, LOGGER_ENTITY_DESCRIPTIONS, METER_ENTITY_DESCRIPTIONS, + OHMPILOT_ENTITY_DESCRIPTIONS, POWER_FLOW_ENTITY_DESCRIPTIONS, STORAGE_ENTITY_DESCRIPTIONS, ) @@ -158,6 +159,19 @@ class FroniusMeterUpdateCoordinator(FroniusCoordinatorBase): return data["meters"] # type: ignore[no-any-return] +class FroniusOhmpilotUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius Ohmpilots and keep track of seen conditions.""" + + default_interval = timedelta(minutes=1) + error_interval = timedelta(minutes=10) + valid_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_system_ohmpilot_data() + return data["ohmpilots"] # type: ignore[no-any-return] + + class FroniusPowerFlowUpdateCoordinator(FroniusCoordinatorBase): """Query Fronius power flow endpoint and keep track of seen conditions.""" diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index fa2a33ca7ea..a20f49962a5 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -52,6 +52,7 @@ if TYPE_CHECKING: FroniusInverterUpdateCoordinator, FroniusLoggerUpdateCoordinator, FroniusMeterUpdateCoordinator, + FroniusOhmpilotUpdateCoordinator, FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) @@ -110,6 +111,10 @@ async def async_setup_entry( solar_net.meter_coordinator.add_entities_for_seen_keys( async_add_entities, MeterSensor ) + if solar_net.ohmpilot_coordinator is not None: + solar_net.ohmpilot_coordinator.add_entities_for_seen_keys( + async_add_entities, OhmpilotSensor + ) if solar_net.power_flow_coordinator is not None: solar_net.power_flow_coordinator.add_entities_for_seen_keys( async_add_entities, PowerFlowSensor @@ -510,6 +515,45 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), ] +OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="energy_real_ac_consumed", + name="Energy consumed", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power_real_ac", + name="Power", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="temperature_channel_1", + name="Temperature Channel 1", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="error_code", + name="Error code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="state_code", + name="State code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="state_message", + name="State message", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +] + POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="energy_day", @@ -760,6 +804,33 @@ class MeterSensor(_FroniusSensorEntity): self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' +class OhmpilotSensor(_FroniusSensorEntity): + """Defines a Fronius Ohmpilot sensor entity.""" + + entity_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS + + def __init__( + self, + coordinator: FroniusOhmpilotUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius meter sensor.""" + self._entity_id_prefix = f"ohmpilot_{solar_net_id}" + super().__init__(coordinator, key, solar_net_id) + device_data = self._device_data() + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_data["serial"]["value"])}, + manufacturer=device_data["manufacturer"]["value"], + model=f"{device_data['model']['value']} {device_data['hardware']['value']}", + name=device_data["model"]["value"], + sw_version=device_data["software"]["value"], + via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), + ) + self._attr_unique_id = f'{device_data["serial"]["value"]}-{key}' + + class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 82ec7afd8ea..cf371a47471 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -373,11 +373,11 @@ async def test_gen24_storage(hass, aioclient_mock): mock_responses(aioclient_mock, fixture_set="gen24_storage") config_entry = await setup_fronius_integration(hass, is_logger=False) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 31 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 63 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 68 # inverter 1 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) @@ -437,6 +437,16 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + # ohmpilot + assert_state( + "sensor.energy_real_ac_consumed_fronius_ohmpilot_0_http_fronius", 1233295.0 + ) + assert_state("sensor.power_real_ac_fronius_ohmpilot_0_http_fronius", 0.0) + assert_state("sensor.temperature_channel_1_fronius_ohmpilot_0_http_fronius", 38.9) + assert_state("sensor.state_code_fronius_ohmpilot_0_http_fronius", 0.0) + assert_state( + "sensor.state_message_fronius_ohmpilot_0_http_fronius", "Up and running" + ) # power_flow assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591) From c0fb1bffcedac3188d52a619ed3a78d930de584a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 2 Dec 2021 12:43:59 +0100 Subject: [PATCH 1209/1452] Use Enums in EntityDescriptions in Fronius (#60832) --- homeassistant/components/fronius/sensor.py | 265 ++++++++++----------- 1 file changed, 128 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index a20f49962a5..b2c6ecbb820 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -9,27 +9,18 @@ import voluptuous as vol from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_RESOURCE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_FREQUENCY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, @@ -38,7 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -130,111 +121,111 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="energy_day", name="Energy day", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_year", name="Energy year", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_total", name="Energy total", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="frequency_ac", name="Frequency AC", native_unit_of_measurement=FREQUENCY_HERTZ, - device_class=DEVICE_CLASS_FREQUENCY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac", name="AC Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_dc", name="DC current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), SensorEntityDescription( key="current_dc_2", name="DC Current 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), SensorEntityDescription( key="power_ac", name="AC power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_ac", name="AC voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_dc", name="DC voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), SensorEntityDescription( key="voltage_dc_2", name="DC voltage 2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), # device status entities SensorEntityDescription( key="inverter_state", name="Inverter state", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="error_code", name="Error code", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="status_code", name="Status code", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="led_state", name="LED state", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="led_color", name="LED color", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), ] @@ -243,19 +234,19 @@ LOGGER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="co2_factor", name="CO₂ factor", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:molecule-co2", ), SensorEntityDescription( key="cash_factor", name="Grid export tariff", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:cash-plus", ), SensorEntityDescription( key="delivery_factor", name="Grid import tariff", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:cash-minus", ), ] @@ -265,31 +256,31 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="current_ac_phase_1", name="Current AC phase 1", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac_phase_2", name="Current AC phase 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_ac_phase_3", name="Current AC phase 3", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_reactive_ac_consumed", name="Energy reactive AC consumed", native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:lightning-bolt-outline", entity_registry_enabled_default=False, ), @@ -297,7 +288,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="energy_reactive_ac_produced", name="Energy reactive AC produced", native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:lightning-bolt-outline", entity_registry_enabled_default=False, ), @@ -305,49 +296,49 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="energy_real_ac_minus", name="Energy real AC minus", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_real_ac_plus", name="Energy real AC plus", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_real_consumed", name="Energy real consumed", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_real_produced", name="Energy real produced", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="frequency_phase_average", name="Frequency phase average", native_unit_of_measurement=FREQUENCY_HERTZ, - device_class=DEVICE_CLASS_FREQUENCY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="meter_location", name="Meter location", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_apparent_phase_1", name="Power apparent phase 1", native_unit_of_measurement=POWER_VOLT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -355,7 +346,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_apparent_phase_2", name="Power apparent phase 2", native_unit_of_measurement=POWER_VOLT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -363,7 +354,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_apparent_phase_3", name="Power apparent phase 3", native_unit_of_measurement=POWER_VOLT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -371,42 +362,42 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_apparent", name="Power apparent", native_unit_of_measurement=POWER_VOLT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_1", name="Power factor phase 1", - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_2", name="Power factor phase 2", - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_3", name="Power factor phase 3", - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor", name="Power factor", - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_reactive_phase_1", name="Power reactive phase 1", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -414,7 +405,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_reactive_phase_2", name="Power reactive phase 2", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -422,7 +413,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_reactive_phase_3", name="Power reactive phase 3", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -430,7 +421,7 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_reactive", name="Power reactive", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, ), @@ -438,79 +429,79 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="power_real_phase_1", name="Power real phase 1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real_phase_2", name="Power real phase 2", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real_phase_3", name="Power real phase 3", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_real", name="Power real", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_ac_phase_1", name="Voltage AC phase 1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_2", name="Voltage AC phase 2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_3", name="Voltage AC phase 3", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_12", name="Voltage AC phase 1-2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_23", name="Voltage AC phase 2-3", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_ac_phase_to_phase_31", name="Voltage AC phase 3-1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), ] @@ -520,37 +511,37 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="energy_real_ac_consumed", name="Energy consumed", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power_real_ac", name="Power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature_channel_1", name="Temperature Channel 1", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="error_code", name="Error code", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="state_code", name="State code", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="state_message", name="State message", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ] @@ -559,71 +550,71 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="energy_day", name="Energy day", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_year", name="Energy year", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_total", name="Energy total", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="meter_mode", name="Mode", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_battery", name="Power battery", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_grid", name="Power grid", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_load", name="Power load", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_photovoltaics", name="Power photovoltaics", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="relative_autonomy", name="Relative autonomy", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:home-circle-outline", ), SensorEntityDescription( key="relative_self_consumption", name="Relative self consumption", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:solar-power", ), ] @@ -633,36 +624,36 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="capacity_maximum", name="Capacity maximum", native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="capacity_designed", name="Capacity designed", native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="current_dc", name="Current DC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), SensorEntityDescription( key="voltage_dc", name="Voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", ), SensorEntityDescription( key="voltage_dc_maximum_cell", name="Voltage DC maximum cell", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", entity_registry_enabled_default=False, ), @@ -670,8 +661,8 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="voltage_dc_minimum_cell", name="Voltage DC minimum cell", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:current-dc", entity_registry_enabled_default=False, ), @@ -679,15 +670,15 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ key="state_of_charge", name="State of charge", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature_cell", name="Temperature cell", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ] From c85bb27d0d6609f9cea1344242ca2f253c97da3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 14:26:45 +0100 Subject: [PATCH 1210/1452] Teach state trigger about entity registry ids (#60271) * Teach state trigger about entity registry ids * Tweak * Add tests * Tweak tests * Fix tests * Resolve entity ids during config validation * Update device_triggers * Fix mistake * Tweak trigger validator to ensure we don't modify the original config * Add index from entry id to entry * Update scaffold * Pre-compile UUID regex * Address review comment * Tweak mock_registry * Tweak * Apply suggestion from code review --- .../alarm_control_panel/device_trigger.py | 2 +- .../binary_sensor/device_trigger.py | 2 +- .../components/button/device_trigger.py | 4 +- .../components/climate/device_trigger.py | 4 +- .../components/cover/device_trigger.py | 4 +- .../device_automation/toggle_entity.py | 2 +- .../homeassistant/triggers/state.py | 35 ++++-- .../components/lock/device_trigger.py | 2 +- .../components/media_player/device_trigger.py | 2 +- .../components/select/device_trigger.py | 4 +- .../components/vacuum/device_trigger.py | 2 +- .../components/zwave_js/device_trigger.py | 2 +- homeassistant/helpers/config_validation.py | 37 +++++- homeassistant/helpers/entity_registry.py | 108 ++++++++++++------ .../integration/device_trigger.py | 10 +- tests/common.py | 7 +- .../homeassistant/triggers/test_state.py | 59 ++++++++++ tests/helpers/test_config_validation.py | 31 ++++- tests/helpers/test_entity_registry.py | 58 ++++++++++ tests/helpers/test_script.py | 23 +++- 20 files changed, 324 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 92c73b07bbd..9eea745862a 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -157,7 +157,7 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 72cd885d467..0f2c7a836a2 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -220,7 +220,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py index 2005bc82194..6d4692234f7 100644 --- a/homeassistant/components/button/device_trigger.py +++ b/homeassistant/components/button/device_trigger.py @@ -11,8 +11,8 @@ from homeassistant.components.automation import ( ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers.state import ( - TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA, async_attach_trigger as async_attach_state_trigger, + async_validate_trigger_config as async_validate_state_trigger_config, ) from homeassistant.const import ( CONF_DEVICE_ID, @@ -67,7 +67,7 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], } - state_config = STATE_TRIGGER_SCHEMA(state_config) + state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 05212e6ab99..3a9e0e45900 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -131,7 +131,9 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config( + hass, state_config + ) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index f4a2f4443d1..b9a0aefb7a2 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -170,7 +170,9 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config( + hass, state_config + ) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index a1ee84da2fb..f9f7555eeb6 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -164,7 +164,7 @@ async def async_attach_trigger( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index f1e2bbf2c09..e16416c2f13 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -3,20 +3,24 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + template, +) from homeassistant.helpers.event import ( Event, async_track_same_state, async_track_state_change_event, process_state_match, ) +from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -30,7 +34,7 @@ CONF_TO = "to" BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "state", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids, vol.Optional(CONF_FOR): cv.positive_time_period_template, vol.Optional(CONF_ATTRIBUTE): cv.match_all, } @@ -52,17 +56,26 @@ TRIGGER_ATTRIBUTE_SCHEMA = BASE_SCHEMA.extend( ) -def TRIGGER_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name - """Validate trigger.""" - if not isinstance(value, dict): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + if not isinstance(config, dict): raise vol.Invalid("Expected a dictionary") # We use this approach instead of vol.Any because # this gives better error messages. - if CONF_ATTRIBUTE in value: - return TRIGGER_ATTRIBUTE_SCHEMA(value) + if CONF_ATTRIBUTE in config: + config = TRIGGER_ATTRIBUTE_SCHEMA(config) + else: + config = TRIGGER_STATE_SCHEMA(config) - return TRIGGER_STATE_SCHEMA(value) + registry = er.async_get(hass) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + + return config async def async_attach_trigger( @@ -74,7 +87,7 @@ async def async_attach_trigger( platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - entity_id = config.get(CONF_ENTITY_ID) + entity_ids = config[CONF_ENTITY_ID] if (from_state := config.get(CONF_FROM)) is None: from_state = MATCH_ALL if (to_state := config.get(CONF_TO)) is None: @@ -196,7 +209,7 @@ async def async_attach_trigger( entity_ids=entity, ) - unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) + unsub = async_track_state_change_event(hass, entity_ids, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index cbdab7abb3d..75415bbf3e1 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -104,7 +104,7 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index 9aa75ab935c..d48a657794b 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -104,7 +104,7 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 6dabacf34e5..2c05b59c5d5 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -14,8 +14,8 @@ from homeassistant.components.homeassistant.triggers.state import ( CONF_FOR, CONF_FROM, CONF_TO, - TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA, async_attach_trigger as async_attach_state_trigger, + async_validate_trigger_config as async_validate_state_trigger_config, ) from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.const import ( @@ -84,7 +84,7 @@ async def async_attach_trigger( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = STATE_TRIGGER_SCHEMA(state_config) + state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index f4fdbcf972e..25a874a1e69 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -92,7 +92,7 @@ async def async_attach_trigger( } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - state_config = state_trigger.TRIGGER_SCHEMA(state_config) + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 368226d36a5..481fc429cb0 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -415,7 +415,7 @@ async def async_attach_trigger( else: raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") - state_config = state.TRIGGER_SCHEMA(state_config) + state_config = await state.async_validate_trigger_config(hass, state_config) return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2d38acafadf..8357746c2cd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Hashable +import contextlib from datetime import ( date as date_sys, datetime as datetime_sys, @@ -262,14 +263,34 @@ def entity_id(value: Any) -> str: raise vol.Invalid(f"Entity ID {value} is an invalid entity ID") -def entity_ids(value: str | list) -> list[str]: - """Validate Entity IDs.""" +def entity_id_or_uuid(value: Any) -> str: + """Validate Entity specified by entity_id or uuid.""" + with contextlib.suppress(vol.Invalid): + return entity_id(value) + with contextlib.suppress(vol.Invalid): + return fake_uuid4_hex(value) + raise vol.Invalid(f"Entity {value} is neither a valid entity ID nor a valid UUID") + + +def _entity_ids(value: str | list, allow_uuid: bool) -> list[str]: + """Help validate entity IDs or UUIDs.""" if value is None: raise vol.Invalid("Entity IDs can not be None") if isinstance(value, str): value = [ent_id.strip() for ent_id in value.split(",")] - return [entity_id(ent_id) for ent_id in value] + validator = entity_id_or_uuid if allow_uuid else entity_id + return [validator(ent_id) for ent_id in value] + + +def entity_ids(value: str | list) -> list[str]: + """Validate Entity IDs.""" + return _entity_ids(value, False) + + +def entity_ids_or_uuids(value: str | list) -> list[str]: + """Validate entities specified by entity IDs or UUIDs.""" + return _entity_ids(value, True) comp_entity_ids = vol.Any( @@ -682,6 +703,16 @@ def uuid4_hex(value: Any) -> str: return result.hex +_FAKE_UUID_4_HEX = re.compile(r"^[0-9a-f]{32}$") + + +def fake_uuid4_hex(value: Any) -> str: + """Validate a fake v4 UUID generated by random_uuid_hex.""" + if not _FAKE_UUID_4_HEX.match(value): + raise vol.Invalid("Invalid UUID") + return cast(str, value) # Pattern.match throws if input is not a string + + def ensure_list_csv(value: Any) -> list: """Ensure that input is a list or make one from comma-separated string.""" if isinstance(value, str): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 036f235e132..da8223dfec9 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -9,12 +9,13 @@ timer. """ from __future__ import annotations -from collections import OrderedDict +from collections import UserDict from collections.abc import Callable, Iterable, Mapping import logging from typing import TYPE_CHECKING, Any, cast import attr +import voluptuous as vol from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -161,14 +162,57 @@ class EntityRegistryStore(storage.Store): return await _async_migrate(old_major_version, old_minor_version, old_data) +class EntityRegistryItems(UserDict): + """Container for entity registry items, maps entity_id -> entry. + + Maintains two additional indexes: + - id -> entry + - (domain, platform, unique_id) -> entry + """ + + def __init__(self) -> None: + """Initialize the container.""" + super().__init__() + self._entry_ids: dict[str, RegistryEntry] = {} + self._index: dict[tuple[str, str, str], str] = {} + + def __setitem__(self, key: str, entry: RegistryEntry) -> None: + """Add an item.""" + if key in self: + old_entry = self[key] + del self._entry_ids[old_entry.id] + del self._index[(old_entry.domain, old_entry.platform, old_entry.unique_id)] + super().__setitem__(key, entry) + self._entry_ids.__setitem__(entry.id, entry) + self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id + + def __delitem__(self, key: str) -> None: + """Remove an item.""" + entry = self[key] + self._entry_ids.__delitem__(entry.id) + self._index.__delitem__((entry.domain, entry.platform, entry.unique_id)) + super().__delitem__(key) + + def __getitem__(self, key: str) -> RegistryEntry: + """Get an item.""" + return cast(RegistryEntry, super().__getitem__(key)) + + def get_entity_id(self, key: tuple[str, str, str]) -> str | None: + """Get entity_id from (domain, platform, unique_id).""" + return self._index.get(key) + + def get_entry(self, key: str) -> RegistryEntry | None: + """Get entry from id.""" + return self._entry_ids.get(key) + + class EntityRegistry: """Class to hold a registry of entities.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize the registry.""" self.hass = hass - self.entities: dict[str, RegistryEntry] - self._index: dict[tuple[str, str, str], str] = {} + self.entities: EntityRegistryItems self._store = EntityRegistryStore( hass, STORAGE_VERSION_MAJOR, @@ -218,7 +262,7 @@ class EntityRegistry: self, domain: str, platform: str, unique_id: str ) -> str | None: """Check if an entity_id is currently registered.""" - return self._index.get((domain, platform, unique_id)) + return self.entities.get_entity_id((domain, platform, unique_id)) @callback def async_generate_entity_id( @@ -320,7 +364,7 @@ class EntityRegistry: ): disabled_by = DISABLED_INTEGRATION - entity = RegistryEntry( + entry = RegistryEntry( area_id=area_id, capabilities=capabilities, config_entry_id=config_entry_id, @@ -336,7 +380,7 @@ class EntityRegistry: unique_id=unique_id, unit_of_measurement=unit_of_measurement, ) - self._register_entry(entity) + self.entities[entity_id] = entry _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) self.async_schedule_save() @@ -344,12 +388,12 @@ class EntityRegistry: EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": entity_id} ) - return entity + return entry @callback def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" - self._unregister_entry(self.entities[entity_id]) + self.entities.pop(entity_id) self.hass.bus.async_fire( EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": entity_id} ) @@ -513,9 +557,7 @@ class EntityRegistry: if not new_values: return old - self._remove_index(old) - new = attr.evolve(old, **new_values) - self._register_entry(new) + new = self.entities[entity_id] = attr.evolve(old, **new_values) self.async_schedule_save() @@ -539,7 +581,7 @@ class EntityRegistry: old_conf_load_func=load_yaml, old_conf_migrate_func=_async_migrate_yaml_to_json, ) - entities: dict[str, RegistryEntry] = OrderedDict() + entities = EntityRegistryItems() if data is not None: for entity in data["entities"]: @@ -571,7 +613,6 @@ class EntityRegistry: ) self.entities = entities - self._rebuild_index() @callback def async_schedule_save(self) -> None: @@ -626,25 +667,6 @@ class EntityRegistry: if area_id == entry.area_id: self._async_update_entity(entity_id, area_id=None) - def _register_entry(self, entry: RegistryEntry) -> None: - self.entities[entry.entity_id] = entry - self._add_index(entry) - - def _add_index(self, entry: RegistryEntry) -> None: - self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id - - def _unregister_entry(self, entry: RegistryEntry) -> None: - self._remove_index(entry) - del self.entities[entry.entity_id] - - def _remove_index(self, entry: RegistryEntry) -> None: - del self._index[(entry.domain, entry.platform, entry.unique_id)] - - def _rebuild_index(self) -> None: - self._index = {} - for entry in self.entities.values(): - self._add_index(entry) - @callback def async_get(hass: HomeAssistant) -> EntityRegistry: @@ -841,3 +863,25 @@ async def async_migrate_entries( if updates is not None: ent_reg.async_update_entity(entry.entity_id, **updates) + + +@callback +def async_resolve_entity_ids( + registry: EntityRegistry, entity_ids_or_uuids: list[str] +) -> list[str]: + """Resolve a list of entity ids or UUIDs to a list of entity ids.""" + + def resolve_entity(entity_id_or_uuid: str) -> str | None: + """Resolve an entity id or UUID to an entity id or None.""" + if valid_entity_id(entity_id_or_uuid): + return entity_id_or_uuid + if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: + raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}") + return entry.entity_id + + tmp = [ + resolved_item + for item in entity_ids_or_uuids + if (resolved_item := resolve_entity(item)) is not None + ] + return tmp diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 45c6adb4dcf..9082d27953a 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -10,7 +10,7 @@ from homeassistant.components.automation import ( AutomationTriggerInfo, ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA -from homeassistant.components.homeassistant.triggers import state +from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, @@ -86,11 +86,11 @@ async def async_attach_trigger( to_state = STATE_OFF state_config = { - state.CONF_PLATFORM: "state", + state_trigger.CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_TO: to_state, + state_trigger.CONF_TO: to_state, } - state_config = state.TRIGGER_SCHEMA(state_config) - return await state.async_attach_trigger( + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) + return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/tests/common.py b/tests/common.py index 19f0aaec44b..55c76e953cd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -440,8 +440,11 @@ def mock_component(hass, component): def mock_registry(hass, mock_entries=None): """Mock the Entity Registry.""" registry = entity_registry.EntityRegistry(hass) - registry.entities = mock_entries or OrderedDict() - registry._rebuild_index() + if mock_entries is None: + mock_entries = {} + registry.entities = entity_registry.EntityRegistryItems() + for key, entry in mock_entries.items(): + registry.entities[key] = entry hass.data[entity_registry.DATA_REGISTRY] = registry return registry diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index c86bb0cc879..026f096022b 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -8,6 +8,7 @@ import homeassistant.components.automation as automation from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -82,6 +83,64 @@ async def test_if_fires_on_entity_change(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_entity_change_uuid(hass, calls): + """Test for firing on entity change.""" + context = Context() + + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="beer" + ) + + assert entry.entity_id == "test.beer" + + hass.states.async_set("test.beer", "hello") + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "state", "entity_id": entry.id}, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + "id", + ) + ) + }, + }, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.beer", "world", context=context) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + assert calls[0].data["some"] == "state - test.beer - hello - world - None - 0" + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) + hass.states.async_set("test.beer", "planet") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_entity_change_with_from_filter(hass, calls): """Test for firing on entity change with filter.""" assert await async_setup_component( diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 4b8e4fe9e49..8327eb2e320 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -172,9 +172,10 @@ def test_entity_id(): assert schema("sensor.LIGHT") == "sensor.light" -def test_entity_ids(): +@pytest.mark.parametrize("validator", [cv.entity_ids, cv.entity_ids_or_uuids]) +def test_entity_ids(validator): """Test entity ID validation.""" - schema = vol.Schema(cv.entity_ids) + schema = vol.Schema(validator) options = ( "invalid_entity", @@ -194,6 +195,32 @@ def test_entity_ids(): assert schema("sensor.LIGHT, light.kitchen ") == ["sensor.light", "light.kitchen"] +def test_entity_ids_or_uuids(): + """Test entity ID validation.""" + schema = vol.Schema(cv.entity_ids_or_uuids) + + valid_uuid = "a266a680b608c32770e6c45bfe6b8411" + valid_uuid2 = "a266a680b608c32770e6c45bfe6b8412" + invalid_uuid_capital_letters = "A266A680B608C32770E6C45bfE6B8412" + options = ( + "invalid_uuid", + invalid_uuid_capital_letters, + f"{valid_uuid},invalid_uuid", + ["invalid_uuid"], + [valid_uuid, "invalid_uuid"], + [f"{valid_uuid},invalid_uuid"], + ) + for value in options: + with pytest.raises(vol.MultipleInvalid): + schema(value) + + options = ([], [valid_uuid], valid_uuid) + for value in options: + schema(value) + + assert schema(f"{valid_uuid}, {valid_uuid2} ") == [valid_uuid, valid_uuid2] + + def test_entity_domain(): """Test entity domain validation.""" schema = vol.Schema(cv.entity_domain("sensor")) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 3dc9cf775c4..0bd0abbc92f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -2,6 +2,7 @@ from unittest.mock import patch import pytest +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE @@ -1023,3 +1024,60 @@ async def test_entity_max_length_exceeded(hass, registry): assert exc_info.value.property_name == "generated_entity_id" assert exc_info.value.max_length == 255 assert exc_info.value.value == f"sensor.{long_entity_id_name}_2" + + +async def test_resolve_entity_ids(hass, registry): + """Test resolving entity IDs.""" + + entry1 = registry.async_get_or_create( + "light", "hue", "1234", suggested_object_id="beer" + ) + assert entry1.entity_id == "light.beer" + + entry2 = registry.async_get_or_create( + "light", "hue", "2345", suggested_object_id="milk" + ) + assert entry2.entity_id == "light.milk" + + expected = ["light.beer", "light.milk"] + assert er.async_resolve_entity_ids(registry, [entry1.id, entry2.id]) == expected + + expected = ["light.beer", "light.milk"] + assert er.async_resolve_entity_ids(registry, ["light.beer", entry2.id]) == expected + + with pytest.raises(vol.Invalid): + er.async_resolve_entity_ids(registry, ["light.beer", "bad_uuid"]) + + expected = ["light.unknown"] + assert er.async_resolve_entity_ids(registry, ["light.unknown"]) == expected + + with pytest.raises(vol.Invalid): + er.async_resolve_entity_ids(registry, ["unknown_uuid"]) + + +def test_entity_registry_items(): + """Test the EntityRegistryItems container.""" + entities = er.EntityRegistryItems() + assert entities.get_entity_id(("a", "b", "c")) is None + assert entities.get_entry("abc") is None + + entry1 = er.RegistryEntry("test.entity1", "1234", "hue") + entry2 = er.RegistryEntry("test.entity2", "2345", "hue") + entities["test.entity1"] = entry1 + entities["test.entity2"] = entry2 + + assert entities["test.entity1"] is entry1 + assert entities["test.entity2"] is entry2 + + assert entities.get_entity_id(("test", "hue", "1234")) is entry1.entity_id + assert entities.get_entry(entry1.id) is entry1 + assert entities.get_entity_id(("test", "hue", "2345")) is entry2.entity_id + assert entities.get_entry(entry2.id) is entry2 + + entities.pop("test.entity1") + del entities["test.entity2"] + + assert entities.get_entity_id(("test", "hue", "1234")) is None + assert entities.get_entry(entry1.id) is None + assert entities.get_entity_id(("test", "hue", "2345")) is None + assert entities.get_entry(entry2.id) is None diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 6c64b64d6de..23768cd95ce 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -748,6 +748,7 @@ async def test_wait_basic(hass, action_type): "to": "off", } sequence = cv.SCRIPT_SCHEMA(action) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, wait_alias) @@ -848,6 +849,7 @@ async def test_wait_basic_times_out(hass, action_type): "to": "off", } sequence = cv.SCRIPT_SCHEMA(action) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, wait_alias) timed_out = False @@ -904,6 +906,7 @@ async def test_multiple_runs_wait(hass, action_type): {"event": event, "event_data": {"value": 2}}, ] ) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script( hass, sequence, "Test Name", "test_domain", script_mode="parallel", max_runs=2 ) @@ -952,6 +955,7 @@ async def test_cancel_wait(hass, action_type): } } sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -1049,6 +1053,7 @@ async def test_wait_timeout(hass, caplog, timeout_param, action_type): action["timeout"] = timeout_param action["continue_on_timeout"] = True sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -1116,6 +1121,7 @@ async def test_wait_continue_on_timeout( if continue_on_timeout is not None: action["continue_on_timeout"] = continue_on_timeout sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -1287,6 +1293,7 @@ async def test_wait_variables_out(hass, mode, action_type): }, ] sequence = cv.SCRIPT_SCHEMA(sequence) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -1326,11 +1333,13 @@ async def test_wait_variables_out(hass, mode, action_type): async def test_wait_for_trigger_bad(hass, caplog): """Test bad wait_for_trigger.""" + sequence = cv.SCRIPT_SCHEMA( + {"wait_for_trigger": {"platform": "state", "entity_id": "sensor.abc"}} + ) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script( hass, - cv.SCRIPT_SCHEMA( - {"wait_for_trigger": {"platform": "state", "entity_id": "sensor.abc"}} - ), + sequence, "Test Name", "test_domain", ) @@ -1356,11 +1365,13 @@ async def test_wait_for_trigger_bad(hass, caplog): async def test_wait_for_trigger_generated_exception(hass, caplog): """Test bad wait_for_trigger.""" + sequence = cv.SCRIPT_SCHEMA( + {"wait_for_trigger": {"platform": "state", "entity_id": "sensor.abc"}} + ) + sequence = await script.async_validate_actions_config(hass, sequence) script_obj = script.Script( hass, - cv.SCRIPT_SCHEMA( - {"wait_for_trigger": {"platform": "state", "entity_id": "sensor.abc"}} - ), + sequence, "Test Name", "test_domain", ) From 49ebb27b447f47f27d6fe1ea3c2e9717eec31cab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 14:38:00 +0100 Subject: [PATCH 1211/1452] Run partial tests without coverage for Python 3.8 (#60827) --- .github/workflows/ci.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5bc99611abc..47dc4a2ca40 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -681,7 +681,7 @@ jobs: -p no:sugar \ tests - name: Run pytest (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.changes.outputs.test_full_suite == 'false' && matrix.python-version != '3.8' run: | . venv/bin/activate python3 -X dev -m pytest \ @@ -697,6 +697,20 @@ jobs: --durations-min=1 \ -p no:sugar \ tests/components/${{ matrix.group }} + - name: Run pytest (partially); no coverage + if: needs.changes.outputs.test_full_suite == 'false' && matrix.python-version == '3.8' + run: | + . venv/bin/activate + python3 -X dev -m pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + -n auto \ + -o console_output_style=count \ + --durations=0 \ + --durations-min=1 \ + -p no:sugar \ + tests/components/${{ matrix.group }} - name: Upload coverage to Codecov (full coverage) if: needs.changes.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v2.1.0 From 82f26392b4a985c29edde1a26e1f5757cc1ddc7f Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 2 Dec 2021 15:03:03 +0100 Subject: [PATCH 1212/1452] Bump xiaomi_miio dependency (#60807) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 0556b6f3ea5..757fca8be1f 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.4", "python-miio==0.5.9"], + "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 8e5bfdb2d43..79eb610833f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ python-kasa==0.4.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.9 +python-miio==0.5.9.1 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e41729ebd5..77be878fb87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1145,7 +1145,7 @@ python-juicenet==1.0.2 python-kasa==0.4.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.9 +python-miio==0.5.9.1 # homeassistant.components.nest python-nest==4.1.0 From bf6ca2527d414bb873cf0f45b8432938ab5d5418 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 15:58:00 +0100 Subject: [PATCH 1213/1452] Upgrade guppy3 to 3.1.2 (#60842) --- homeassistant/components/profiler/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json index b448e3d1793..6e6dde8df1b 100644 --- a/homeassistant/components/profiler/manifest.json +++ b/homeassistant/components/profiler/manifest.json @@ -2,7 +2,7 @@ "domain": "profiler", "name": "Profiler", "documentation": "https://www.home-assistant.io/integrations/profiler", - "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.0", "objgraph==3.4.1"], + "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.2", "objgraph==3.4.1"], "codeowners": ["@bdraco"], "quality_scale": "internal", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 79eb610833f..350b53c5a2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ growattServer==1.1.0 gstreamer-player==1.1.2 # homeassistant.components.profiler -guppy3==3.1.0 +guppy3==3.1.2 # homeassistant.components.stream ha-av==8.0.4-rc.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77be878fb87..a21eeaf9634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -476,7 +476,7 @@ greeneye_monitor==2.1 growattServer==1.1.0 # homeassistant.components.profiler -guppy3==3.1.0 +guppy3==3.1.2 # homeassistant.components.stream ha-av==8.0.4-rc.1 From c8781bbe3bd21846d9c0b235352943d4ab9f6629 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 2 Dec 2021 16:30:01 +0100 Subject: [PATCH 1214/1452] Simplify zwave_js USB discovery add-on form (#60845) --- .../components/zwave_js/config_flow.py | 36 ++++++++++--------- tests/components/zwave_js/test_config_flow.py | 7 ++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 023baf10bd4..7b59cce36a9 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -301,6 +301,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): super().__init__() self.use_addon = False self._title: str | None = None + self._usb_discovery = False @property def flow_manager(self) -> config_entries.ConfigEntriesFlowManager: @@ -387,6 +388,8 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({}), ) + self._usb_discovery = True + return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) async def async_step_manual( @@ -504,7 +507,8 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): self.s2_access_control_key = user_input[CONF_S2_ACCESS_CONTROL_KEY] self.s2_authenticated_key = user_input[CONF_S2_AUTHENTICATED_KEY] self.s2_unauthenticated_key = user_input[CONF_S2_UNAUTHENTICATED_KEY] - self.usb_path = user_input[CONF_USB_PATH] + if not self._usb_discovery: + self.usb_path = user_input[CONF_USB_PATH] new_addon_config = { **addon_config, @@ -534,21 +538,21 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): CONF_ADDON_S2_UNAUTHENTICATED_KEY, self.s2_unauthenticated_key or "" ) - data_schema = vol.Schema( - { - vol.Required(CONF_USB_PATH, default=usb_path): str, - vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, - vol.Optional( - CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key - ): str, - vol.Optional( - CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key - ): str, - vol.Optional( - CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key - ): str, - } - ) + schema = { + vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, + vol.Optional( + CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key + ): str, + vol.Optional(CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key): str, + vol.Optional( + CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key + ): str, + } + + if not self._usb_discovery: + schema = {vol.Required(CONF_USB_PATH, default=usb_path): str, **schema} + + data_schema = vol.Schema(schema) return self.async_show_form(step_id="configure_addon", data_schema=data_schema) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index d5829283ff2..3094d10b2bd 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -475,7 +475,6 @@ async def test_usb_discovery( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "usb_path": "/test", "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -488,7 +487,7 @@ async def test_usb_discovery( "core_zwave_js", { "options": { - "device": "/test", + "device": USB_DISCOVERY_INFO["device"], "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -516,7 +515,7 @@ async def test_usb_discovery( assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", - "usb_path": "/test", + "usb_path": USB_DISCOVERY_INFO["device"], "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -557,7 +556,6 @@ async def test_usb_discovery_addon_not_running( # Make sure the discovered usb device is preferred. data_schema = result["data_schema"] assert data_schema({}) == { - "usb_path": USB_DISCOVERY_INFO["device"], "s0_legacy_key": "", "s2_access_control_key": "", "s2_authenticated_key": "", @@ -567,7 +565,6 @@ async def test_usb_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "usb_path": USB_DISCOVERY_INFO["device"], "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", From 7019c524fabed5ba24d52f6dc392accbfff7a336 Mon Sep 17 00:00:00 2001 From: Matt <5032824+mdawsonuk@users.noreply.github.com> Date: Thu, 2 Dec 2021 15:36:41 +0000 Subject: [PATCH 1215/1452] Add configuration_url to Forecast.Solar integration (#60384) Co-authored-by: Franck Nijhof --- homeassistant/components/forecast_solar/sensor.py | 1 + tests/components/forecast_solar/test_config_flow.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 4efad600d66..6088da6e645 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -59,6 +59,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): manufacturer="Forecast.Solar", model=coordinator.data.account_type.value, name="Solar Production Forecast", + configuration_url="https://forecast.solar", ) @property diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index ac950d38b51..d175be986ca 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -61,6 +61,11 @@ async def test_options_flow( ) -> None: """Test config flow options.""" mock_config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.forecast_solar.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) From d6dff403c9ee251bd0f100029af9ae54bcb28996 Mon Sep 17 00:00:00 2001 From: Kyle Hildebrandt Date: Thu, 2 Dec 2021 10:37:29 -0500 Subject: [PATCH 1216/1452] Set _attr_is_on to True for avion on init (#60433) --- homeassistant/components/avion/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 0bf1787aac7..e8f42e6a816 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -68,6 +68,7 @@ class AvionLight(LightEntity): _attr_supported_features = SUPPORT_AVION_LED _attr_should_poll = False _attr_assumed_state = True + _attr_is_on = True def __init__(self, device): """Initialize the light.""" From 4107063a5a6ca8f68afe77de849359aaacc77480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 2 Dec 2021 19:02:29 +0200 Subject: [PATCH 1217/1452] Make entity registry disabled_by an enum (#60424) --- homeassistant/helpers/entity_platform.py | 8 +-- homeassistant/helpers/entity_registry.py | 84 ++++++++++++++++-------- tests/helpers/test_entity_registry.py | 13 ++++ 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d3eea95f545..d8cb8477f11 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -42,7 +42,7 @@ from . import ( service, ) from .device_registry import DeviceRegistry -from .entity_registry import DISABLED_DEVICE, DISABLED_INTEGRATION, EntityRegistry +from .entity_registry import EntityRegistry, RegistryEntryDisabler from .event import async_call_later, async_track_time_interval from .typing import ConfigType, DiscoveryInfoType @@ -502,9 +502,9 @@ class EntityPlatform: except RequiredParameterMissing: pass - disabled_by: str | None = None + disabled_by: RegistryEntryDisabler | None = None if not entity.entity_registry_enabled_default: - disabled_by = DISABLED_INTEGRATION + disabled_by = RegistryEntryDisabler.INTEGRATION entry = entity_registry.async_get_or_create( self.domain, @@ -526,7 +526,7 @@ class EntityPlatform: if device and device.disabled and not entry.disabled: entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=DISABLED_DEVICE + entry.entity_id, disabled_by=RegistryEntryDisabler.DEVICE ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index da8223dfec9..6614091341f 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -39,8 +39,10 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, storage from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.enum import StrEnum from homeassistant.util.yaml import load_yaml from .typing import UNDEFINED, UndefinedType @@ -53,11 +55,6 @@ DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) -DISABLED_CONFIG_ENTRY = "config_entry" -DISABLED_DEVICE = "device" -DISABLED_HASS = "hass" -DISABLED_INTEGRATION = "integration" -DISABLED_USER = "user" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 4 @@ -76,6 +73,24 @@ ENTITY_DESCRIBING_ATTRIBUTES = { } +class RegistryEntryDisabler(StrEnum): + """What disabled a registry entry.""" + + CONFIG_ENTRY = "config_entry" + DEVICE = "device" + HASS = "hass" + INTEGRATION = "integration" + USER = "user" + + +# DISABLED_* are deprecated, to be removed in 2022.3 +DISABLED_CONFIG_ENTRY = RegistryEntryDisabler.CONFIG_ENTRY.value +DISABLED_DEVICE = RegistryEntryDisabler.DEVICE.value +DISABLED_HASS = RegistryEntryDisabler.HASS.value +DISABLED_INTEGRATION = RegistryEntryDisabler.INTEGRATION.value +DISABLED_USER = RegistryEntryDisabler.USER.value + + @attr.s(slots=True, frozen=True) class RegistryEntry: """Entity Registry Entry.""" @@ -89,19 +104,7 @@ class RegistryEntry: device_class: str | None = attr.ib(default=None) device_id: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) - disabled_by: str | None = attr.ib( - default=None, - validator=attr.validators.in_( - ( - DISABLED_CONFIG_ENTRY, - DISABLED_DEVICE, - DISABLED_HASS, - DISABLED_INTEGRATION, - DISABLED_USER, - None, - ) - ), - ) + disabled_by: RegistryEntryDisabler | None = attr.ib(default=None) entity_category: str | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) @@ -311,7 +314,7 @@ class EntityRegistry: known_object_ids: Iterable[str] | None = None, suggested_object_id: str | None = None, # To disable an entity if it gets created - disabled_by: str | None = None, + disabled_by: RegistryEntryDisabler | None = None, # Data that we want entry to have area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, @@ -357,12 +360,22 @@ class EntityRegistry: domain, suggested_object_id or f"{platform}_{unique_id}", known_object_ids ) - if ( + if isinstance(disabled_by, str) and not isinstance( + disabled_by, RegistryEntryDisabler + ): + report( # type: ignore[unreachable] + "uses str for entity registry disabled_by. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "RegistryEntryDisabler instead", + error_if_core=False, + ) + disabled_by = RegistryEntryDisabler(disabled_by) + elif ( disabled_by is None and config_entry and config_entry.pref_disable_new_entities ): - disabled_by = DISABLED_INTEGRATION + disabled_by = RegistryEntryDisabler.INTEGRATION entry = RegistryEntry( area_id=area_id, @@ -429,7 +442,7 @@ class EntityRegistry: self, event.data["device_id"], include_disabled_entities=True ) for entity in entities: - if entity.disabled_by != DISABLED_DEVICE: + if entity.disabled_by is not RegistryEntryDisabler.DEVICE: continue self.async_update_entity(entity.entity_id, disabled_by=None) return @@ -441,7 +454,9 @@ class EntityRegistry: # Fetch entities which are not already disabled entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: - self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE) + self.async_update_entity( + entity.entity_id, disabled_by=RegistryEntryDisabler.DEVICE + ) @callback def async_update_entity( @@ -451,7 +466,7 @@ class EntityRegistry: area_id: str | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, entity_category: str | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, @@ -490,7 +505,7 @@ class EntityRegistry: config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, device_id: str | None | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, entity_category: str | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, @@ -508,6 +523,17 @@ class EntityRegistry: new_values = {} # Dict with new key/value pairs old_values = {} # Dict with old key/value pairs + if isinstance(disabled_by, str) and not isinstance( + disabled_by, RegistryEntryDisabler + ): + report( # type: ignore[unreachable] + "uses str for entity registry disabled_by. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "RegistryEntryDisabler instead", + error_if_core=False, + ) + disabled_by = RegistryEntryDisabler(disabled_by) + for attr_name, value in ( ("area_id", area_id), ("capabilities", capabilities), @@ -597,7 +623,9 @@ class EntityRegistry: config_entry_id=entity["config_entry_id"], device_class=entity["device_class"], device_id=entity["device_id"], - disabled_by=entity["disabled_by"], + disabled_by=RegistryEntryDisabler(entity["disabled_by"]) + if entity["disabled_by"] + else None, entity_category=entity["entity_category"], entity_id=entity["entity_id"], icon=entity["icon"], @@ -739,7 +767,7 @@ def async_config_entry_disabled_by_changed( if not config_entry.disabled_by: for entity in entities: - if entity.disabled_by != DISABLED_CONFIG_ENTRY: + if entity.disabled_by is not RegistryEntryDisabler.CONFIG_ENTRY: continue registry.async_update_entity(entity.entity_id, disabled_by=None) return @@ -749,7 +777,7 @@ def async_config_entry_disabled_by_changed( # Entity already disabled, do not overwrite continue registry.async_update_entity( - entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY + entity.entity_id, disabled_by=RegistryEntryDisabler.CONFIG_ENTRY ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 0bd0abbc92f..e34e33db005 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1081,3 +1081,16 @@ def test_entity_registry_items(): assert entities.get_entry(entry1.id) is None assert entities.get_entity_id(("test", "hue", "2345")) is None assert entities.get_entry(entry2.id) is None + + +async def test_deprecated_disabled_by_str(hass, registry, caplog): + """Test deprecated str use of disabled_by converts to enum and logs a warning.""" + entry = registry.async_get_or_create( + "light", + "hue", + "5678", + disabled_by=er.RegistryEntryDisabler.USER.value, + ) + + assert entry.disabled_by is er.RegistryEntryDisabler.USER + assert " str for entity registry disabled_by. This is deprecated " in caplog.text From 7d3fcfbd30422e2fca40aab3031af13710d0a3b6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 18:07:09 +0100 Subject: [PATCH 1218/1452] Add Platform StrEnum to entity_platform (#60818) --- homeassistant/components/wled/__init__.py | 22 +++++--------- homeassistant/helpers/entity_platform.py | 36 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index ec2ad1bb6fe..0518bd736d0 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,27 +1,21 @@ """Support for WLED.""" from __future__ import annotations -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import Platform from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator PLATFORMS = ( - BINARY_SENSOR_DOMAIN, - BUTTON_DOMAIN, - LIGHT_DOMAIN, - SELECT_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, - NUMBER_DOMAIN, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d8cb8477f11..45795a44c42 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -34,6 +34,7 @@ from homeassistant.exceptions import ( ) from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.enum import StrEnum from . import ( config_validation as cv, @@ -62,6 +63,41 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = getLogger(__name__) +class Platform(StrEnum): + """Available platforms.""" + + AIR_QUALITY = "air_quality" + ALARM_CONTROL_PANEL = "alarm_control_panel" + BINARY_SENSOR = "binary_sensor" + BUTTON = "button" + CALENDAR = "calendar" + CAMERA = "camera" + CLIMATE = "climate" + COVER = "cover" + DEVICE_TRACKER = "device_tracker" + FAN = "fan" + GEO_LOCATION = "geo_location" + HUMIDIFIER = "humidifier" + IMAGE_PROCESSING = "image_processing" + LIGHT = "light" + LOCK = "lock" + MAILBOX = "mailbox" + MEDIA_PLAYER = "media_player" + NOTIFY = "notify" + NUMBER = "number" + REMOTE = "remote" + SCENE = "scene" + SELECT = "select" + SENSOR = "sensor" + SIREN = "siren" + SST = "sst" + SWITCH = "switch" + TTS = "tts" + VACUUM = "vacuum" + WATER_HEATER = "water_heater" + WEATHER = "weather" + + class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" From e56a676fd5b56165ce144434125e39ffe38a7b69 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Dec 2021 11:21:49 -0600 Subject: [PATCH 1219/1452] Import helpers in Sonos instead of accessing `hass` (#60848) --- homeassistant/components/sonos/__init__.py | 8 +++++--- homeassistant/components/sonos/speaker.py | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c8defb1ff14..8320d7f681a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -24,6 +24,7 @@ from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval, call_later from .alarms import SonosAlarms from .const import ( @@ -236,8 +237,8 @@ class SonosDiscoveryManager: if soco and soco.is_visible: self._discovered_player(soco) - self.data.hosts_heartbeat = self.hass.helpers.event.call_later( - DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts + self.data.hosts_heartbeat = call_later( + self.hass, DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts ) def _discovered_ip(self, ip_address): @@ -331,7 +332,8 @@ class SonosDiscoveryManager: ) self.entry.async_on_unload( - self.hass.helpers.event.async_track_time_interval( + async_track_time_interval( + self.hass, partial( async_dispatcher_send, self.hass, diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f45a81060f8..a6a1ffc1fe4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, dispatcher_send, ) +from homeassistant.helpers.event import async_track_time_interval, track_time_interval from homeassistant.util import dt as dt_util from .alarms import SonosAlarms @@ -240,8 +241,8 @@ class SonosSpeaker: if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info # Battery events can be infrequent, polling is still necessary - self._battery_poll_timer = self.hass.helpers.event.track_time_interval( - self.async_poll_battery, BATTERY_SCAN_INTERVAL + self._battery_poll_timer = track_time_interval( + self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) else: @@ -337,7 +338,8 @@ class SonosSpeaker: # Create a polling task in case subscriptions fail or callback events do not arrive if not self._poll_timer: - self._poll_timer = self.hass.helpers.event.async_track_time_interval( + self._poll_timer = async_track_time_interval( + self.hass, partial( async_dispatcher_send, self.hass, From f46055de994396a31c000eb0340da213d35a0e6e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 2 Dec 2021 19:26:00 +0200 Subject: [PATCH 1220/1452] Fix Shelly device name for older firmware (#60826) --- homeassistant/components/shelly/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d5fcf154e28..a77f338a51e 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -57,7 +57,7 @@ def get_block_device_name(device: BlockDevice) -> str: def get_rpc_device_name(device: RpcDevice) -> str: """Naming for device.""" - return cast(str, device.config["sys"]["device"]["name"] or device.hostname) + return cast(str, device.config["sys"]["device"].get("name") or device.hostname) def get_number_of_channels(device: BlockDevice, block: Block) -> int: From 36734972f04d6a2e010c7fbc409a3f3ac6e3528d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 18:45:40 +0100 Subject: [PATCH 1221/1452] Teach numeric state trigger about entity registry ids (#60835) --- .../components/climate/device_trigger.py | 4 +- .../components/cover/device_trigger.py | 4 +- .../homeassistant/triggers/numeric_state.py | 26 +++++-- .../components/humidifier/device_trigger.py | 6 +- .../components/sensor/device_trigger.py | 4 +- .../triggers/test_numeric_state.py | 72 ++++++++++++++++--- 6 files changed, 99 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 3a9e0e45900..6bd6f4c3e02 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -159,7 +159,9 @@ async def async_attach_trigger( if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index b9a0aefb7a2..f960fcdcce6 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -192,7 +192,9 @@ async def async_attach_trigger( CONF_ABOVE: min_pos, CONF_VALUE_TEMPLATE: value_template, } - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 3f280f581b3..823bb608b4d 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -13,12 +13,18 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, callback -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, + template, +) from homeassistant.helpers.event import ( async_track_same_state, async_track_state_change_event, ) +from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -43,11 +49,11 @@ def validate_above_below(value): return value -TRIGGER_SCHEMA = vol.All( +_TRIGGER_SCHEMA = vol.All( cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "numeric_state", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids, vol.Optional(CONF_BELOW): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_ABOVE): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -62,6 +68,18 @@ TRIGGER_SCHEMA = vol.All( _LOGGER = logging.getLogger(__name__) +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + config = _TRIGGER_SCHEMA(config) + registry = er.async_get(hass) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="numeric_state" ) -> CALLBACK_TYPE: diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 9c6ca5cea55..1c7a09305e1 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -100,8 +100,10 @@ async def async_attach_trigger( if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA( - numeric_state_config + numeric_state_config = ( + await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 6f82ba1a34c..a9d014e6856 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -162,7 +162,9 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 0e71594937f..c0e5fdbdd99 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -12,6 +12,7 @@ from homeassistant.components.homeassistant.triggers import ( ) from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -126,6 +127,59 @@ async def test_if_fires_on_entity_change_below(hass, calls, below): assert calls[0].data["id"] == 0 +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) +async def test_if_fires_on_entity_change_below_uuid(hass, calls, below): + """Test the firing with changed entity specified by registry entry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + + context = Context() + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": entry.id, + "below": below, + }, + "action": { + "service": "test.automation", + "data_template": {"id": "{{ trigger.id}}"}, + }, + } + }, + ) + # 9 is below 10 + hass.states.async_set("test.entity", 9, context=context) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + + # Set above 12 so the automation will fire again + hass.states.async_set("test.entity", 12) + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["id"] == 0 + + @pytest.mark.parametrize( "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") ) @@ -1644,31 +1698,33 @@ async def test_if_fires_on_entities_change_overlap_for_template( assert calls[1].data["some"] == "test.entity_2 - 0:00:10" -def test_below_above(): +async def test_below_above(hass): """Test above cannot be above below.""" with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( - {"platform": "numeric_state", "above": 1200, "below": 1000} + await numeric_state_trigger.async_validate_trigger_config( + hass, {"platform": "numeric_state", "above": 1200, "below": 1000} ) -def test_schema_unacceptable_entities(): +async def test_schema_unacceptable_entities(hass): """Test input_number, number & sensor only is accepted for above/below.""" with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( + await numeric_state_trigger.async_validate_trigger_config( + hass, { "platform": "numeric_state", "above": "input_datetime.some_input", "below": 1000, - } + }, ) with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( + await numeric_state_trigger.async_validate_trigger_config( + hass, { "platform": "numeric_state", "below": "input_datetime.some_input", "above": 1200, - } + }, ) From d775c66194ff22b8cf2b63430957ccda21b4b7be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:47:20 +0100 Subject: [PATCH 1222/1452] Tidy up ssdp_location parsing (#60846) Co-authored-by: epenet --- homeassistant/components/heos/config_flow.py | 5 ++++- homeassistant/components/huawei_lte/config_flow.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 5e534e3e986..63a020c41d9 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Heos.""" +from typing import TYPE_CHECKING from urllib.parse import urlparse from pyheos import Heos, HeosError @@ -25,7 +26,9 @@ class HeosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered Heos device.""" # Store discovered host - hostname = urlparse(discovery_info.ssdp_location or "").hostname + if TYPE_CHECKING: + assert discovery_info.ssdp_location + hostname = urlparse(discovery_info.ssdp_location).hostname friendly_name = ( f"{discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]} ({hostname})" ) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 9c9bbb7ee54..be2a149b4d5 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection @@ -214,10 +214,12 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="not_huawei_lte") + if TYPE_CHECKING: + assert discovery_info.ssdp_location url = url_normalize( discovery_info.upnp.get( ssdp.ATTR_UPNP_PRESENTATION_URL, - f"http://{urlparse(discovery_info.ssdp_location or '').hostname}/", + f"http://{urlparse(discovery_info.ssdp_location).hostname}/", ) ) From 66494b0238b8dc8ab3f2671fc126c172a91de2fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:49:34 +0100 Subject: [PATCH 1223/1452] Use dataclass properties in dlna_dmr (#60794) Co-authored-by: epenet --- homeassistant/components/dlna_dmr/config_flow.py | 12 +++++------- homeassistant/components/dlna_dmr/media_player.py | 6 ++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 141ff159bea..69437d99e3d 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import logging from pprint import pformat -from typing import Any, Mapping, Optional +from typing import Any, Mapping, Optional, cast from urllib.parse import urlparse from async_upnp_client.client import UpnpError @@ -93,8 +93,8 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual() self._discoveries = { - discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) - or urlparse(discovery[ssdp.ATTR_SSDP_LOCATION]).hostname: discovery + discovery.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or cast(str, urlparse(discovery.ssdp_location).hostname): discovery for discovery in discoveries } @@ -158,7 +158,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Find the device in the list of unconfigured devices for discovery in discoveries: - if discovery[ssdp.ATTR_SSDP_LOCATION] == self._location: + if discovery.ssdp_location == self._location: # Device found via SSDP, it shouldn't need polling self._options[CONF_POLL_AVAILABILITY] = False # Discovery info has everything required to create config entry @@ -376,9 +376,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for entry in self._async_current_entries(include_ignore=False) } discoveries = [ - disc - for disc in discoveries - if disc[ssdp.ATTR_SSDP_UDN] not in current_unique_ids + disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids ] return discoveries diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index bd39b070228..2b529609d9e 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,7 +6,7 @@ from collections.abc import Sequence import contextlib from datetime import datetime, timedelta import functools -from typing import Any, Callable, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast from async_upnp_client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType @@ -278,7 +278,9 @@ class DlnaDmrEntity(MediaPlayerEntity): await self._device_disconnect() if change == ssdp.SsdpChange.ALIVE and not self._device: - location = info.ssdp_location or "" + if TYPE_CHECKING: + assert info.ssdp_location + location = info.ssdp_location try: await self._device_connect(location) except UpnpError as err: From 0c18d710cc691ca2c94d597e18433c83194426e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:50:35 +0100 Subject: [PATCH 1224/1452] Use dataclass properties in yamaha_musiccast (#60787) Co-authored-by: epenet --- .../components/yamaha_musiccast/__init__.py | 5 ++--- .../components/yamaha_musiccast/config_flow.py | 7 ++++++- .../components/yamaha_musiccast/test_config_flow.py | 13 +++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 06a242c6070..194168b2eee 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -39,11 +39,10 @@ SCAN_INTERVAL = timedelta(seconds=60) async def get_upnp_desc(hass: HomeAssistant, host: str): """Get the upnp description URL for a given host, using the SSPD scanner.""" ssdp_entries = await ssdp.async_get_discovery_info_by_st(hass, "upnp:rootdevice") - matches = [w for w in ssdp_entries if w.get("_host", "") == host] + matches = [w for w in ssdp_entries if w.ssdp_headers.get("_host", "") == host] upnp_desc = None for match in matches: - if match.get(ssdp.ATTR_SSDP_LOCATION): - upnp_desc = match[ssdp.ATTR_SSDP_LOCATION] + if upnp_desc := match.ssdp_location: break if not upnp_desc: diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 2d4e723d745..4049a5d6a37 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -92,8 +92,13 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="yxc_control_url_missing") self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] - self.host = urlparse(discovery_info.ssdp_location or "").hostname or "" self.upnp_description = discovery_info.ssdp_location + + # ssdp_location and hostname have been checked in check_yamaha_ssdp so it is safe to ignore type assignment + self.host = urlparse( + discovery_info.ssdp_location + ).hostname # type: ignore[assignment] + await self.async_set_unique_id(self.serial_number) self._abort_if_unique_id_configured( { diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 6229f25f908..bddc350cff8 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -94,10 +94,15 @@ def mock_valid_discovery_information(): with patch( "homeassistant.components.ssdp.async_get_discovery_info_by_st", return_value=[ - { - "ssdp_location": "http://127.0.0.1:9000/MediaRenderer/desc.xml", - "_host": "127.0.0.1", - } + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1:9000/MediaRenderer/desc.xml", + ssdp_headers={ + "_host": "127.0.0.1", + }, + upnp={}, + ) ], ): yield From 8e0ef52cc85bc8fe2b7346f156585f38ff38d2b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Dec 2021 07:53:08 -1000 Subject: [PATCH 1225/1452] Ensure sonos ssdp callbacks use dataclass methods (#60782) --- homeassistant/components/sonos/__init__.py | 18 +++++++++++++----- tests/components/sonos/conftest.py | 12 ++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 8320d7f681a..32ae234434e 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -266,19 +266,27 @@ class SonosDiscoveryManager: self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery" ) - async def _async_ssdp_discovered_player(self, info, change): + async def _async_ssdp_discovered_player( + self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange + ) -> None: if change == ssdp.SsdpChange.BYEBYE: return - uid = info.get(ssdp.ATTR_UPNP_UDN) + uid = info.upnp[ssdp.ATTR_UPNP_UDN] if not uid.startswith("uuid:RINCON_"): return uid = uid[5:] - discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname - boot_seqnum = info.get("X-RINCON-BOOTSEQ") + discovered_ip = urlparse(info.ssdp_location).hostname + boot_seqnum = info.ssdp_headers.get("X-RINCON-BOOTSEQ") self.async_discovered_player( - "SSDP", info, discovered_ip, uid, boot_seqnum, info.get("modelName"), None + "SSDP", + info, + discovered_ip, + uid, + boot_seqnum, + info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME), + None, ) @callback diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index cb31604081f..8a55d285a41 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -94,10 +94,14 @@ def discover_fixture(soco): async def do_callback(hass, callback, *args, **kwargs): await callback( - { - ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", - ssdp.ATTR_SSDP_LOCATION: f"http://{soco.ip_address}/", - }, + ssdp.SsdpServiceInfo( + ssdp_location=f"http://{soco.ip_address}/", + ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", + ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1", + upnp={ + ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", + }, + ), ssdp.SsdpChange.ALIVE, ) return MagicMock() From 411b0f0b1577b8d22e22e6e2031ea0097fda7213 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 18:55:46 +0100 Subject: [PATCH 1226/1452] Generate external statistics in demo component (#58470) --- homeassistant/components/demo/__init__.py | 85 +++++++++++++++++++++ homeassistant/components/demo/manifest.json | 3 +- tests/components/demo/test_init.py | 29 ++++++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 952b7cbd9b2..e4ec3bd671c 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -1,13 +1,20 @@ """Set up the demo environment that mimics interaction with devices.""" import asyncio +import datetime +from random import random from homeassistant import bootstrap, config_entries +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, +) from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, SOUND_PRESSURE_DB, ) import homeassistant.core as ha +import homeassistant.util.dt as dt_util DOMAIN = "demo" @@ -150,6 +157,82 @@ async def async_setup(hass, config): return True +def _generate_mean_statistics(start, end, init_value, max_diff): + statistics = [] + mean = init_value + now = start + while now < end: + mean = mean + random() * max_diff - max_diff / 2 + statistics.append( + { + "start": now, + "mean": mean, + "min": mean - random() * max_diff, + "max": mean + random() * max_diff, + } + ) + now = now + datetime.timedelta(hours=1) + + return statistics + + +def _generate_sum_statistics(start, end, init_value, max_diff): + statistics = [] + now = start + sum_ = init_value + while now < end: + sum_ = sum_ + random() * max_diff + statistics.append( + { + "start": now, + "sum": sum_, + } + ) + now = now + datetime.timedelta(hours=1) + + return statistics + + +async def _insert_statistics(hass): + """Insert some fake statistics.""" + now = dt_util.now() + yesterday = now - datetime.timedelta(days=1) + yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) + + # Fake yesterday's temperatures + metadata = { + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:temperature_outdoor", + "unit_of_measurement": "°C", + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics( + yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), 15, 1 + ) + async_add_external_statistics(hass, metadata, statistics) + + # Fake yesterday's energy consumption + metadata = { + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:energy_consumption", + "unit_of_measurement": "kWh", + "has_mean": False, + "has_sum": True, + } + statistic_id = f"{DOMAIN}:energy_consumption" + sum_ = 0 + last_stats = await hass.async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True + ) + if "domain:energy_consumption" in last_stats: + sum_ = last_stats["domain.electricity_total"]["sum"] or 0 + statistics = _generate_sum_statistics( + yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 1 + ) + async_add_external_statistics(hass, metadata, statistics) + + async def async_setup_entry(hass, config_entry): """Set the config entry up.""" # Set up demo platforms with config entry @@ -157,6 +240,8 @@ async def async_setup_entry(hass, config_entry): hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) ) + if "recorder" in hass.config.components: + await _insert_statistics(hass) return True diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 0997868fbfd..df6fa494079 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -2,7 +2,8 @@ "domain": "demo", "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", - "dependencies": ["conversation", "zone", "group"], + "after_dependencies": ["recorder"], + "dependencies": ["conversation", "group", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "calculated" diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 68d4dfbf379..a446856de7b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -7,8 +7,11 @@ import pytest from homeassistant.components.demo import DOMAIN from homeassistant.components.device_tracker.legacy import YAML_DEVICES +from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component + +from tests.components.recorder.common import wait_recording_done @pytest.fixture(autouse=True) @@ -40,3 +43,27 @@ async def test_setting_up_demo(hass): "Unable to convert all demo entities to JSON. " "Wrong data in state machine!" ) + + +def test_demo_statistics(hass_recorder): + """Test that the demo components makes some statistics available.""" + hass = hass_recorder() + + assert setup_component(hass, DOMAIN, {DOMAIN: {}}) + hass.block_till_done() + hass.start() + wait_recording_done(hass) + + statistic_ids = list_statistic_ids(hass) + assert { + "name": None, + "source": "demo", + "statistic_id": "demo:temperature_outdoor", + "unit_of_measurement": "°C", + } in statistic_ids + assert { + "name": None, + "source": "demo", + "statistic_id": "demo:energy_consumption", + "unit_of_measurement": "kWh", + } in statistic_ids From 65a7563ac9ecf36395bb7331542b945b2cec5404 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 19:00:17 +0100 Subject: [PATCH 1227/1452] CI: Upload coverage results in a single step (#60834) --- .github/workflows/ci.yaml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 47dc4a2ca40..ebbd2b2fb9e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -711,18 +711,31 @@ jobs: --durations-min=1 \ -p no:sugar \ tests/components/${{ matrix.group }} + - name: Upload coverage artifact + uses: actions/upload-artifact@v2.2.4 + with: + name: coverage-${{ matrix.python-version }}-${{ matrix.group }} + path: coverage.xml + - name: Check dirty + run: | + ./script/check_dirty + + coverage: + name: Upload test coverage to Codecov + runs-on: ubuntu-latest + needs: + - changes + - pytest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2 - name: Upload coverage to Codecov (full coverage) if: needs.changes.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v2.1.0 with: - files: coverage.xml flags: full-suite - name: Upload coverage to Codecov (partial coverage) if: needs.changes.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v2.1.0 - with: - files: coverage.xml - flags: ${{ matrix.group }} - - name: Check dirty - run: | - ./script/check_dirty From 60adccd549c3ec9c17e80e5c4cce90604b1c42f5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Dec 2021 12:11:08 -0600 Subject: [PATCH 1228/1452] Do not print full traceback during Sonos resubscription failure (#60644) --- homeassistant/components/sonos/speaker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index a6a1ffc1fe4..4cec0fbb6ab 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,10 +407,12 @@ class SonosSpeaker: self.zone_name, ) else: + exc_info = exception if _LOGGER.isEnabledFor(logging.DEBUG) else None _LOGGER.error( - "Subscription renewals for %s failed", + "Subscription renewals for %s failed: %s", self.zone_name, - exc_info=exception, + exception, + exc_info=exc_info, ) await self.async_offline() From 824e5ed6b884a992befe348f93e04aefe92fad56 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 2 Dec 2021 15:12:58 -0300 Subject: [PATCH 1229/1452] Tuya Vacuum: Adjusted comand PAUSE (#60849) --- homeassistant/components/tuya/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 4b7c7660556..2afeb1880f7 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -171,7 +171,7 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): def pause(self, **kwargs: Any) -> None: """Pause the device.""" - self._send_command([{"code": DPCode.POWER_GO, "value": True}]) + self._send_command([{"code": DPCode.POWER_GO, "value": False}]) def return_to_base(self, **kwargs: Any) -> None: """Return device to dock.""" From a67a4873dcc2d7963337ca3b97c01279d2d2d91f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 19:26:10 +0100 Subject: [PATCH 1230/1452] Minor refactor of template cover (#59537) --- .../components/template/binary_sensor.py | 15 +-- homeassistant/components/template/const.py | 1 + homeassistant/components/template/cover.py | 108 ++++++------------ homeassistant/components/template/sensor.py | 25 ++-- .../components/template/template_entity.py | 59 +++++++++- 5 files changed, 104 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index cae751df170..06af6dca925 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -46,7 +46,11 @@ from .const import ( CONF_OBJECT_ID, CONF_PICTURE, ) -from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_SCHEMA, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) from .trigger_entity import TriggerEntity CONF_DELAY_ON = "delay_on" @@ -106,14 +110,7 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: for object_id, entity_cfg in cfg.items(): entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} - for from_key, to_key in LEGACY_FIELDS.items(): - if from_key not in entity_cfg or to_key in entity_cfg: - continue - - val = entity_cfg.pop(from_key) - if isinstance(val, str): - val = template.Template(val) - entity_cfg[to_key] = val + entity_cfg = rewrite_common_legacy_to_modern_conf(entity_cfg, LEGACY_FIELDS) if CONF_NAME not in entity_cfg: entity_cfg[CONF_NAME] = template.Template(object_id) diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 54d213be0b1..9dfbe4a11d9 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -25,5 +25,6 @@ PLATFORMS = [ CONF_AVAILABILITY = "availability" CONF_ATTRIBUTES = "attributes" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_PICTURE = "picture" CONF_OBJECT_ID = "object_id" diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index d5cd04e94f2..dcfd8b41521 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -23,9 +23,7 @@ from homeassistant.const import ( CONF_COVERS, CONF_DEVICE_CLASS, CONF_ENTITY_ID, - CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, - CONF_ICON_TEMPLATE, CONF_OPTIMISTIC, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -40,8 +38,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ @@ -79,11 +81,8 @@ COVER_SCHEMA = vol.All( vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, @@ -93,7 +92,7 @@ COVER_SCHEMA = vol.All( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ), + ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) @@ -106,44 +105,17 @@ async def _async_create_entities(hass, config): """Create the Template cover.""" covers = [] - for device, device_config in config[CONF_COVERS].items(): - state_template = device_config.get(CONF_VALUE_TEMPLATE) - position_template = device_config.get(CONF_POSITION_TEMPLATE) - tilt_template = device_config.get(CONF_TILT_TEMPLATE) - icon_template = device_config.get(CONF_ICON_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + for object_id, entity_config in config[CONF_COVERS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) - device_class = device_config.get(CONF_DEVICE_CLASS) - open_action = device_config.get(OPEN_ACTION) - close_action = device_config.get(CLOSE_ACTION) - stop_action = device_config.get(STOP_ACTION) - position_action = device_config.get(POSITION_ACTION) - tilt_action = device_config.get(TILT_ACTION) - optimistic = device_config.get(CONF_OPTIMISTIC) - tilt_optimistic = device_config.get(CONF_TILT_OPTIMISTIC) - unique_id = device_config.get(CONF_UNIQUE_ID) + entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + + unique_id = entity_config.get(CONF_UNIQUE_ID) covers.append( CoverTemplate( hass, - device, - friendly_name, - device_class, - state_template, - position_template, - tilt_template, - icon_template, - entity_picture_template, - availability_template, - open_action, - close_action, - stop_action, - position_action, - tilt_action, - optimistic, - tilt_optimistic, + object_id, + entity_config, unique_id, ) ) @@ -162,55 +134,41 @@ class CoverTemplate(TemplateEntity, CoverEntity): def __init__( self, hass, - device_id, - friendly_name, - device_class, - state_template, - position_template, - tilt_template, - icon_template, - entity_picture_template, - availability_template, - open_action, - close_action, - stop_action, - position_action, - tilt_action, - optimistic, - tilt_optimistic, + object_id, + config, unique_id, ): """Initialize the Template cover.""" - super().__init__( - availability_template=availability_template, - icon_template=icon_template, - entity_picture_template=entity_picture_template, - ) + super().__init__(config=config) self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = friendly_name - self._template = state_template - self._position_template = position_template - self._tilt_template = tilt_template - self._device_class = device_class + self._name = friendly_name = config.get(CONF_FRIENDLY_NAME, object_id) + self._template = config.get(CONF_VALUE_TEMPLATE) + self._position_template = config.get(CONF_POSITION_TEMPLATE) + self._tilt_template = config.get(CONF_TILT_TEMPLATE) + self._device_class = config.get(CONF_DEVICE_CLASS) self._open_script = None - if open_action is not None: + if (open_action := config.get(OPEN_ACTION)) is not None: self._open_script = Script(hass, open_action, friendly_name, DOMAIN) self._close_script = None - if close_action is not None: + if (close_action := config.get(CLOSE_ACTION)) is not None: self._close_script = Script(hass, close_action, friendly_name, DOMAIN) self._stop_script = None - if stop_action is not None: + if (stop_action := config.get(STOP_ACTION)) is not None: self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) self._position_script = None - if position_action is not None: + if (position_action := config.get(POSITION_ACTION)) is not None: self._position_script = Script(hass, position_action, friendly_name, DOMAIN) self._tilt_script = None - if tilt_action is not None: + if (tilt_action := config.get(TILT_ACTION)) is not None: self._tilt_script = Script(hass, tilt_action, friendly_name, DOMAIN) - self._optimistic = optimistic or (not state_template and not position_template) - self._tilt_optimistic = tilt_optimistic or not tilt_template + optimistic = config.get(CONF_OPTIMISTIC) + self._optimistic = optimistic or ( + not self._template and not self._position_template + ) + tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC) + self._tilt_optimistic = tilt_optimistic or not self._tilt_template self._position = None self._is_opening = False self._is_closing = False diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a89a30af556..a31e49db570 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME_TEMPLATE, - CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, CONF_SENSORS, @@ -36,21 +35,18 @@ from homeassistant.helpers.entity import async_generate_entity_id from .const import ( CONF_ATTRIBUTE_TEMPLATES, - CONF_ATTRIBUTES, - CONF_AVAILABILITY, CONF_AVAILABILITY_TEMPLATE, CONF_OBJECT_ID, - CONF_PICTURE, CONF_TRIGGER, ) -from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_SCHEMA, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) from .trigger_entity import TriggerEntity LEGACY_FIELDS = { - CONF_ICON_TEMPLATE: CONF_ICON, - CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, - CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, - CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, CONF_FRIENDLY_NAME: CONF_NAME, CONF_VALUE_TEMPLATE: CONF_STATE, @@ -106,20 +102,13 @@ def extra_validation_checks(val): def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: - """Rewrite a legacy sensor definitions to modern ones.""" + """Rewrite legacy sensor definitions to modern ones.""" sensors = [] for object_id, entity_cfg in cfg.items(): entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} - for from_key, to_key in LEGACY_FIELDS.items(): - if from_key not in entity_cfg or to_key in entity_cfg: - continue - - val = entity_cfg.pop(from_key) - if isinstance(val, str): - val = template.Template(val) - entity_cfg[to_key] = val + entity_cfg = rewrite_common_legacy_to_modern_conf(entity_cfg, LEGACY_FIELDS) if CONF_NAME not in entity_cfg: entity_cfg[CONF_NAME] = template.Template(object_id) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2e7799bd95b..55c9dbcf45b 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -2,12 +2,18 @@ from __future__ import annotations from collections.abc import Callable +import itertools import logging from typing import Any import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON, + CONF_ICON_TEMPLATE, +) from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -20,7 +26,13 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.template import Template, result_as_boolean -from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE +from .const import ( + CONF_ATTRIBUTE_TEMPLATES, + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_PICTURE, +) _LOGGER = logging.getLogger(__name__) @@ -34,6 +46,49 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( } ) +TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY = vol.Schema( + { + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + } +) + +TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY = vol.Schema( + { + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + } +).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) + + +LEGACY_FIELDS = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, +} + + +def rewrite_common_legacy_to_modern_conf( + entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] = None +) -> list[dict]: + """Rewrite legacy config.""" + entity_cfg = {**entity_cfg} + if extra_legacy_fields is None: + extra_legacy_fields = {} + + for from_key, to_key in itertools.chain( + LEGACY_FIELDS.items(), extra_legacy_fields.items() + ): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = Template(val) + entity_cfg[to_key] = val + + return entity_cfg + class _TemplateAttribute: """Attribute value linked to template result.""" From 5b8d081441de5717ac1e8380bfef6a873895ecc7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 19:28:21 +0100 Subject: [PATCH 1231/1452] Teach zone trigger about entity registry ids (#60838) --- .../device_tracker/device_trigger.py | 2 +- homeassistant/components/zone/trigger.py | 26 +++++- tests/components/zone/test_trigger.py | 80 +++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 49a52fa887e..926519c2243 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -89,7 +89,7 @@ async def async_attach_trigger( CONF_ZONE: config[CONF_ZONE], CONF_EVENT: event, } - zone_config = zone.TRIGGER_SCHEMA(zone_config) + zone_config = await zone.async_validate_trigger_config(hass, zone_config) return await zone.async_attach_trigger( hass, zone_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index ef054b39714..373727e3f4d 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -8,9 +8,15 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, callback -from homeassistant.helpers import condition, config_validation as cv, location +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, + location, +) from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-defs # mypy: no-check-untyped-defs @@ -21,10 +27,10 @@ DEFAULT_EVENT = EVENT_ENTER _EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"} -TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( +_TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "zone", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids, vol.Required(CONF_ZONE): cv.entity_id, vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any( EVENT_ENTER, EVENT_LEAVE @@ -33,6 +39,18 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + config = _TRIGGER_SCHEMA(config) + registry = er.async_get(hass) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, config[CONF_ENTITY_ID] + ) + return config + + async def async_attach_trigger( hass, config, action, automation_info, *, platform_type: str = "zone" ) -> CALLBACK_TYPE: diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 978603ae242..ee62dd5df3a 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components import automation, zone from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component @@ -108,6 +109,85 @@ async def test_if_fires_on_zone_enter(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_zone_enter_uuid(hass, calls): + """Test for firing on zone enter when device is specified by entity registry id.""" + context = Context() + + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + + hass.states.async_set( + "test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758} + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "zone", + "entity_id": entry.id, + "zone": "zone.test", + "event": "enter", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "zone.name", + "id", + ) + ) + }, + }, + } + }, + ) + + hass.states.async_set( + "test.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564}, + context=context, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + assert calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0" + + # Set out of zone again so we can trigger call + hass.states.async_set( + "test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758} + ) + await hass.async_block_till_done() + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) + + hass.states.async_set( + "test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564} + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + + async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): """Test for not firing on zone leave.""" hass.states.async_set( From 2bc049353590e29a06aba9e71d78c526c0fd957c Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Thu, 2 Dec 2021 19:32:35 +0100 Subject: [PATCH 1232/1452] Adding presets for new tuya TRV (#60408) Adding presets for _TZE200_2atgpdho and _TYST11_2atgpdho and also deleting it for Beca Smart with old Zigbee module then its looks like they is not releasing devices with it. --- homeassistant/components/zha/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 300fdb9ef66..69c1ce35849 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -613,10 +613,11 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_ywdxldoj", "_TZE200_cwnjrr72", "_TZE200_b6wax7g0", + "_TZE200_2atgpdho", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", - "_TYST11_b6wax7g0", + "_TYST11_2atgpdho", }, ) class MoesThermostat(Thermostat): From c466f3767abc61c1d9f08d3c51ddd5812951c153 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 2 Dec 2021 13:32:55 -0500 Subject: [PATCH 1233/1452] Bump ZHA dependencies (#60852) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a1c7a1697ef..33597feb900 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.63", + "zha-quirks==0.0.64", "zigpy-deconz==0.14.0", - "zigpy==0.41.0", + "zigpy==0.42.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.6.1" diff --git a/requirements_all.txt b/requirements_all.txt index 350b53c5a2c..0eb4df56194 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2489,7 +2489,7 @@ zengge==0.2 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.63 +zha-quirks==0.0.64 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2510,7 +2510,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.6.1 # homeassistant.components.zha -zigpy==0.41.0 +zigpy==0.42.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a21eeaf9634..7f70f39fbcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1476,7 +1476,7 @@ youless-api==0.15 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.63 +zha-quirks==0.0.64 # homeassistant.components.zha zigpy-deconz==0.14.0 @@ -1491,7 +1491,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.6.1 # homeassistant.components.zha -zigpy==0.41.0 +zigpy==0.42.0 # homeassistant.components.zwave_js zwave-js-server-python==0.33.0 From d9c567e2057d5738b6f5299068159ffa80055c6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Dec 2021 10:55:52 -0800 Subject: [PATCH 1234/1452] Fix Ring sensors with timestamp class returning a string (#60850) --- homeassistant/components/ring/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index c36b44f5ee5..2745c69d50a 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -164,7 +164,7 @@ class HistoryRingSensor(RingSensor): if self._latest_event is None: return None - return self._latest_event["created_at"].isoformat() + return self._latest_event["created_at"] @property def extra_state_attributes(self): From e641214c603125e4d329a835d33ff910770bdd45 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 2 Dec 2021 12:07:14 -0700 Subject: [PATCH 1235/1452] Reorganize SimpliSafe services (#58722) --- .../components/simplisafe/__init__.py | 234 ++++++++++-------- .../simplisafe/alarm_control_panel.py | 4 +- .../components/simplisafe/services.yaml | 30 ++- homeassistant/components/simplisafe/typing.py | 7 + 4 files changed, 164 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/simplisafe/typing.py diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 044bd3651bc..2e740977d0f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Callable, Iterable from datetime import timedelta from typing import TYPE_CHECKING, Any, cast @@ -14,7 +14,6 @@ from simplipy.errors import ( SimplipyError, ) from simplipy.system import SystemNotification -from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import ( MAX_ALARM_DURATION, MAX_ENTRY_DELAY_AWAY, @@ -45,9 +44,10 @@ from simplipy.websocket import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_CODE, + ATTR_DEVICE_ID, CONF_CODE, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, @@ -88,6 +88,7 @@ from .const import ( DOMAIN, LOGGER, ) +from .typing import SystemType ATTR_CATEGORY = "category" ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by" @@ -131,18 +132,37 @@ VOLUME_MAP = { "off": Volume.OFF, } -SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) +SERVICE_NAME_CLEAR_NOTIFICATIONS = "clear_notifications" +SERVICE_NAME_REMOVE_PIN = "remove_pin" +SERVICE_NAME_SET_PIN = "set_pin" +SERVICE_NAME_SET_SYSTEM_PROPERTIES = "set_system_properties" -SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( - {vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string} +SERVICES = ( + SERVICE_NAME_CLEAR_NOTIFICATIONS, + SERVICE_NAME_REMOVE_PIN, + SERVICE_NAME_SET_PIN, + SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) -SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( - {vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string} -) -SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( +SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + } +) + +SERVICE_SET_PIN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_PIN_LABEL): cv.string, + vol.Required(ATTR_PIN_VALUE): cv.string, + } +) + +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, vol.Optional(ATTR_ALARM_DURATION): vol.All( cv.time_period, lambda value: value.total_seconds(), @@ -191,6 +211,58 @@ WEBSOCKET_EVENTS_TO_FIRE_HASS_EVENT = [ CONFIG_SCHEMA = cv.deprecated(DOMAIN) +@callback +def _async_get_system_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> SystemType: + """Get the SimpliSafe system related to a service call (by device ID).""" + device_id = call.data[ATTR_DEVICE_ID] + device_registry = dr.async_get(hass) + + if ( + alarm_control_panel_device_entry := device_registry.async_get(device_id) + ) is None: + raise vol.Invalid("Invalid device ID specified") + + if TYPE_CHECKING: + assert alarm_control_panel_device_entry.via_device_id + + if ( + base_station_device_entry := device_registry.async_get( + alarm_control_panel_device_entry.via_device_id + ) + ) is None: + raise ValueError("No base station registered for alarm control panel") + + [system_id] = [ + identity[1] + for identity in base_station_device_entry.identifiers + if identity[0] == DOMAIN + ] + + for entry_id in base_station_device_entry.config_entries: + if (simplisafe := hass.data[DOMAIN].get(entry_id)) is None: + continue + return cast(SystemType, simplisafe.systems[system_id]) + + raise ValueError(f"No system for device ID: {device_id}") + + +@callback +def _async_register_base_station( + hass: HomeAssistant, entry: ConfigEntry, system: SystemType +) -> None: + """Register a new bridge.""" + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, system.system_id)}, + manufacturer="SimpliSafe", + model=system.version, + name=system.address, + ) + + @callback def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Bring a config entry up to current standards.""" @@ -216,24 +288,7 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> hass.config_entries.async_update_entry(entry, **entry_updates) -@callback -def _async_register_base_station( - hass: HomeAssistant, entry: ConfigEntry, system: SystemV2 | SystemV3 -) -> None: - """Register a new bridge.""" - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, system.system_id)}, - manufacturer="SimpliSafe", - model=system.version, - name=system.address, - ) - - -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SimpliSafe as config entry.""" _async_standardize_config_entry(hass, entry) @@ -263,94 +318,64 @@ async def async_setup_entry( # noqa: C901 hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback - def verify_system_exists( - coro: Callable[..., Awaitable] - ) -> Callable[..., Awaitable]: - """Log an error if a service call uses an invalid system ID.""" + def extract_system(func: Callable) -> Callable: + """Define a decorator to get the correct system for a service call.""" - async def decorator(call: ServiceCall) -> None: - """Decorate.""" - system_id = int(call.data[ATTR_SYSTEM_ID]) - if system_id not in simplisafe.systems: - LOGGER.error("Unknown system ID in service call: %s", system_id) - return - await coro(call) + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + system = _async_get_system_for_service_call(hass, call) - return decorator + try: + await func(call, system) + except SimplipyError as err: + LOGGER.error("Error while executing %s: %s", func.__name__, err) - @callback - def v3_only(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]: - """Log an error if the decorated coroutine is called with a v2 system.""" + return wrapper - async def decorator(call: ServiceCall) -> None: - """Decorate.""" - system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] - if system.version != 3: - LOGGER.error("Service only available on V3 systems") - return - await coro(call) - - return decorator - - @verify_system_exists @_verify_domain_control - async def clear_notifications(call: ServiceCall) -> None: + @extract_system + async def async_clear_notifications(call: ServiceCall, system: SystemType) -> None: """Clear all active notifications.""" - system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] - try: - await system.async_clear_notifications() - except SimplipyError as err: - LOGGER.error("Error during service call: %s", err) + await system.async_clear_notifications() - @verify_system_exists @_verify_domain_control - async def remove_pin(call: ServiceCall) -> None: + @extract_system + async def async_remove_pin(call: ServiceCall, system: SystemType) -> None: """Remove a PIN.""" - system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] - try: - await system.async_remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) - except SimplipyError as err: - LOGGER.error("Error during service call: %s", err) + await system.async_remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) - @verify_system_exists @_verify_domain_control - async def set_pin(call: ServiceCall) -> None: + @extract_system + async def async_set_pin(call: ServiceCall, system: SystemType) -> None: """Set a PIN.""" - system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] - try: - await system.async_set_pin( - call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE] - ) - except SimplipyError as err: - LOGGER.error("Error during service call: %s", err) + await system.async_set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) - @verify_system_exists - @v3_only @_verify_domain_control - async def set_system_properties(call: ServiceCall) -> None: + @extract_system + async def async_set_system_properties( + call: ServiceCall, system: SystemType + ) -> None: """Set one or more system parameters.""" - system = cast(SystemV3, simplisafe.systems[call.data[ATTR_SYSTEM_ID]]) - try: - await system.async_set_properties( - { - prop: value - for prop, value in call.data.items() - if prop != ATTR_SYSTEM_ID - } - ) - except SimplipyError as err: - LOGGER.error("Error during service call: %s", err) + if not isinstance(system, SystemV3): + LOGGER.error("Can only set system properties on V3 systems") + return + + await system.async_set_properties( + {prop: value for prop, value in call.data.items() if prop != ATTR_DEVICE_ID} + ) for service, method, schema in ( - ("clear_notifications", clear_notifications, None), - ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), - ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), + (SERVICE_NAME_CLEAR_NOTIFICATIONS, async_clear_notifications, None), + (SERVICE_NAME_REMOVE_PIN, async_remove_pin, SERVICE_REMOVE_PIN_SCHEMA), + (SERVICE_NAME_SET_PIN, async_set_pin, SERVICE_SET_PIN_SCHEMA), ( - "set_system_properties", - set_system_properties, + SERVICE_NAME_SET_SYSTEM_PROPERTIES, + async_set_system_properties, SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA, ), ): + if hass.services.has_service(DOMAIN, service): + continue async_register_admin_service(hass, DOMAIN, service, method, schema=schema) current_options = {**entry.options} @@ -383,6 +408,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of SimpliSafe, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok @@ -396,13 +432,13 @@ class SimpliSafe: self._system_notifications: dict[int, set[SystemNotification]] = {} self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} - self.systems: dict[int, SystemV2 | SystemV3] = {} + self.systems: dict[int, SystemType] = {} # This will get filled in by async_init: self.coordinator: DataUpdateCoordinator | None = None @callback - def _async_process_new_notifications(self, system: SystemV2 | SystemV3) -> None: + def _async_process_new_notifications(self, system: SystemType) -> None: """Act on any new system notifications.""" if self._hass.state != CoreState.running: # If HASS isn't fully running yet, it may cause the SIMPLISAFE_NOTIFICATION @@ -543,7 +579,7 @@ class SimpliSafe: async def async_update(self) -> None: """Get updated data from SimpliSafe.""" - async def async_update_system(system: SystemV2 | SystemV3) -> None: + async def async_update_system(system: SystemType) -> None: """Update a system.""" await system.async_update(cached=system.version != 3) self._async_process_new_notifications(system) @@ -571,7 +607,7 @@ class SimpliSafeEntity(CoordinatorEntity): def __init__( self, simplisafe: SimpliSafe, - system: SystemV2 | SystemV3, + system: SystemType, *, device: Device | None = None, additional_websocket_events: Iterable[str] | None = None, diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 7cbf62b04e8..ac3d4721ccb 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from simplipy.errors import SimplipyError from simplipy.system import SystemStates -from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 from simplipy.websocket import ( EVENT_ALARM_CANCELED, @@ -58,6 +57,7 @@ from .const import ( DOMAIN, LOGGER, ) +from .typing import SystemType ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" ATTR_GSM_STRENGTH = "gsm_strength" @@ -119,7 +119,7 @@ async def async_setup_entry( class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Representation of a SimpliSafe alarm.""" - def __init__(self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3) -> None: + def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None: """Initialize the SimpliSafe alarm.""" super().__init__( simplisafe, diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index b9ee798f464..273aa02c300 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -3,13 +3,14 @@ remove_pin: name: Remove PIN description: Remove a PIN by its label or value. fields: - system_id: - name: System ID - description: The SimpliSafe system ID to affect. + device_id: + name: System + description: The system to remove the PIN from required: true - example: 123987 selector: - text: + device: + integration: simplisafe + model: alarm_control_panel label_or_pin: name: Label/PIN description: The label/value to remove. @@ -21,13 +22,14 @@ set_pin: name: Set PIN description: Set/update a PIN fields: - system_id: - name: System ID - description: The SimpliSafe system ID to affect + device_id: + name: System + description: The system to set the PIN on required: true - example: 123987 selector: - text: + device: + integration: simplisafe + model: alarm_control_panel label: name: Label description: The label of the PIN @@ -46,6 +48,14 @@ set_system_properties: name: Set system properties description: Set one or more system properties fields: + device_id: + name: System + description: The system whose properties should be set + required: true + selector: + device: + integration: simplisafe + model: alarm_control_panel alarm_duration: name: Alarm duration description: The length of a triggered alarm diff --git a/homeassistant/components/simplisafe/typing.py b/homeassistant/components/simplisafe/typing.py new file mode 100644 index 00000000000..10f4fadc1c5 --- /dev/null +++ b/homeassistant/components/simplisafe/typing.py @@ -0,0 +1,7 @@ +"""Define typing helpers for SimpliSafe.""" +from typing import Union + +from simplipy.system.v2 import SystemV2 +from simplipy.system.v3 import SystemV3 + +SystemType = Union[SystemV2, SystemV3] From 0723b1c539767a8e0ac125b95095585e4b91ffc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 20:17:54 +0100 Subject: [PATCH 1236/1452] Introduce HassioServiceInfo (#60844) Co-authored-by: epenet --- homeassistant/components/hassio/discovery.py | 33 ++++++++++++++++++++ tests/components/almond/test_config_flow.py | 5 ++- tests/components/hassio/test_discovery.py | 27 ++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 9f15ff6e8b8..67090d78a96 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -1,6 +1,11 @@ """Implement the services discovery feature from Hass.io for Add-ons.""" +from __future__ import annotations + import asyncio +from collections.abc import Mapping +from dataclasses import dataclass import logging +from typing import Any from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable @@ -9,6 +14,8 @@ from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.helpers.frame import report from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID from .handler import HassioAPIError @@ -16,6 +23,32 @@ from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) +@dataclass +class HassioServiceInfo(BaseServiceInfo): + """Prepared info from hassio entries.""" + + config: Mapping[str, Any] + + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.config['{name}']; this will fail in version 2022.6", + exclude_integrations={"hassio"}, + error_if_core=False, + level=logging.DEBUG, + ) + self._warning_logged = True + return self.config[name] + + @callback def async_setup_discovery_view(hass: HomeAssistant, hassio): """Discovery setup.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index e0e88d0f43b..5e4283fb611 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow from homeassistant.components.almond.const import DOMAIN +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -51,7 +52,9 @@ async def test_hassio(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, + data=HassioServiceInfo( + config={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"} + ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index fc99b06619f..de424c4d339 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -2,6 +2,7 @@ from http import HTTPStatus from unittest.mock import Mock, patch +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component @@ -168,3 +169,29 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): "addon": "Mosquitto Test", } ) + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } + ) + + # Ensure first call get logged + assert discovery_info["broker"] == "mock-broker" + assert "Detected code that accessed discovery_info['broker']" in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["broker"] == "mock-broker" + assert "Detected code that accessed discovery_info['broker']" not in caplog.text From 9aa0994809b9003c327950586480ccf51317fd68 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 2 Dec 2021 12:42:31 -0700 Subject: [PATCH 1237/1452] Add configuration URL for SimpliSafe (#60860) --- homeassistant/components/simplisafe/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2e740977d0f..250b3e6c3b1 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -106,13 +106,13 @@ ATTR_PIN_VALUE = "pin" ATTR_SYSTEM_ID = "system_id" ATTR_TIMESTAMP = "timestamp" +DEFAULT_CONFIG_URL = "https://webapp.simplisafe.com/new/#/dashboard" DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel" DEFAULT_ERROR_THRESHOLD = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 - DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" @@ -650,6 +650,7 @@ class SimpliSafeEntity(CoordinatorEntity): } self._attr_device_info = DeviceInfo( + configuration_url=DEFAULT_CONFIG_URL, identifiers={(DOMAIN, serial)}, manufacturer="SimpliSafe", model=model, From 3f2519bedf45b96c791a7e3b9cbe1931db13c651 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 2 Dec 2021 12:47:15 -0700 Subject: [PATCH 1238/1452] Reorganize Guardian services (#58632) --- homeassistant/components/guardian/__init__.py | 171 +++++++++++++++++- homeassistant/components/guardian/const.py | 1 - .../components/guardian/services.yaml | 103 +++++++---- homeassistant/components/guardian/switch.py | 118 +----------- .../components/guardian/translations/en.json | 3 - 5 files changed, 232 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 0edd7088b89..892080b9afe 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -2,14 +2,23 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable +from collections.abc import Awaitable, Callable from typing import cast from aioguardian import Client +from aioguardian.errors import GuardianError +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_FILENAME, + CONF_IP_ADDRESS, + CONF_PORT, + CONF_URL, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -28,16 +37,66 @@ from .const import ( DATA_CLIENT, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_PAIRED_SENSOR_MANAGER, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) from .util import GuardianDataUpdateCoordinator +DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" + +SERVICE_NAME_DISABLE_AP = "disable_ap" +SERVICE_NAME_ENABLE_AP = "enable_ap" +SERVICE_NAME_PAIR_SENSOR = "pair_sensor" +SERVICE_NAME_REBOOT = "reboot" +SERVICE_NAME_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" +SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" +SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" + +SERVICES = ( + SERVICE_NAME_DISABLE_AP, + SERVICE_NAME_ENABLE_AP, + SERVICE_NAME_PAIR_SENSOR, + SERVICE_NAME_REBOOT, + SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_NAME_UPGRADE_FIRMWARE, +) + +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_UID): cv.string, + } +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, +) + + PLATFORMS = ["binary_sensor", "sensor", "switch"] +@callback +def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: + """Get the entry ID related to a service call (by device ID).""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.entry_id in device_entry.config_entries: + return entry.entry_id + + raise ValueError(f"No client for device ID: {device_id}") + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -95,6 +154,97 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up all of the Guardian entity platforms: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + @callback + def extract_client(func: Callable) -> Callable: + """Define a decorator to get the correct client for a service call.""" + + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + entry_id = async_get_entry_id_for_service_call(hass, call) + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + + try: + async with client: + await func(call, client) + except GuardianError as err: + LOGGER.error("Error while executing %s: %s", func.__name__, err) + + return wrapper + + @extract_client + async def async_disable_ap(call: ServiceCall, client: Client) -> None: + """Disable the onboard AP.""" + await client.wifi.disable_ap() + + @extract_client + async def async_enable_ap(call: ServiceCall, client: Client) -> None: + """Enable the onboard AP.""" + await client.wifi.enable_ap() + + @extract_client + async def async_pair_sensor(call: ServiceCall, client: Client) -> None: + """Add a new paired sensor.""" + entry_id = async_get_entry_id_for_service_call(hass, call) + paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + uid = call.data[CONF_UID] + + await client.sensor.pair_sensor(uid) + await paired_sensor_manager.async_pair_sensor(uid) + + @extract_client + async def async_reboot(call: ServiceCall, client: Client) -> None: + """Reboot the valve controller.""" + await client.system.reboot() + + @extract_client + async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + """Fully reset system motor diagnostics.""" + await client.valve.reset() + + @extract_client + async def async_unpair_sensor(call: ServiceCall, client: Client) -> None: + """Remove a paired sensor.""" + entry_id = async_get_entry_id_for_service_call(hass, call) + paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + uid = call.data[CONF_UID] + + await client.sensor.unpair_sensor(uid) + await paired_sensor_manager.async_unpair_sensor(uid) + + @extract_client + async def async_upgrade_firmware(call: ServiceCall, client: Client) -> None: + """Upgrade the device firmware.""" + await client.system.upgrade_firmware( + url=call.data[CONF_URL], + port=call.data[CONF_PORT], + filename=call.data[CONF_FILENAME], + ) + + for service_name, schema, method in ( + (SERVICE_NAME_DISABLE_AP, None, async_disable_ap), + (SERVICE_NAME_ENABLE_AP, None, async_enable_ap), + ( + SERVICE_NAME_PAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_pair_sensor, + ), + (SERVICE_NAME_REBOOT, None, async_reboot), + (SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics), + ( + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_unpair_sensor, + ), + ( + SERVICE_NAME_UPGRADE_FIRMWARE, + SERVICE_UPGRADE_FIRMWARE_SCHEMA, + async_upgrade_firmware, + ), + ): + if hass.services.has_service(DOMAIN, service_name): + continue + hass.services.async_register(DOMAIN, service_name, method, schema=schema) + return True @@ -104,6 +254,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of Guardian, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 5e3779cc447..3499db24c03 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -17,6 +17,5 @@ CONF_UID = "uid" DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" -DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index cb2e7827657..4d48783c955 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -2,25 +2,36 @@ disable_ap: name: Disable AP description: Disable the device's onboard access point. - target: - entity: - integration: guardian - domain: switch + fields: + device_id: + name: Valve Controller + description: The valve controller whose AP should be disabled + required: true + selector: + device: + integration: guardian enable_ap: name: Enable AP description: Enable the device's onboard access point. - target: - entity: - integration: guardian - domain: switch -pair_sensor: - name: Pair sensor - description: Add a new paired sensor to the valve controller. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller whose AP should be enabled + required: true + selector: + device: + integration: guardian +pair_sensor: + name: Pair Sensor + description: Add a new paired sensor to the valve controller. + fields: + device_id: + name: Valve Controller + description: The valve controller to add the sensor to + required: true + selector: + device: + integration: guardian uid: name: UID description: The UID of the paired sensor @@ -31,25 +42,36 @@ pair_sensor: reboot: name: Reboot description: Reboot the device. - target: - entity: - integration: guardian - domain: switch -reset_valve_diagnostics: - name: Reset valve diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - target: - entity: - integration: guardian - domain: switch -unpair_sensor: - name: Unpair sensor - description: Remove a paired sensor from the valve controller. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller to reboot + required: true + selector: + device: + integration: guardian +reset_valve_diagnostics: + name: Reset Valve Diagnostics + description: Fully (and irrecoverably) reset all valve diagnostics. + fields: + device_id: + name: Valve Controller + description: The valve controller whose diagnostics should be reset + required: true + selector: + device: + integration: guardian +unpair_sensor: + name: Unpair Sensor + description: Remove a paired sensor from the valve controller. + fields: + device_id: + name: Valve Controller + description: The valve controller to remove the sensor from + required: true + selector: + device: + integration: guardian uid: name: UID description: The UID of the paired sensor @@ -60,11 +82,14 @@ unpair_sensor: upgrade_firmware: name: Upgrade firmware description: Upgrade the device firmware. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller whose firmware should be upgraded + required: true + selector: + device: + integration: guardian url: name: URL description: The URL of the server hosting the firmware file. @@ -76,7 +101,9 @@ upgrade_firmware: description: The port on which the firmware file is served. example: 443 selector: - text: + number: + min: 1 + max: 65535 filename: name: Filename description: The firmware filename. diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 7417499e53a..17e28c1901a 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -5,40 +5,21 @@ from typing import Any from aioguardian import Client from aioguardian.errors import GuardianError -import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FILENAME, CONF_PORT, CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ValveControllerEntity -from .const import ( - API_VALVE_STATUS, - CONF_UID, - DATA_CLIENT, - DATA_COORDINATOR, - DATA_PAIRED_SENSOR_MANAGER, - DOMAIN, - LOGGER, -) +from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN, LOGGER ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" ATTR_TRAVEL_COUNT = "travel_count" -SERVICE_DISABLE_AP = "disable_ap" -SERVICE_ENABLE_AP = "enable_ap" -SERVICE_PAIR_SENSOR = "pair_sensor" -SERVICE_REBOOT = "reboot" -SERVICE_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" -SERVICE_UNPAIR_SENSOR = "unpair_sensor" -SERVICE_UPGRADE_FIRMWARE = "upgrade_firmware" - SWITCH_KIND_VALVE = "valve" SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( @@ -52,31 +33,6 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - platform = entity_platform.async_get_current_platform() - - for service_name, schema, method in ( - (SERVICE_DISABLE_AP, {}, "async_disable_ap"), - (SERVICE_ENABLE_AP, {}, "async_enable_ap"), - (SERVICE_PAIR_SENSOR, {vol.Required(CONF_UID): cv.string}, "async_pair_sensor"), - (SERVICE_REBOOT, {}, "async_reboot"), - (SERVICE_RESET_VALVE_DIAGNOSTICS, {}, "async_reset_valve_diagnostics"), - ( - SERVICE_UPGRADE_FIRMWARE, - { - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, - "async_upgrade_firmware", - ), - ( - SERVICE_UNPAIR_SENSOR, - {vol.Required(CONF_UID): cv.string}, - "async_unpair_sensor", - ), - ): - platform.async_register_entity_service(service_name, schema, method) - async_add_entities( [ ValveControllerSwitch( @@ -135,78 +91,6 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): } ) - async def async_disable_ap(self) -> None: - """Disable the device's onboard access point.""" - try: - async with self._client: - await self._client.wifi.disable_ap() - except GuardianError as err: - LOGGER.error("Error while disabling valve controller AP: %s", err) - - async def async_enable_ap(self) -> None: - """Enable the device's onboard access point.""" - try: - async with self._client: - await self._client.wifi.enable_ap() - except GuardianError as err: - LOGGER.error("Error while enabling valve controller AP: %s", err) - - async def async_pair_sensor(self, *, uid: str) -> None: - """Add a new paired sensor.""" - try: - async with self._client: - await self._client.sensor.pair_sensor(uid) - except GuardianError as err: - LOGGER.error("Error while adding paired sensor: %s", err) - return - - await self.hass.data[DOMAIN][self._entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ].async_pair_sensor(uid) - - async def async_reboot(self) -> None: - """Reboot the device.""" - try: - async with self._client: - await self._client.system.reboot() - except GuardianError as err: - LOGGER.error("Error while rebooting valve controller: %s", err) - - async def async_reset_valve_diagnostics(self) -> None: - """Fully reset system motor diagnostics.""" - try: - async with self._client: - await self._client.valve.reset() - except GuardianError as err: - LOGGER.error("Error while resetting valve diagnostics: %s", err) - - async def async_unpair_sensor(self, *, uid: str) -> None: - """Add a new paired sensor.""" - try: - async with self._client: - await self._client.sensor.unpair_sensor(uid) - except GuardianError as err: - LOGGER.error("Error while removing paired sensor: %s", err) - return - - await self.hass.data[DOMAIN][self._entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ].async_unpair_sensor(uid) - - async def async_upgrade_firmware( - self, *, url: str, port: int, filename: str - ) -> None: - """Upgrade the device firmware.""" - try: - async with self._client: - await self._client.system.upgrade_firmware( - url=url, - port=port, - filename=filename, - ) - except GuardianError as err: - LOGGER.error("Error while upgrading firmware: %s", err) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn the valve off (closed).""" try: diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 52932cce02b..310f550bcc1 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Configure a local Elexa Guardian device." - }, - "zeroconf_confirm": { - "description": "Do you want to set up this Guardian device?" } } } From dff624f371d20a4b7f9c14862c107ddb1b57a0df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 20:56:54 +0100 Subject: [PATCH 1239/1452] Use HassioServiceInfo in ozw tests (#60864) --- tests/components/ozw/test_config_flow.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 384e16b57ed..f06c315500f 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -4,19 +4,22 @@ from unittest.mock import patch import pytest from homeassistant import config_entries +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN from tests.common import MockConfigEntry -ADDON_DISCOVERY_INFO = { - "addon": "OpenZWave", - "host": "host1", - "port": 1234, - "username": "name1", - "password": "pass1", -} +ADDON_DISCOVERY_INFO = HassioServiceInfo( + config={ + "addon": "OpenZWave", + "host": "host1", + "port": 1234, + "username": "name1", + "password": "pass1", + } +) @pytest.fixture(name="supervisor") From 2be939b45b725bc3eb9668eeb991e1db49c1e194 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 20:57:45 +0100 Subject: [PATCH 1240/1452] Use HassioServiceInfo in zwave_js tests (#60863) --- tests/components/zwave_js/test_config_flow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3094d10b2bd..0542457a23b 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -8,17 +8,20 @@ from zwave_js_server.version import VersionInfo from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN from tests.common import MockConfigEntry -ADDON_DISCOVERY_INFO = { - "addon": "Z-Wave JS", - "host": "host1", - "port": 3001, -} +ADDON_DISCOVERY_INFO = HassioServiceInfo( + config={ + "addon": "Z-Wave JS", + "host": "host1", + "port": 3001, + } +) USB_DISCOVERY_INFO = usb.UsbServiceInfo( From ade228ebc68cc9f345790e68fac17fa09b05efd3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:09:26 +0100 Subject: [PATCH 1241/1452] Use HassioServiceInfo in motioneye tests (#60861) --- tests/components/motioneye/test_config_flow.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 878795a5a70..35988ed8fbb 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -8,6 +8,7 @@ from motioneye_client.client import ( ) from homeassistant import config_entries, data_entry_flow +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, @@ -74,7 +75,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye", "url": TEST_URL}, + data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -349,7 +350,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye", "url": TEST_URL}, + data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT @@ -364,7 +365,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye", "url": TEST_URL}, + data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT @@ -380,7 +381,7 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye", "url": TEST_URL}, + data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT @@ -392,7 +393,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye", "url": TEST_URL}, + data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM From 272b14755091ab85abd5c1aeb00b7ce52928c75c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:10:15 +0100 Subject: [PATCH 1242/1452] Use HassioServiceInfo in adguard tests (#60866) --- tests/components/adguard/test_config_flow.py | 25 ++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 75d138f400c..b87d0796a54 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -3,6 +3,7 @@ import aiohttp from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -121,7 +122,13 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, + data=HassioServiceInfo( + config={ + "addon": "AdGuard Home Addon", + "host": "mock-adguard", + "port": "3000", + } + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -137,7 +144,13 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, + data=HassioServiceInfo( + config={ + "addon": "AdGuard Home Addon", + "host": "mock-adguard", + "port": "3000", + } + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -157,7 +170,9 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, + data=HassioServiceInfo( + config={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000} + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -191,7 +206,9 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, + data=HassioServiceInfo( + config={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000} + ), context={"source": config_entries.SOURCE_HASSIO}, ) From 37200decf95fea4175eae5455aea2995013aa349 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:10:50 +0100 Subject: [PATCH 1243/1452] Use HassioServiceInfo in mqtt tests (#60865) --- tests/components/mqtt/test_config_flow.py | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 208541d702c..91f8483bde6 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import mqtt +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -110,7 +111,9 @@ async def test_hassio_already_configured(hass): MockConfigEntry(domain="mqtt").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_HASSIO} + "mqtt", + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo(config={}), ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -124,7 +127,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( mqtt.DOMAIN, - data={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"}, + data=HassioServiceInfo( + config={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"} + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -140,14 +145,16 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( "mqtt", - data={ - "addon": "Mock Addon", - "host": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - }, + data=HassioServiceInfo( + config={ + "addon": "Mock Addon", + "host": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + } + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == "form" From d7c4a669f164cb536cc6c6571f9e287c4c608f3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Dec 2021 10:13:41 -1000 Subject: [PATCH 1244/1452] Revert "Use HassioServiceInfo in mqtt tests" (#60867) --- tests/components/mqtt/test_config_flow.py | 27 +++++++++-------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 91f8483bde6..208541d702c 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import mqtt -from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -111,9 +110,7 @@ async def test_hassio_already_configured(hass): MockConfigEntry(domain="mqtt").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", - context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config={}), + "mqtt", context={"source": config_entries.SOURCE_HASSIO} ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -127,9 +124,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( mqtt.DOMAIN, - data=HassioServiceInfo( - config={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"} - ), + data={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"}, context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -145,16 +140,14 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( "mqtt", - data=HassioServiceInfo( - config={ - "addon": "Mock Addon", - "host": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - } - ), + data={ + "addon": "Mock Addon", + "host": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + }, context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == "form" From 9e96f3e22794eb8356cac5a1599b5496a53561d5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:33:23 +0100 Subject: [PATCH 1245/1452] Use HassioServiceInfo in deconz tests (#60868) --- tests/components/deconz/test_config_flow.py | 45 ++++++++++++--------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 11b0a3bdb31..a87a9df408b 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.deconz.const import ( CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( SOURCE_HASSIO, @@ -542,13 +543,15 @@ async def test_flow_hassio_discovery(hass): """Test hassio discovery flow works.""" result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - "addon": "Mock Addon", - CONF_HOST: "mock-deconz", - CONF_PORT: 80, - CONF_SERIAL: BRIDGEID, - CONF_API_KEY: API_KEY, - }, + data=HassioServiceInfo( + config={ + "addon": "Mock Addon", + CONF_HOST: "mock-deconz", + CONF_PORT: 80, + CONF_SERIAL: BRIDGEID, + CONF_API_KEY: API_KEY, + } + ), context={"source": SOURCE_HASSIO}, ) assert result["type"] == RESULT_TYPE_FORM @@ -583,12 +586,14 @@ async def test_hassio_discovery_update_configuration(hass, aioclient_mock): ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, - CONF_API_KEY: "updated", - CONF_SERIAL: BRIDGEID, - }, + data=HassioServiceInfo( + config={ + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, + CONF_API_KEY: "updated", + CONF_SERIAL: BRIDGEID, + } + ), context={"source": SOURCE_HASSIO}, ) await hass.async_block_till_done() @@ -607,12 +612,14 @@ async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data={ - CONF_HOST: "1.2.3.4", - CONF_PORT: 80, - CONF_API_KEY: API_KEY, - CONF_SERIAL: BRIDGEID, - }, + data=HassioServiceInfo( + config={ + CONF_HOST: "1.2.3.4", + CONF_PORT: 80, + CONF_API_KEY: API_KEY, + CONF_SERIAL: BRIDGEID, + } + ), context={"source": SOURCE_HASSIO}, ) From 3c66706a49e0d0a6147f352b6b8d425d7b874fae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 21:35:11 +0100 Subject: [PATCH 1246/1452] Clean up deprecated sanitize_filename and sanitize_path (#60859) --- homeassistant/util/__init__.py | 33 --------------------------------- tests/util/test_init.py | 16 ---------------- 2 files changed, 49 deletions(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index b71fd8fa5e4..637b98a1a75 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -15,7 +15,6 @@ from typing import Any, TypeVar import slugify as unicode_slug -from ..helpers.deprecation import deprecated_function from .dt import as_local, utcnow T = TypeVar("T") @@ -46,38 +45,6 @@ def raise_if_invalid_path(path: str) -> None: raise ValueError(f"{path} is not a safe path") -@deprecated_function(replacement="raise_if_invalid_filename") -def sanitize_filename(filename: str) -> str: - """Check if a filename is safe. - - Only to be used to compare to original filename to check if changed. - If result changed, the given path is not safe and should not be used, - raise an error. - - DEPRECATED. - """ - # Backwards compatible fix for misuse of method - if RE_SANITIZE_FILENAME.sub("", filename) != filename: - return "" - return filename - - -@deprecated_function(replacement="raise_if_invalid_path") -def sanitize_path(path: str) -> str: - """Check if a path is safe. - - Only to be used to compare to original path to check if changed. - If result changed, the given path is not safe and should not be used, - raise an error. - - DEPRECATED. - """ - # Backwards compatible fix for misuse of method - if RE_SANITIZE_PATH.sub("", path) != path: - return "" - return path - - def slugify(text: str | None, *, separator: str = "_") -> str: """Slugify a given text.""" if text == "" or text is None: diff --git a/tests/util/test_init.py b/tests/util/test_init.py index b2ede0c881b..e80c3fd6989 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -8,22 +8,6 @@ from homeassistant import util import homeassistant.util.dt as dt_util -def test_sanitize_filename(): - """Test sanitize_filename.""" - assert util.sanitize_filename("test") == "test" - assert util.sanitize_filename("/test") == "" - assert util.sanitize_filename("..test") == "" - assert util.sanitize_filename("\\test") == "" - assert util.sanitize_filename("\\../test") == "" - - -def test_sanitize_path(): - """Test sanitize_path.""" - assert util.sanitize_path("test/path") == "test/path" - assert util.sanitize_path("~test/path") == "" - assert util.sanitize_path("~/../test/path") == "" - - def test_raise_if_invalid_filename(): """Test raise_if_invalid_filename.""" assert util.raise_if_invalid_filename("test") is None From fd8e7ae5d9a9c853aecf096ac09412f8196fda66 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 2 Dec 2021 16:05:15 -0500 Subject: [PATCH 1247/1452] Add PPB VOC sensor to ZHA (#60853) --- homeassistant/components/zha/sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d5df223ffb0..304a3d155f5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -30,6 +30,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_ENERGY, ELECTRIC_CURRENT_AMPERE, @@ -541,6 +542,16 @@ class VOCLevel(Sensor): _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER +@STRICT_MATCH(channel_names="voc_level", models="lumi.airmonitor.acn01") +class PPBVOCLevel(Sensor): + """VOC Level sensor.""" + + SENSOR_ATTR = "measured_value" + _decimals = 0 + _multiplier = 1 + _unit = CONCENTRATION_PARTS_PER_BILLION + + @STRICT_MATCH(channel_names="formaldehyde_concentration") class FormaldehydeConcentration(Sensor): """Formaldehyde Concentration sensor.""" From 6e220d5d17c828dc6705fc23cbde4dc7230b3226 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 22:30:52 +0100 Subject: [PATCH 1248/1452] Move Platform StrEnum to const (#60857) --- homeassistant/components/wled/__init__.py | 2 +- homeassistant/const.py | 38 +++++++++++++++++++++++ homeassistant/helpers/entity_platform.py | 36 --------------------- homeassistant/setup.py | 30 ++---------------- homeassistant/util/dt.py | 4 +-- script/hassfest/dependencies.py | 7 ++--- 6 files changed, 44 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 0518bd736d0..659df1baad9 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import Platform from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator diff --git a/homeassistant/const.py b/homeassistant/const.py index fb8dfc6089a..59532b84c54 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Final +from homeassistant.util.enum import StrEnum + MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 PATCH_VERSION: Final = "0.dev0" @@ -16,6 +18,42 @@ REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2022.1" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" + +class Platform(StrEnum): + """Available entity platforms.""" + + AIR_QUALITY = "air_quality" + ALARM_CONTROL_PANEL = "alarm_control_panel" + BINARY_SENSOR = "binary_sensor" + BUTTON = "button" + CALENDAR = "calendar" + CAMERA = "camera" + CLIMATE = "climate" + COVER = "cover" + DEVICE_TRACKER = "device_tracker" + FAN = "fan" + GEO_LOCATION = "geo_location" + HUMIDIFIER = "humidifier" + IMAGE_PROCESSING = "image_processing" + LIGHT = "light" + LOCK = "lock" + MAILBOX = "mailbox" + MEDIA_PLAYER = "media_player" + NOTIFY = "notify" + NUMBER = "number" + REMOTE = "remote" + SCENE = "scene" + SELECT = "select" + SENSOR = "sensor" + SIREN = "siren" + SST = "sst" + SWITCH = "switch" + TTS = "tts" + VACUUM = "vacuum" + WATER_HEATER = "water_heater" + WEATHER = "weather" + + # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL: Final = "*" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 45795a44c42..d8cb8477f11 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -34,7 +34,6 @@ from homeassistant.exceptions import ( ) from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.enum import StrEnum from . import ( config_validation as cv, @@ -63,41 +62,6 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = getLogger(__name__) -class Platform(StrEnum): - """Available platforms.""" - - AIR_QUALITY = "air_quality" - ALARM_CONTROL_PANEL = "alarm_control_panel" - BINARY_SENSOR = "binary_sensor" - BUTTON = "button" - CALENDAR = "calendar" - CAMERA = "camera" - CLIMATE = "climate" - COVER = "cover" - DEVICE_TRACKER = "device_tracker" - FAN = "fan" - GEO_LOCATION = "geo_location" - HUMIDIFIER = "humidifier" - IMAGE_PROCESSING = "image_processing" - LIGHT = "light" - LOCK = "lock" - MAILBOX = "mailbox" - MEDIA_PLAYER = "media_player" - NOTIFY = "notify" - NUMBER = "number" - REMOTE = "remote" - SCENE = "scene" - SELECT = "select" - SENSOR = "sensor" - SIREN = "siren" - SST = "sst" - SWITCH = "switch" - TTS = "tts" - VACUUM = "vacuum" - WATER_HEATER = "water_heater" - WEATHER = "weather" - - class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 67740c54254..3e6db28a194 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -14,6 +14,7 @@ from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, + Platform, ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import HomeAssistantError @@ -26,34 +27,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" -BASE_PLATFORMS = { - "air_quality", - "alarm_control_panel", - "binary_sensor", - "camera", - "calendar", - "climate", - "cover", - "device_tracker", - "fan", - "humidifier", - "image_processing", - "light", - "lock", - "media_player", - "notify", - "number", - "remote", - "scene", - "select", - "sensor", - "siren", - "switch", - "tts", - "vacuum", - "water_heater", - "weather", -} +BASE_PLATFORMS = {platform.value for platform in Platform} DATA_SETUP_DONE = "setup_done" DATA_SETUP_STARTED = "setup_started" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 39f8a63e53f..0c8a1cd9aad 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -10,8 +10,6 @@ from typing import Any, cast import ciso8601 -from homeassistant.const import MATCH_ALL - if sys.version_info[:2] >= (3, 9): import zoneinfo else: @@ -215,7 +213,7 @@ def get_age(date: dt.datetime) -> str: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]: """Parse the time expression part and return a list of times to match.""" - if parameter is None or parameter == MATCH_ALL: + if parameter is None or parameter == "*": res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str): if parameter.startswith("/"): diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 3f0bd9c1236..c52a926e4e1 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -4,8 +4,8 @@ from __future__ import annotations import ast from pathlib import Path +from homeassistant.const import Platform from homeassistant.requirements import DISCOVERY_INTEGRATIONS -from homeassistant.setup import BASE_PLATFORMS from .model import Integration @@ -91,12 +91,11 @@ class ImportCollector(ast.NodeVisitor): ALLOWED_USED_COMPONENTS = { + *{platform.value for platform in Platform}, # Internal integrations "alert", "automation", - "button", "conversation", - "button", "device_automation", "frontend", "group", @@ -119,8 +118,6 @@ ALLOWED_USED_COMPONENTS = { "webhook", "websocket_api", "zone", - # Entity integrations with platforms - *BASE_PLATFORMS, # Other "mjpeg", # base class, has no reqs or component to load. "stream", # Stream cannot install on all systems, can be imported without reqs. From c8b0a3b6672ab0267c4d3c563aea11690b0535be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 23:19:08 +0100 Subject: [PATCH 1249/1452] Revert "Move Platform StrEnum to const" (#60875) --- homeassistant/components/wled/__init__.py | 2 +- homeassistant/const.py | 38 ----------------------- homeassistant/helpers/entity_platform.py | 36 +++++++++++++++++++++ homeassistant/setup.py | 30 ++++++++++++++++-- homeassistant/util/dt.py | 4 ++- script/hassfest/dependencies.py | 7 +++-- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 659df1baad9..0518bd736d0 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import Platform from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator diff --git a/homeassistant/const.py b/homeassistant/const.py index 59532b84c54..fb8dfc6089a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Final -from homeassistant.util.enum import StrEnum - MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 PATCH_VERSION: Final = "0.dev0" @@ -18,42 +16,6 @@ REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2022.1" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" - -class Platform(StrEnum): - """Available entity platforms.""" - - AIR_QUALITY = "air_quality" - ALARM_CONTROL_PANEL = "alarm_control_panel" - BINARY_SENSOR = "binary_sensor" - BUTTON = "button" - CALENDAR = "calendar" - CAMERA = "camera" - CLIMATE = "climate" - COVER = "cover" - DEVICE_TRACKER = "device_tracker" - FAN = "fan" - GEO_LOCATION = "geo_location" - HUMIDIFIER = "humidifier" - IMAGE_PROCESSING = "image_processing" - LIGHT = "light" - LOCK = "lock" - MAILBOX = "mailbox" - MEDIA_PLAYER = "media_player" - NOTIFY = "notify" - NUMBER = "number" - REMOTE = "remote" - SCENE = "scene" - SELECT = "select" - SENSOR = "sensor" - SIREN = "siren" - SST = "sst" - SWITCH = "switch" - TTS = "tts" - VACUUM = "vacuum" - WATER_HEATER = "water_heater" - WEATHER = "weather" - - # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL: Final = "*" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d8cb8477f11..45795a44c42 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -34,6 +34,7 @@ from homeassistant.exceptions import ( ) from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.enum import StrEnum from . import ( config_validation as cv, @@ -62,6 +63,41 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = getLogger(__name__) +class Platform(StrEnum): + """Available platforms.""" + + AIR_QUALITY = "air_quality" + ALARM_CONTROL_PANEL = "alarm_control_panel" + BINARY_SENSOR = "binary_sensor" + BUTTON = "button" + CALENDAR = "calendar" + CAMERA = "camera" + CLIMATE = "climate" + COVER = "cover" + DEVICE_TRACKER = "device_tracker" + FAN = "fan" + GEO_LOCATION = "geo_location" + HUMIDIFIER = "humidifier" + IMAGE_PROCESSING = "image_processing" + LIGHT = "light" + LOCK = "lock" + MAILBOX = "mailbox" + MEDIA_PLAYER = "media_player" + NOTIFY = "notify" + NUMBER = "number" + REMOTE = "remote" + SCENE = "scene" + SELECT = "select" + SENSOR = "sensor" + SIREN = "siren" + SST = "sst" + SWITCH = "switch" + TTS = "tts" + VACUUM = "vacuum" + WATER_HEATER = "water_heater" + WEATHER = "weather" + + class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 3e6db28a194..67740c54254 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -14,7 +14,6 @@ from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, - Platform, ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import HomeAssistantError @@ -27,7 +26,34 @@ _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" -BASE_PLATFORMS = {platform.value for platform in Platform} +BASE_PLATFORMS = { + "air_quality", + "alarm_control_panel", + "binary_sensor", + "camera", + "calendar", + "climate", + "cover", + "device_tracker", + "fan", + "humidifier", + "image_processing", + "light", + "lock", + "media_player", + "notify", + "number", + "remote", + "scene", + "select", + "sensor", + "siren", + "switch", + "tts", + "vacuum", + "water_heater", + "weather", +} DATA_SETUP_DONE = "setup_done" DATA_SETUP_STARTED = "setup_started" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 0c8a1cd9aad..39f8a63e53f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -10,6 +10,8 @@ from typing import Any, cast import ciso8601 +from homeassistant.const import MATCH_ALL + if sys.version_info[:2] >= (3, 9): import zoneinfo else: @@ -213,7 +215,7 @@ def get_age(date: dt.datetime) -> str: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]: """Parse the time expression part and return a list of times to match.""" - if parameter is None or parameter == "*": + if parameter is None or parameter == MATCH_ALL: res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str): if parameter.startswith("/"): diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c52a926e4e1..3f0bd9c1236 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -4,8 +4,8 @@ from __future__ import annotations import ast from pathlib import Path -from homeassistant.const import Platform from homeassistant.requirements import DISCOVERY_INTEGRATIONS +from homeassistant.setup import BASE_PLATFORMS from .model import Integration @@ -91,11 +91,12 @@ class ImportCollector(ast.NodeVisitor): ALLOWED_USED_COMPONENTS = { - *{platform.value for platform in Platform}, # Internal integrations "alert", "automation", + "button", "conversation", + "button", "device_automation", "frontend", "group", @@ -118,6 +119,8 @@ ALLOWED_USED_COMPONENTS = { "webhook", "websocket_api", "zone", + # Entity integrations with platforms + *BASE_PLATFORMS, # Other "mjpeg", # base class, has no reqs or component to load. "stream", # Stream cannot install on all systems, can be imported without reqs. From a07f75c6b0578c9fa4101b11e5c06f9a0174baee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 23:37:19 +0100 Subject: [PATCH 1250/1452] Revert "Allow template int filter to render from a bytes based integer" (#60855) --- homeassistant/helpers/template.py | 18 ++------------- tests/helpers/test_template.py | 37 ++----------------------------- 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3c7e1cb2952..62e9f3fab6a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1567,15 +1567,8 @@ def forgiving_float_filter(value, default=_SENTINEL): return default -def forgiving_int(value, default=_SENTINEL, base=10, little_endian=False): +def forgiving_int(value, default=_SENTINEL, base=10): """Try to convert value to an int, and warn if it fails.""" - if isinstance(value, bytes) and value: - if base != 10: - _LOGGER.warning( - "Template warning: 'int' got 'bytes' type input, ignoring base=%s parameter", - base, - ) - return convert_to_int(value, little_endian=little_endian) result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: warn_no_default("int", value, value) @@ -1583,15 +1576,8 @@ def forgiving_int(value, default=_SENTINEL, base=10, little_endian=False): return result -def forgiving_int_filter(value, default=_SENTINEL, base=10, little_endian=False): +def forgiving_int_filter(value, default=_SENTINEL, base=10): """Try to convert value to an int, and warn if it fails.""" - if isinstance(value, bytes) and value: - if base != 10: - _LOGGER.warning( - "Template warning: 'int' got 'bytes' type input, ignoring base=%s parameter", - base, - ) - return convert_to_int(value, little_endian=little_endian) result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: warn_no_default("int", value, 0) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 40e390b2070..bd405acf597 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -252,7 +252,7 @@ def test_float_filter(hass): assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 -def test_int_filter(hass, caplog): +def test_int_filter(hass): """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ states.sensor.temperature.state | int }}") == 12 @@ -265,25 +265,8 @@ def test_int_filter(hass, caplog): assert render(hass, "{{ 'bad' | int(1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 - # Test with bytes based integer - variables = {"value": b"\xde\xad\xbe\xef"} - assert (render(hass, "{{ value | int }}", variables=variables)) == 0xDEADBEEF - assert ( - render(hass, "{{ value | int(little_endian=True) }}", variables=variables) - == 0xEFBEADDE - ) - # Test with base and base parameter set - assert ( - render(hass, "{{ value | int(base=16) }}", variables=variables) - ) == 0xDEADBEEF - assert ( - "Template warning: 'int' got 'bytes' type input, ignoring base=16 parameter" - in caplog.text - ) - - -def test_int_function(hass, caplog): +def test_int_function(hass): """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ int(states.sensor.temperature.state) }}") == 12 @@ -296,22 +279,6 @@ def test_int_function(hass, caplog): assert render(hass, "{{ int('bad', 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 - # Test with base and base parameter set - variables = {"value": b"\xde\xad\xbe\xef"} - assert (render(hass, "{{ int(value) }}", variables=variables)) == 0xDEADBEEF - assert ( - render(hass, "{{ int(value, little_endian=True) }}", variables=variables) - == 0xEFBEADDE - ) - - assert ( - render(hass, "{{ int(value, base=16) }}", variables=variables) - ) == 0xDEADBEEF - assert ( - "Template warning: 'int' got 'bytes' type input, ignoring base=16 parameter" - in caplog.text - ) - @pytest.mark.parametrize( "value, expected", From 0e3bc21d54c2346e4faf7be2fd1ef64c4cd7a32b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 23:55:12 +0100 Subject: [PATCH 1251/1452] Teach state and numeric_state conditions about entity registry ids (#60841) --- .../alarm_control_panel/device_condition.py | 7 +- .../binary_sensor/device_condition.py | 5 +- .../components/climate/device_condition.py | 4 +- .../components/cover/device_condition.py | 4 +- .../device_automation/toggle_entity.py | 5 +- .../device_tracker/device_condition.py | 4 +- .../components/fan/device_condition.py | 4 +- .../components/humidifier/device_condition.py | 6 +- .../components/light/device_condition.py | 6 +- .../components/lock/device_condition.py | 4 +- .../media_player/device_condition.py | 4 +- .../components/remote/device_condition.py | 6 +- .../components/select/device_condition.py | 4 +- .../components/sensor/device_condition.py | 7 +- .../components/switch/device_condition.py | 6 +- .../components/vacuum/device_condition.py | 4 +- .../components/zwave_js/device_condition.py | 4 +- homeassistant/helpers/condition.py | 37 ++++++++- homeassistant/helpers/config_validation.py | 4 +- .../integration/device_condition.py | 4 +- .../zwave_js/test_device_condition.py | 2 +- tests/helpers/test_condition.py | 53 +++++++++++- tests/helpers/test_script.py | 83 ++++++++++++++++++- 23 files changed, 236 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 6eb0144a4b4..cc01f4a5954 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.entity import get_supported_features @@ -104,7 +104,10 @@ async def async_get_conditions( return conditions -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +@callback +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == CONDITION_TRIGGERED: state = STATE_ALARM_TRIGGERED diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index e6202ef66a4..6f1a0ba4f5f 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -264,7 +264,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type in IS_ON: @@ -279,6 +281,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] state_config = cv.STATE_CONDITION_SCHEMA(state_config) + state_config = condition.state_validate_config(hass, state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 03e456965fe..f3e01b5a387 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -70,7 +70,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_hvac_mode": attribute = const.ATTR_HVAC_MODE diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index d719c4835e2..cca608187a2 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -119,7 +119,9 @@ async def async_get_condition_capabilities( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] in STATE_CONDITION_TYPES: if config[CONF_TYPE] == "is_open": diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f9f7555eeb6..2d0254b9a0a 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -127,7 +127,9 @@ async def async_call_action_from_config( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" if config[CONF_TYPE] == CONF_IS_ON: stat = "on" @@ -142,6 +144,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke state_config[CONF_FOR] = config[CONF_FOR] state_config = cv.STATE_CONDITION_SCHEMA(state_config) + state_config = condition.state_validate_config(hass, state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 78cedd6e900..2762a271cab 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -55,7 +55,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" reverse = config[CONF_TYPE] == "is_not_home" diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 4e3aed026dc..b0882137d7f 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -55,7 +55,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_on": state = STATE_ON diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 28d74d1efff..c8204c91a29 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -65,12 +65,14 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_mode": attribute = ATTR_MODE else: - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 908af72ecfd..12e86c1e23d 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index b3fe4ffcfe1..a818a2b5fa4 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -67,7 +67,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_jammed": state = STATE_JAMMED diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 41034bbe870..d099eb9a8a4 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -59,7 +59,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_playing": state = STATE_PLAYING diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py index ffbfc1c1061..33f680e6829 100644 --- a/homeassistant/components/remote/device_condition.py +++ b/homeassistant/components/remote/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index eadfebbc711..aa22f117ea8 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -52,7 +52,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" @callback diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 6444f58a1de..612ebe0abd5 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -186,7 +186,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" numeric_state_config = { condition.CONF_CONDITION: "numeric_state", @@ -198,6 +200,9 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] numeric_state_config = cv.NUMERIC_STATE_CONDITION_SCHEMA(numeric_state_config) + numeric_state_config = condition.numeric_state_validate_config( + hass, numeric_state_config + ) return condition.async_numeric_state_from_config(numeric_state_config) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 94469275ab3..7f47983ba67 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 1c7b0c93332..7a973c93694 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -53,7 +53,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_docked": test_states = [STATE_DOCKED] diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 4258a0b0892..4b1843782e2 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -147,7 +147,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" condition_type = config[CONF_TYPE] device_id = config[CONF_DEVICE_ID] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a4c62709778..57e8f8e5ba2 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -51,7 +51,7 @@ from homeassistant.exceptions import ( HomeAssistantError, TemplateError, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -71,8 +71,9 @@ from .trace import ( # mypy: disallow-any-generics -FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" +FROM_CONFIG_FORMAT = "{}_from_config" +VALIDATE_CONFIG_FORMAT = "{}_validate_config" _LOGGER = logging.getLogger(__name__) @@ -885,7 +886,7 @@ async def async_device_from_config( return trace_condition_function( cast( ConditionCheckerType, - platform.async_condition_from_config(config), # type: ignore + platform.async_condition_from_config(hass, config), # type: ignore ) ) @@ -908,6 +909,30 @@ async def async_trigger_from_config( return trigger_if +def numeric_state_validate_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate numeric_state condition config.""" + + registry = er.async_get(hass) + config = dict(config) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + +def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: + """Validate state condition config.""" + + registry = er.async_get(hass) + config = dict(config) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + async def async_validate_condition_config( hass: HomeAssistant, config: ConfigType | Template ) -> ConfigType | Template: @@ -933,6 +958,12 @@ async def async_validate_condition_config( return await platform.async_validate_condition_config(hass, config) # type: ignore return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + if condition in ("numeric_state", "state"): + validator = getattr( + sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition) + ) + return validator(hass, config) # type: ignore + return config diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8357746c2cd..cbcfb551dad 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1028,7 +1028,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "numeric_state", - vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids, vol.Optional(CONF_ATTRIBUTE): str, CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA, CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA, @@ -1041,7 +1041,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( STATE_CONDITION_BASE_SCHEMA = { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "state", - vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids, vol.Optional(CONF_ATTRIBUTE): str, vol.Optional(CONF_FOR): positive_time_period, # To support use_trigger_value in automation diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index ca0eda163b8..6f129289af8 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -58,7 +58,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_on": state = STATE_ON diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 1f6a9d9ca90..725d9574605 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -552,7 +552,7 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): with pytest.raises(HomeAssistantError): await device_condition.async_condition_from_config( - {"type": "failed.test", "device_id": device.id} + hass, {"type": "failed.test", "device_id": device.id} ) with patch( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 92bad8777bd..c4ceff89b64 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -16,7 +16,12 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.exceptions import ConditionError, HomeAssistantError -from homeassistant.helpers import condition, config_validation as cv, trace +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, + trace, +) from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -1107,6 +1112,29 @@ async def test_state_attribute_boolean(hass): assert test(hass) +async def test_state_entity_registry_id(hass): + """Test with entity specified by entity registry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "switch", "hue", "1234", suggested_object_id="test" + ) + assert entry.entity_id == "switch.test" + config = { + "condition": "state", + "entity_id": entry.id, + "state": "on", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("switch.test", "on") + assert test(hass) + + hass.states.async_set("switch.test", "off") + assert not test(hass) + + async def test_state_using_input_entities(hass): """Test state conditions using input_* entities.""" await async_setup_component( @@ -1419,6 +1447,29 @@ async def test_numeric_state_attribute(hass): assert not test(hass) +async def test_numeric_state_entity_registry_id(hass): + """Test with entity specified by entity registry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "sensor", "hue", "1234", suggested_object_id="test" + ) + assert entry.entity_id == "sensor.test" + config = { + "condition": "numeric_state", + "entity_id": entry.id, + "above": 100, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.test", "110") + assert test(hass) + + hass.states.async_set("sensor.test", "90") + assert not test(hass) + + async def test_numeric_state_using_input_number(hass): """Test numeric_state conditions using input_number entities.""" hass.states.async_set("number.low", 10) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 23768cd95ce..962fe4b1366 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -25,7 +25,13 @@ from homeassistant.const import ( ) from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback from homeassistant.exceptions import ConditionError, ServiceNotFound -from homeassistant.helpers import config_validation as cv, script, template, trace +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + script, + template, + trace, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -1492,6 +1498,81 @@ async def test_condition_basic(hass, caplog): ) +async def test_condition_validation(hass, caplog): + """Test if we can use conditions which validate late in a script.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + event = "test_event" + events = async_capture_events(hass, event) + alias = "condition step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "alias": alias, + "condition": "state", + "entity_id": entry.id, + "state": "hello", + }, + {"event": event}, + ] + ) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: True" in caplog.text + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/entity_id/0": [ + {"result": {"result": True, "state": "hello", "wanted_state": "hello"}} + ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + hass.states.async_set("test.entity", "goodbye") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: False" in caplog.text + assert len(events) == 3 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [ + { + "error_type": script._StopScript, + "result": {"result": False}, + } + ], + "1/entity_id/0": [ + { + "result": { + "result": False, + "state": "goodbye", + "wanted_state": "hello", + } + } + ], + }, + expected_script_execution="aborted", + ) + + @patch("homeassistant.helpers.script.condition.async_from_config") async def test_condition_created_once(async_from_config, hass): """Test that the conditions do not get created multiple times.""" From e8c6e0ef2b3133e9d0c57ab326951f8970e5bf49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Dec 2021 15:19:09 -0800 Subject: [PATCH 1252/1452] Bump frontend to 20211202.0 (#60877) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index be8766c2608..2eecc6d9a0f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211201.0" + "home-assistant-frontend==20211202.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43425199d99..bdfe44cc8ac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211201.0 +home-assistant-frontend==20211202.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 0eb4df56194..6a9457e4583 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211201.0 +home-assistant-frontend==20211202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f70f39fbcc..8e242ecf7d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211201.0 +home-assistant-frontend==20211202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 42fff989d17f2076c9a732a1978c2a27cc500c56 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 3 Dec 2021 00:13:00 +0000 Subject: [PATCH 1253/1452] [ci skip] Translation update --- .../bmw_connected_drive/translations/sv.json | 30 +++++++++++++++++++ .../components/guardian/translations/en.json | 3 ++ .../components/nest/translations/id.json | 13 +++++++- .../components/nest/translations/tr.json | 2 +- .../components/nest/translations/zh-Hant.json | 2 +- .../components/tailscale/translations/et.json | 26 ++++++++++++++++ .../components/tailscale/translations/id.json | 26 ++++++++++++++++ .../components/tailscale/translations/ja.json | 24 +++++++++++++++ .../components/tailscale/translations/no.json | 26 ++++++++++++++++ .../components/tailscale/translations/tr.json | 26 ++++++++++++++++ .../tailscale/translations/zh-Hant.json | 26 ++++++++++++++++ .../tractive/translations/sensor.ca.json | 10 +++++++ .../tractive/translations/sensor.de.json | 10 +++++++ .../tractive/translations/sensor.et.json | 10 +++++++ .../tractive/translations/sensor.id.json | 10 +++++++ .../tractive/translations/sensor.ja.json | 10 +++++++ .../tractive/translations/sensor.nl.json | 10 +++++++ .../tractive/translations/sensor.no.json | 10 +++++++ .../tractive/translations/sensor.ru.json | 10 +++++++ .../tractive/translations/sensor.tr.json | 10 +++++++ .../tractive/translations/sensor.zh-Hant.json | 10 +++++++ .../translations/ca.json | 6 ++++ .../translations/de.json | 6 ++++ .../translations/et.json | 6 ++++ .../translations/id.json | 23 ++++++++++++++ .../translations/ja.json | 4 +++ .../translations/nl.json | 6 ++++ .../translations/no.json | 6 ++++ .../translations/ru.json | 6 ++++ .../translations/tr.json | 6 ++++ .../translations/zh-Hant.json | 6 ++++ .../tuya/translations/select.id.json | 4 +++ .../tuya/translations/select.nl.json | 1 + .../yale_smart_alarm/translations/id.json | 3 +- .../yale_smart_alarm/translations/tr.json | 3 +- 35 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bmw_connected_drive/translations/sv.json create mode 100644 homeassistant/components/tailscale/translations/et.json create mode 100644 homeassistant/components/tailscale/translations/id.json create mode 100644 homeassistant/components/tailscale/translations/ja.json create mode 100644 homeassistant/components/tailscale/translations/no.json create mode 100644 homeassistant/components/tailscale/translations/tr.json create mode 100644 homeassistant/components/tailscale/translations/zh-Hant.json create mode 100644 homeassistant/components/tractive/translations/sensor.ca.json create mode 100644 homeassistant/components/tractive/translations/sensor.de.json create mode 100644 homeassistant/components/tractive/translations/sensor.et.json create mode 100644 homeassistant/components/tractive/translations/sensor.id.json create mode 100644 homeassistant/components/tractive/translations/sensor.ja.json create mode 100644 homeassistant/components/tractive/translations/sensor.nl.json create mode 100644 homeassistant/components/tractive/translations/sensor.no.json create mode 100644 homeassistant/components/tractive/translations/sensor.ru.json create mode 100644 homeassistant/components/tractive/translations/sensor.tr.json create mode 100644 homeassistant/components/tractive/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/id.json diff --git a/homeassistant/components/bmw_connected_drive/translations/sv.json b/homeassistant/components/bmw_connected_drive/translations/sv.json new file mode 100644 index 00000000000..93cd828041e --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "region": "ConnectedDrive Region", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Endast l\u00e4sbar (endast sensorer och meddelanden, ingen k\u00f6rning av tj\u00e4nster, ingen l\u00e5sning)", + "use_location": "Anv\u00e4nd plats f\u00f6r Home Assistant som plats f\u00f6r bilen (kr\u00e4vs f\u00f6r icke i3/i8-fordon tillverkade f\u00f6re 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 310f550bcc1..52932cce02b 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -15,6 +15,9 @@ "port": "Port" }, "description": "Configure a local Elexa Guardian device." + }, + "zeroconf_confirm": { + "description": "Do you want to set up this Guardian device?" } } } diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index b78b1764343..cc7cdc600a8 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", "reauth_successful": "Autentikasi ulang berhasil", @@ -12,10 +13,13 @@ "default": "Berhasil diautentikasi" }, "error": { + "bad_project_id": "Masukkan Cloud Project ID yang valid (periksa Cloud Console)", "internal_error": "Kesalahan internal saat memvalidasi kode", "invalid_pin": "Invalid Kode PIN", + "subscriber_error": "Kesalahan pelanggan tidak diketahui, lihat log", "timeout": "Tenggang waktu memvalidasi kode telah habis.", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_project_id": "Masukkan Cloud Project ID yang valid (Device Access Project ID yang ditemukan)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Pilih Metode Autentikasi" }, + "pubsub": { + "data": { + "cloud_project_id": "ID Proyek Google Cloud" + }, + "description": "Kunjungi [Cloud Console]( {url} ) untuk menemukan ID Proyek Google Cloud Anda.", + "title": "Konfigurasikan Google Cloud" + }, "reauth_confirm": { "description": "Integrasi Nest perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 03ff79824af..f39b0fc935e 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", - "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 50c5e31f0b9..afae41f7d7a 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token]", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", diff --git a/homeassistant/components/tailscale/translations/et.json b/homeassistant/components/tailscale/translations/et.json new file mode 100644 index 00000000000..d542c930ecd --- /dev/null +++ b/homeassistant/components/tailscale/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Tailscale API v\u00f5tmed kehtivad 90 p\u00e4eva. Saad luua v\u00e4rske Tailscale API v\u00f5tme aadressil https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "tailnet": "Tailnet" + }, + "description": "Tailscale'iga autentimiseks pead looma API v\u00f5tme aadressil https://login.tailscale.com/admin/settings/authkeys. \n\n Tailnet on Tailscale v\u00f5rgu nimi. Leiad selle Tailscale halduspaneeli vasakus \u00fclanurgas (Tailscale logo k\u00f5rval)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/id.json b/homeassistant/components/tailscale/translations/id.json new file mode 100644 index 00000000000..d88a47fa82e --- /dev/null +++ b/homeassistant/components/tailscale/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Token API Tailscale berlaku selama 90 hari. Anda dapat membuat kunci API Tailscale baru di https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "Kunci API", + "tailnet": "Tailnet" + }, + "description": "Untuk mengautentikasi dengan Tailscale, Anda harus membuat kunci API di https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet adalah nama jaringan Tailscale Anda. Anda dapat menemukannya di pojok kiri atas di Panel Admin Tailscale (di samping logo Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/ja.json b/homeassistant/components/tailscale/translations/ja.json new file mode 100644 index 00000000000..bed4a13e8d3 --- /dev/null +++ b/homeassistant/components/tailscale/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "tailnet": "Tailnet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/no.json b/homeassistant/components/tailscale/translations/no.json new file mode 100644 index 00000000000..627facd8f66 --- /dev/null +++ b/homeassistant/components/tailscale/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Tailscale API-tokens er gyldige i 90 dager. Du kan opprette en ny Tailscale API-n\u00f8kkel p\u00e5 https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "tailnet": "Tailnet" + }, + "description": "For \u00e5 autentisere med Tailscale m\u00e5 du opprette en API-n\u00f8kkel p\u00e5 https://login.tailscale.com/admin/settings/authkeys. \n\n Et Tailnet er navnet p\u00e5 Tailscale-nettverket ditt. Du finner den i \u00f8verste venstre hj\u00f8rne i Tailscale Admin Panel (ved siden av Tailscale-logoen)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/tr.json b/homeassistant/components/tailscale/translations/tr.json new file mode 100644 index 00000000000..680acd65d70 --- /dev/null +++ b/homeassistant/components/tailscale/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "Tailnet API belirte\u00e7leri 90 g\u00fcn boyunca ge\u00e7erlidir. https://login.tailscale.com/admin/settings/authkeys adresinde yeni bir Tailscale API anahtar\u0131 olu\u015fturabilirsiniz." + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "tailnet": "Tailnet" + }, + "description": "Tailscale ile kimlik do\u011frulamas\u0131 yapmak i\u00e7in https://login.tailscale.com/admin/settings/authkeys adresinde bir API anahtar\u0131 olu\u015fturman\u0131z gerekir. \n\n Kuyruk a\u011f\u0131, Kuyruk \u00f6l\u00e7e\u011fi a\u011f\u0131n\u0131z\u0131n ad\u0131d\u0131r. Bunu, Tailscale Y\u00f6netici Panelinin sol \u00fcst k\u00f6\u015fesinde (Tailscale logosunun yan\u0131nda) bulabilirsiniz." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/zh-Hant.json b/homeassistant/components/tailscale/translations/zh-Hant.json new file mode 100644 index 00000000000..b47dd0cc57b --- /dev/null +++ b/homeassistant/components/tailscale/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "Tailscale API \u6b0a\u6756\u6709\u6548\u671f\u70ba 90 \u5929\u3002\u53ef\u4ee5\u65bc https://login.tailscale.com/admin/settings/authkeys \u53d6\u5f97\u66f4\u65b0 Tailscale API \u5bc6\u9470\u3002" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "tailnet": "Tailnet" + }, + "description": "\u6b32\u4f7f\u7528 Tailscale \u8a8d\u8b49\u3001\u5c07\u9700\u8981\u65bc https://login.tailscale.com/admin/settings/authkeys \u65b0\u589e\u4e00\u7d44 API \u5bc6\u9470 \n\nTailnet \u70ba Tailscale \u7db2\u8def\u7684\u540d\u7a31\uff0c\u53ef\u4ee5\u65bc Tailscale \u7ba1\u7406\u9762\u677f\uff08Tailscale Logo \u65c1\uff09\u7684\u5de6\u4e0a\u65b9\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.ca.json b/homeassistant/components/tractive/translations/sensor.ca.json new file mode 100644 index 00000000000..c463819dc19 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.ca.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Sense informaci\u00f3", + "operational": "Operatiu", + "system_shutdown_user": "Aturada del sistema d'usuari", + "system_startup": "Engegada del sistema" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.de.json b/homeassistant/components/tractive/translations/sensor.de.json new file mode 100644 index 00000000000..53577399924 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.de.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Keine Meldung", + "operational": "Betriebsbereit", + "system_shutdown_user": "Benutzer zum Herunterfahren des Systems", + "system_startup": "Systemstart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.et.json b/homeassistant/components/tractive/translations/sensor.et.json new file mode 100644 index 00000000000..24f647a0619 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.et.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Ei vasta", + "operational": "Tegevuses", + "system_shutdown_user": "S\u00fcsteemi sulgemise kasutaja", + "system_startup": "S\u00fcsteemi k\u00e4ivitamine" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.id.json b/homeassistant/components/tractive/translations/sensor.id.json new file mode 100644 index 00000000000..408fdf83b67 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.id.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Tidak melaporkan", + "operational": "Operasional", + "system_shutdown_user": "Pengguna mematikan sistem", + "system_startup": "Sistem mulai" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.ja.json b/homeassistant/components/tractive/translations/sensor.ja.json new file mode 100644 index 00000000000..163376fe070 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.ja.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "\u5831\u544a\u3057\u306a\u3044", + "operational": "\u904b\u7528", + "system_shutdown_user": "System shutdown user", + "system_startup": "\u30b7\u30b9\u30c6\u30e0\u306e\u8d77\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.nl.json b/homeassistant/components/tractive/translations/sensor.nl.json new file mode 100644 index 00000000000..c5e32bdacd1 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.nl.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Niet rapporteren", + "operational": "Operationeel", + "system_shutdown_user": "Systeem afsluiten gebruiker", + "system_startup": "Systeem opstarten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.no.json b/homeassistant/components/tractive/translations/sensor.no.json new file mode 100644 index 00000000000..a7369b11036 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.no.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Rapporterer ikke", + "operational": "Operativ", + "system_shutdown_user": "Bruker av systemavslutning", + "system_startup": "Oppstart av systemet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.ru.json b/homeassistant/components/tractive/translations/sensor.ru.json new file mode 100644 index 00000000000..2e4c645e8a3 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.ru.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "\u041d\u0435 \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442\u0441\u044f", + "operational": "\u042d\u043a\u0441\u043f\u043b\u0443\u0430\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "system_shutdown_user": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u043b \u0441\u0438\u0441\u0442\u0435\u043c\u0443", + "system_startup": "\u0417\u0430\u043f\u0443\u0441\u043a \u0441\u0438\u0441\u0442\u0435\u043c\u044b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.tr.json b/homeassistant/components/tractive/translations/sensor.tr.json new file mode 100644 index 00000000000..6a76cc304e2 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.tr.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Rapor edilmiyor", + "operational": "Operasyonel", + "system_shutdown_user": "Sistem kapatma kullan\u0131c\u0131s\u0131", + "system_startup": "Sistem ba\u015flatma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.zh-Hant.json b/homeassistant/components/tractive/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..fdd46f0cefa --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.zh-Hant.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "\u672a\u56de\u5831", + "operational": "\u53ef\u64cd\u4f5c", + "system_shutdown_user": "\u7cfb\u7d71\u95dc\u6a5f\u7528\u6236", + "system_startup": "\u7cfb\u7d71\u555f\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ca.json b/homeassistant/components/trafikverket_weatherstation/translations/ca.json index 00656cb09f2..80fe04ba83f 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ca.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ca.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "El compte ja est\u00e0 configurat" }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_station": "No s'ha pogut trobar una estaci\u00f3 meteorol\u00f2gica amb el nom especificat", + "more_stations": "S'han trobat diverses estacions meteorol\u00f2giques amb el nom especificat" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/de.json b/homeassistant/components/trafikverket_weatherstation/translations/de.json index 51f66aed22e..47d3dfc2e9b 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/de.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/de.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "Konto wurde bereits konfiguriert" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_station": "Eine Wetterstation mit dem angegebenen Namen konnte nicht gefunden werden", + "more_stations": "Mehrere Wetterstationen mit dem angegebenen Namen gefunden" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/et.json b/homeassistant/components/trafikverket_weatherstation/translations/et.json index c5c5f76ae9d..7350031bd36 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/et.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/et.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "Konto on juba h\u00e4\u00e4lestatud" }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_station": "M\u00e4\u00e4ratud nimega ilmajaama ei leitud", + "more_stations": "Leiti mitu m\u00e4\u00e4ratud nimega ilmajaama" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/id.json b/homeassistant/components/trafikverket_weatherstation/translations/id.json new file mode 100644 index 00000000000..0db961235d4 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_station": "Tidak dapat menemukan stasiun cuaca dengan nama yang ditentukan", + "more_stations": "Ditemukan beberapa stasiun cuaca dengan nama yang ditentukan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "conditions": "Kondisi yang dipantau", + "name": "Nama Pengguna", + "station": "Stasiun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ja.json b/homeassistant/components/trafikverket_weatherstation/translations/ja.json index ec86eeea9cd..d6e2cf28221 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ja.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ja.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/nl.json b/homeassistant/components/trafikverket_weatherstation/translations/nl.json index 39970fa00b6..1e8d8ae9f3a 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/nl.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/nl.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "Account is al geconfigureerd" }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_station": "Kon geen weerstation vinden met de opgegeven naam", + "more_stations": "Meerdere weerstations gevonden met de opgegeven naam" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/no.json b/homeassistant/components/trafikverket_weatherstation/translations/no.json index 7c3b2c3a183..fea917f9c38 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/no.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/no.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert" }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_station": "Kunne ikke finne en v\u00e6rstasjon med det angitte navnet", + "more_stations": "Fant flere v\u00e6rstasjoner med det angitte navnet" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ru.json b/homeassistant/components/trafikverket_weatherstation/translations/ru.json index 948b43fb80e..d7658cda81b 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ru.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ru.json @@ -3,6 +3,12 @@ "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." }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_station": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u044e \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c.", + "more_stations": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/tr.json b/homeassistant/components/trafikverket_weatherstation/translations/tr.json index 858bc2c4c94..e999f7b50d1 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/tr.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/tr.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_station": "Belirtilen ada sahip bir hava durumu istasyonu bulunamad\u0131", + "more_stations": "Belirtilen ada sahip birden fazla hava durumu istasyonu bulundu" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json index ecf0be95000..4323ed42b7a 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_station": "\u8a72\u6307\u5b9a\u540d\u7a31\u7121\u6cd5\u627e\u5230\u5929\u6c23\u89c0\u6e2c\u7ad9", + "more_stations": "\u8a72\u6307\u5b9a\u540d\u7a31\u627e\u5230\u591a\u500b\u5929\u6c23\u89c0\u6e2c\u7ad9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json index fa65d7a36d3..4ea665e4a79 100644 --- a/homeassistant/components/tuya/translations/select.id.json +++ b/homeassistant/components/tuya/translations/select.id.json @@ -14,6 +14,10 @@ "0": "Sensitivitas rendah", "1": "Sensitivitas tinggi" }, + "tuya__fingerbot_mode": { + "click": "Dorong", + "switch": "Sakelar" + }, "tuya__ipc_work_mode": { "0": "Mode daya rendah", "1": "Mode kerja terus menerus" diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index 97a971043d9..ef2dff99dbe 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -15,6 +15,7 @@ "1": "Hoge gevoeligheid" }, "tuya__fingerbot_mode": { + "click": "Duw", "switch": "Schakelaar" }, "tuya__ipc_work_mode": { diff --git a/homeassistant/components/yale_smart_alarm/translations/id.json b/homeassistant/components/yale_smart_alarm/translations/id.json index ee24f03a33c..37b88ddc68c 100644 --- a/homeassistant/components/yale_smart_alarm/translations/id.json +++ b/homeassistant/components/yale_smart_alarm/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid" diff --git a/homeassistant/components/yale_smart_alarm/translations/tr.json b/homeassistant/components/yale_smart_alarm/translations/tr.json index a49189eb66f..24b37440160 100644 --- a/homeassistant/components/yale_smart_alarm/translations/tr.json +++ b/homeassistant/components/yale_smart_alarm/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" From 4758a4fdc8e1e7b76229131ff776ea9618a82c92 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 2 Dec 2021 20:27:35 -0500 Subject: [PATCH 1254/1452] Bump ZHA dependency zigpy-znp from 0.6.1 to 0.6.3 (#60871) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 33597feb900..aa167fc2df4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.42.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.6.1" + "zigpy-znp==0.6.3" ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, diff --git a/requirements_all.txt b/requirements_all.txt index 6a9457e4583..b28d5821c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2507,7 +2507,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.1 +zigpy-znp==0.6.3 # homeassistant.components.zha zigpy==0.42.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e242ecf7d5..02ae17a2d2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.1 +zigpy-znp==0.6.3 # homeassistant.components.zha zigpy==0.42.0 From 9d1985ab033920c2f4d965b576fbc9771b893a15 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:36:31 -0800 Subject: [PATCH 1255/1452] Move screenlogic SCG levels to number platform (#60872) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/screenlogic/__init__.py | 2 +- .../components/screenlogic/number.py | 78 +++++++++++++++++++ .../components/screenlogic/sensor.py | 2 - 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/screenlogic/number.py diff --git a/.coveragerc b/.coveragerc index bf26b41649d..fc033c77369 100644 --- a/.coveragerc +++ b/.coveragerc @@ -915,6 +915,7 @@ omit = homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/light.py + homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index bc7761857d5..72c1f162552 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -40,7 +40,7 @@ HEATER_COOLDOWN_DELAY = 6 # These seem to be constant across all controller models PRIMARY_CIRCUIT_IDS = [500, 505] # [Spa, Pool] -PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "climate", "light", "number", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py new file mode 100644 index 00000000000..68b9f1d62c3 --- /dev/null +++ b/homeassistant/components/screenlogic/number.py @@ -0,0 +1,78 @@ +"""Support for a ScreenLogic number entity.""" +import logging + +from screenlogicpy.const import BODY_TYPE, DATA as SL_DATA, EQUIPMENT, SCG + +from homeassistant.components.number import NumberEntity + +from . import ScreenlogicEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_SCG_NUMBERS = ( + "scg_level1", + "scg_level2", +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + async_add_entities( + [ + ScreenLogicNumber(coordinator, scg_level) + for scg_level in coordinator.data[SL_DATA.KEY_SCG] + if scg_level in SUPPORTED_SCG_NUMBERS + ] + ) + + +class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): + """Class to represent a ScreenLogic Number.""" + + def __init__(self, coordinator, data_key, enabled=True): + """Initialize of the entity.""" + super().__init__(coordinator, data_key, enabled) + self._body_type = SUPPORTED_SCG_NUMBERS.index(self._data_key) + self._attr_max_value = SCG.LIMIT_FOR_BODY[self._body_type] + self._attr_name = f"{self.gateway_name} {self.sensor['name']}" + self._attr_unit_of_measurement = self.sensor["unit"] + + @property + def value(self) -> float: + """Return the current value.""" + return self.sensor["value"] + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + # Need to set both levels at the same time, so we gather + # both existing level values and override the one that changed. + levels = {} + for level in SUPPORTED_SCG_NUMBERS: + levels[level] = self.coordinator.data[SL_DATA.KEY_SCG][level]["value"] + levels[self._data_key] = int(value) + + if await self.coordinator.gateway.async_set_scg_config( + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]], + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]], + ): + _LOGGER.debug( + "Set SCG to %i, %i", + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]], + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]], + ) + await self._async_refresh() + else: + _LOGGER.warning( + "Failed to set_scg to %i, %i", + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]], + levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]], + ) + + @property + def sensor(self) -> dict: + """Shortcut to access the level sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index cd7ba068be0..7ab20164400 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -34,8 +34,6 @@ SUPPORTED_CHEM_SENSORS = ( ) SUPPORTED_SCG_SENSORS = ( - "scg_level1", - "scg_level2", "scg_salt_ppm", "scg_super_chlor_timer", ) From e1b4e40ac64700947b428c24a4a3607c63c391da Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 2 Dec 2021 22:43:15 -0800 Subject: [PATCH 1256/1452] Limit parallel updates for screenlogic number ents (#60886) --- homeassistant/components/screenlogic/number.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 68b9f1d62c3..0072962a7d7 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -10,6 +10,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + SUPPORTED_SCG_NUMBERS = ( "scg_level1", "scg_level2", From 4207d5a85f08c04f24b5ff85643e54f3b79402d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:32:42 +0100 Subject: [PATCH 1257/1452] Use dataclass properties in upnp (#60893) Co-authored-by: epenet --- homeassistant/components/upnp/__init__.py | 17 +++++------ homeassistant/components/upnp/config_flow.py | 32 +++++++++----------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 36b6278d968..da0585df987 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -15,7 +15,6 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.binary_sensor import BinarySensorEntityDescription from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.components.ssdp import SsdpChange from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -91,16 +90,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Register device discovered-callback. device_discovered_event = asyncio.Event() - discovery_info: Mapping[str, Any] | None = None + discovery_info: ssdp.SsdpServiceInfo | None = None - async def device_discovered(headers: Mapping[str, Any], change: SsdpChange) -> None: - if change == SsdpChange.BYEBYE: + async def device_discovered( + headers: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange + ) -> None: + if change == ssdp.SsdpChange.BYEBYE: return nonlocal discovery_info - LOGGER.debug( - "Device discovered: %s, at: %s", usn, headers[ssdp.ATTR_SSDP_LOCATION] - ) + LOGGER.debug("Device discovered: %s, at: %s", usn, headers.ssdp_location) discovery_info = headers device_discovered_event.set() @@ -121,9 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_discovered_callback() # Create device. - location = discovery_info[ # pylint: disable=unsubscriptable-object - ssdp.ATTR_SSDP_LOCATION - ] + location = discovery_info.ssdp_location try: device = await Device.async_create_device(hass, location) except UpnpConnectionError as err: diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index fd4ed9d4051..74acb88983b 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.components.ssdp import SsdpChange +from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -52,14 +52,14 @@ async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: """Wait for a device to be discovered.""" device_discovered_event = asyncio.Event() - async def device_discovered(info: Mapping[str, Any], change: SsdpChange) -> None: + async def device_discovered(info: SsdpServiceInfo, change: SsdpChange) -> None: if change == SsdpChange.BYEBYE: return LOGGER.info( "Device discovered: %s, at: %s", - info[ssdp.ATTR_SSDP_USN], - info[ssdp.ATTR_SSDP_LOCATION], + info.ssdp_usn, + info.ssdp_location, ) device_discovered_event.set() @@ -112,7 +112,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the UPnP/IGD config flow.""" - self._discoveries: Mapping = None + self._discoveries: list[SsdpServiceInfo] | None = None async def async_step_user( self, user_input: Mapping | None = None @@ -125,15 +125,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): matching_discoveries = [ discovery for discovery in self._discoveries - if discovery[ssdp.ATTR_SSDP_USN] == user_input["unique_id"] + if discovery.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[ssdp.ATTR_SSDP_USN], raise_on_progress=False - ) + await self.async_set_unique_id(discovery.ssdp_usn, raise_on_progress=False) return await self._async_create_entry_from_discovery(discovery) # Discover devices. @@ -148,7 +146,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for discovery in discoveries if ( _is_complete_discovery(discovery) - and discovery[ssdp.ATTR_SSDP_USN] not in current_unique_ids + and discovery.ssdp_usn not in current_unique_ids ) ] @@ -160,9 +158,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required("unique_id"): vol.In( { - discovery[ssdp.ATTR_SSDP_USN]: _friendly_name_from_discovery( - discovery - ) + discovery.ssdp_usn: _friendly_name_from_discovery(discovery) for discovery in self._discoveries } ), @@ -204,7 +200,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. - unique_id = discovery[ssdp.ATTR_SSDP_USN] + unique_id = discovery.ssdp_usn await self.async_set_unique_id(unique_id) return await self._async_create_entry_from_discovery(discovery) @@ -269,7 +265,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_discovery( self, - discovery: Mapping, + discovery: SsdpServiceInfo, ) -> Mapping[str, Any]: """Create an entry from discovery.""" LOGGER.debug( @@ -279,9 +275,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = _friendly_name_from_discovery(discovery) data = { - CONFIG_ENTRY_UDN: discovery[ssdp.ATTR_UPNP_UDN], - CONFIG_ENTRY_ST: discovery[ssdp.ATTR_SSDP_ST], - CONFIG_ENTRY_HOSTNAME: discovery["_host"], + CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ST: discovery.ssdp_st, + CONFIG_ENTRY_HOSTNAME: discovery.ssdp_headers["_host"], } return self.async_create_entry(title=title, data=data) From 8eb056396c3453825a8cb13f15b9dbf1c1d88ed2 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:37:19 +0100 Subject: [PATCH 1258/1452] Use entity category enums in Nut (#60883) --- homeassistant/components/nut/const.py | 124 +++++++++++++------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 11b07f04b78..23ef55f3f09 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, @@ -20,6 +19,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_SECONDS, ) +from homeassistant.helpers.entity import EntityCategory DOMAIN = "nut" @@ -62,7 +62,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.load": SensorEntityDescription( @@ -77,14 +77,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Overload Setting", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.id": SensorEntityDescription( key="ups.id", name="System identifier", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.delay.start": SensorEntityDescription( @@ -92,7 +92,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load Restart Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.delay.reboot": SensorEntityDescription( @@ -100,7 +100,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="UPS Reboot Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.delay.shutdown": SensorEntityDescription( @@ -108,7 +108,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="UPS Shutdown Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.timer.start": SensorEntityDescription( @@ -116,7 +116,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load Start Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.timer.reboot": SensorEntityDescription( @@ -124,7 +124,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load Reboot Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.timer.shutdown": SensorEntityDescription( @@ -132,7 +132,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load Shutdown Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.test.interval": SensorEntityDescription( @@ -140,35 +140,35 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Self-Test Interval", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.test.result": SensorEntityDescription( key="ups.test.result", name="Self-Test Result", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.test.date": SensorEntityDescription( key="ups.test.date", name="Self-Test Date", icon="mdi:calendar", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.display.language": SensorEntityDescription( key="ups.display.language", name="Language", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.contacts": SensorEntityDescription( key="ups.contacts", name="External Contacts", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.efficiency": SensorEntityDescription( @@ -177,7 +177,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.power": SensorEntityDescription( @@ -186,7 +186,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.power.nominal": SensorEntityDescription( @@ -194,7 +194,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.realpower": SensorEntityDescription( @@ -203,7 +203,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.realpower.nominal": SensorEntityDescription( @@ -211,56 +211,56 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Real Power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.beeper.status": SensorEntityDescription( key="ups.beeper.status", name="Beeper Status", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.type": SensorEntityDescription( key="ups.type", name="UPS Type", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.watchdog.status": SensorEntityDescription( key="ups.watchdog.status", name="Watchdog Status", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.start.auto": SensorEntityDescription( key="ups.start.auto", name="Start on AC", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.start.battery": SensorEntityDescription( key="ups.start.battery", name="Start on Battery", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.start.reboot": SensorEntityDescription( key="ups.start.reboot", name="Reboot on Battery", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ups.shutdown": SensorEntityDescription( key="ups.shutdown", name="Shutdown Ability", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.charge": SensorEntityDescription( @@ -275,7 +275,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.charge.restart": SensorEntityDescription( @@ -283,7 +283,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Minimum Battery to Start", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.charge.warning": SensorEntityDescription( @@ -291,7 +291,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Warning Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.charger.status": SensorEntityDescription( @@ -305,7 +305,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.voltage.nominal": SensorEntityDescription( @@ -313,7 +313,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.voltage.low": SensorEntityDescription( @@ -321,7 +321,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.voltage.high": SensorEntityDescription( @@ -329,7 +329,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="High Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.capacity": SensorEntityDescription( @@ -337,7 +337,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Battery Capacity", native_unit_of_measurement="Ah", icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.current": SensorEntityDescription( @@ -346,7 +346,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.current.total": SensorEntityDescription( @@ -354,7 +354,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Total Battery Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.temperature": SensorEntityDescription( @@ -363,7 +363,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.runtime": SensorEntityDescription( @@ -371,7 +371,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.runtime.low": SensorEntityDescription( @@ -379,7 +379,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.runtime.restart": SensorEntityDescription( @@ -387,56 +387,56 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Minimum Battery Runtime to Start", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.alarm.threshold": SensorEntityDescription( key="battery.alarm.threshold", name="Battery Alarm Threshold", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.date": SensorEntityDescription( key="battery.date", name="Battery Date", icon="mdi:calendar", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.mfr.date": SensorEntityDescription( key="battery.mfr.date", name="Battery Manuf. Date", icon="mdi:calendar", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.packs": SensorEntityDescription( key="battery.packs", name="Number of Batteries", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.packs.bad": SensorEntityDescription( key="battery.packs.bad", name="Number of Bad Batteries", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "battery.type": SensorEntityDescription( key="battery.type", name="Battery Chemistry", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.sensitivity": SensorEntityDescription( key="input.sensitivity", name="Input Power Sensitivity", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.transfer.low": SensorEntityDescription( @@ -444,7 +444,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.transfer.high": SensorEntityDescription( @@ -452,14 +452,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="High Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.transfer.reason": SensorEntityDescription( key="input.transfer.reason", name="Voltage Transfer Reason", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.voltage": SensorEntityDescription( @@ -474,7 +474,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.frequency": SensorEntityDescription( @@ -483,7 +483,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.frequency.nominal": SensorEntityDescription( @@ -491,14 +491,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Input Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "input.frequency.status": SensorEntityDescription( key="input.frequency.status", name="Input Frequency Status", icon="mdi:information-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "output.current": SensorEntityDescription( @@ -507,7 +507,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "output.current.nominal": SensorEntityDescription( @@ -515,7 +515,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "output.voltage": SensorEntityDescription( @@ -530,7 +530,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "output.frequency": SensorEntityDescription( @@ -539,7 +539,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "output.frequency.nominal": SensorEntityDescription( @@ -547,7 +547,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ambient.humidity": SensorEntityDescription( @@ -556,7 +556,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "ambient.temperature": SensorEntityDescription( @@ -565,7 +565,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), "watts": SensorEntityDescription( From c33e3e465bc696750d2688e8a54087ecc31f9504 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:37:51 +0100 Subject: [PATCH 1259/1452] Use entity category enums in AsusWrt (#60882) --- homeassistant/components/asuswrt/sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 3475473b21b..7c375ed3c20 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -10,12 +10,9 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DATA_GIGABYTES, - DATA_RATE_MEGABITS_PER_SECOND, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -92,7 +89,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (1m)", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -102,7 +99,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (5m)", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -112,7 +109,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (15m)", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, From a64ff2ae27b4589d1312cd70fd2d40028276da2d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:49:35 +0100 Subject: [PATCH 1260/1452] Use dataclass properties in system_bridge tests (#60888) Co-authored-by: epenet --- tests/components/system_bridge/test_config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index f8163eb98d9..ecc36641e6c 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -81,9 +81,7 @@ FIXTURE_BASE_URL = ( f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}" ) -FIXTURE_ZEROCONF_BASE_URL = ( - f"http://{FIXTURE_ZEROCONF[CONF_HOST]}:{FIXTURE_ZEROCONF[CONF_PORT]}" -) +FIXTURE_ZEROCONF_BASE_URL = f"http://{FIXTURE_ZEROCONF.host}:{FIXTURE_ZEROCONF.port}" async def test_user_flow( From 9f4a99fe812542d2c727d75255e50546dbe6d43a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 3 Dec 2021 08:50:29 +0100 Subject: [PATCH 1261/1452] Revert "Add bytes support for bitwise template operations" (#60854) --- homeassistant/helpers/template.py | 19 ++--- homeassistant/util/__init__.py | 25 ------ tests/components/mqtt/test_binary_sensor.py | 33 -------- tests/components/mqtt/test_discovery.py | 94 --------------------- tests/components/mqtt/test_sensor.py | 29 ------- tests/helpers/test_template.py | 66 --------------- tests/util/test_init.py | 16 ---- 7 files changed, 5 insertions(+), 277 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 62e9f3fab6a..6ef9ef57329 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -52,12 +52,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass -from homeassistant.util import ( - convert, - convert_to_int, - dt as dt_util, - location as loc_util, -) +from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException @@ -1634,18 +1629,14 @@ def regex_findall(value, find="", ignorecase=False): return re.findall(find, value, flags) -def bitwise_and(first_value, second_value, little_endian=False): +def bitwise_and(first_value, second_value): """Perform a bitwise and operation.""" - return convert_to_int(first_value, little_endian=little_endian) & convert_to_int( - second_value, little_endian=little_endian - ) + return first_value & second_value -def bitwise_or(first_value, second_value, little_endian=False): +def bitwise_or(first_value, second_value): """Perform a bitwise or operation.""" - return convert_to_int(first_value, little_endian=little_endian) | convert_to_int( - second_value, little_endian=little_endian - ) + return first_value | second_value def base64_encode(value): diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 637b98a1a75..b7758df0cb0 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -76,31 +76,6 @@ def convert( return default -def convert_to_int( - value: Any, default: int | None = None, little_endian: bool = False -) -> int | None: - """Convert value or bytes to int, returns default if fails. - - This supports bitwise integer operations on `bytes` objects. - By default the conversion is in Big-endian style (The last byte contains the least significant bit). - In Little-endian style the first byte contains the least significant bit. - """ - if isinstance(value, int): - return value - if isinstance(value, bytes) and value: - bytes_value = bytearray(value) - return_value = 0 - while len(bytes_value): - return_value <<= 8 - if little_endian: - return_value |= bytes_value.pop(len(bytes_value) - 1) - else: - return_value |= bytes_value.pop(0) - - return return_value - return convert(value, int, default=default) - - def ensure_unique_string( preferred_string: str, current_strings: Iterable[str] | KeysView[str] ) -> str: diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 6e1bdd60972..ba601fd094d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -373,39 +373,6 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( assert "template output: 'ILLEGAL'" in caplog.text -async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( - hass, mqtt_mock, caplog -): - """Test processing a raw value via MQTT.""" - assert await async_setup_component( - hass, - binary_sensor.DOMAIN, - { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "encoding": "", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": "{%if value|bitwise_and(1)-%}ON{%else%}OFF{%-endif-%}", - } - }, - ) - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test-topic", b"\x01") - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_ON - - async_fire_mqtt_message(hass, "test-topic", b"\x00") - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF - - async def test_setting_sensor_value_via_mqtt_message_empty_template( hass, mqtt_mock, caplog ): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 6293d99aff4..faa9f714ae0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -737,100 +737,6 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): ) -async def test_discovery_expansion_without_encoding_and_value_template_1( - hass, mqtt_mock, caplog -): - """Test expansion of raw availability payload with a template as list.""" - data = ( - '{ "~": "some/base/topic",' - ' "name": "DiscoveryExpansionTest1",' - ' "stat_t": "test_topic/~",' - ' "cmd_t": "~/test_topic",' - ' "encoding":"",' - ' "availability": [{' - ' "topic":"~/avail_item1",' - ' "payload_available": "1",' - ' "payload_not_available": "0",' - ' "value_template":"{{ value | bitwise_and(1) }}"' - " }]," - ' "dev":{' - ' "ids":["5706DF"],' - ' "name":"DiscoveryExpansionTest1 Device",' - ' "mdl":"Generic",' - ' "sw":"1.2.3.4",' - ' "mf":"None",' - ' "sa":"default_area"' - " }" - "}" - ) - - async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) - await hass.async_block_till_done() - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") - await hass.async_block_till_done() - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state is not None - assert state.name == "DiscoveryExpansionTest1" - assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state.state == STATE_UNAVAILABLE - - -async def test_discovery_expansion_without_encoding_and_value_template_2( - hass, mqtt_mock, caplog -): - """Test expansion of raw availability payload with a template directly.""" - data = ( - '{ "~": "some/base/topic",' - ' "name": "DiscoveryExpansionTest1",' - ' "stat_t": "test_topic/~",' - ' "cmd_t": "~/test_topic",' - ' "availability_topic":"~/avail_item1",' - ' "payload_available": "1",' - ' "payload_not_available": "0",' - ' "encoding":"",' - ' "availability_template":"{{ value | bitwise_and(1) }}",' - ' "dev":{' - ' "ids":["5706DF"],' - ' "name":"DiscoveryExpansionTest1 Device",' - ' "mdl":"Generic",' - ' "sw":"1.2.3.4",' - ' "mf":"None",' - ' "sa":"default_area"' - " }" - "}" - ) - - async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) - await hass.async_block_till_done() - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") - await hass.async_block_till_done() - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state is not None - assert state.name == "DiscoveryExpansionTest1" - assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") - - state = hass.states.get("switch.DiscoveryExpansionTest1") - assert state.state == STATE_UNAVAILABLE - - ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE", diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 3b395c823e9..680bafb3b2b 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -893,35 +893,6 @@ async def test_entity_category(hass, mqtt_mock): await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) -async def test_value_template_with_raw_data(hass, mqtt_mock): - """Test processing a raw value via MQTT.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "encoding": "", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value | bitwise_and(255) }}", - } - }, - ) - await hass.async_block_till_done() - - async_fire_mqtt_message(hass, "test-topic", b"\xff") - state = hass.states.get("sensor.test") - - assert state.state == "255" - - async_fire_mqtt_message(hass, "test-topic", b"\x01\x10") - state = hass.states.get("sensor.test") - - assert state.state == "16" - - async def test_value_template_with_entity_id(hass, mqtt_mock): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index bd405acf597..99c57983aa5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1479,24 +1479,6 @@ def test_bitwise_and(hass): ) assert tpl.async_render() == 8 & 2 - tpl = template.Template( - """ -{{ ( value_a ) | bitwise_and(value_b) }} - """, - hass, - ) - variables = {"value_a": b"\x9b\xc2", "value_b": 0xFF00} - assert tpl.async_render(variables=variables) == 0x9B00 - - tpl = template.Template( - """ -{{ ( value_a ) | bitwise_and(value_b, little_endian=True) }} - """, - hass, - ) - variables = {"value_a": b"\xc2\x9b", "value_b": 0xFFFF} - assert tpl.async_render(variables=variables) == 0x9BC2 - def test_bitwise_or(hass): """Test bitwise_or method.""" @@ -1522,54 +1504,6 @@ def test_bitwise_or(hass): ) assert tpl.async_render() == 8 | 2 - tpl = template.Template( - """ -{{ value_a | bitwise_or(value_b) }} - """, - hass, - ) - variables = { - "value_a": b"\xc2\x9b", - "value_b": 0xFFFF, - } - assert tpl.async_render(variables=variables) == 65535 # 39874 - - tpl = template.Template( - """ -{{ ( value_a ) | bitwise_or(value_b) }} - """, - hass, - ) - variables = { - "value_a": 0xFF00, - "value_b": b"\xc2\x9b", - } - assert tpl.async_render(variables=variables) == 0xFF9B - - tpl = template.Template( - """ -{{ ( value_a ) | bitwise_or(value_b) }} - """, - hass, - ) - variables = { - "value_a": b"\xc2\x9b", - "value_b": 0x0000, - } - assert tpl.async_render(variables=variables) == 0xC29B - - tpl = template.Template( - """ -{{ ( value_a ) | bitwise_or(value_b, little_endian=True) }} - """, - hass, - ) - variables = { - "value_a": b"\xc2\x9b", - "value_b": 0, - } - assert tpl.async_render(variables=variables) == 0x9BC2 - def test_distance_function_with_1_state(hass): """Test distance function with 1 state.""" diff --git a/tests/util/test_init.py b/tests/util/test_init.py index e80c3fd6989..d6508b40c37 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -82,22 +82,6 @@ def test_convert(): assert util.convert(object, int, 1) == 1 -def test_convert_to_int(): - """Test convert of bytes and numbers to int.""" - assert util.convert_to_int(b"\x9b\xc2") == 39874 - assert util.convert_to_int(b"") is None - assert util.convert_to_int(b"\x9b\xc2", 10) == 39874 - assert util.convert_to_int(b"\xc2\x9b", little_endian=True) == 39874 - assert util.convert_to_int(b"\xc2\x9b", 10, little_endian=True) == 39874 - assert util.convert_to_int("abc", 10) == 10 - assert util.convert_to_int("11.0", 10) == 10 - assert util.convert_to_int("12", 10) == 12 - assert util.convert_to_int("\xc2\x9b", 10) == 10 - assert util.convert_to_int(None, 10) == 10 - assert util.convert_to_int(None) is None - assert util.convert_to_int("NOT A NUMBER", 1) == 1 - - def test_ensure_unique_string(): """Test ensure_unique_string.""" assert util.ensure_unique_string("Beer", ["Beer", "Beer_2"]) == "Beer_3" From 2da9a519196a6c0ef846f60bb84302df7d97a902 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:50:52 +0100 Subject: [PATCH 1262/1452] Use dataclass properties in keenetic_ndms2 tests (#60892) Co-authored-by: epenet --- tests/components/keenetic_ndms2/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 4d417705ac3..0b438487c49 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -218,7 +218,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=keenetic.DOMAIN, source=config_entries.SOURCE_IGNORE, - unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN], ) entry.add_to_hass(hass) @@ -240,7 +240,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS, - unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN], ) entry.add_to_hass(hass) From b70d24394e155b8e04d4a9652935c476edbf1330 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:51:13 +0100 Subject: [PATCH 1263/1452] Use dataclass properties in homekit_controller tests (#60891) Co-authored-by: epenet --- .../homekit_controller/test_config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 21a3792cd62..d077bb8eb4e 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -159,7 +159,7 @@ def get_device_discovery_info( ) if missing_csharp: - del result["properties"]["c#"] + del result.properties["c#"] if upper_case_props: result.properties = { @@ -255,7 +255,7 @@ async def test_pair_already_paired_1(hass, controller): discovery_info = get_device_discovery_info(device) # Flag device as already paired - discovery_info["properties"]["sf"] = 0x0 + discovery_info.properties["sf"] = 0x0 # Device is discovered result = await hass.config_entries.flow.async_init( @@ -458,8 +458,8 @@ async def test_discovery_already_configured(hass, controller): ) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data["AccessoryIP"] == discovery_info["host"] - assert entry.data["AccessoryPort"] == discovery_info["port"] + assert entry.data["AccessoryIP"] == discovery_info.host + assert entry.data["AccessoryPort"] == discovery_info.port async def test_discovery_already_configured_update_csharp(hass, controller): @@ -498,8 +498,8 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert result["reason"] == "already_configured" await hass.async_block_till_done() - assert entry.data["AccessoryIP"] == discovery_info["host"] - assert entry.data["AccessoryPort"] == discovery_info["port"] + assert entry.data["AccessoryIP"] == discovery_info.host + assert entry.data["AccessoryPort"] == discovery_info.port assert connection_mock.async_refresh_entity_map.await_count == 1 From c04bfcc7f4cbda0bdea6fce50ddbed4c3a6d6250 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:51:42 +0100 Subject: [PATCH 1264/1452] Use dataclass properties in devolo_home_network tests (#60889) Co-authored-by: epenet --- tests/components/devolo_home_network/__init__.py | 5 +++-- tests/components/devolo_home_network/conftest.py | 4 ++-- tests/components/devolo_home_network/test_config_flow.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index b9026d8453b..913193be3f7 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -1,5 +1,6 @@ """Tests for the devolo Home Network integration.""" +import dataclasses from typing import Any from devolo_plc_api.device_api.deviceapi import DeviceApi @@ -27,5 +28,5 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" - self.plcnet = PlcNetApi(IP, None, DISCOVERY_INFO) - self.device = DeviceApi(IP, None, DISCOVERY_INFO) + self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index 255a9ec1f7c..1d8d2a6da19 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -30,8 +30,8 @@ def mock_device(): def mock_validate_input(): """Mock setup entry and user input.""" info = { - "serial_number": DISCOVERY_INFO["properties"]["SN"], - "title": DISCOVERY_INFO["properties"]["Product"], + "serial_number": DISCOVERY_INFO.properties["SN"], + "title": DISCOVERY_INFO.properties["Product"], } with patch( diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 1af3c39bca2..7d115a26b15 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -99,7 +99,7 @@ async def test_zeroconf(hass: HomeAssistant): assert ( context["title_placeholders"][CONF_NAME] - == DISCOVERY_INFO["hostname"].split(".", maxsplit=1)[0] + == DISCOVERY_INFO.hostname.split(".", maxsplit=1)[0] ) with patch( From 3188a364e270c2753ba978dc2f1929e1d2148142 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 3 Dec 2021 08:58:15 +0100 Subject: [PATCH 1265/1452] Add template filters pack and unpack (#60836) * add pack and unpack template filters * Add unpack test with offset * use unpack_from * Simplify unpack_from statement --- homeassistant/helpers/template.py | 33 ++++++++ tests/helpers/test_template.py | 134 ++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6ef9ef57329..26a8add3de5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -17,6 +17,7 @@ from operator import attrgetter import random import re import statistics +from struct import error as StructError, pack, unpack_from import sys from typing import Any, cast from urllib.parse import urlencode as urllib_urlencode @@ -1639,6 +1640,34 @@ def bitwise_or(first_value, second_value): return first_value | second_value +def struct_pack(value: Any | None, format_string: str) -> bytes | None: + """Pack an object into a bytes object.""" + try: + return pack(format_string, value) + except StructError: + _LOGGER.warning( + "Template warning: 'pack' unable to pack object '%s' with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information", + str(value), + type(value).__name__, + format_string, + ) + return None + + +def struct_unpack(value: bytes, format_string: str, offset: int = 0) -> Any | None: + """Unpack an object from bytes an return the first native object.""" + try: + return unpack_from(format_string, value, offset)[0] + except StructError: + _LOGGER.warning( + "Template warning: 'unpack' unable to unpack object '%s' with format_string '%s' and offset %s see https://docs.python.org/3/library/struct.html for more information", + value, + format_string, + offset, + ) + return None + + def base64_encode(value): """Perform base64 encode.""" return base64.b64encode(value.encode("utf-8")).decode("utf-8") @@ -1823,6 +1852,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["regex_findall_index"] = regex_findall_index self.filters["bitwise_and"] = bitwise_and self.filters["bitwise_or"] = bitwise_or + self.filters["pack"] = struct_pack + self.filters["unpack"] = struct_unpack self.filters["ord"] = ord self.filters["is_number"] = is_number self.filters["float"] = forgiving_float_filter @@ -1853,6 +1884,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["min"] = min self.globals["is_number"] = is_number self.globals["int"] = forgiving_int + self.globals["pack"] = struct_pack + self.globals["unpack"] = struct_unpack self.tests["match"] = regex_match self.tests["search"] = regex_search diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 99c57983aa5..4a97b99d05d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1505,6 +1505,140 @@ def test_bitwise_or(hass): assert tpl.async_render() == 8 | 2 +def test_pack(hass, caplog): + """Test struct pack method.""" + + # render as filter + tpl = template.Template( + """ +{{ value | pack('>I') }} + """, + hass, + ) + variables = { + "value": 0xDEADBEEF, + } + assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef" + + # render as function + tpl = template.Template( + """ +{{ pack(value, '>I') }} + """, + hass, + ) + variables = { + "value": 0xDEADBEEF, + } + assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef" + + # test with None value + tpl = template.Template( + """ +{{ pack(value, '>I') }} + """, + hass, + ) + variables = { + "value": None, + } + # "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information" + assert tpl.async_render(variables=variables) is None + assert ( + "Template warning: 'pack' unable to pack object 'None' with type 'NoneType' and format_string '>I' see https://docs.python.org/3/library/struct.html for more information" + in caplog.text + ) + + # test with invalid filter + tpl = template.Template( + """ +{{ pack(value, 'invalid filter') }} + """, + hass, + ) + variables = { + "value": 0xDEADBEEF, + } + # "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information" + assert tpl.async_render(variables=variables) is None + assert ( + "Template warning: 'pack' unable to pack object '3735928559' with type 'int' and format_string 'invalid filter' see https://docs.python.org/3/library/struct.html for more information" + in caplog.text + ) + + +def test_unpack(hass, caplog): + """Test struct unpack method.""" + + # render as filter + tpl = template.Template( + """ +{{ value | unpack('>I') }} + """, + hass, + ) + variables = { + "value": b"\xde\xad\xbe\xef", + } + assert tpl.async_render(variables=variables) == 0xDEADBEEF + + # render as function + tpl = template.Template( + """ +{{ unpack(value, '>I') }} + """, + hass, + ) + variables = { + "value": b"\xde\xad\xbe\xef", + } + assert tpl.async_render(variables=variables) == 0xDEADBEEF + + # unpack with offset + tpl = template.Template( + """ +{{ unpack(value, '>H', offset=2) }} + """, + hass, + ) + variables = { + "value": b"\xde\xad\xbe\xef", + } + assert tpl.async_render(variables=variables) == 0xBEEF + + # test with an empty bytes object + tpl = template.Template( + """ +{{ unpack(value, '>I') }} + """, + hass, + ) + variables = { + "value": b"", + } + assert tpl.async_render(variables=variables) is None + assert ( + "Template warning: 'unpack' unable to unpack object 'b''' with format_string '>I' and offset 0 see https://docs.python.org/3/library/struct.html for more information" + in caplog.text + ) + + # test with invalid filter + tpl = template.Template( + """ +{{ unpack(value, 'invalid filter') }} + """, + hass, + ) + variables = { + "value": b"", + } + assert tpl.async_render(variables=variables) is None + assert ( + "Template warning: 'unpack' unable to unpack object 'b''' with format_string 'invalid filter' and offset 0 see https://docs.python.org/3/library/struct.html for more information" + in caplog.text + ) + + def test_distance_function_with_1_state(hass): """Test distance function with 1 state.""" _set_up_units(hass) From 707fe67c0053af7c3955077fabd2734633c03ecd Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 3 Dec 2021 09:31:17 +0100 Subject: [PATCH 1266/1452] Move StrEnum to homeassistant.backports and move Platform to homeassistant.const (#60880) Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/backports/__init__.py | 1 + homeassistant/{util => backports}/enum.py | 2 +- .../components/binary_sensor/__init__.py | 2 +- homeassistant/components/button/__init__.py | 2 +- homeassistant/components/cover/__init__.py | 2 +- .../components/humidifier/__init__.py | 2 +- .../components/media_player/__init__.py | 2 +- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 2 +- homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/wled/__init__.py | 2 +- homeassistant/const.py | 38 +++++++++++++++++++ homeassistant/helpers/device_registry.py | 2 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_platform.py | 36 ------------------ homeassistant/helpers/entity_registry.py | 2 +- homeassistant/setup.py | 30 +-------------- homeassistant/util/dt.py | 4 +- script/hassfest/dependencies.py | 7 +--- tests/backports/__init__.py | 1 + tests/{util => backports}/test_enum.py | 2 +- 21 files changed, 59 insertions(+), 86 deletions(-) create mode 100644 homeassistant/backports/__init__.py rename homeassistant/{util => backports}/enum.py (96%) create mode 100644 tests/backports/__init__.py rename tests/{util => backports}/test_enum.py (93%) diff --git a/homeassistant/backports/__init__.py b/homeassistant/backports/__init__.py new file mode 100644 index 00000000000..e3dc736417a --- /dev/null +++ b/homeassistant/backports/__init__.py @@ -0,0 +1 @@ +"""Backports from newer Python versions.""" diff --git a/homeassistant/util/enum.py b/homeassistant/backports/enum.py similarity index 96% rename from homeassistant/util/enum.py rename to homeassistant/backports/enum.py index 2ebd1678138..3fa9a582f79 100644 --- a/homeassistant/util/enum.py +++ b/homeassistant/backports/enum.py @@ -1,4 +1,4 @@ -"""Enum related utilities.""" +"""Enum backports from standard lib.""" from __future__ import annotations from enum import Enum diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index c9301eb7978..0604d5da586 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -8,6 +8,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -18,7 +19,6 @@ 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, StateType -from homeassistant.util.enum import StrEnum _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 49249baac61..2e9a8c05163 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -8,6 +8,7 @@ from typing import final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -19,7 +20,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from homeassistant.util.enum import StrEnum from .const import DOMAIN, SERVICE_PRESS diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 3b8558658eb..e654cf1a0d0 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -9,6 +9,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_CLOSE_COVER, @@ -34,7 +35,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass -from homeassistant.util.enum import StrEnum # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index e6ec8dc72f2..bf3a45d6e91 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -8,6 +8,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_MODE, @@ -26,7 +27,6 @@ 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.enum import StrEnum from .const import ( # noqa: F401 ATTR_AVAILABLE_MODES, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 12d0eae29d4..d1d51f525e4 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -22,6 +22,7 @@ import async_timeout import voluptuous as vol from yarl import URL +from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.websocket_api.const import ( @@ -63,7 +64,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass -from homeassistant.util.enum import StrEnum from .const import ( ATTR_APP_ID, diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 7e2f9093b80..af88b5c86b5 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -8,6 +8,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE from homeassistant.core import HomeAssistant, ServiceCall @@ -18,7 +19,6 @@ 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.util.enum import StrEnum from .const import ( ATTR_MAX, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 972914a0c68..75db36b91b2 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,6 +12,7 @@ from typing import Any, Final, cast, final import ciso8601 import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 DEVICE_CLASS_AQI, @@ -53,7 +54,6 @@ 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, StateType -from homeassistant.util.enum import StrEnum from .const import CONF_STATE_CLASS # noqa: F401 diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index fec5a58f414..ac29e99bb73 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -8,6 +8,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, @@ -24,7 +25,6 @@ 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.enum import StrEnum DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 0518bd736d0..659df1baad9 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import Platform from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator diff --git a/homeassistant/const.py b/homeassistant/const.py index fb8dfc6089a..9da00de9a9a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Final +from homeassistant.backports.enum import StrEnum + MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 PATCH_VERSION: Final = "0.dev0" @@ -16,6 +18,42 @@ REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2022.1" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" + +class Platform(StrEnum): + """Available entity platforms.""" + + AIR_QUALITY = "air_quality" + ALARM_CONTROL_PANEL = "alarm_control_panel" + BINARY_SENSOR = "binary_sensor" + BUTTON = "button" + CALENDAR = "calendar" + CAMERA = "camera" + CLIMATE = "climate" + COVER = "cover" + DEVICE_TRACKER = "device_tracker" + FAN = "fan" + GEO_LOCATION = "geo_location" + HUMIDIFIER = "humidifier" + IMAGE_PROCESSING = "image_processing" + LIGHT = "light" + LOCK = "lock" + MAILBOX = "mailbox" + MEDIA_PLAYER = "media_player" + NOTIFY = "notify" + NUMBER = "number" + REMOTE = "remote" + SCENE = "scene" + SELECT = "select" + SENSOR = "sensor" + SIREN = "siren" + STT = "stt" + SWITCH = "switch" + TTS = "tts" + VACUUM = "vacuum" + WATER_HEATER = "water_heater" + WEATHER = "weather" + + # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL: Final = "*" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 7a78bf69ba7..c8ae7fd148c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -8,13 +8,13 @@ from typing import TYPE_CHECKING, Any, NamedTuple, cast import attr +from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import RequiredParameterMissing from homeassistant.helpers import storage from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass -from homeassistant.util.enum import StrEnum import homeassistant.util.uuid as uuid_util from .debounce import Debouncer diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c303b97eb5f..86c8fa86af5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -15,6 +15,7 @@ from typing import Any, Final, Literal, TypedDict, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -43,7 +44,6 @@ from homeassistant.helpers.event import Event, async_track_entity_registry_updat from homeassistant.helpers.typing import StateType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify -from homeassistant.util.enum import StrEnum _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 45795a44c42..d8cb8477f11 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -34,7 +34,6 @@ from homeassistant.exceptions import ( ) from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.enum import StrEnum from . import ( config_validation as cv, @@ -63,41 +62,6 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = getLogger(__name__) -class Platform(StrEnum): - """Available platforms.""" - - AIR_QUALITY = "air_quality" - ALARM_CONTROL_PANEL = "alarm_control_panel" - BINARY_SENSOR = "binary_sensor" - BUTTON = "button" - CALENDAR = "calendar" - CAMERA = "camera" - CLIMATE = "climate" - COVER = "cover" - DEVICE_TRACKER = "device_tracker" - FAN = "fan" - GEO_LOCATION = "geo_location" - HUMIDIFIER = "humidifier" - IMAGE_PROCESSING = "image_processing" - LIGHT = "light" - LOCK = "lock" - MAILBOX = "mailbox" - MEDIA_PLAYER = "media_player" - NOTIFY = "notify" - NUMBER = "number" - REMOTE = "remote" - SCENE = "scene" - SELECT = "select" - SENSOR = "sensor" - SIREN = "siren" - SST = "sst" - SWITCH = "switch" - TTS = "tts" - VACUUM = "vacuum" - WATER_HEATER = "water_heater" - WEATHER = "weather" - - class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 6614091341f..b79e9af209e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, cast import attr import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -42,7 +43,6 @@ from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util -from homeassistant.util.enum import StrEnum from homeassistant.util.yaml import load_yaml from .typing import UNDEFINED, UndefinedType diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 67740c54254..3e6db28a194 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -14,6 +14,7 @@ from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, + Platform, ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import HomeAssistantError @@ -26,34 +27,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" -BASE_PLATFORMS = { - "air_quality", - "alarm_control_panel", - "binary_sensor", - "camera", - "calendar", - "climate", - "cover", - "device_tracker", - "fan", - "humidifier", - "image_processing", - "light", - "lock", - "media_player", - "notify", - "number", - "remote", - "scene", - "select", - "sensor", - "siren", - "switch", - "tts", - "vacuum", - "water_heater", - "weather", -} +BASE_PLATFORMS = {platform.value for platform in Platform} DATA_SETUP_DONE = "setup_done" DATA_SETUP_STARTED = "setup_started" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 39f8a63e53f..0c8a1cd9aad 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -10,8 +10,6 @@ from typing import Any, cast import ciso8601 -from homeassistant.const import MATCH_ALL - if sys.version_info[:2] >= (3, 9): import zoneinfo else: @@ -215,7 +213,7 @@ def get_age(date: dt.datetime) -> str: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]: """Parse the time expression part and return a list of times to match.""" - if parameter is None or parameter == MATCH_ALL: + if parameter is None or parameter == "*": res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str): if parameter.startswith("/"): diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 3f0bd9c1236..c52a926e4e1 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -4,8 +4,8 @@ from __future__ import annotations import ast from pathlib import Path +from homeassistant.const import Platform from homeassistant.requirements import DISCOVERY_INTEGRATIONS -from homeassistant.setup import BASE_PLATFORMS from .model import Integration @@ -91,12 +91,11 @@ class ImportCollector(ast.NodeVisitor): ALLOWED_USED_COMPONENTS = { + *{platform.value for platform in Platform}, # Internal integrations "alert", "automation", - "button", "conversation", - "button", "device_automation", "frontend", "group", @@ -119,8 +118,6 @@ ALLOWED_USED_COMPONENTS = { "webhook", "websocket_api", "zone", - # Entity integrations with platforms - *BASE_PLATFORMS, # Other "mjpeg", # base class, has no reqs or component to load. "stream", # Stream cannot install on all systems, can be imported without reqs. diff --git a/tests/backports/__init__.py b/tests/backports/__init__.py new file mode 100644 index 00000000000..3f701810a5d --- /dev/null +++ b/tests/backports/__init__.py @@ -0,0 +1 @@ +"""The tests for the backports.""" diff --git a/tests/util/test_enum.py b/tests/backports/test_enum.py similarity index 93% rename from tests/util/test_enum.py rename to tests/backports/test_enum.py index 8fb2091bd11..645db2bd7ca 100644 --- a/tests/util/test_enum.py +++ b/tests/backports/test_enum.py @@ -4,7 +4,7 @@ from enum import auto import pytest -from homeassistant.util.enum import StrEnum +from homeassistant.backports.enum import StrEnum def test_strenum(): From 67f9118cbf38e08fdb615f88dcbeea80fc98bce6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 3 Dec 2021 09:58:27 +0100 Subject: [PATCH 1267/1452] Use native datetime value in Brother uptime sensor (#60363) --- homeassistant/components/brother/sensor.py | 419 +++++++++++---------- 1 file changed, 219 insertions(+), 200 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index bdec3049f8f..be46bc33b6b 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,6 +1,8 @@ """Support for the Brother service.""" from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime from typing import Any, cast from homeassistant.components.sensor import ( @@ -84,198 +86,6 @@ ATTRS_MAP: dict[str, tuple[str, str]] = { ), } -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=ATTR_STATUS, - icon="mdi:printer", - name=ATTR_STATUS.title(), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PAGE_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_PAGE_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BW_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_BW_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_COLOR_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_COLOR_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_DUPLEX_COUNTER, - icon="mdi:file-document-outline", - name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BELT_UNIT_REMAINING_LIFE, - icon="mdi:current-ac", - name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_FUSER_REMAINING_LIFE, - icon="mdi:water-outline", - name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_LASER_REMAINING_LIFE, - icon="mdi:spotlight-beam", - name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PF_KIT_1_REMAINING_LIFE, - icon="mdi:printer-3d", - name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_PF_KIT_MP_REMAINING_LIFE, - icon="mdi:printer-3d", - name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_BLACK_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_CYAN_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_MAGENTA_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_YELLOW_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_UPTIME, - name=ATTR_UPTIME.title(), - entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -296,7 +106,9 @@ async def async_setup_entry( for description in SENSOR_TYPES: if description.key in coordinator.data: - sensors.append(BrotherPrinterSensor(coordinator, description, device_info)) + sensors.append( + description.entity_class(coordinator, description, device_info) + ) async_add_entities(sensors, False) @@ -306,7 +118,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: BrotherDataUpdateCoordinator, - description: SensorEntityDescription, + description: BrotherSensorEntityDescription, device_info: DeviceInfo, ) -> None: """Initialize.""" @@ -318,13 +130,8 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state.""" - if self.entity_description.key == ATTR_UPTIME: - return cast( - StateType, - getattr(self.coordinator.data, self.entity_description.key).isoformat(), - ) return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) ) @@ -341,3 +148,215 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): ) self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter) return self._attrs + + +class BrotherPrinterUptimeSensor(BrotherPrinterSensor): + """Define an Brother Printer Uptime sensor.""" + + @property + def native_value(self) -> datetime: + """Return the state.""" + return cast( + datetime, getattr(self.coordinator.data, self.entity_description.key) + ) + + +@dataclass +class BrotherSensorEntityDescription(SensorEntityDescription): + """A class that describes sensor entities.""" + + entity_class: type[BrotherPrinterSensor] = BrotherPrinterSensor + + +SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( + BrotherSensorEntityDescription( + key=ATTR_STATUS, + icon="mdi:printer", + name=ATTR_STATUS.title(), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_PAGE_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_PAGE_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_BW_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_BW_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_COLOR_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_COLOR_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_DUPLEX_COUNTER, + icon="mdi:file-document-outline", + name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + native_unit_of_measurement=UNIT_PAGES, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_BLACK_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_CYAN_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_YELLOW_DRUM_REMAINING_LIFE, + icon="mdi:chart-donut", + name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_BELT_UNIT_REMAINING_LIFE, + icon="mdi:current-ac", + name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_FUSER_REMAINING_LIFE, + icon="mdi:water-outline", + name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_LASER_REMAINING_LIFE, + icon="mdi:spotlight-beam", + name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_PF_KIT_1_REMAINING_LIFE, + icon="mdi:printer-3d", + name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_PF_KIT_MP_REMAINING_LIFE, + icon="mdi:printer-3d", + name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_BLACK_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_CYAN_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_MAGENTA_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_YELLOW_TONER_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_BLACK_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_CYAN_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_MAGENTA_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_YELLOW_INK_REMAINING, + icon="mdi:printer-3d-nozzle", + name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + BrotherSensorEntityDescription( + key=ATTR_UPTIME, + name=ATTR_UPTIME.title(), + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_class=BrotherPrinterUptimeSensor, + ), +) From 85293d8073a845ea9e1041f076816b06ee37f498 Mon Sep 17 00:00:00 2001 From: Tuen Lee Date: Fri, 3 Dec 2021 10:23:15 +0100 Subject: [PATCH 1268/1452] Update tuya cover, fix Up/down position (#59858) --- homeassistant/components/tuya/cover.py | 62 +++++++++++++++++++++----- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 09ae829f382..fd1f2aae972 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -36,7 +36,7 @@ class TuyaCoverEntityDescription(CoverEntityDescription): current_state: DPCode | None = None current_state_inverse: bool = False - current_position: DPCode | None = None + current_position: DPCode | tuple[DPCode, ...] | None = None set_position: DPCode | None = None @@ -49,7 +49,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { key=DPCode.CONTROL, name="Curtain", current_state=DPCode.SITUATION_SET, - current_position=DPCode.PERCENT_STATE, + current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE), set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.CURTAIN, ), @@ -173,6 +173,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): _set_position_type: IntegerTypeData | None = None _tilt_dpcode: DPCode | None = None _tilt_type: IntegerTypeData | None = None + _position_dpcode: DPCode | None = None entity_description: TuyaCoverEntityDescription def __init__( @@ -235,21 +236,34 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): device.status_range[tilt_dpcode].values ) + # Determine current_position DPCodes + if ( + self.entity_description.current_position is None + and self.entity_description.set_position is not None + ): + self._position_dpcode = self.entity_description.set_position + elif isinstance(self.entity_description.current_position, DPCode): + self._position_dpcode = self.entity_description.current_position + elif isinstance(self.entity_description.current_position, tuple): + self._position_dpcode = next( + ( + dpcode + for dpcode in self.entity_description.current_position + if self.device.status.get(dpcode) is not None + ), + None, + ) + @property def current_cover_position(self) -> int | None: """Return cover current position.""" if self._current_position_type is None: return None - if not ( - dpcode := ( - self.entity_description.current_position - or self.entity_description.set_position - ) - ): + if not self._position_dpcode: return None - if (position := self.device.status.get(dpcode)) is None: + if (position := self.device.status.get(self._position_dpcode)) is None: return None return round( @@ -296,14 +310,40 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): value: bool | str = True if self.device.function[self.entity_description.key].type == "Enum": value = "open" - self._send_command([{"code": self.entity_description.key, "value": value}]) + + commands: list[dict[str, str | int]] = [ + {"code": self.entity_description.key, "value": value} + ] + + if (self.entity_description.set_position) is not None: + commands.append( + { + "code": self.entity_description.set_position, + "value": 0, + } + ) + + self._send_command(commands) def close_cover(self, **kwargs: Any) -> None: """Close cover.""" value: bool | str = True if self.device.function[self.entity_description.key].type == "Enum": value = "close" - self._send_command([{"code": self.entity_description.key, "value": value}]) + + commands: list[dict[str, str | int]] = [ + {"code": self.entity_description.key, "value": value} + ] + + if (self.entity_description.set_position) is not None: + commands.append( + { + "code": self.entity_description.set_position, + "value": 100, + } + ) + + self._send_command(commands) def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" From d784c8dd1a193ca5dd0c11fc6594b7d4ec67a36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 3 Dec 2021 10:35:57 +0100 Subject: [PATCH 1269/1452] Remove Supervisor panel title and icon (#60894) --- homeassistant/components/hassio/__init__.py | 2 -- tests/components/hassio/test_init.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e9776dcb7e1..a6f130e0656 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -461,8 +461,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await hass.components.panel_custom.async_register_panel( frontend_url_path="hassio", webcomponent_name="hassio-main", - sidebar_title="Supervisor", - sidebar_icon="hass:home-assistant", js_url="/api/hassio/app/entrypoint.js", embed_iframe=True, require_admin=True, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 417fc77b527..e006cf9d829 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -165,8 +165,8 @@ async def test_setup_api_panel(hass, aioclient_mock): assert panels.get("hassio").to_response() == { "component_name": "custom", - "icon": "hass:home-assistant", - "title": "Supervisor", + "icon": None, + "title": None, "url_path": "hassio", "require_admin": True, "config": { From e8b4b7074785df75c2474512fcde78a0982916b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 11:22:28 +0100 Subject: [PATCH 1270/1452] Mark config schema as removed in Verisure (#60896) --- homeassistant/components/verisure/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 8abb3e59a9f..bea82a4785c 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -31,7 +31,7 @@ PLATFORMS = [ SWITCH_DOMAIN, ] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: From 358922db5642a94d73a1ca582293fc63da9f5b99 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:05:56 +0100 Subject: [PATCH 1271/1452] Use dataclass for HassioServiceInfo (#60824) Co-authored-by: epenet --- .../components/adguard/config_flow.py | 5 +- .../components/almond/config_flow.py | 5 +- .../components/deconz/config_flow.py | 15 ++-- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/hassio/discovery.py | 27 +----- .../components/motioneye/config_flow.py | 5 +- homeassistant/components/mqtt/config_flow.py | 6 +- homeassistant/components/ozw/config_flow.py | 5 +- .../components/vlc_telnet/config_flow.py | 9 +- .../components/zwave_js/config_flow.py | 8 +- homeassistant/config_entries.py | 5 +- tests/components/adguard/test_config_flow.py | 2 +- tests/components/almond/test_config_flow.py | 4 +- tests/components/deconz/test_config_flow.py | 2 +- tests/components/hassio/test_discovery.py | 82 +++++++------------ .../components/motioneye/test_config_flow.py | 2 +- tests/components/mqtt/test_config_flow.py | 23 ++++-- tests/components/ozw/test_config_flow.py | 2 +- .../components/vlc_telnet/test_config_flow.py | 39 +++++---- tests/components/zwave_js/test_config_flow.py | 30 ++++--- tests/test_config_entries.py | 3 +- 21 files changed, 131 insertions(+), 150 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index aa85345179e..aadbed49980 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -6,6 +6,7 @@ from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -104,14 +105,14 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. """ await self._async_handle_discovery_without_unique_id() - self._hassio_discovery = discovery_info + self._hassio_discovery = discovery_info.config return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index 32d90f7b03b..ba6fcb6d83c 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -12,6 +12,7 @@ import voluptuous as vol from yarl import URL from homeassistant import config_entries, core, data_entry_flow +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -94,12 +95,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): data={"type": TYPE_LOCAL, "host": user_input["host"]}, ) - async def async_step_hassio(self, discovery_info): + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Receive a Hass.io discovery.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - self.hassio_discovery = discovery_info + self.hassio_discovery = discovery_info.config return await self.async_step_hassio_confirm() diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index ca8e95f70ce..473cbd72971 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -20,6 +20,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.deconz.gateway import DeconzGateway +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback @@ -233,25 +234,25 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_link() - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Prepare configuration for a Hass.io deCONZ bridge. This flow is triggered by the discovery component. """ - LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info)) + LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info.config)) - self.bridge_id = normalize_bridge_id(discovery_info[CONF_SERIAL]) + self.bridge_id = normalize_bridge_id(discovery_info.config[CONF_SERIAL]) await self.async_set_unique_id(self.bridge_id) self._abort_if_unique_id_configured( updates={ - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], - CONF_API_KEY: discovery_info[CONF_API_KEY], + CONF_HOST: discovery_info.config[CONF_HOST], + CONF_PORT: discovery_info.config[CONF_PORT], + CONF_API_KEY: discovery_info.config[CONF_API_KEY], } ) - self._hassio_discovery = discovery_info + self._hassio_discovery = discovery_info.config return await self.async_step_hassio_confirm() diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a6f130e0656..7991c50563c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -57,7 +57,7 @@ from .const import ( DOMAIN, SupervisorEntityModel, ) -from .discovery import async_setup_discovery_view +from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401 from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView from .ingress import async_setup_ingress_view diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 67090d78a96..587457f2ca2 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping from dataclasses import dataclass import logging from typing import Any @@ -15,7 +14,6 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo -from homeassistant.helpers.frame import report from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID from .handler import HassioAPIError @@ -27,26 +25,7 @@ _LOGGER = logging.getLogger(__name__) class HassioServiceInfo(BaseServiceInfo): """Prepared info from hassio entries.""" - config: Mapping[str, Any] - - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - - def __getitem__(self, name: str) -> Any: - """ - Allow property access by name for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.config['{name}']; this will fail in version 2022.6", - exclude_integrations={"hassio"}, - error_if_core=False, - level=logging.DEBUG, - ) - self._warning_logged = True - return self.config[name] + config: dict[str, Any] @callback @@ -121,7 +100,9 @@ class HassIODiscovery(HomeAssistantView): # Use config flow await self.hass.config_entries.flow.async_init( - service, context={"source": config_entries.SOURCE_HASSIO}, data=config_data + service, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo(config=config_data), ) async def async_process_del(self, data): diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 54b4a9f0b09..e90a6068ebc 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -10,6 +10,7 @@ from motioneye_client.client import ( ) import voluptuous as vol +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ( SOURCE_REAUTH, ConfigEntry, @@ -162,9 +163,9 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a reauthentication flow.""" return await self.async_step_user(config_data) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle Supervisor discovery.""" - self._hassio_discovery = discovery_info + self._hassio_discovery = discovery_info.config await self._async_handle_discovery_without_unique_id() return await self.async_step_hassio_confirm() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 172657ded98..84322ddc1ee 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -5,6 +5,7 @@ import queue import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -14,6 +15,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) +from homeassistant.data_entry_flow import FlowResult from .const import ( ATTR_PAYLOAD, @@ -93,11 +95,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="configuration.yaml", data={}) - async def async_step_hassio(self, discovery_info): + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Receive a Hass.io discovery.""" await self._async_handle_discovery_without_unique_id() - self._hassio_discovery = discovery_info + self._hassio_discovery = discovery_info.config return await self.async_step_hassio_confirm() diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index c1dbbe2e093..e39a3e2ba89 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -4,8 +4,9 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, FlowResult from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN @@ -48,7 +49,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_on_supervisor() - async def async_step_hassio(self, discovery_info): + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Receive configuration from add-on discovery info. This flow is triggered by the OpenZWave add-on. diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index e86ab635517..bb503229ebb 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -9,6 +9,7 @@ from aiovlc.exceptions import AuthError, ConnectError import voluptuous as vol from homeassistant import core, exceptions +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResult @@ -151,13 +152,13 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle the discovery step via hassio.""" await self.async_set_unique_id("hassio") - self._abort_if_unique_id_configured(discovery_info) + self._abort_if_unique_id_configured(discovery_info.config) - self.hassio_discovery = discovery_info - self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} + self.hassio_discovery = discovery_info.config + self.context["title_placeholders"] = {"host": discovery_info.config[CONF_HOST]} return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 7b59cce36a9..32f406d7476 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -13,7 +13,7 @@ from zwave_js_server.version import VersionInfo, get_server_version from homeassistant import config_entries, exceptions from homeassistant.components import usb -from homeassistant.components.hassio import is_hassio +from homeassistant.components.hassio import HassioServiceInfo, is_hassio from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import ( @@ -430,7 +430,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -438,7 +438,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): if self._async_in_progress(): return self.async_abort(reason="already_in_progress") - self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + self.ws_address = ( + f"ws://{discovery_info.config['host']}:{discovery_info.config['port']}" + ) try: version_info = await async_get_version_info(self.hass, self.ws_address) except CannotConnect: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 3fa905daf48..c6ad8d0a587 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -34,6 +34,7 @@ import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: from homeassistant.components.dhcp import DhcpServiceInfo + from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.mqtt.discovery import MqttServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.usb import UsbServiceInfo @@ -1353,10 +1354,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) async def async_step_hassio( - self, discovery_info: DiscoveryInfoType + self, discovery_info: HassioServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" - return await self.async_step_discovery(discovery_info) + return await self.async_step_discovery(discovery_info.config) async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index b87d0796a54..f5b30308a52 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -3,7 +3,7 @@ import aiohttp from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard.const import DOMAIN -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 5e4283fb611..32f49cff043 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow from homeassistant.components.almond.const import DOMAIN -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -90,7 +90,7 @@ async def test_abort_if_existing_entry(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" - result = await flow.async_step_hassio({}) + result = await flow.async_step_hassio(HassioServiceInfo(config={})) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index a87a9df408b..ecfb324207f 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -18,7 +18,7 @@ from homeassistant.components.deconz.const import ( CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( SOURCE_HASSIO, diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index de424c4d339..71f5f8acf96 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -2,7 +2,7 @@ from http import HTTPStatus from unittest.mock import Mock, patch -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component @@ -49,14 +49,16 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( - { - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } + HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } + ) ) @@ -110,14 +112,16 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( - { - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } + HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } + ) ) @@ -160,38 +164,14 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( - { - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } + HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } + ) ) - - -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = HassioServiceInfo( - config={ - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } - ) - - # Ensure first call get logged - assert discovery_info["broker"] == "mock-broker" - assert "Detected code that accessed discovery_info['broker']" in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["broker"] == "mock-broker" - assert "Detected code that accessed discovery_info['broker']" not in caplog.text diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 35988ed8fbb..57def069d59 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -8,7 +8,7 @@ from motioneye_client.client import ( ) from homeassistant import config_entries, data_entry_flow -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 208541d702c..befdc139eeb 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import mqtt +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -124,7 +125,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( mqtt.DOMAIN, - data={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"}, + data=HassioServiceInfo( + config={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"} + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result @@ -140,14 +143,16 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( "mqtt", - data={ - "addon": "Mock Addon", - "host": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - }, + data=HassioServiceInfo( + config={ + "addon": "Mock Addon", + "host": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + } + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == "form" diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index f06c315500f..9c65372ca98 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 5ef8b6c0400..6a0659b6360 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -8,6 +8,7 @@ from aiovlc.exceptions import AuthError, ConnectError import pytest from homeassistant import config_entries +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.vlc_telnet.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -278,13 +279,15 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: "homeassistant.components.vlc_telnet.async_setup_entry", return_value=True, ) as mock_setup_entry: - test_data = { - "password": "test-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - "addon": "vlc", - } + test_data = HassioServiceInfo( + config={ + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + "addon": "vlc", + } + ) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -298,8 +301,8 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == test_data["name"] - assert result2["data"] == test_data + assert result2["title"] == test_data.config["name"] + assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -320,7 +323,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=entry_data, + data=HassioServiceInfo(config=entry_data), ) await hass.async_block_till_done() @@ -354,13 +357,15 @@ async def test_hassio_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data={ - "password": "test-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - "addon": "vlc", - }, + data=HassioServiceInfo( + config={ + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + "addon": "vlc", + } + ), ) await hass.async_block_till_done() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 0542457a23b..bf5dda3ade2 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -8,20 +8,18 @@ from zwave_js_server.version import VersionInfo from homeassistant import config_entries from homeassistant.components import usb -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN from tests.common import MockConfigEntry -ADDON_DISCOVERY_INFO = HassioServiceInfo( - config={ - "addon": "Z-Wave JS", - "host": "host1", - "port": 3001, - } -) +ADDON_DISCOVERY_INFO = { + "addon": "Z-Wave JS", + "host": "host1", + "port": 3001, +} USB_DISCOVERY_INFO = usb.UsbServiceInfo( @@ -285,7 +283,7 @@ async def test_supervisor_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) with patch( @@ -325,7 +323,7 @@ async def test_supervisor_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["type"] == "abort" @@ -347,7 +345,7 @@ async def test_clean_discovery_on_user_create( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["type"] == "form" @@ -410,7 +408,7 @@ async def test_abort_discovery_with_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["type"] == "abort" @@ -434,7 +432,7 @@ async def test_abort_hassio_discovery_with_existing_flow( result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result2["type"] == "abort" @@ -629,7 +627,7 @@ async def test_discovery_addon_not_running( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["step_id"] == "hassio_confirm" @@ -711,7 +709,7 @@ async def test_discovery_addon_not_installed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["step_id"] == "hassio_confirm" @@ -792,7 +790,7 @@ async def test_abort_usb_discovery_with_existing_flow(hass, supervisor, addon_op result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, + data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), ) assert result["type"] == "form" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6b710caed90..0e743fda91e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, callback from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo @@ -2356,7 +2357,7 @@ async def test_async_setup_update_entry(hass): (config_entries.SOURCE_HOMEKIT, BaseServiceInfo()), (config_entries.SOURCE_DHCP, BaseServiceInfo()), (config_entries.SOURCE_ZEROCONF, BaseServiceInfo()), - (config_entries.SOURCE_HASSIO, {}), + (config_entries.SOURCE_HASSIO, HassioServiceInfo(config={})), ), ) async def test_flow_with_default_discovery(hass, manager, discovery_source): From e64f901e92b43de5adc1194577e50d266dcbb737 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:47:56 +0100 Subject: [PATCH 1272/1452] Use dataclass properties in zwave_js (#60913) Co-authored-by: epenet --- tests/components/zwave_js/test_config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index bf5dda3ade2..66fc9934ee1 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -488,7 +488,7 @@ async def test_usb_discovery( "core_zwave_js", { "options": { - "device": USB_DISCOVERY_INFO["device"], + "device": USB_DISCOVERY_INFO.device, "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -516,7 +516,7 @@ async def test_usb_discovery( assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", - "usb_path": USB_DISCOVERY_INFO["device"], + "usb_path": USB_DISCOVERY_INFO.device, "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -578,7 +578,7 @@ async def test_usb_discovery_addon_not_running( "core_zwave_js", { "options": { - "device": USB_DISCOVERY_INFO["device"], + "device": USB_DISCOVERY_INFO.device, "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -606,7 +606,7 @@ async def test_usb_discovery_addon_not_running( assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", - "usb_path": USB_DISCOVERY_INFO["device"], + "usb_path": USB_DISCOVERY_INFO.device, "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", From deae8dd07bcd5301af0e9a1151c50f853d00e1b4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 3 Dec 2021 15:30:09 +0100 Subject: [PATCH 1273/1452] Bump pytradfri to 7.2.1 (#60910) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 00c8143e0f1..b13abda25b0 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.2.0"], + "requirements": ["pytradfri[async]==7.2.1"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index b28d5821c00..ce5cc925856 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pytouchline==0.7 pytraccar==0.10.0 # homeassistant.components.tradfri -pytradfri[async]==7.2.0 +pytradfri[async]==7.2.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02ae17a2d2f..f4a2a36a552 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pytile==5.2.4 pytraccar==0.10.0 # homeassistant.components.tradfri -pytradfri[async]==7.2.0 +pytradfri[async]==7.2.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation From 7d8b3e9de33e5657e753e630bf4afca8ae9c13f9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Dec 2021 08:34:18 -0600 Subject: [PATCH 1274/1452] Bump soco to 0.25.0 (#60915) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 249a6d4cc00..1e31d2004b0 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.24.0"], + "requirements": ["soco==0.25.0"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index ce5cc925856..4aee015fcb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2187,7 +2187,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.24.0 +soco==0.25.0 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4a2a36a552..b03cdbf85a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1291,7 +1291,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.24.0 +soco==0.25.0 # homeassistant.components.solaredge solaredge==0.0.2 From b883014ed4d07f7e0d86a36b81a56766075a1952 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Dec 2021 09:05:40 -0600 Subject: [PATCH 1275/1452] Add Sonos subwoofer and surround on/off controls (#60918) --- homeassistant/components/sonos/speaker.py | 2 ++ homeassistant/components/sonos/switch.py | 8 ++++++++ tests/components/sonos/conftest.py | 2 ++ tests/components/sonos/test_switch.py | 12 +++++++++++- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4cec0fbb6ab..4838da69c50 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -196,6 +196,8 @@ class SonosSpeaker: self.cross_fade: bool | None = None self.bass: int | None = None self.treble: int | None = None + self.sub_enabled: bool | None = None + self.surround_enabled: bool | None = None # Misc features self.buttons_enabled: bool | None = None diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 78469dc8bbc..f6fe81953f5 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -38,6 +38,8 @@ ATTR_CROSSFADE = "cross_fade" ATTR_NIGHT_SOUND = "night_mode" ATTR_SPEECH_ENHANCEMENT = "dialog_mode" ATTR_STATUS_LIGHT = "status_light" +ATTR_SUB_ENABLED = "sub_enabled" +ATTR_SURROUND_ENABLED = "surround_enabled" ATTR_TOUCH_CONTROLS = "buttons_enabled" ALL_FEATURES = ( @@ -45,6 +47,8 @@ ALL_FEATURES = ( ATTR_CROSSFADE, ATTR_NIGHT_SOUND, ATTR_SPEECH_ENHANCEMENT, + ATTR_SUB_ENABLED, + ATTR_SURROUND_ENABLED, ATTR_STATUS_LIGHT, ) @@ -60,6 +64,8 @@ FRIENDLY_NAMES = { ATTR_NIGHT_SOUND: "Night Sound", ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", ATTR_STATUS_LIGHT: "Status Light", + ATTR_SUB_ENABLED: "Subwoofer Enabled", + ATTR_SURROUND_ENABLED: "Surround Enabled", ATTR_TOUCH_CONTROLS: "Touch Controls", } @@ -68,6 +74,8 @@ FEATURE_ICONS = { ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing", ATTR_CROSSFADE: "mdi:swap-horizontal", ATTR_STATUS_LIGHT: "mdi:led-on", + ATTR_SUB_ENABLED: "mdi:dog", + ATTR_SURROUND_ENABLED: "mdi:surround-sound", ATTR_TOUCH_CONTROLS: "mdi:gesture-tap", } diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8a55d285a41..a17f07c3c35 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -72,6 +72,8 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.volume = 19 mock_soco.bass = 1 mock_soco.treble = -1 + mock_soco.sub_enabled = False + mock_soco.surround_enabled = True mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = [mock_soco] yield mock_soco diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index c2f997dbcb6..9c25e2b8cc7 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -14,7 +14,7 @@ from homeassistant.components.sonos.switch import ( ATTR_VOLUME, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.const import ATTR_TIME, STATE_OFF, STATE_ON from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -42,6 +42,8 @@ async def test_entity_registry(hass, config_entry, config): assert "switch.sonos_zone_a_status_light" in entity_registry.entities assert "switch.sonos_zone_a_night_sound" in entity_registry.entities assert "switch.sonos_zone_a_speech_enhancement" in entity_registry.entities + assert "switch.sonos_zone_a_subwoofer_enabled" in entity_registry.entities + assert "switch.sonos_zone_a_surround_enabled" in entity_registry.entities assert "switch.sonos_zone_a_touch_controls" in entity_registry.entities @@ -83,6 +85,14 @@ async def test_switch_attributes(hass, config_entry, config, soco): touch_controls = entity_registry.entities["switch.sonos_zone_a_touch_controls"] assert hass.states.get(touch_controls.entity_id) is None + sub_switch = entity_registry.entities["switch.sonos_zone_a_subwoofer_enabled"] + sub_switch_state = hass.states.get(sub_switch.entity_id) + assert sub_switch_state.state == STATE_OFF + + surround_switch = entity_registry.entities["switch.sonos_zone_a_surround_enabled"] + surround_switch_state = hass.states.get(surround_switch.entity_id) + assert surround_switch_state.state == STATE_ON + # Enable disabled switches for entity in (status_light, touch_controls): entity_registry.async_update_entity( From ef458b237cfd0266944edf24ef428ee7b5ad0914 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 16:34:26 +0100 Subject: [PATCH 1276/1452] Return if user is local only (#60917) --- homeassistant/components/config/auth.py | 1 + tests/components/config/test_auth.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 9170b028482..c46ef78b0b2 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -150,6 +150,7 @@ def _user_info(user): "name": user.name, "is_owner": user.is_owner, "is_active": user.is_active, + "local_only": user.local_only, "system_generated": user.system_generated, "group_ids": [group.id for group in user.groups], "credentials": [{"type": c.auth_provider_type} for c in user.credentials], diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 363910ffd72..7460de6a751 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -66,6 +66,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): "name": "Mock User", "is_owner": False, "is_active": True, + "local_only": False, "system_generated": False, "group_ids": [group.id for group in hass_admin_user.groups], "credentials": [{"type": "homeassistant"}], @@ -76,6 +77,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): "name": "Test Owner", "is_owner": True, "is_active": True, + "local_only": False, "system_generated": False, "group_ids": [group.id for group in owner.groups], "credentials": [{"type": "homeassistant"}], @@ -86,6 +88,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): "name": "Test Hass.io", "is_owner": False, "is_active": True, + "local_only": False, "system_generated": True, "group_ids": [], "credentials": [], @@ -96,6 +99,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): "name": "Inactive User", "is_owner": False, "is_active": False, + "local_only": False, "system_generated": False, "group_ids": [group.id for group in inactive.groups], "credentials": [], From f57d42a9e8009f82f7231291ac3764adaaa29a97 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 3 Dec 2021 17:51:30 +0100 Subject: [PATCH 1277/1452] Use platform enum (1) [A-D] (#60908) * Use platform enum (1) [A-D] * Fix imports * Fix tests * Use Platform even in tests --- homeassistant/components/abode/__init__.py | 17 ++++---- .../components/accuweather/__init__.py | 4 +- homeassistant/components/acmeda/__init__.py | 3 +- homeassistant/components/adax/__init__.py | 3 +- homeassistant/components/adguard/__init__.py | 3 +- .../components/advantage_air/__init__.py | 11 +++++- homeassistant/components/aemet/const.py | 3 +- .../components/agent_dvr/__init__.py | 9 +++-- homeassistant/components/airly/__init__.py | 4 +- homeassistant/components/airnow/__init__.py | 10 ++++- .../components/airthings/__init__.py | 3 +- .../components/airtouch4/__init__.py | 4 +- .../components/airvisual/__init__.py | 3 +- .../components/alarmdecoder/__init__.py | 7 +++- homeassistant/components/ambee/__init__.py | 5 +-- .../components/amberelectric/const.py | 4 +- .../components/ambient_station/__init__.py | 3 +- .../components/arcam_fmj/__init__.py | 4 +- homeassistant/components/asuswrt/__init__.py | 3 +- homeassistant/components/atag/__init__.py | 6 +-- homeassistant/components/atag/climate.py | 6 +-- homeassistant/components/atag/water_heater.py | 6 +-- homeassistant/components/august/const.py | 9 ++++- homeassistant/components/aurora/__init__.py | 4 +- .../aurora_abb_powerone/__init__.py | 4 +- homeassistant/components/awair/__init__.py | 4 +- .../components/azure_devops/__init__.py | 3 +- homeassistant/components/balboa/const.py | 3 +- homeassistant/components/blebox/__init__.py | 11 +++++- homeassistant/components/blink/const.py | 9 ++++- .../bmw_connected_drive/__init__.py | 13 +++++-- homeassistant/components/bond/__init__.py | 14 ++++++- .../components/bosch_shc/__init__.py | 4 +- homeassistant/components/braviatv/__init__.py | 6 +-- homeassistant/components/broadlink/const.py | 13 +++---- homeassistant/components/broadlink/switch.py | 7 ++-- homeassistant/components/brother/__init__.py | 4 +- homeassistant/components/brunt/const.py | 4 +- homeassistant/components/bsblan/__init__.py | 11 ++++-- .../components/buienradar/__init__.py | 3 +- homeassistant/components/canary/__init__.py | 8 +++- .../components/cert_expiry/__init__.py | 9 ++++- .../components/climacell/__init__.py | 5 +-- .../components/co2signal/__init__.py | 4 +- homeassistant/components/coinbase/__init__.py | 4 +- homeassistant/components/control4/__init__.py | 3 +- .../components/coolmaster/__init__.py | 4 +- .../components/coronavirus/__init__.py | 3 +- homeassistant/components/crownstone/const.py | 4 +- homeassistant/components/daikin/__init__.py | 4 +- homeassistant/components/deconz/const.py | 39 +++++++------------ homeassistant/components/denonavr/__init__.py | 4 +- .../components/devolo_home_control/const.py | 11 +++++- .../components/devolo_home_network/const.py | 4 +- homeassistant/components/dexcom/const.py | 4 +- homeassistant/components/directv/__init__.py | 4 +- homeassistant/components/dlna_dmr/__init__.py | 4 +- homeassistant/components/doorbird/const.py | 4 +- homeassistant/components/dsmr/const.py | 4 +- homeassistant/components/dunehd/__init__.py | 4 +- homeassistant/components/dynalite/const.py | 4 +- tests/components/atag/test_climate.py | 19 +++++---- tests/components/atag/test_water_heater.py | 10 ++--- tests/components/broadlink/test_remote.py | 20 +++++----- tests/components/broadlink/test_sensors.py | 25 ++++++------ 65 files changed, 272 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 3407d387169..3da4155dafd 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -53,14 +54,14 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) PLATFORMS = [ - "alarm_control_panel", - "binary_sensor", - "lock", - "switch", - "cover", - "camera", - "light", - "sensor", + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SWITCH, + Platform.COVER, + Platform.CAMERA, + Platform.LIGHT, + Platform.SENSOR, ] diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index a2b428cf597..bc8ae459fd7 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -11,7 +11,7 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -20,7 +20,7 @@ from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index 078c499f2be..49b4d9b85e8 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -1,13 +1,14 @@ """The Rollease Acmeda Automate integration.""" from homeassistant import config_entries, core +from homeassistant.const import Platform from .const import DOMAIN from .hub import PulseHub CONF_HUBS = "hubs" -PLATFORMS = ["cover", "sensor"] +PLATFORMS = [Platform.COVER, Platform.SENSOR] async def async_setup_entry( diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index 0a14648af26..bf339e810c6 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index e408338cd57..9b419c444ce 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -46,7 +47,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema( {vol.Optional(CONF_FORCE, default=False): cv.boolean} ) -PLATFORMS = ["sensor", "switch"] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 4af0b3e5f60..21b70ab9fc6 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -5,14 +5,21 @@ import logging from advantage_air import ApiError, advantage_air -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ADVANTAGE_AIR_RETRY, DOMAIN ADVANTAGE_AIR_SYNC_INTERVAL = 15 -PLATFORMS = ["binary_sensor", "climate", "cover", "select", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 0074a5be6d8..40542d88506 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -33,11 +33,12 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, + Platform, ) ATTRIBUTION = "Powered by AEMET OpenData" CONF_STATION_UPDATES = "station_updates" -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] DEFAULT_NAME = "AEMET" DOMAIN = "aemet" ENTRY_NAME = "name" diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index bb464d09723..373b5c2e291 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -3,6 +3,7 @@ from agent import AgentError from agent.a import Agent +from homeassistant.const import Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -12,7 +13,7 @@ from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL ATTRIBUTION = "ispyconnect.com" DEFAULT_BRAND = "Agent DVR by ispyconnect.com" -FORWARDS = ["alarm_control_panel", "camera"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA] async def async_setup_entry(hass, config_entry): @@ -46,14 +47,16 @@ async def async_setup_entry(hass, config_entry): sw_version=agent_client.version, ) - hass.config_entries.async_setup_platforms(config_entry, FORWARDS) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(config_entry, FORWARDS) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close() diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 14a58abc248..83a9a50ec7f 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -13,7 +13,7 @@ import async_timeout from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -33,7 +33,7 @@ from .const import ( NO_AIRLY_SENSORS, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index be385d19645..9c6babe1136 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -8,7 +8,13 @@ from pyairnow.conv import aqi_to_concentration from pyairnow.errors import AirNowError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -33,7 +39,7 @@ from .const import ( ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 601396d36da..352c0249637 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -7,6 +7,7 @@ import logging from airthings import Airthings, AirthingsError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,7 +16,7 @@ from .const import CONF_ID, CONF_SECRET, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[str] = ["sensor"] +PLATFORMS: list[Platform] = [Platform.SENSOR] SCAN_INTERVAL = timedelta(minutes=6) diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index 0ec63161ea3..7b0673ecfe4 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -6,7 +6,7 @@ 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.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,7 +15,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 15e5407138b..876cb270e99 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SHOW_ON_MAP, CONF_STATE, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed @@ -51,7 +52,7 @@ from .const import ( LOGGER, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 69870450869..fd0b76a5c8a 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -35,7 +36,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.SENSOR, + Platform.BINARY_SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index 362dc26d851..4481afb09ca 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -3,16 +3,15 @@ from __future__ import annotations from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN -PLATFORMS = (SENSOR_DOMAIN,) +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index fe2e5f9bb88..f3cda887150 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -1,6 +1,8 @@ """Amber Electric Constants.""" import logging +from homeassistant.const import Platform + DOMAIN = "amberelectric" CONF_API_TOKEN = "api_token" CONF_SITE_NAME = "site_name" @@ -10,4 +12,4 @@ CONF_SITE_NMI = "site_nmi" ATTRIBUTION = "Data provided by Amber Electric" LOGGER = logging.getLogger(__package__) -PLATFORMS = ["sensor", "binary_sensor"] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 385722bf597..6406b5a10df 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_NAME, CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -34,7 +35,7 @@ from .const import ( TYPE_SOLARRADIATION_LX, ) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] DATA_CONFIG = "config" diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 8784e0f13ae..125dd7af6ae 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -8,7 +8,7 @@ from arcam.fmj.client import Client import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def _await_cancel(task): diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 534ce6bd7df..fb6c547cc6b 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -33,7 +34,7 @@ from .const import ( ) from .router import AsusWrtRouter -PLATFORMS = ["device_tracker", "sensor"] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] CONF_PUB_KEY = "pub_key" SECRET_GROUP = "Password or SSH Key" diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 920b910269d..82340032a1a 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -5,10 +5,8 @@ import logging import async_timeout from pyatag import AtagException, AtagOne -from homeassistant.components.climate import DOMAIN as CLIMATE -from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.water_heater import DOMAIN as WATER_HEATER from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -21,7 +19,7 @@ from homeassistant.helpers.update_coordinator import ( _LOGGER = logging.getLogger(__name__) DOMAIN = "atag" -PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.WATER_HEATER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 1a5e2a597cf..d58d475d506 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -12,9 +12,9 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, Platform -from . import CLIMATE, DOMAIN, AtagEntity +from . import DOMAIN, AtagEntity PRESET_MAP = { "Manual": "manual", @@ -31,7 +31,7 @@ HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] async def async_setup_entry(hass, entry, async_add_entities): """Load a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([AtagThermostat(coordinator, CLIMATE)]) + async_add_entities([AtagThermostat(coordinator, Platform.CLIMATE)]) class AtagThermostat(AtagEntity, ClimateEntity): diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 5fce2abf63e..b45e877a310 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -5,9 +5,9 @@ from homeassistant.components.water_heater import ( STATE_PERFORMANCE, WaterHeaterEntity, ) -from homeassistant.const import STATE_OFF, TEMP_CELSIUS +from homeassistant.const import STATE_OFF, TEMP_CELSIUS, Platform -from . import DOMAIN, WATER_HEATER, AtagEntity +from . import DOMAIN, AtagEntity SUPPORT_FLAGS_HEATER = 0 OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] @@ -16,7 +16,7 @@ OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize DHW device from config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([AtagWaterHeater(coordinator, WATER_HEATER)]) + async_add_entities([AtagWaterHeater(coordinator, Platform.WATER_HEATER)]) class AtagWaterHeater(AtagEntity, WaterHeaterEntity): diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 57e0d5a7fb7..dfe9cc0f700 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -2,6 +2,8 @@ from datetime import timedelta +from homeassistant.const import Platform + DEFAULT_TIMEOUT = 10 CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" @@ -43,4 +45,9 @@ ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10) LOGIN_METHODS = ["phone", "email"] -PLATFORMS = ["camera", "binary_sensor", "lock", "sensor"] +PLATFORMS = [ + Platform.CAMERA, + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, +] diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index a88378edaa9..a029f2cf61b 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -7,7 +7,7 @@ from aiohttp import ClientError from auroranoaa import AuroraForecast from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import DeviceEntryType @@ -30,7 +30,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 21724acaff6..08103193e7b 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -13,14 +13,14 @@ import logging from aurorapy.client import AuroraError, AuroraSerialClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .config_flow import validate_and_connect from .const import ATTR_SERIAL_NUMBER, DOMAIN -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index d78931e5d2f..2cfaa88022d 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -8,14 +8,14 @@ from async_timeout import timeout from python_awair import Awair from python_awair.exceptions import AuthError -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass, config_entry) -> bool: diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index d3599aa3021..213dc19ff9e 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -12,6 +12,7 @@ from aioazuredevops.core import DevOpsProject import aiohttp from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.device_registry import DeviceEntryType @@ -26,7 +27,7 @@ from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index 59ca0041bf6..f5b28804952 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, ) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_OFF +from homeassistant.const import Platform _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,7 @@ CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] CONF_SYNC_TIME = "sync_time" DEFAULT_SYNC_TIME = False FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] -PLATFORMS = ["binary_sensor", "climate"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE] AUX = "Aux" CIRC_PUMP = "Circ Pump" diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 681fff4a9bc..b6a0045940d 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -6,7 +6,7 @@ from blebox_uniapi.products import Products from blebox_uniapi.session import ApiHost from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +16,14 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"] +PLATFORMS = [ + Platform.COVER, + Platform.SENSOR, + Platform.SWITCH, + Platform.AIR_QUALITY, + Platform.LIGHT, + Platform.CLIMATE, +] PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index c93adbec46b..8986782031f 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -1,4 +1,6 @@ """Constants for Blink.""" +from homeassistant.const import Platform + DOMAIN = "blink" DEVICE_ID = "Home Assistant" @@ -23,4 +25,9 @@ SERVICE_TRIGGER = "trigger_camera" SERVICE_SAVE_VIDEO = "save_video" SERVICE_SEND_PIN = "send_pin" -PLATFORMS = ("alarm_control_panel", "binary_sensor", "camera", "sensor") +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.SENSOR, +] diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 78a41ec8548..a520214ca6a 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_REGION, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -67,7 +68,13 @@ DEFAULT_OPTIONS = { CONF_USE_LOCATION: False, } -PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.LOCK, + Platform.NOTIFY, + Platform.SENSOR, +] UPDATE_INTERVAL = 5 # in minutes SERVICE_UPDATE_STATE = "update_state" @@ -150,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_update_all() hass.config_entries.async_setup_platforms( - entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) # set up notify platform, no entry support for notify platform yet, @@ -171,7 +178,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( - entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) # Only remove services if it is the last account and not read only diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 501ddfb6888..95fa740f24c 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -8,7 +8,12 @@ from aiohttp import ClientError, ClientResponseError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -18,7 +23,12 @@ from homeassistant.helpers.entity import SLOW_UPDATE_WARNING from .const import BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB from .utils import BondHub -PLATFORMS = ["cover", "fan", "light", "switch"] +PLATFORMS = [ + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SWITCH, +] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index 2909350fc1c..afcf2571c31 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -6,7 +6,7 @@ from boschshcpy.exceptions import SHCAuthenticationError, SHCConnectionError from homeassistant.components.zeroconf import async_get_instance from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -19,7 +19,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = ["binary_sensor", "cover", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 744c856a143..38dbc4f0ebc 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -10,10 +10,8 @@ from typing import Final from bravia_tv import BraviaRC from bravia_tv.braviarc import NoIPControl -from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -22,7 +20,7 @@ from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME _LOGGER = logging.getLogger(__name__) -PLATFORMS: Final[list[str]] = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] SCAN_INTERVAL: Final = timedelta(seconds=10) diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 174c7edde3a..3f7744ecbb4 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,14 +1,11 @@ """Constants.""" -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform DOMAIN = "broadlink" DOMAINS_AND_TYPES = { - REMOTE_DOMAIN: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, - SENSOR_DOMAIN: { + Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, + Platform.SENSOR: { "A1", "RM4MINI", "RM4PRO", @@ -18,7 +15,7 @@ DOMAINS_AND_TYPES = { "SP4", "SP4B", }, - SWITCH_DOMAIN: { + Platform.SWITCH: { "BG1", "MP1", "RM4MINI", @@ -34,7 +31,7 @@ DOMAINS_AND_TYPES = { "SP4", "SP4B", }, - LIGHT_DOMAIN: {"LB1"}, + Platform.LIGHT: {"LB1"}, } DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values()) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 0649b526537..d8fc9632321 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -22,11 +22,12 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TYPE, STATE_ON, + Platform, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN, SWITCH_DOMAIN +from .const import DOMAIN from .entity import BroadlinkEntity from .helpers import data_packet, import_device, mac_address @@ -90,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) if switches: - platform_data = hass.data[DOMAIN].platforms.setdefault(SWITCH_DOMAIN, {}) + platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) platform_data.setdefault(mac_addr, []).extend(switches) else: @@ -110,7 +111,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): switches = [] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: - platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {}) + platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) user_defined_switches = platform_data.get(device.api.mac, {}) switches.extend( BroadlinkRMSwitch(device, config) for config in user_defined_switches diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 45053c74f03..ce715e991b0 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -8,14 +8,14 @@ from brother import Brother, DictToObj, SnmpError, UnsupportedModel import pysnmp.hlapi.asyncio as SnmpEngine from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP from .utils import get_snmp_engine -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/brunt/const.py b/homeassistant/components/brunt/const.py index 4ffaf2875c9..cc85ac9a415 100644 --- a/homeassistant/components/brunt/const.py +++ b/homeassistant/components/brunt/const.py @@ -1,12 +1,14 @@ """Constants for Brunt.""" from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "brunt" ATTR_REQUEST_POSITION = "request_position" NOTIFICATION_ID = "brunt_notification" NOTIFICATION_TITLE = "Brunt Cover Setup" ATTRIBUTION = "Based on an unofficial Brunt SDK." -PLATFORMS = ["cover"] +PLATFORMS = [Platform.COVER] DATA_BAPI = "bapi" DATA_COOR = "coordinator" diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 6c6c8a18336..7ada5b01e46 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -3,9 +3,14 @@ from datetime import timedelta from bsblan import BSBLan, BSBLanConnectionError -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -14,7 +19,7 @@ from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN SCAN_INTERVAL = timedelta(seconds=30) -PLATFORMS = [CLIMATE_DOMAIN] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index d7ec47d2bf8..64b94cfbaa8 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS = ["camera", "sensor", "weather"] +PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index b276fc4ed34..9b020a2f09d 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -49,7 +49,11 @@ CONFIG_SCHEMA: Final = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS: Final[list[str]] = ["alarm_control_panel", "camera", "sensor"] +PLATFORMS: Final[list[Platform]] = [ + Platform.ALARM_CONTROL_PANEL, + Platform.CAMERA, + Platform.SENSOR, +] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 61c7a0758c7..2980d4ca5d4 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -6,7 +6,12 @@ import logging from typing import Optional from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STARTED, + Platform, +) from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index bd22e8f92aa..aceb24b695f 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -15,8 +15,6 @@ from pyclimacell.exceptions import ( UnknownException, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -24,6 +22,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -76,7 +75,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index 26f41ac2e67..a87aed64564 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -8,7 +8,7 @@ from typing import TypedDict, cast import CO2Signal from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 111064924af..238ff1db87d 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -9,7 +9,7 @@ from coinbase.wallet.error import AuthenticationError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv @@ -28,7 +28,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index d593f759eb3..ee2f51303f2 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -41,7 +42,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 1bcf20f4d5e..fc0040bf245 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -4,7 +4,7 @@ import logging from pycoolmasternet_async import CoolMasterNet from homeassistant.components.climate import SCAN_INTERVAL -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -12,7 +12,7 @@ from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 5deceb5cddc..27085c88ef2 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -6,13 +6,14 @@ import async_timeout import coronavirus from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform 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"] +PLATFORMS = [Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/crownstone/const.py b/homeassistant/components/crownstone/const.py index 21a14b99e86..a362435b9ce 100644 --- a/homeassistant/components/crownstone/const.py +++ b/homeassistant/components/crownstone/const.py @@ -3,9 +3,11 @@ from __future__ import annotations from typing import Final +from homeassistant.const import Platform + # Platforms DOMAIN: Final = "crownstone" -PLATFORMS: Final[list[str]] = ["light"] +PLATFORMS: Final[list[Platform]] = [Platform.LIGHT] # Listeners SSE_LISTENERS: Final = "sse_listeners" diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 507ec2f5d79..f588f66761c 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -8,7 +8,7 @@ from async_timeout import timeout from pydaikin.daikin_base import Appliance from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -PLATFORMS = ["climate", "sensor", "switch"] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] CONFIG_SCHEMA = cv.deprecated(DOMAIN) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 09f0cd15141..ad668934acf 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,20 +1,7 @@ """Constants for the deCONZ component.""" import logging -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, -) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform LOGGER = logging.getLogger(__package__) @@ -34,18 +21,18 @@ CONF_ALLOW_NEW_DEVICES = "allow_new_devices" CONF_MASTER_GATEWAY = "master" PLATFORMS = [ - ALARM_CONTROL_PANEL_DOMAIN, - BINARY_SENSOR_DOMAIN, - CLIMATE_DOMAIN, - COVER_DOMAIN, - FAN_DOMAIN, - LIGHT_DOMAIN, - LOCK_DOMAIN, - NUMBER_DOMAIN, - SCENE_DOMAIN, - SENSOR_DOMAIN, - SIREN_DOMAIN, - SWITCH_DOMAIN, + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, ] ATTR_DARK = "dark" diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 75bd69cf2e3..d58bbe963d7 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -4,7 +4,7 @@ import logging from denonavr.exceptions import AvrNetworkError, AvrTimoutError from homeassistant import config_entries, core -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er from homeassistant.helpers.httpx_client import get_async_client @@ -23,7 +23,7 @@ from .receiver import ConnectDenonAVR CONF_RECEIVER = "receiver" UNDO_UPDATE_LISTENER = "undo_update_listener" -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index b15c0acf622..e2ac3a42416 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -1,9 +1,18 @@ """Constants for the devolo_home_control integration.""" import re +from homeassistant.const import Platform + DOMAIN = "devolo_home_control" DEFAULT_MYDEVOLO = "https://www.mydevolo.com" -PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] CONF_MYDEVOLO = "mydevolo_url" GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}") SUPPORTED_MODEL_TYPES = ["2600", "2601"] diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 9276acebe41..bd7170bfde5 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -2,8 +2,10 @@ from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "devolo_home_network" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] PRODUCT = "product" SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/dexcom/const.py b/homeassistant/components/dexcom/const.py index 40b7e32df6c..cb75f3bd500 100644 --- a/homeassistant/components/dexcom/const.py +++ b/homeassistant/components/dexcom/const.py @@ -1,8 +1,8 @@ """Constants for the Dexcom integration.""" +from homeassistant.const import Platform DOMAIN = "dexcom" -PLATFORMS = ["sensor"] - +PLATFORMS = [Platform.SENSOR] GLUCOSE_VALUE_ICON = "mdi:diabetes" GLUCOSE_TREND_ICON = [ diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 2fec28db14a..55961869f79 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -6,7 +6,7 @@ from datetime import timedelta from directv import DIRECTV, DIRECTVError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -16,7 +16,7 @@ from .const import DOMAIN CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["media_player", "remote"] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index 6a53490819f..d34d8550355 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations from homeassistant import config_entries -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import LOGGER -PLATFORMS = [MEDIA_PLAYER_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry( diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 46a95f0d500..46c37e5b050 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -1,8 +1,8 @@ """The DoorBird integration constants.""" - +from homeassistant.const import Platform DOMAIN = "doorbird" -PLATFORMS = ["switch", "camera"] +PLATFORMS = [Platform.SWITCH, Platform.CAMERA] DOOR_STATION = "door_station" DOOR_STATION_INFO = "door_station_info" DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids" diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index baf9f036a35..8f5620bfd7c 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -6,6 +6,7 @@ import logging from dsmr_parser import obis_references from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import Platform from .models import DSMRSensorEntityDescription @@ -13,8 +14,7 @@ DOMAIN = "dsmr" LOGGER = logging.getLogger(__package__) -PLATFORMS = ["sensor"] - +PLATFORMS = [Platform.SENSOR] CONF_DSMR_VERSION = "dsmr_version" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_PRECISION = "precision" diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 24851dac4e8..839f79bc3f4 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -6,12 +6,12 @@ from typing import Final from pdunehd import DuneHDPlayer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: Final[list[str]] = ["media_player"] +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index eda43305461..83cc639d1da 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -1,12 +1,12 @@ """Constants for the Dynalite component.""" import logging -from homeassistant.const import CONF_ROOM +from homeassistant.const import CONF_ROOM, Platform LOGGER = logging.getLogger(__package__) DOMAIN = "dynalite" -PLATFORMS = ["light", "switch", "cover"] +PLATFORMS = [Platform.LIGHT, Platform.SWITCH, Platform.COVER] CONF_ACTIVE = "active" diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 3c9a9c3f820..ba6bc892e40 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -1,7 +1,7 @@ """Tests for the Atag climate platform.""" from unittest.mock import PropertyMock, patch -from homeassistant.components.atag.climate import CLIMATE, DOMAIN, PRESET_MAP +from homeassistant.components.atag.climate import DOMAIN, PRESET_MAP from homeassistant.components.climate import ( ATTR_HVAC_ACTION, ATTR_HVAC_MODE, @@ -13,7 +13,12 @@ from homeassistant.components.climate import ( ) from homeassistant.components.climate.const import CURRENT_HVAC_IDLE, PRESET_AWAY from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -21,7 +26,7 @@ from homeassistant.setup import async_setup_component from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker -CLIMATE_ID = f"{CLIMATE}.{DOMAIN}" +CLIMATE_ID = f"{Platform.CLIMATE}.{DOMAIN}" async def test_climate( @@ -33,7 +38,7 @@ async def test_climate( assert entity_registry.async_is_registered(CLIMATE_ID) entity = entity_registry.async_get(CLIMATE_ID) - assert entity.unique_id == f"{UID}-{CLIMATE}" + assert entity.unique_id == f"{UID}-{Platform.CLIMATE}" assert hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE @@ -44,7 +49,7 @@ async def test_setting_climate( await init_integration(hass, aioclient_mock) with patch("pyatag.entities.Climate.set_temp") as mock_set_temp: await hass.services.async_call( - CLIMATE, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_TEMPERATURE: 15}, blocking=True, @@ -54,7 +59,7 @@ async def test_setting_climate( with patch("pyatag.entities.Climate.set_preset_mode") as mock_set_preset: await hass.services.async_call( - CLIMATE, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -64,7 +69,7 @@ async def test_setting_climate( with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac: await hass.services.async_call( - CLIMATE, + Platform.CLIMATE, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, blocking=True, diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index 4c78302224d..df83fa6d40b 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -1,16 +1,16 @@ """Tests for the Atag water heater platform.""" from unittest.mock import patch -from homeassistant.components.atag import DOMAIN, WATER_HEATER +from homeassistant.components.atag import DOMAIN from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker -WATER_HEATER_ID = f"{WATER_HEATER}.{DOMAIN}" +WATER_HEATER_ID = f"{Platform.WATER_HEATER}.{DOMAIN}" async def test_water_heater( @@ -23,7 +23,7 @@ async def test_water_heater( assert registry.async_is_registered(WATER_HEATER_ID) entry = registry.async_get(WATER_HEATER_ID) - assert entry.unique_id == f"{UID}-{WATER_HEATER}" + assert entry.unique_id == f"{UID}-{Platform.WATER_HEATER}" async def test_setting_target_temperature( @@ -33,7 +33,7 @@ async def test_setting_target_temperature( await init_integration(hass, aioclient_mock) with patch("pyatag.entities.DHW.set_temp") as mock_set_temp: await hass.services.async_call( - WATER_HEATER, + Platform.WATER_HEATER, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: WATER_HEATER_ID, ATTR_TEMPERATURE: 50}, blocking=True, diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index abc500479ea..1ee48063613 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -2,13 +2,13 @@ from base64 import b64decode from unittest.mock import call -from homeassistant.components.broadlink.const import DOMAIN, REMOTE_DOMAIN +from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.remote import ( SERVICE_SEND_COMMAND, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device @@ -34,7 +34,7 @@ async def test_remote_setup_works(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} assert len(remotes) == 1 remote = remotes.pop() @@ -54,12 +54,12 @@ async def test_remote_send_command(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} assert len(remotes) == 1 remote = remotes.pop() await hass.services.async_call( - REMOTE_DOMAIN, + Platform.REMOTE, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, @@ -81,12 +81,12 @@ async def test_remote_turn_off_turn_on(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} assert len(remotes) == 1 remote = remotes.pop() await hass.services.async_call( - REMOTE_DOMAIN, + Platform.REMOTE, SERVICE_TURN_OFF, {"entity_id": remote.entity_id}, blocking=True, @@ -94,7 +94,7 @@ async def test_remote_turn_off_turn_on(hass): assert hass.states.get(remote.entity_id).state == STATE_OFF await hass.services.async_call( - REMOTE_DOMAIN, + Platform.REMOTE, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, @@ -102,7 +102,7 @@ async def test_remote_turn_off_turn_on(hass): assert mock_setup.api.send_data.call_count == 0 await hass.services.async_call( - REMOTE_DOMAIN, + Platform.REMOTE, SERVICE_TURN_ON, {"entity_id": remote.entity_id}, blocking=True, @@ -110,7 +110,7 @@ async def test_remote_turn_off_turn_on(hass): assert hass.states.get(remote.entity_id).state == STATE_ON await hass.services.async_call( - REMOTE_DOMAIN, + Platform.REMOTE, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index b7fccd2e2ff..28caa212278 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -1,8 +1,9 @@ """Tests for Broadlink sensors.""" from datetime import timedelta -from homeassistant.components.broadlink.const import DOMAIN, SENSOR_DOMAIN +from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.broadlink.updater import BroadlinkSP4UpdateManager +from homeassistant.const import Platform from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.util import dt @@ -33,7 +34,7 @@ async def test_a1_sensor_setup(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 5 sensors_and_states = { @@ -70,7 +71,7 @@ async def test_a1_sensor_update(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 5 mock_setup.api.check_sensors_raw.return_value = { @@ -114,7 +115,7 @@ async def test_rm_pro_sensor_setup(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 1 sensors_and_states = { @@ -139,7 +140,7 @@ async def test_rm_pro_sensor_update(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": 25.8} @@ -173,7 +174,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": -7} @@ -205,7 +206,7 @@ async def test_rm_mini3_no_sensor(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 0 @@ -225,7 +226,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 2 sensors_and_states = { @@ -253,7 +254,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 2 mock_setup.api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} @@ -288,7 +289,7 @@ async def test_rm4_pro_no_sensor(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = {entry for entry in entries if entry.domain == Platform.SENSOR} assert len(sensors) == 0 @@ -318,7 +319,7 @@ async def test_scb1e_sensor_setup(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 5 sensors_and_states = { @@ -363,7 +364,7 @@ async def test_scb1e_sensor_update(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] assert len(sensors) == 5 mock_setup.api.get_state.return_value = { From 17dc609363d6d1ac87f8cc270365ccd28e35c9d8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 3 Dec 2021 18:08:28 +0100 Subject: [PATCH 1278/1452] Correct validation of conditions in scripts and automations (#60890) * Correct validation of conditions in scripts and automations * Fix test --- homeassistant/helpers/condition.py | 9 ++ homeassistant/helpers/script.py | 14 ++ tests/helpers/test_script.py | 220 ++++++++++++++++++++++++++++- 3 files changed, 237 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 57e8f8e5ba2..030e5dacfd5 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -967,6 +967,15 @@ async def async_validate_condition_config( return config +async def async_validate_conditions_config( + hass: HomeAssistant, conditions: list[ConfigType | Template] +) -> list[ConfigType | Template]: + """Validate config.""" + return await asyncio.gather( + *(async_validate_condition_config(hass, cond) for cond in conditions) + ) + + @callback def async_extract_entities(config: ConfigType | Template) -> set[str]: """Extract entities from a condition.""" diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 933b44d9ec9..d4d37e1b4ac 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -270,6 +270,16 @@ async def async_validate_action_config( ) elif action_type == cv.SCRIPT_ACTION_REPEAT: + if CONF_UNTIL in config[CONF_REPEAT]: + conditions = await condition.async_validate_conditions_config( + hass, config[CONF_REPEAT][CONF_UNTIL] + ) + config[CONF_REPEAT][CONF_UNTIL] = conditions + if CONF_WHILE in config[CONF_REPEAT]: + conditions = await condition.async_validate_conditions_config( + hass, config[CONF_REPEAT][CONF_WHILE] + ) + config[CONF_REPEAT][CONF_WHILE] = conditions config[CONF_REPEAT][CONF_SEQUENCE] = await async_validate_actions_config( hass, config[CONF_REPEAT][CONF_SEQUENCE] ) @@ -281,6 +291,10 @@ async def async_validate_action_config( ) for choose_conf in config[CONF_CHOOSE]: + conditions = await condition.async_validate_conditions_config( + hass, choose_conf[CONF_CONDITIONS] + ) + choose_conf[CONF_CONDITIONS] = conditions choose_conf[CONF_SEQUENCE] = await async_validate_actions_config( hass, choose_conf[CONF_SEQUENCE] ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 962fe4b1366..b0a93c85b2b 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -56,8 +56,11 @@ def compare_trigger_item(actual_trigger, expected_trigger): assert actual_trigger["description"] == expected_trigger["description"] -def compare_result_item(key, actual, expected): - """Compare an item in the result dict.""" +def compare_result_item(key, actual, expected, path): + """Compare an item in the result dict. + + Note: Unused variable 'path' is passed to get helpful errors from pytest. + """ if key == "wait" and (expected.get("trigger") is not None): assert "trigger" in actual expected_trigger = expected.pop("trigger") @@ -78,7 +81,7 @@ def assert_element(trace_element, expected_element, path): # The redundant set operation gives helpful errors from pytest assert not set(expected_result) - set(trace_element._result or {}) for result_key, result in expected_result.items(): - compare_result_item(result_key, trace_element._result[result_key], result) + compare_result_item(result_key, trace_element._result[result_key], result, path) assert trace_element._result[result_key] == result # Check for unexpected items in trace_element @@ -1819,6 +1822,126 @@ async def test_repeat_conditional(hass, condition, direct_template): assert event.data.get("index") == index + 1 +async def test_repeat_until_condition_validation(hass, caplog): + """Test if we can use conditions in repeat until conditions which validate late.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + { + "repeat": { + "sequence": [ + {"event": event}, + ], + "until": [ + { + "condition": "state", + "entity_id": entry.id, + "state": "hello", + } + ], + } + }, + ] + ) + hass.states.async_set("test.entity", "hello") + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + caplog.clear() + assert len(events) == 1 + + assert_action_trace( + { + "0": [{"result": {}}], + "0/repeat/sequence/0": [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": {"repeat": {"first": True, "index": 1}}, + } + ], + "0/repeat": [ + { + "result": {"result": True}, + "variables": {"repeat": {"first": True, "index": 1}}, + } + ], + "0/repeat/until/0": [{"result": {"result": True}}], + "0/repeat/until/0/entity_id/0": [ + {"result": {"result": True, "state": "hello", "wanted_state": "hello"}} + ], + } + ) + + +async def test_repeat_while_condition_validation(hass, caplog): + """Test if we can use conditions in repeat while conditions which validate late.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + { + "repeat": { + "sequence": [ + {"event": event}, + ], + "while": [ + { + "condition": "state", + "entity_id": entry.id, + "state": "hello", + } + ], + } + }, + ] + ) + hass.states.async_set("test.entity", "goodbye") + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + caplog.clear() + assert len(events) == 0 + + assert_action_trace( + { + "0": [{"result": {}}], + "0/repeat": [ + { + "result": {"result": False}, + "variables": {"repeat": {"first": True, "index": 1}}, + } + ], + "0/repeat/while/0": [{"result": {"result": False}}], + "0/repeat/while/0/entity_id/0": [ + { + "result": { + "result": False, + "state": "goodbye", + "wanted_state": "hello", + } + } + ], + } + ) + + @pytest.mark.parametrize("condition", ["while", "until"]) async def test_repeat_var_in_condition(hass, condition): """Test repeat action w/ while option.""" @@ -2182,6 +2305,88 @@ async def test_choose(hass, caplog, var, result): assert_action_trace(expected_trace) +async def test_choose_condition_validation(hass, caplog): + """Test if we can use conditions in choose actions which validate late.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "choose": [ + { + "alias": "choice one", + "conditions": { + "condition": "state", + "entity_id": entry.id, + "state": "hello", + }, + "sequence": { + "alias": "sequence one", + "event": event, + "event_data": {"choice": "first"}, + }, + }, + ] + }, + ] + ) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"choice": 0}}], + "1/choose/0": [{"result": {"result": True}}], + "1/choose/0/conditions/0": [{"result": {"result": True}}], + "1/choose/0/conditions/0/entity_id/0": [ + {"result": {"result": True, "state": "hello", "wanted_state": "hello"}} + ], + "1/choose/0/sequence/0": [ + {"result": {"event": "test_event", "event_data": {"choice": "first"}}} + ], + } + ) + + hass.states.async_set("test.entity", "goodbye") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert len(events) == 3 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {}}], + "1/choose/0": [{"result": {"result": False}}], + "1/choose/0/conditions/0": [{"result": {"result": False}}], + "1/choose/0/conditions/0/entity_id/0": [ + { + "result": { + "result": False, + "state": "goodbye", + "wanted_state": "hello", + } + } + ], + }, + ) + + @pytest.mark.parametrize( "action", [ @@ -3132,7 +3337,9 @@ async def test_validate_action_config(hass): }, cv.SCRIPT_ACTION_FIRE_EVENT: {"event": "my_event"}, cv.SCRIPT_ACTION_CHECK_CONDITION: { - "condition": "{{ states.light.kitchen.state == 'on' }}" + "condition": "state", + "entity_id": "light.kitchen", + "state": "on", }, cv.SCRIPT_ACTION_DEVICE_AUTOMATION: templated_device_action("device"), cv.SCRIPT_ACTION_ACTIVATE_SCENE: {"scene": "scene.relax"}, @@ -3145,7 +3352,7 @@ async def test_validate_action_config(hass): cv.SCRIPT_ACTION_CHOOSE: { "choose": [ { - "condition": "{{ states.light.kitchen.state == 'on' }}", + "conditions": "{{ states.light.kitchen.state == 'on' }}", "sequence": [templated_device_action("choose_event")], } ], @@ -3182,8 +3389,9 @@ async def test_validate_action_config(hass): for action_type, config in configs.items(): assert cv.determine_script_action(config) == action_type try: + validated_config[action_type] = cv.ACTION_TYPE_SCHEMAS[action_type](config) validated_config[action_type] = await script.async_validate_action_config( - hass, config + hass, validated_config[action_type] ) except vol.Invalid as err: assert False, f"{action_type} config invalid: {err}" From 0a2ca1f7d529d11660ff2b2a9c66d7cef1b3318e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 18:16:18 +0100 Subject: [PATCH 1279/1452] Temporarily disable CI concurrency (#60926) --- .github/workflows/ci.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebbd2b2fb9e..c9b7e797177 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,9 +15,10 @@ env: PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +# Temporary disabled: https://github.com/actions/runner/issues/1532 +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true jobs: changes: From adf2fa5664d41a09c3f904e9df1343e08f8893b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:20:00 +0100 Subject: [PATCH 1280/1452] Use EntityCategory enum in Onewire (#60907) * Use EntityCategory enum in Onewire * Add checks for the entity_category * Fix typo Co-authored-by: epenet --- homeassistant/components/onewire/switch.py | 5 +++-- tests/components/onewire/__init__.py | 4 ++++ tests/components/onewire/const.py | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 3146f4fb8a6..49f1ece51ca 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Any from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -62,7 +63,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { OneWireSwitchEntityDescription( key="IAD", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, name="IAD", read_mode=READ_MODE_BOOL, ), diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 36035c1c85b..3ae6dbab050 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from .const import ( ATTR_DEFAULT_DISABLED, ATTR_DEVICE_FILE, + ATTR_ENTITY_CATEGORY, ATTR_INJECT_READS, ATTR_UNIQUE_ID, FIXED_ATTRIBUTES, @@ -77,6 +78,9 @@ def check_entities( entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None + assert registry_entry.entity_category == expected_entity.get( + ATTR_ENTITY_CATEGORY + ) assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) assert state.state == expected_entity[ATTR_STATE] diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index ebf52f2a25f..c38f5e53847 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -36,10 +36,12 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import EntityCategory ATTR_DEFAULT_DISABLED = "default_disabled" ATTR_DEVICE_FILE = "device_file" ATTR_DEVICE_INFO = "device_info" +ATTR_ENTITY_CATEGORY = "entity_category" ATTR_INJECT_READS = "inject_reads" ATTR_UNIQUE_ID = "unique_id" ATTR_UNKNOWN_DEVICE = "unknown_device" @@ -404,6 +406,7 @@ MOCK_OWPROXY_DEVICES = { SWITCH_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, ATTR_ENTITY_ID: "switch.26_111111111111_iad", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, From a6cd3e2a02ca3df19edd7b935da5f393c59a3b65 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:20:58 +0100 Subject: [PATCH 1281/1452] Use dataclass properties in yeelight (#60912) Co-authored-by: epenet --- homeassistant/components/yeelight/scanner.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 4b372df3744..648168ff84a 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -174,17 +174,18 @@ class YeelightScanner: # of another discovery async_call_later(self._hass, 1, _async_start_flow) - async def _async_process_entry(self, response): + async def _async_process_entry(self, response: ssdp.SsdpServiceInfo): """Process a discovery.""" _LOGGER.debug("Discovered via SSDP: %s", response) - unique_id = response["id"] - host = urlparse(response["location"]).hostname + headers = response.ssdp_headers + unique_id = headers["id"] + host = urlparse(headers["location"]).hostname current_entry = self._unique_id_capabilities.get(unique_id) # Make sure we handle ip changes if not current_entry or host != urlparse(current_entry["location"]).hostname: - _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 + _LOGGER.debug("Yeelight discovered with %s", headers) + self._async_discovered_by_ssdp(headers) + self._host_capabilities[host] = headers + self._unique_id_capabilities[unique_id] = headers for event in self._host_discovered_events.get(host, []): event.set() From 231d434b760097ac142da3968892ce034e5d0f38 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:25:22 +0100 Subject: [PATCH 1282/1452] Use new Platform enum in Onewire (#60904) * Use new Platform enum in Onewire * Use Platform in tests Co-authored-by: epenet --- homeassistant/components/onewire/const.py | 10 ++-- tests/components/onewire/const.py | 60 +++++++++---------- .../components/onewire/test_binary_sensor.py | 10 ++-- tests/components/onewire/test_sensor.py | 16 ++--- tests/components/onewire/test_switch.py | 12 ++-- 5 files changed, 52 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index f9f40e49ced..91f931e2517 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -1,9 +1,7 @@ """Constants for 1-Wire component.""" from __future__ import annotations -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform CONF_MOUNT_DIR = "mount_dir" CONF_NAMES = "names" @@ -48,7 +46,7 @@ READ_MODE_FLOAT = "float" READ_MODE_INT = "int" PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, ] diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index c38f5e53847..2153e153961 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -2,20 +2,18 @@ from pi1wire import InvalidCRCException, UnsupportResponseException from pyownet.protocol import Error as ProtocolError -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.const import ( DOMAIN, MANUFACTURER_EDS, MANUFACTURER_HOBBYBOARDS, MANUFACTURER_MAXIM, + Platform, ) from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -70,7 +68,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2405", ATTR_NAME: "05.111111111111", }, - SWITCH_DOMAIN: [ + Platform.SWITCH: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "switch.05_111111111111_pio", @@ -90,7 +88,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS18S20", ATTR_NAME: "10.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", @@ -112,7 +110,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2406", ATTR_NAME: "12.111111111111", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "binary_sensor.12_111111111111_sensed_a", @@ -128,7 +126,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIQUE_ID: "/12.111111111111/sensed.B", }, ], - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, @@ -150,7 +148,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, ], - SWITCH_DOMAIN: [ + Platform.SWITCH: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "switch.12_111111111111_pio_a", @@ -191,7 +189,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2423", ATTR_NAME: "1D.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", ATTR_INJECT_READS: b" 251123", @@ -236,7 +234,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: [ b"DS2423", # read device type ], - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.A", ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", @@ -270,7 +268,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS1822", ATTR_NAME: "22.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", @@ -292,7 +290,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2438", ATTR_NAME: "26.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.26_111111111111_temperature", @@ -403,7 +401,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, }, ], - SWITCH_DOMAIN: [ + Platform.SWITCH: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, @@ -424,7 +422,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS18B20", ATTR_NAME: "28.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", @@ -446,7 +444,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2408", ATTR_NAME: "29.111111111111", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_0", @@ -504,7 +502,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIQUE_ID: "/29.111111111111/sensed.7", }, ], - SWITCH_DOMAIN: [ + Platform.SWITCH: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "switch.29_111111111111_pio_0", @@ -629,7 +627,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS2413", ATTR_NAME: "3A.111111111111", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "binary_sensor.3a_111111111111_sensed_a", @@ -645,7 +643,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIQUE_ID: "/3A.111111111111/sensed.B", }, ], - SWITCH_DOMAIN: [ + Platform.SWITCH: [ { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "switch.3a_111111111111_pio_a", @@ -672,7 +670,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS1825", ATTR_NAME: "3B.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", @@ -694,7 +692,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "DS28EA00", ATTR_NAME: "42.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", @@ -716,7 +714,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "HobbyBoards_EF", ATTR_NAME: "EF.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity", @@ -760,7 +758,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "HB_MOISTURE_METER", ATTR_NAME: "EF.111111111112", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_0", @@ -810,7 +808,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "EDS0068", ATTR_NAME: "7E.111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.7e_111111111111_temperature", @@ -860,7 +858,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MODEL: "EDS0066", ATTR_NAME: "7E.222222222222", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.7e_222222222222_temperature", @@ -894,7 +892,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "10", ATTR_NAME: "10-111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", @@ -913,7 +911,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "22", ATTR_NAME: "22-111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", @@ -932,7 +930,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "28", ATTR_NAME: "28-111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", @@ -951,7 +949,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "3B", ATTR_NAME: "3B-111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", @@ -970,7 +968,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "42", ATTR_NAME: "42-111111111111", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", @@ -989,7 +987,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "42", ATTR_NAME: "42-111111111112", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111112_temperature", @@ -1008,7 +1006,7 @@ MOCK_SYSBUS_DEVICES = { ATTR_MODEL: "42", ATTR_NAME: "42-111111111113", }, - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.42_111111111113_temperature", diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 32a2c018028..ee55a550cea 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ensure_list @@ -23,7 +23,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + with patch("homeassistant.components.onewire.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -42,10 +42,10 @@ async def test_owserver_binary_sensor( entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] - expected_entities = mock_device.get(BINARY_SENSOR_DOMAIN, []) + expected_entities = mock_device.get(Platform.BINARY_SENSOR, []) expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) - setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.BINARY_SENSOR, [device_id]) with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -58,7 +58,7 @@ async def test_owserver_binary_sensor( assert len(entity_registry.entities) == len(expected_entities) check_and_enable_disabled_entities(entity_registry, expected_entities) - setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.BINARY_SENSOR, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 81143943057..6bfc68d85c8 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ensure_list @@ -29,7 +29,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]): yield @@ -48,14 +48,14 @@ async def test_owserver_sensor( entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] - expected_entities = mock_device.get(SENSOR_DOMAIN, []) + expected_entities = mock_device.get(Platform.SENSOR, []) if "branches" in mock_device: for branch_details in mock_device["branches"].values(): for sub_device in branch_details.values(): - expected_entities += sub_device[SENSOR_DOMAIN] + expected_entities += sub_device[Platform.SENSOR] expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) - setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -68,7 +68,7 @@ async def test_owserver_sensor( assert len(entity_registry.entities) == len(expected_entities) check_and_enable_disabled_entities(entity_registry, expected_entities) - setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() @@ -88,11 +88,11 @@ async def test_onewiredirect_setup_valid_device( entity_registry = mock_registry(hass) glob_result, read_side_effect = setup_sysbus_mock_devices( - SENSOR_DOMAIN, [device_id] + Platform.SENSOR, [device_id] ) mock_device = MOCK_SYSBUS_DEVICES[device_id] - expected_entities = mock_device.get(SENSOR_DOMAIN, []) + expected_entities = mock_device.get(Platform.SENSOR, []) expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) with patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index f170d8a85d6..336dafb15a1 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,6 +11,7 @@ from homeassistant.const import ( SERVICE_TOGGLE, STATE_OFF, STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ensure_list @@ -30,7 +30,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [SWITCH_DOMAIN]): + with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield @@ -49,10 +49,10 @@ async def test_owserver_switch( entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] - expected_entities = mock_device.get(SWITCH_DOMAIN, []) + expected_entities = mock_device.get(Platform.SWITCH, []) expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) - setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -65,7 +65,7 @@ async def test_owserver_switch( assert len(entity_registry.entities) == len(expected_entities) check_and_enable_disabled_entities(entity_registry, expected_entities) - setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) + setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() @@ -83,7 +83,7 @@ async def test_owserver_switch( expected_entity[ATTR_STATE] = STATE_ON await hass.services.async_call( - SWITCH_DOMAIN, + Platform.SWITCH, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True, From 75ec937359fa82e48b5400e5db6ad2bc58957627 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:28:04 +0100 Subject: [PATCH 1283/1452] Use new Platform enum in Renault (#60903) * Use Platform enum in Renault * Use Platform enum in Renault tests Co-authored-by: epenet --- homeassistant/components/renault/const.py | 16 +++--- tests/components/renault/const.py | 50 ++++++++----------- .../components/renault/test_binary_sensor.py | 11 ++-- tests/components/renault/test_button.py | 19 ++++--- .../components/renault/test_device_tracker.py | 11 ++-- tests/components/renault/test_select.py | 13 +++-- tests/components/renault/test_sensor.py | 11 ++-- 7 files changed, 58 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 2a0ea3a0d49..89bf322c2bf 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -1,9 +1,5 @@ """Constants for the Renault component.""" -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import Platform DOMAIN = "renault" @@ -13,11 +9,11 @@ CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" DEFAULT_SCAN_INTERVAL = 300 # 5 minutes PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - BUTTON_DOMAIN, - DEVICE_TRACKER_DOMAIN, - SELECT_DOMAIN, - SENSOR_DOMAIN, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.SELECT, + Platform.SENSOR, ] DEVICE_CLASS_PLUG_STATE = "renault__plug_state" diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index a3e7b521b23..e1d7a3fc28c 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,10 +1,5 @@ """Constants for the Renault integration tests.""" -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, -) -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -13,11 +8,9 @@ from homeassistant.components.renault.const import ( DEVICE_CLASS_PLUG_STATE, DOMAIN, ) -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) @@ -46,6 +39,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_MINUTES, VOLUME_LITERS, + Platform, ) ATTR_DEFAULT_DISABLED = "default_disabled" @@ -97,7 +91,7 @@ MOCK_VEHICLES = { "cockpit": "cockpit_ev.json", "hvac_status": "hvac_status.json", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", @@ -111,7 +105,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], - BUTTON_DOMAIN: [ + Platform.BUTTON: [ { ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", ATTR_ICON: "mdi:air-conditioner", @@ -125,8 +119,8 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_charge", }, ], - DEVICE_TRACKER_DOMAIN: [], - SELECT_DOMAIN: [ + Platform.DEVICE_TRACKER: [], + Platform.SELECT: [ { ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, ATTR_ENTITY_ID: "select.reg_number_charge_mode", @@ -136,7 +130,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, ], - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", @@ -245,7 +239,7 @@ MOCK_VEHICLES = { "cockpit": "cockpit_ev.json", "location": "location.json", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", @@ -259,7 +253,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], - BUTTON_DOMAIN: [ + Platform.BUTTON: [ { ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", ATTR_ICON: "mdi:air-conditioner", @@ -273,7 +267,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_start_charge", }, ], - DEVICE_TRACKER_DOMAIN: [ + Platform.DEVICE_TRACKER: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", @@ -281,7 +275,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_location", } ], - SELECT_DOMAIN: [ + Platform.SELECT: [ { ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, ATTR_ENTITY_ID: "select.reg_number_charge_mode", @@ -291,7 +285,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, ], - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", @@ -399,7 +393,7 @@ MOCK_VEHICLES = { "cockpit": "cockpit_fuel.json", "location": "location.json", }, - BINARY_SENSOR_DOMAIN: [ + Platform.BINARY_SENSOR: [ { ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PLUG, ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", @@ -413,7 +407,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging", }, ], - BUTTON_DOMAIN: [ + Platform.BUTTON: [ { ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", ATTR_ICON: "mdi:air-conditioner", @@ -427,7 +421,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_start_charge", }, ], - DEVICE_TRACKER_DOMAIN: [ + Platform.DEVICE_TRACKER: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", @@ -435,7 +429,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_location", } ], - SELECT_DOMAIN: [ + Platform.SELECT: [ { ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, ATTR_ENTITY_ID: "select.reg_number_charge_mode", @@ -445,7 +439,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_mode", }, ], - SENSOR_DOMAIN: [ + Platform.SENSOR: [ { ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", @@ -567,8 +561,8 @@ MOCK_VEHICLES = { "cockpit": "cockpit_fuel.json", "location": "location.json", }, - BINARY_SENSOR_DOMAIN: [], - BUTTON_DOMAIN: [ + Platform.BINARY_SENSOR: [], + Platform.BUTTON: [ { ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", ATTR_ICON: "mdi:air-conditioner", @@ -576,7 +570,7 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_start_air_conditioner", }, ], - DEVICE_TRACKER_DOMAIN: [ + Platform.DEVICE_TRACKER: [ { ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", @@ -584,8 +578,8 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_location", } ], - SELECT_DOMAIN: [], - SENSOR_DOMAIN: [ + Platform.SELECT: [], + Platform.SENSOR: [ { ATTR_ENTITY_ID: "sensor.reg_number_fuel_autonomy", ATTR_ICON: "mdi:gas-station", diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index 440018c01c2..0a2460edca1 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -3,9 +3,8 @@ from unittest.mock import patch import pytest -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from . import ( @@ -24,7 +23,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + with patch("homeassistant.components.renault.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -42,7 +41,7 @@ async def test_binary_sensors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.BINARY_SENSOR] assert len(entity_registry.entities) == len(expected_entities) check_entities(hass, entity_registry, expected_entities) @@ -62,7 +61,7 @@ async def test_binary_sensor_empty( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.BINARY_SENSOR] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_OFF) @@ -81,7 +80,7 @@ async def test_binary_sensor_errors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.BINARY_SENSOR] assert len(entity_registry.entities) == len(expected_entities) check_entities_unavailable(hass, entity_registry, expected_entities) diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index 729eb89d74c..cf6fc1902e9 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -4,10 +4,9 @@ from unittest.mock import patch import pytest from renault_api.kamereon import schemas -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from . import check_device_registry, check_entities_no_data @@ -21,7 +20,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.renault.PLATFORMS", [BUTTON_DOMAIN]): + with patch("homeassistant.components.renault.PLATFORMS", [Platform.BUTTON]): yield @@ -40,7 +39,7 @@ async def test_buttons( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BUTTON_DOMAIN] + expected_entities = mock_vehicle[Platform.BUTTON] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -61,7 +60,7 @@ async def test_button_empty( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BUTTON_DOMAIN] + expected_entities = mock_vehicle[Platform.BUTTON] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -81,7 +80,7 @@ async def test_button_errors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BUTTON_DOMAIN] + expected_entities = mock_vehicle[Platform.BUTTON] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -103,7 +102,7 @@ async def test_button_access_denied( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BUTTON_DOMAIN] + expected_entities = mock_vehicle[Platform.BUTTON] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -125,7 +124,7 @@ async def test_button_not_supported( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[BUTTON_DOMAIN] + expected_entities = mock_vehicle[Platform.BUTTON] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -151,7 +150,7 @@ async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntr ), ) as mock_action: await hass.services.async_call( - BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True + Platform.BUTTON, SERVICE_PRESS, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () @@ -179,7 +178,7 @@ async def test_button_start_air_conditioner( ), ) as mock_action: await hass.services.async_call( - BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True + Platform.BUTTON, SERVICE_PRESS, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (21, None) diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index a2c8b165b32..6d1ace17754 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -3,9 +3,8 @@ from unittest.mock import patch import pytest -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from . import ( @@ -24,7 +23,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + with patch("homeassistant.components.renault.PLATFORMS", [Platform.DEVICE_TRACKER]): yield @@ -43,7 +42,7 @@ async def test_device_trackers( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + expected_entities = mock_vehicle[Platform.DEVICE_TRACKER] assert len(entity_registry.entities) == len(expected_entities) check_entities(hass, entity_registry, expected_entities) @@ -64,7 +63,7 @@ async def test_device_tracker_empty( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + expected_entities = mock_vehicle[Platform.DEVICE_TRACKER] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -84,7 +83,7 @@ async def test_device_tracker_errors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + expected_entities = mock_vehicle[Platform.DEVICE_TRACKER] assert len(entity_registry.entities) == len(expected_entities) check_entities_unavailable(hass, entity_registry, expected_entities) diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 9d2655bfe1c..e0cb4413a7e 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -4,10 +4,9 @@ from unittest.mock import patch import pytest from renault_api.kamereon import schemas -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTION, SERVICE_SELECT_OPTION from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from . import ( @@ -26,7 +25,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): + with patch("homeassistant.components.renault.PLATFORMS", [Platform.SELECT]): yield @@ -44,7 +43,7 @@ async def test_selects( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SELECT_DOMAIN] + expected_entities = mock_vehicle[Platform.SELECT] assert len(entity_registry.entities) == len(expected_entities) check_entities(hass, entity_registry, expected_entities) @@ -64,7 +63,7 @@ async def test_select_empty( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SELECT_DOMAIN] + expected_entities = mock_vehicle[Platform.SELECT] assert len(entity_registry.entities) == len(expected_entities) check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @@ -83,7 +82,7 @@ async def test_select_errors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SELECT_DOMAIN] + expected_entities = mock_vehicle[Platform.SELECT] assert len(entity_registry.entities) == len(expected_entities) check_entities_unavailable(hass, entity_registry, expected_entities) @@ -146,7 +145,7 @@ async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry ), ) as mock_action: await hass.services.async_call( - SELECT_DOMAIN, SERVICE_SELECT_OPTION, service_data=data, blocking=True + Platform.SELECT, SERVICE_SELECT_OPTION, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == ("always",) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index c9a70c8e026..2e584326e89 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -4,9 +4,8 @@ from unittest.mock import patch import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -26,7 +25,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) def override_platforms(): """Override PLATFORMS.""" - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + with patch("homeassistant.components.renault.PLATFORMS", [Platform.SENSOR]): yield @@ -57,7 +56,7 @@ async def test_sensors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.SENSOR] assert len(entity_registry.entities) == len(expected_entities) _check_and_enable_disabled_entities(entity_registry, expected_entities) @@ -81,7 +80,7 @@ async def test_sensor_empty( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.SENSOR] assert len(entity_registry.entities) == len(expected_entities) _check_and_enable_disabled_entities(entity_registry, expected_entities) @@ -105,7 +104,7 @@ async def test_sensor_errors( mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) - expected_entities = mock_vehicle[SENSOR_DOMAIN] + expected_entities = mock_vehicle[Platform.SENSOR] assert len(entity_registry.entities) == len(expected_entities) _check_and_enable_disabled_entities(entity_registry, expected_entities) From 77cd7515437cb08734c6bd323eaebb5517501350 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 3 Dec 2021 18:29:15 +0100 Subject: [PATCH 1284/1452] DHCP discovery for Fronius integration (#60806) Co-authored-by: J. Nick Koston --- .../components/fronius/config_flow.py | 71 ++++++++++++++++--- .../components/fronius/manifest.json | 5 ++ homeassistant/components/fronius/strings.json | 7 +- .../components/fronius/translations/en.json | 7 +- homeassistant/generated/dhcp.py | 4 ++ tests/components/fronius/test_config_flow.py | 65 +++++++++++++++++ 6 files changed, 147 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index fdcd5301830..86654f00c36 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Fronius integration.""" from __future__ import annotations +import asyncio import logging -from typing import Any +from typing import Any, Final from pyfronius import Fronius, FroniusError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.const import CONF_HOST, CONF_RESOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -18,6 +20,8 @@ from .const import DOMAIN, FroniusConfigEntryData _LOGGER = logging.getLogger(__name__) +DHCP_REQUEST_DELAY: Final = 60 + STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, @@ -25,14 +29,21 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input( - hass: HomeAssistant, data: dict[str, Any] +def create_title(info: FroniusConfigEntryData) -> str: + """Return the title of the config flow.""" + return ( + f"SolarNet {'Datalogger' if info['is_logger'] else 'Inverter'}" + f" at {info['host']}" + ) + + +async def validate_host( + hass: HomeAssistant, host: str ) -> tuple[str, FroniusConfigEntryData]: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - host = data[CONF_HOST] fronius = Fronius(async_get_clientsession(hass), host) try: @@ -67,6 +78,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize flow.""" + self.info: FroniusConfigEntryData + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -79,7 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - unique_id, info = await validate_input(self.hass, user_input) + unique_id, info = await validate_host(self.hass, user_input[CONF_HOST]) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -90,11 +105,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured( updates=dict(info), reload_on_update=False ) - title = ( - f"SolarNet {'Datalogger' if info['is_logger'] else 'Inverter'}" - f" at {info['host']}" - ) - return self.async_create_entry(title=title, data=info) + return self.async_create_entry(title=create_title(info), data=info) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors @@ -104,6 +115,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Import a configuration from config.yaml.""" return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]}) + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle a flow initiated by the DHCP client.""" + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST].lstrip("http://").rstrip("/").lower() in ( + discovery_info.ip, + discovery_info.hostname, + ): + return self.async_abort(reason="already_configured") + # Symo Datalogger devices need up to 1 minute at boot from DHCP request + # to respond to API requests (connection refused until then) + await asyncio.sleep(DHCP_REQUEST_DELAY) + try: + unique_id, self.info = await validate_host(self.hass, discovery_info.ip) + except CannotConnect: + return self.async_abort(reason="invalid_host") + + await self.async_set_unique_id(unique_id, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates=dict(self.info), reload_on_update=False + ) + + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Attempt to confim.""" + title = create_title(self.info) + if user_input is not None: + return self.async_create_entry(title=title, data=self.info) + + self._set_confirm_only() + self.context.update({"title_placeholders": {"device": title}}) + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={ + "device": title, + }, + ) + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 902a20b9b4e..edce9bab944 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,5 +1,10 @@ { "domain": "fronius", + "dhcp": [ + { + "macaddress": "0003AC*" + } + ], "name": "Fronius", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fronius", diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index 7e411476559..711e363eeba 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{device}", "step": { "user": { "title": "Fronius SolarNet", @@ -7,6 +8,9 @@ "data": { "host": "[%key:common::config_flow::data::host%]" } + }, + "confirm_discovery": { + "description": "Do you want to add {device} to Home Assistant?" } }, "error": { @@ -14,7 +18,8 @@ "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%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]" } } } diff --git a/homeassistant/components/fronius/translations/en.json b/homeassistant/components/fronius/translations/en.json index 75bbeede6e0..244949935e9 100644 --- a/homeassistant/components/fronius/translations/en.json +++ b/homeassistant/components/fronius/translations/en.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "invalid_host": "Invalid hostname or IP address" }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Do you want to add {device} to Home Assistant?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 17189705056..bdef30cf201 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -158,6 +158,10 @@ DHCP = [ "macaddress": "C82E47*", "hostname": "sta*" }, + { + "domain": "fronius", + "macaddress": "0003AC*" + }, { "domain": "goalzero", "hostname": "yeti*" diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index f0ff1e0ce48..427c8e4a163 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pyfronius import FroniusError from homeassistant import config_entries +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_HOST, CONF_RESOURCE @@ -28,6 +29,11 @@ INVERTER_INFO_RETURN_VALUE = { ] } LOGGER_INFO_RETURN_VALUE = {"unique_identifier": {"value": "123.4567"}} +MOCK_DHCP_DATA = DhcpServiceInfo( + hostname="fronius", + ip="10.2.3.4", + macaddress="00:03:ac:11:22:33", +) async def test_form_with_logger(hass: HomeAssistant) -> None: @@ -261,3 +267,62 @@ async def test_import(hass, aioclient_mock): "host": MOCK_HOST, "is_logger": True, } + + +async def test_dhcp(hass, aioclient_mock): + """Test starting a flow from discovery.""" + with patch( + "homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0 + ), patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm_discovery" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" + assert result["data"] == { + "host": MOCK_DHCP_DATA.ip, + "is_logger": True, + } + + +async def test_dhcp_already_configured(hass, aioclient_mock): + """Test starting a flow from discovery.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: f"http://{MOCK_DHCP_DATA.ip}/", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_invalid(hass, aioclient_mock): + """Test starting a flow from discovery.""" + with patch( + "homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0 + ), patch("pyfronius.Fronius.current_logger_info", side_effect=FroniusError,), patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_host" From 3baa7b679db0df6bd398fcfb7314d7e5ae9ad019 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 3 Dec 2021 18:29:38 +0100 Subject: [PATCH 1285/1452] Use new Platform enum in KNX (#60902) --- homeassistant/components/knx/__init__.py | 15 ++++---- homeassistant/components/knx/binary_sensor.py | 11 ++---- homeassistant/components/knx/button.py | 6 ++-- homeassistant/components/knx/climate.py | 6 ++-- homeassistant/components/knx/const.py | 33 +++++++++--------- homeassistant/components/knx/cover.py | 13 ++++--- homeassistant/components/knx/fan.py | 6 ++-- homeassistant/components/knx/light.py | 14 ++------ homeassistant/components/knx/number.py | 6 ++-- homeassistant/components/knx/scene.py | 8 ++--- homeassistant/components/knx/schema.py | 34 +++++++++---------- homeassistant/components/knx/select.py | 6 ++-- homeassistant/components/knx/sensor.py | 8 ++--- homeassistant/components/knx/switch.py | 13 ++----- homeassistant/components/knx/weather.py | 8 ++--- tests/components/knx/test_binary_sensor.py | 14 ++++---- tests/components/knx/test_button.py | 6 ++-- tests/components/knx/test_fan.py | 6 ++-- tests/components/knx/test_light.py | 28 ++++++++------- tests/components/knx/test_number.py | 4 +-- tests/components/knx/test_scene.py | 2 +- tests/components/knx/test_select.py | 6 ++-- tests/components/knx/test_sensor.py | 4 +-- tests/components/knx/test_switch.py | 6 ++-- tests/components/knx/test_weather.py | 2 +- 25 files changed, 118 insertions(+), 147 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 786c1264248..3e75c614f1e 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -47,7 +47,7 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, - SupportedPlatforms, + SUPPORTED_PLATFORMS, ) from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure from .schema import ( @@ -251,16 +251,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.config_entries.async_setup_platforms( - entry, - [platform.value for platform in SupportedPlatforms if platform.value in config], + entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config] ) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. - if NotifySchema.PLATFORM_NAME in conf: + if NotifySchema.PLATFORM in conf: hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM_NAME], config + hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config ) ) @@ -310,9 +309,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms( entry, [ - platform.value - for platform in SupportedPlatforms - if platform.value in hass.data[DATA_KNX_CONFIG] + platform + for platform in SUPPORTED_PLATFORMS + if platform in hass.data[DATA_KNX_CONFIG] ], ) if unload_ok: diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index b3dbdc0db12..9005ca707b9 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -15,19 +15,14 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .const import ( - ATTR_COUNTER, - ATTR_SOURCE, - DATA_KNX_CONFIG, - DOMAIN, - SupportedPlatforms, -) +from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -43,7 +38,7 @@ async def async_setup_entry( async_add_entities( KNXBinarySensor(xknx, entity_config) - for entity_config in config[SupportedPlatforms.BINARY_SENSOR.value] + for entity_config in config[Platform.BINARY_SENSOR] ) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 457e1d6d43d..274ced80146 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -6,7 +6,7 @@ from xknx.devices import RawValue as XknxRawValue from homeassistant import config_entries from homeassistant.components.button import ButtonEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -17,7 +17,6 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, - SupportedPlatforms, ) from .knx_entity import KnxEntity @@ -32,8 +31,7 @@ async def async_setup_entry( config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) - for entity_config in config[SupportedPlatforms.BUTTON.value] + KNXButton(xknx, entity_config) for entity_config in config[Platform.BUTTON] ) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 8517eed7ace..63fbb170ca7 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,7 +34,6 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, PRESET_MODES, - SupportedPlatforms, ) from .knx_entity import KnxEntity from .schema import ClimateSchema @@ -50,9 +50,7 @@ async def async_setup_entry( ) -> None: """Set up climate(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.CLIMATE.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.CLIMATE] async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index e12d9594795..950deff95c1 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -20,6 +20,7 @@ from homeassistant.components.climate.const import ( PRESET_NONE, PRESET_SLEEP, ) +from homeassistant.const import Platform DOMAIN: Final = "knx" @@ -54,23 +55,21 @@ class ColorTempModes(Enum): RELATIVE = "DPT-5.001" -class SupportedPlatforms(Enum): - """Supported platforms.""" - - BINARY_SENSOR = "binary_sensor" - BUTTON = "button" - CLIMATE = "climate" - COVER = "cover" - FAN = "fan" - LIGHT = "light" - NOTIFY = "notify" - NUMBER = "number" - SCENE = "scene" - SELECT = "select" - SENSOR = "sensor" - SWITCH = "switch" - WEATHER = "weather" - +SUPPORTED_PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.NOTIFY, + Platform.NUMBER, + Platform.SCENE, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.WEATHER, +] # Map KNX controller modes to HA modes. This list might not be complete. CONTROLLER_MODES: Final = { diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 8b00f6232f3..96996e0ef27 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -23,13 +23,18 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_NAME, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.typing import ConfigType -from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms +from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import CoverSchema @@ -41,9 +46,7 @@ async def async_setup_entry( ) -> None: """Set up cover(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.COVER.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.COVER] async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index bdb0bbf9dcc..38c90aa149d 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -9,7 +9,7 @@ from xknx.devices import Fan as XknxFan from homeassistant import config_entries from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -19,7 +19,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema @@ -33,7 +33,7 @@ async def async_setup_entry( ) -> None: """Set up fan(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][SupportedPlatforms.FAN.value] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.FAN] async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 580293a15d8..0f8f2fe89af 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -23,19 +23,13 @@ from homeassistant.components.light import ( COLOR_MODE_XY, LightEntity, ) -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .const import ( - DATA_KNX_CONFIG, - DOMAIN, - KNX_ADDRESS, - ColorTempModes, - SupportedPlatforms, -) +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes from .knx_entity import KnxEntity from .schema import LightSchema @@ -47,9 +41,7 @@ async def async_setup_entry( ) -> None: """Set up light(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.LIGHT.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.LIGHT] async_add_entities(KNXLight(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 7d4ea8a717b..9f12aa4ce24 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_TYPE, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,7 +28,6 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, - SupportedPlatforms, ) from .knx_entity import KnxEntity from .schema import NumberSchema @@ -40,9 +40,7 @@ async def async_setup_entry( ) -> None: """Set up number(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.NUMBER.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NUMBER] async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 658c6d6d298..a028cebc8f7 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -8,12 +8,12 @@ from xknx.devices import Scene as XknxScene from homeassistant import config_entries from homeassistant.components.scene import Scene -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SceneSchema @@ -25,9 +25,7 @@ async def async_setup_entry( ) -> None: """Set up scene(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.SCENE.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SCENE] async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 218a84b2485..4b7105f15f1 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TYPE, + Platform, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA @@ -50,7 +51,6 @@ from .const import ( KNX_ADDRESS, PRESET_MODES, ColorTempModes, - SupportedPlatforms, ) ################## @@ -275,14 +275,14 @@ class EventSchema: class KNXPlatformSchema(ABC): """Voluptuous schema for KNX platform entity configuration.""" - PLATFORM_NAME: ClassVar[str] + PLATFORM: ClassVar[Platform | str] ENTITY_SCHEMA: ClassVar[vol.Schema] @classmethod def platform_node(cls) -> dict[vol.Optional, vol.All]: """Return a schema node for the platform.""" return { - vol.Optional(cls.PLATFORM_NAME): vol.All( + vol.Optional(str(cls.PLATFORM)): vol.All( cv.ensure_list, [cls.ENTITY_SCHEMA] ) } @@ -291,7 +291,7 @@ class KNXPlatformSchema(ABC): class BinarySensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX binary sensors.""" - PLATFORM_NAME = SupportedPlatforms.BINARY_SENSOR.value + PLATFORM = Platform.BINARY_SENSOR CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE @@ -327,7 +327,7 @@ class BinarySensorSchema(KNXPlatformSchema): class ButtonSchema(KNXPlatformSchema): """Voluptuous schema for KNX buttons.""" - PLATFORM_NAME = SupportedPlatforms.BUTTON.value + PLATFORM = Platform.BUTTON CONF_VALUE = "value" DEFAULT_NAME = "KNX Button" @@ -388,7 +388,7 @@ class ButtonSchema(KNXPlatformSchema): class ClimateSchema(KNXPlatformSchema): """Voluptuous schema for KNX climate devices.""" - PLATFORM_NAME = SupportedPlatforms.CLIMATE.value + PLATFORM = Platform.CLIMATE CONF_ACTIVE_STATE_ADDRESS = "active_state_address" CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" @@ -507,7 +507,7 @@ class ClimateSchema(KNXPlatformSchema): class CoverSchema(KNXPlatformSchema): """Voluptuous schema for KNX covers.""" - PLATFORM_NAME = SupportedPlatforms.COVER.value + PLATFORM = Platform.COVER CONF_MOVE_LONG_ADDRESS = "move_long_address" CONF_MOVE_SHORT_ADDRESS = "move_short_address" @@ -562,7 +562,7 @@ class CoverSchema(KNXPlatformSchema): class ExposeSchema(KNXPlatformSchema): """Voluptuous schema for KNX exposures.""" - PLATFORM_NAME = CONF_KNX_EXPOSE + PLATFORM = CONF_KNX_EXPOSE CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" @@ -599,7 +599,7 @@ class ExposeSchema(KNXPlatformSchema): class FanSchema(KNXPlatformSchema): """Voluptuous schema for KNX fans.""" - PLATFORM_NAME = SupportedPlatforms.FAN.value + PLATFORM = Platform.FAN CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_OSCILLATION_ADDRESS = "oscillation_address" @@ -624,7 +624,7 @@ class FanSchema(KNXPlatformSchema): class LightSchema(KNXPlatformSchema): """Voluptuous schema for KNX lights.""" - PLATFORM_NAME = SupportedPlatforms.LIGHT.value + PLATFORM = Platform.LIGHT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_BRIGHTNESS_ADDRESS = "brightness_address" @@ -764,7 +764,7 @@ class LightSchema(KNXPlatformSchema): class NotifySchema(KNXPlatformSchema): """Voluptuous schema for KNX notifications.""" - PLATFORM_NAME = SupportedPlatforms.NOTIFY.value + PLATFORM = Platform.NOTIFY DEFAULT_NAME = "KNX Notify" @@ -779,7 +779,7 @@ class NotifySchema(KNXPlatformSchema): class NumberSchema(KNXPlatformSchema): """Voluptuous schema for KNX numbers.""" - PLATFORM_NAME = SupportedPlatforms.NUMBER.value + PLATFORM = Platform.NUMBER CONF_MAX = "max" CONF_MIN = "min" @@ -810,7 +810,7 @@ class NumberSchema(KNXPlatformSchema): class SceneSchema(KNXPlatformSchema): """Voluptuous schema for KNX scenes.""" - PLATFORM_NAME = SupportedPlatforms.SCENE.value + PLATFORM = Platform.SCENE CONF_SCENE_NUMBER = "scene_number" @@ -830,7 +830,7 @@ class SceneSchema(KNXPlatformSchema): class SelectSchema(KNXPlatformSchema): """Voluptuous schema for KNX selects.""" - PLATFORM_NAME = SupportedPlatforms.SELECT.value + PLATFORM = Platform.SELECT CONF_OPTION = "option" CONF_OPTIONS = "options" @@ -863,7 +863,7 @@ class SelectSchema(KNXPlatformSchema): class SensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX sensors.""" - PLATFORM_NAME = SupportedPlatforms.SENSOR.value + PLATFORM = Platform.SENSOR CONF_ALWAYS_CALLBACK = "always_callback" CONF_STATE_ADDRESS = CONF_STATE_ADDRESS @@ -886,7 +886,7 @@ class SensorSchema(KNXPlatformSchema): class SwitchSchema(KNXPlatformSchema): """Voluptuous schema for KNX switches.""" - PLATFORM_NAME = SupportedPlatforms.SWITCH.value + PLATFORM = Platform.SWITCH CONF_INVERT = CONF_INVERT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS @@ -907,7 +907,7 @@ class SwitchSchema(KNXPlatformSchema): class WeatherSchema(KNXPlatformSchema): """Voluptuous schema for KNX weather station.""" - PLATFORM_NAME = SupportedPlatforms.WEATHER.value + PLATFORM = Platform.WEATHER CONF_SYNC_STATE = CONF_SYNC_STATE CONF_KNX_TEMPERATURE_ADDRESS = "address_temperature" diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index aefa4749e88..5baa068eaa6 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,7 +27,6 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, - SupportedPlatforms, ) from .knx_entity import KnxEntity from .schema import SelectSchema @@ -39,9 +39,7 @@ async def async_setup_entry( ) -> None: """Set up select(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.SELECT.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SELECT] async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index a9a1feca9e3..ceb9f435d83 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -12,12 +12,12 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES, SensorEntity, ) -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, StateType -from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms +from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import SensorSchema @@ -29,9 +29,7 @@ async def async_setup_entry( ) -> None: """Set up sensor(s) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.SENSOR.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SENSOR] async_add_entities(KNXSensor(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 3bbb419a22b..9f4eb6fc632 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -14,19 +14,14 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_RESPOND_TO_READ, - DATA_KNX_CONFIG, - DOMAIN, - KNX_ADDRESS, - SupportedPlatforms, -) +from .const import CONF_RESPOND_TO_READ, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SwitchSchema @@ -38,9 +33,7 @@ async def async_setup_entry( ) -> None: """Set up switch(es) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.SWITCH.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SWITCH] async_add_entities(KNXSwitch(xknx, entity_config) for entity_config in config) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index b52ee644b39..6e71c09501f 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -6,12 +6,12 @@ from xknx.devices import Weather as XknxWeather from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms +from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import WeatherSchema @@ -23,9 +23,7 @@ async def async_setup_entry( ) -> None: """Set up switch(es) for KNX platform.""" xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][ - SupportedPlatforms.WEATHER.value - ] + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.WEATHER] async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 9adc7205543..5513cefcbb4 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -26,7 +26,7 @@ async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKi """Test KNX binary sensor entity category.""" await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test_normal", CONF_STATE_ADDRESS: "1/1/1", @@ -49,7 +49,7 @@ async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit): """Test KNX binary sensor and inverted binary_sensor.""" await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test_normal", CONF_STATE_ADDRESS: "1/1/1", @@ -104,7 +104,7 @@ async def test_binary_sensor_ignore_internal_state( await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test_normal", CONF_STATE_ADDRESS: "1/1/1", @@ -156,7 +156,7 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", @@ -223,7 +223,7 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", @@ -259,7 +259,7 @@ async def test_binary_sensor_restore_and_respond(hass, knx): ): await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test", CONF_STATE_ADDRESS: _ADDRESS, @@ -291,7 +291,7 @@ async def test_binary_sensor_restore_invert(hass, knx): ): await knx.setup_integration( { - BinarySensorSchema.PLATFORM_NAME: [ + BinarySensorSchema.PLATFORM: [ { CONF_NAME: "test", CONF_STATE_ADDRESS: _ADDRESS, diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index 0e5f40670f7..eff81a3c6b6 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -21,7 +21,7 @@ async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit): events = async_capture_events(hass, "state_changed") await knx.setup_integration( { - ButtonSchema.PLATFORM_NAME: { + ButtonSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", } @@ -57,7 +57,7 @@ async def test_button_raw(hass: HomeAssistant, knx: KNXTestKit): """Test KNX button with raw payload.""" await knx.setup_integration( { - ButtonSchema.PLATFORM_NAME: { + ButtonSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", CONF_PAYLOAD: False, @@ -76,7 +76,7 @@ async def test_button_type(hass: HomeAssistant, knx: KNXTestKit): """Test KNX button with encoded payload.""" await knx.setup_integration( { - ButtonSchema.PLATFORM_NAME: { + ButtonSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", ButtonSchema.CONF_VALUE: 21.5, diff --git a/tests/components/knx/test_fan.py b/tests/components/knx/test_fan.py index cc2365888f0..37a69911e51 100644 --- a/tests/components/knx/test_fan.py +++ b/tests/components/knx/test_fan.py @@ -11,7 +11,7 @@ async def test_fan_percent(hass: HomeAssistant, knx: KNXTestKit): """Test KNX fan with percentage speed.""" await knx.setup_integration( { - FanSchema.PLATFORM_NAME: { + FanSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", } @@ -56,7 +56,7 @@ async def test_fan_step(hass: HomeAssistant, knx: KNXTestKit): """Test KNX fan with speed steps.""" await knx.setup_integration( { - FanSchema.PLATFORM_NAME: { + FanSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", FanSchema.CONF_MAX_STEP: 4, @@ -109,7 +109,7 @@ async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit): """Test KNX fan oscillation.""" await knx.setup_integration( { - FanSchema.PLATFORM_NAME: { + FanSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/1/1", FanSchema.CONF_OSCILLATION_ADDRESS: "2/2/2", diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 2988bf189d3..9c7ef0e91ec 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS @@ -35,7 +36,7 @@ async def test_light_simple(hass: HomeAssistant, knx: KNXTestKit): test_address = "1/1/1" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: { + LightSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: test_address, } @@ -86,7 +87,7 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit): test_brightness_state = "1/1/3" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: { + LightSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: test_address, LightSchema.CONF_BRIGHTNESS_ADDRESS: test_brightness, @@ -96,6 +97,9 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit): ) # StateUpdater initialize state await knx.assert_read(test_brightness_state) + await knx.xknx.connection_manager.connection_state_changed( + XknxConnectionState.CONNECTED + ) # turn on light via brightness await hass.services.async_call( "light", @@ -139,7 +143,7 @@ async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit): test_ct_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -193,7 +197,7 @@ async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): test_ct_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -251,7 +255,7 @@ async def test_light_hs_color(hass: HomeAssistant, knx: KNXTestKit): test_sat_state = "1/1/8" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -335,7 +339,7 @@ async def test_light_xyy_color(hass: HomeAssistant, knx: KNXTestKit): test_xyy_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -410,7 +414,7 @@ async def test_light_xyy_color_with_brightness(hass: HomeAssistant, knx: KNXTest test_xyy_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -488,7 +492,7 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): test_blue_state = "1/1/8" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", LightSchema.CONF_INDIVIDUAL_COLORS: { @@ -636,7 +640,7 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit): test_white_state = "1/1/10" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", LightSchema.CONF_INDIVIDUAL_COLORS: { @@ -810,7 +814,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): test_rgb_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -918,7 +922,7 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): test_rgbw_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, @@ -1031,7 +1035,7 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): test_rgbw_state = "1/1/6" await knx.setup_integration( { - LightSchema.PLATFORM_NAME: [ + LightSchema.PLATFORM: [ { CONF_NAME: "test", KNX_ADDRESS: test_address, diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index d14f01ee5fe..668b046df74 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -16,7 +16,7 @@ async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit): test_address = "1/1/1" await knx.setup_integration( { - NumberSchema.PLATFORM_NAME: { + NumberSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: test_address, CONF_TYPE: "percent", @@ -72,7 +72,7 @@ async def test_number_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit): ): await knx.setup_integration( { - NumberSchema.PLATFORM_NAME: { + NumberSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: [test_address, test_passive_address], CONF_RESPOND_TO_READ: True, diff --git a/tests/components/knx/test_scene.py b/tests/components/knx/test_scene.py index 452e5347932..c2f15df6f6c 100644 --- a/tests/components/knx/test_scene.py +++ b/tests/components/knx/test_scene.py @@ -19,7 +19,7 @@ async def test_activate_knx_scene(hass: HomeAssistant, knx: KNXTestKit): """Test KNX scene.""" await knx.setup_integration( { - SceneSchema.PLATFORM_NAME: [ + SceneSchema.PLATFORM: [ { CONF_NAME: "test", SceneSchema.CONF_SCENE_NUMBER: 24, diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py index f7f80c2ebf3..c8db3625c05 100644 --- a/tests/components/knx/test_select.py +++ b/tests/components/knx/test_select.py @@ -28,7 +28,7 @@ async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): test_address = "1/1/1" await knx.setup_integration( { - SelectSchema.PLATFORM_NAME: { + SelectSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: test_address, CONF_SYNC_STATE: False, @@ -105,7 +105,7 @@ async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): ): await knx.setup_integration( { - SelectSchema.PLATFORM_NAME: { + SelectSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: [test_address, test_passive_address], CONF_RESPOND_TO_READ: True, @@ -143,7 +143,7 @@ async def test_select_dpt_20_103_all_options(hass: HomeAssistant, knx: KNXTestKi await knx.setup_integration( { - SelectSchema.PLATFORM_NAME: { + SelectSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: [test_address, test_passive_address], CONF_STATE_ADDRESS: test_state_address, diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py index 16ea5e8d385..8deb4629d8b 100644 --- a/tests/components/knx/test_sensor.py +++ b/tests/components/knx/test_sensor.py @@ -14,7 +14,7 @@ async def test_sensor(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - SensorSchema.PLATFORM_NAME: { + SensorSchema.PLATFORM: { CONF_NAME: "test", CONF_STATE_ADDRESS: "1/1/1", CONF_TYPE: "current", # 2 byte unsigned int @@ -47,7 +47,7 @@ async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit): events = async_capture_events(hass, "state_changed") await knx.setup_integration( { - SensorSchema.PLATFORM_NAME: [ + SensorSchema.PLATFORM: [ { CONF_NAME: "test_normal", CONF_STATE_ADDRESS: "1/1/1", diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index eff34243ca8..07fe8793fd8 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -17,7 +17,7 @@ async def test_switch_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX switch.""" await knx.setup_integration( { - SwitchSchema.PLATFORM_NAME: { + SwitchSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: "1/2/3", } @@ -59,7 +59,7 @@ async def test_switch_state(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - SwitchSchema.PLATFORM_NAME: { + SwitchSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: _ADDRESS, CONF_STATE_ADDRESS: _STATE_ADDRESS, @@ -122,7 +122,7 @@ async def test_switch_restore_and_respond(hass, knx): ): await knx.setup_integration( { - SwitchSchema.PLATFORM_NAME: { + SwitchSchema.PLATFORM: { CONF_NAME: "test", KNX_ADDRESS: _ADDRESS, CONF_RESPOND_TO_READ: True, diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py index 0fc1255bf26..21d80248b97 100644 --- a/tests/components/knx/test_weather.py +++ b/tests/components/knx/test_weather.py @@ -17,7 +17,7 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit): await knx.setup_integration( { - WeatherSchema.PLATFORM_NAME: { + WeatherSchema.PLATFORM: { CONF_NAME: "test", WeatherSchema.CONF_KNX_WIND_ALARM_ADDRESS: "1/1/1", WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS: "1/1/2", From a80447f096a4c70c739c29f20c83367b66240783 Mon Sep 17 00:00:00 2001 From: yanuino <36410910+yanuino@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:32:04 +0100 Subject: [PATCH 1286/1452] Use state class enum for DHT (#60916) --- homeassistant/components/dht/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 810db33e5e4..7c4ae5610f7 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_MONITORED_CONDITIONS, @@ -44,12 +45,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), ) From cf7a61430954f4ca5c72c6b878b668974368dc3c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Dec 2021 18:33:24 +0100 Subject: [PATCH 1287/1452] Add battery sensor for Netatmo climate devices (#60911) --- homeassistant/components/netatmo/climate.py | 22 +++++- homeassistant/components/netatmo/const.py | 1 + .../components/netatmo/data_handler.py | 10 +++ homeassistant/components/netatmo/sensor.py | 79 +++++++++++++++++++ tests/components/netatmo/test_sensor.py | 14 ++++ 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 726c0ed43c8..49e02f566a0 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -31,7 +31,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,6 +50,7 @@ from .const import ( EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, + NETATMO_CREATE_BATTERY, SERVICE_SET_SCHEDULE, SIGNAL_NAME, TYPE_ENERGY, @@ -55,6 +59,7 @@ from .data_handler import ( CLIMATE_STATE_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler, + NetatmoDevice, ) from .netatmo_entity_base import NetatmoBase @@ -241,6 +246,21 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) + for module in self._room.modules.values(): + if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]: + continue + + async_dispatcher_send( + self.hass, + NETATMO_CREATE_BATTERY, + NetatmoDevice( + self.data_handler, + module, + self._id, + self._climate_state_class, + ), + ) + @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 14e165b5cb4..a642d59ff1e 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -69,6 +69,7 @@ CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" +NETATMO_CREATE_BATTERY = "netatmo_create_battery" CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_WEATHER_AREAS = "weather_areas" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 97321b0da53..c62522a931a 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -57,6 +57,16 @@ DEFAULT_INTERVALS = { SCAN_INTERVAL = 60 +@dataclass +class NetatmoDevice: + """Netatmo device class.""" + + data_handler: NetatmoDataHandler + device: pyatmo.climate.NetatmoModule + parent_id: str + state_class_name: str + + @dataclass class NetatmoDataClass: """Class for keeping track of Netatmo data class metadata.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index d06c96b3c6a..5b3416e3b09 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -42,6 +42,7 @@ from .const import ( DATA_HANDLER, DOMAIN, MANUFACTURER, + NETATMO_CREATE_BATTERY, SIGNAL_NAME, TYPE_WEATHER, ) @@ -50,6 +51,7 @@ from .data_handler import ( PUBLICDATA_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, NetatmoDataHandler, + NetatmoDevice, ) from .helper import NetatmoArea from .netatmo_entity_base import NetatmoBase @@ -454,6 +456,16 @@ async def async_setup_entry( hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoClimateBatterySensor(netatmo_device) + _LOGGER.debug("Adding climate battery sensor %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity) + ) + await add_public_entities(False) if platform_not_ready: @@ -561,6 +573,73 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self.async_write_ha_state() +class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo sensor.""" + + entity_description: NetatmoSensorEntityDescription + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_device.data_handler) + self.entity_description = NetatmoSensorEntityDescription( + key="battery_percent", + name="Battery Percent", + netatmo_name="battery_percent", + entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + ) + + self._module = netatmo_device.device + self._id = netatmo_device.parent_id + self._attr_name = f"{self._module.name} {self.entity_description.name}" + + self._state_class_name = netatmo_device.state_class_name + self._room_id = self._module.room_id + self._model = getattr(self._module.device_type, "value") + + self._attr_unique_id = ( + f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + ) + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if not self._module.reachable: + if self.available: + self._attr_available = False + self._attr_native_value = None + return + + self._attr_available = True + self._attr_native_value = self._process_battery_state() + + def _process_battery_state(self) -> int | None: + """Construct room status.""" + if battery_state := self._module.battery_state: + return process_battery_percentage(battery_state) + + return None + + +def process_battery_percentage(data: str) -> int: + """Process battery data and return percent (int) for display.""" + mapping = { + "max": 100, + "full": 90, + "high": 75, + "medium": 50, + "low": 25, + "very low": 10, + } + return mapping[data] + + def fix_angle(angle: int) -> int: """Fix angle when value is negative.""" if angle < 0: diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index bebd8e0191c..b1b5b11265a 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -233,3 +233,17 @@ async def test_weather_sensor_enabling( assert len(hass.states.async_all()) > states_before assert hass.states.get(f"sensor.{name}").state == expected + + +async def test_climate_battery_sensor(hass, config_entry, netatmo_auth): + """Test climate device battery sensor.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms( + ["sensor", "climate"] + ): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + prefix = "sensor.livingroom_" + + assert hass.states.get(f"{prefix}battery_percent").state == "75" From 943c12e5fa024f31aab20d53fae1e49b1fda80a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 18:56:07 +0100 Subject: [PATCH 1288/1452] Revert "Temporarily disable CI concurrency" (#60928) --- .github/workflows/ci.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c9b7e797177..ebbd2b2fb9e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,10 +15,9 @@ env: PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 -# Temporary disabled: https://github.com/actions/runner/issues/1532 -# concurrency: -# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} -# cancel-in-progress: true +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: changes: From e0562385f1a1491fa234fe5b7e537f1740534b47 Mon Sep 17 00:00:00 2001 From: Pedro Rodriguez Tavarez Date: Fri, 3 Dec 2021 13:00:39 -0500 Subject: [PATCH 1289/1452] Implement privacy_mode for amcrest integration (#57210) --- homeassistant/components/amcrest/__init__.py | 12 +++ homeassistant/components/amcrest/switch.py | 90 ++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 homeassistant/components/amcrest/switch.py diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 18aa2006f72..3ee6e685eb5 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -18,6 +18,7 @@ from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.camera import DOMAIN as CAMERA from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_AUTHENTICATION, @@ -28,6 +29,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, + CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, @@ -56,6 +58,7 @@ from .const import ( ) from .helpers import service_signal from .sensor import SENSOR_KEYS +from .switch import SWITCH_KEYS _LOGGER = logging.getLogger(__name__) @@ -111,6 +114,9 @@ AMCREST_SCHEMA = vol.Schema( vol.Unique(), check_binary_sensors, ), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [vol.In(SWITCH_KEYS)], vol.Unique() + ), vol.Optional(CONF_SENSORS): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)], vol.Unique() ), @@ -273,6 +279,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) + switches = device.get(CONF_SWITCHES) stream_source = device[CONF_STREAM_SOURCE] control_light = device.get(CONF_CONTROL_LIGHT) @@ -320,6 +327,11 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config ) + if switches: + discovery.load_platform( + hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config + ) + if not hass.data[DATA_AMCREST][DEVICES]: return False diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py new file mode 100644 index 00000000000..67dc551fcb9 --- /dev/null +++ b/homeassistant/components/amcrest/switch.py @@ -0,0 +1,90 @@ +"""Support for Amcrest Switches.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import CONF_NAME, CONF_SWITCHES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import DATA_AMCREST, DEVICES + +if TYPE_CHECKING: + from . import AmcrestDevice + +PRIVACY_MODE_KEY = "privacy_mode" + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key=PRIVACY_MODE_KEY, + name="Privacy Mode", + icon="mdi:eye-off", + ), +) + +SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up amcrest platform switches.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST][DEVICES][name] + switches = discovery_info[CONF_SWITCHES] + async_add_entities( + [ + AmcrestSwitch(name, device, description) + for description in SWITCH_TYPES + if description.key in switches + ], + True, + ) + + +class AmcrestSwitch(SwitchEntity): + """Representation of an Amcrest Camera Switch.""" + + def __init__( + self, + name: str, + device: AmcrestDevice, + entity_description: SwitchEntityDescription, + ): + """Initialize switch.""" + self._api = device.api + self.entity_description = entity_description + self._attr_name = f"{name} {entity_description.name}" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._api.available + + def turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + self._turn_switch(True) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + self._turn_switch(False) + + def _turn_switch(self, mode: bool) -> None: + """Set privacy mode.""" + lower_str = str(mode).lower() + self._api.command( + f"configManager.cgi?action=setConfig&LeLensMask[0].Enable={lower_str}" + ) + + def update(self) -> None: + """Update switch.""" + io_res = self._api.privacy_config().splitlines()[0].split("=")[1] + self._attr_is_on = io_res == "true" From b61dede8264975e12de918c9efc8e1b9396a4696 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Fri, 3 Dec 2021 19:06:32 +0100 Subject: [PATCH 1290/1452] Add command_line icon_template (#58877) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/command_line/switch.py | 13 +++++++++++++ tests/components/command_line/test_switch.py | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index ae6c1c0c925..fae4cdbcc6b 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_COMMAND_ON, CONF_COMMAND_STATE, CONF_FRIENDLY_NAME, + CONF_ICON_TEMPLATE, CONF_SWITCHES, CONF_VALUE_TEMPLATE, ) @@ -31,6 +32,7 @@ SWITCH_SCHEMA = vol.Schema( vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } ) @@ -54,6 +56,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + icon_template = device_config.get(CONF_ICON_TEMPLATE) + if icon_template is not None: + icon_template.hass = hass + switches.append( CommandSwitch( hass, @@ -62,6 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_config[CONF_COMMAND_ON], device_config[CONF_COMMAND_OFF], device_config.get(CONF_COMMAND_STATE), + icon_template, value_template, device_config[CONF_COMMAND_TIMEOUT], ) @@ -85,6 +92,7 @@ class CommandSwitch(SwitchEntity): command_on, command_off, command_state, + icon_template, value_template, timeout, ): @@ -96,6 +104,7 @@ class CommandSwitch(SwitchEntity): self._command_on = command_on self._command_off = command_off self._command_state = command_state + self._icon_template = icon_template self._value_template = value_template self._timeout = timeout @@ -152,6 +161,10 @@ class CommandSwitch(SwitchEntity): """Update device state.""" if self._command_state: payload = str(self._query_state()) + if self._icon_template: + self._attr_icon = self._icon_template.render_with_possible_json_value( + payload + ) if self._value_template: payload = self._value_template.render_with_possible_json_value(payload) self._state = payload.lower() == "true" diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 3eeded7278b..910d990d07d 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -90,6 +90,7 @@ async def test_state_value(hass: HomeAssistant) -> None: "command_on": f"echo 1 > {path}", "command_off": f"echo 0 > {path}", "value_template": '{{ value=="1" }}', + "icon_template": '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}', } }, ) @@ -108,6 +109,7 @@ async def test_state_value(hass: HomeAssistant) -> None: entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_ON + assert entity_state.attributes.get("icon") == "mdi:on" await hass.services.async_call( DOMAIN, @@ -119,6 +121,7 @@ async def test_state_value(hass: HomeAssistant) -> None: entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF + assert entity_state.attributes.get("icon") == "mdi:off" async def test_state_json_value(hass: HomeAssistant) -> None: @@ -136,6 +139,7 @@ async def test_state_json_value(hass: HomeAssistant) -> None: "command_on": f"echo '{oncmd}' > {path}", "command_off": f"echo '{offcmd}' > {path}", "value_template": '{{ value_json.status=="ok" }}', + "icon_template": '{% if value_json.status=="ok" %} mdi:on {% else %} mdi:off {% endif %}', } }, ) @@ -154,6 +158,7 @@ async def test_state_json_value(hass: HomeAssistant) -> None: entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_ON + assert entity_state.attributes.get("icon") == "mdi:on" await hass.services.async_call( DOMAIN, @@ -165,6 +170,7 @@ async def test_state_json_value(hass: HomeAssistant) -> None: entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF + assert entity_state.attributes.get("icon") == "mdi:off" async def test_state_code(hass: HomeAssistant) -> None: From cbf2bf2e1fa02b62a57d8859420fc4741ef40d59 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Dec 2021 12:06:56 -0600 Subject: [PATCH 1291/1452] Add audio input format sensor to Sonos HT devices (#60884) --- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/sensor.py | 44 +++++++++++++++++++++-- homeassistant/components/sonos/speaker.py | 6 ++++ tests/components/sonos/conftest.py | 1 + tests/components/sonos/test_sensor.py | 11 ++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 4b636b3e0f6..523ac9f561b 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -144,6 +144,7 @@ PLAYABLE_MEDIA_TYPES = [ SONOS_CHECK_ACTIVITY = "sonos_check_activity" SONOS_CREATE_ALARM = "sonos_create_alarm" +SONOS_CREATE_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_LEVELS = "sonos_create_levels" diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 599e5434fb4..7c9235cf4af 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -7,9 +7,10 @@ from homeassistant.const import ( ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import SONOS_CREATE_BATTERY +from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .entity import SonosEntity from .speaker import SonosSpeaker @@ -17,12 +18,27 @@ from .speaker import SonosSpeaker async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - async def _async_create_entity(speaker: SonosSpeaker) -> None: + @callback + def _async_create_audio_format_entity( + speaker: SonosSpeaker, audio_format: str + ) -> None: + entity = SonosAudioInputFormatSensorEntity(speaker, audio_format) + async_add_entities([entity]) + + @callback + def _async_create_battery_sensor(speaker: SonosSpeaker) -> None: entity = SonosBatteryEntity(speaker) async_add_entities([entity]) config_entry.async_on_unload( - async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity) + async_dispatcher_connect( + hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, _async_create_audio_format_entity + ) + ) + config_entry.async_on_unload( + async_dispatcher_connect( + hass, SONOS_CREATE_BATTERY, _async_create_battery_sensor + ) ) @@ -64,3 +80,25 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): def available(self) -> bool: """Return whether this device is available.""" return self.speaker.available and self.speaker.power_source + + +class SonosAudioInputFormatSensorEntity(SonosEntity, SensorEntity): + """Representation of a Sonos audio import format sensor entity.""" + + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_icon = "mdi:import" + _attr_should_poll = True + + def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: + """Initialize the audio input format sensor.""" + super().__init__(speaker) + self._attr_unique_id = f"{self.soco.uid}-audio-format" + self._attr_name = f"{self.speaker.zone_name} Audio Input Format" + self._attr_native_value = audio_format + + def update(self) -> None: + """Poll the device for the current state.""" + self._attr_native_value = self.soco.soundbar_audio_input_format + + async def _async_poll(self) -> None: + """Provide a stub for required ABC method.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4838da69c50..05787a354f0 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -46,6 +46,7 @@ from .const import ( SCAN_INTERVAL, SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, + SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY, SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, @@ -240,6 +241,11 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_LEVELS, self) + if audio_format := self.soco.soundbar_audio_input_format: + dispatcher_send( + self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format + ) + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info # Battery events can be infrequent, polling is still necessary diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index a17f07c3c35..f7f8d67589f 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -74,6 +74,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.treble = -1 mock_soco.sub_enabled = False mock_soco.surround_enabled = True + mock_soco.soundbar_audio_input_format = "Dolby 5.1" mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = [mock_soco] yield mock_soco diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 18cd87ca9be..a45d587cc08 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -123,3 +123,14 @@ async def test_device_payload_without_battery_and_ignored_keys( await hass.async_block_till_done() assert ignored_payload not in caplog.text + + +async def test_audio_input_sensor(hass, config_entry, config, soco): + """Test sonos device with battery state.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] + audio_input_state = hass.states.get(audio_input_sensor.entity_id) + assert audio_input_state.state == "Dolby 5.1" From e50a47621fa56cf459cc97a1e19210e50e817b05 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 3 Dec 2021 19:08:23 +0100 Subject: [PATCH 1292/1452] Enable basic type checking for climacell (#55334) --- homeassistant/components/climacell/__init__.py | 4 ++-- homeassistant/components/climacell/sensor.py | 6 +++++- homeassistant/components/climacell/weather.py | 11 ++++++----- mypy.ini | 3 +++ script/hassfest/mypy_config.py | 1 + 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index aceb24b695f..e3edc778955 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) - params = {} + params: dict[str, Any] = {} # If config entry options not set up, set them up if not entry.options: params["options"] = { @@ -206,7 +206,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - data = {FORECASTS: {}} + data: dict[str, Any] = {FORECASTS: {}} try: if self._api_version == 3: data[CURRENT] = await self._api.realtime( diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index f934449fdb0..597e1095f89 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -28,6 +28,8 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] + api_class: type[BaseClimaCellSensorEntity] + sensor_types: tuple[ClimaCellSensorEntityDescription, ...] if (api_version := config_entry.data[CONF_API_VERSION]) == 3: api_class = ClimaCellV3SensorEntity @@ -81,6 +83,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): state = self._state if ( state is not None + and not isinstance(state, str) and self.entity_description.unit_imperial is not None and self.entity_description.metric_conversion != 1.0 and self.entity_description.is_metric_check is not None @@ -95,7 +98,8 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): return round(state * conversion, 4) if self.entity_description.value_map is not None and state is not None: - return self.entity_description.value_map(state).name.lower() + # mypy bug: "Literal[IntEnum.value]" not callable + return self.entity_description.value_map(state).name.lower() # type: ignore[misc] return state diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index bf1c95b524a..eafb47aac99 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Mapping from datetime import datetime -from typing import Any +from typing import Any, cast from pyclimacell.const import ( CURRENT, @@ -136,7 +136,7 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): @staticmethod @abstractmethod def _translate_condition( - condition: int | None, sun_is_up: bool = True + condition: str | int | None, sun_is_up: bool = True ) -> str | None: """Translate ClimaCell condition into an HA condition.""" @@ -144,7 +144,7 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): self, forecast_dt: datetime, use_datetime: bool, - condition: str, + condition: int | str, precipitation: float | None, precipitation_probability: float | None, temp: float | None, @@ -274,7 +274,7 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): @staticmethod def _translate_condition( - condition: int | None, sun_is_up: bool = True + condition: int | str | None, sun_is_up: bool = True ) -> str | None: """Translate ClimaCell condition into an HA condition.""" if condition is None: @@ -422,11 +422,12 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): @staticmethod def _translate_condition( - condition: str | None, sun_is_up: bool = True + condition: int | str | None, sun_is_up: bool = True ) -> str | None: """Translate ClimaCell condition into an HA condition.""" if not condition: return None + condition = cast(str, condition) if "clear" in condition.lower(): if sun_is_up: return CLEAR_CONDITIONS["day"] diff --git a/mypy.ini b/mypy.ini index 483b8bd22f1..7346cc83ba9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1711,6 +1711,9 @@ ignore_errors = true [mypy-homeassistant.components.climacell.*] ignore_errors = true +[mypy-homeassistant.components.cloud.*] +ignore_errors = true + [mypy-homeassistant.components.config.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 41dfa1ad05e..6fc6c0e3993 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,6 +16,7 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.blueprint.*", "homeassistant.components.climacell.*", + "homeassistant.components.cloud.*", "homeassistant.components.config.*", "homeassistant.components.conversation.*", "homeassistant.components.deconz.*", From df36b3dcb8e74b91e223d0eac1eea622d23f2767 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 19:20:09 +0100 Subject: [PATCH 1293/1452] Update frontend to 20211203.0 (#60925) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2eecc6d9a0f..1ca97e4cdd8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211202.0" + "home-assistant-frontend==20211203.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bdfe44cc8ac..029f889fd85 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211202.0 +home-assistant-frontend==20211203.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 4aee015fcb1..0160ed64b6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211202.0 +home-assistant-frontend==20211203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b03cdbf85a4..82d1aefb1bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211202.0 +home-assistant-frontend==20211203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 02b5449648e2a9b70648060725a32bacfdaf73c2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 3 Dec 2021 13:23:57 -0500 Subject: [PATCH 1294/1452] Add support for siren entities in ZHA (#60920) --- homeassistant/components/zha/core/const.py | 2 + .../components/zha/core/discovery.py | 1 + .../components/zha/core/registries.py | 2 + homeassistant/components/zha/siren.py | 143 ++++++++++++++++++ tests/components/zha/test_siren.py | 139 +++++++++++++++++ tests/components/zha/zha_devices_list.py | 12 +- 6 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zha/siren.py create mode 100644 tests/components/zha/test_siren.py diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index de29ac0f9f6..813f268bbe5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -22,6 +22,7 @@ from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.siren import DOMAIN as SIREN from homeassistant.components.switch import DOMAIN as SWITCH import homeassistant.helpers.config_validation as cv @@ -120,6 +121,7 @@ PLATFORMS = ( LOCK, NUMBER, SENSOR, + SIREN, SWITCH, ) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index df257dbbecc..1d9edb82980 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -25,6 +25,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, lock, number, sensor, + siren, switch, ) from .channels import base diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index eeee0c5c629..f624ef9289d 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -21,6 +21,7 @@ from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.siren import DOMAIN as SIREN from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries @@ -113,6 +114,7 @@ DEVICE_CLASS = { zigpy.profiles.zha.DeviceType.SHADE: COVER, zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL: ALARM, + zigpy.profiles.zha.DeviceType.IAS_WARNING_DEVICE: SIREN, }, zigpy.profiles.zll.PROFILE_ID: { zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py new file mode 100644 index 00000000000..75f527cfcf1 --- /dev/null +++ b/homeassistant/components/zha/siren.py @@ -0,0 +1,143 @@ +"""Support for ZHA sirens.""" + +from __future__ import annotations + +import functools +from typing import Any + +from homeassistant.components.siren import ( + ATTR_DURATION, + DOMAIN, + SUPPORT_DURATION, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SirenEntity, +) +from homeassistant.components.siren.const import ( + ATTR_TONE, + ATTR_VOLUME_LEVEL, + SUPPORT_TONES, + SUPPORT_VOLUME_SET, +) +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 homeassistant.helpers.event import async_call_later + +from .core import discovery +from .core.channels.security import IasWd +from .core.const import ( + CHANNEL_IAS_WD, + DATA_ZHA, + SIGNAL_ADD_ENTITIES, + WARNING_DEVICE_MODE_BURGLAR, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_MODE_EMERGENCY_PANIC, + WARNING_DEVICE_MODE_FIRE, + WARNING_DEVICE_MODE_FIRE_PANIC, + WARNING_DEVICE_MODE_POLICE_PANIC, + WARNING_DEVICE_MODE_STOP, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_STROBE_NO, +) +from .core.registries import ZHA_ENTITIES +from .core.typing import ChannelType, ZhaDeviceType +from .entity import ZhaEntity + +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +DEFAULT_DURATION = 5 # seconds + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Zigbee Home Automation siren from config entry.""" + entities_to_create = hass.data[DATA_ZHA][DOMAIN] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, + ), + ) + config_entry.async_on_unload(unsub) + + +@STRICT_MATCH(channel_names=CHANNEL_IAS_WD) +class ZHASiren(ZhaEntity, SirenEntity): + """Representation of a ZHA siren.""" + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> None: + """Init this siren.""" + self._attr_supported_features = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_DURATION + | SUPPORT_VOLUME_SET + | SUPPORT_TONES + ) + self._attr_available_tones: list[int | str] | dict[int, str] | None = { + WARNING_DEVICE_MODE_BURGLAR: "Burglar", + WARNING_DEVICE_MODE_FIRE: "Fire", + WARNING_DEVICE_MODE_EMERGENCY: "Emergency", + WARNING_DEVICE_MODE_POLICE_PANIC: "Police Panic", + WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic", + WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic", + } + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel: IasWd = channels[0] + self._attr_is_on: bool = False + self._off_listener = None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on siren.""" + if self._off_listener: + self._off_listener() + self._off_listener = None + siren_tone = WARNING_DEVICE_MODE_EMERGENCY + siren_duration = DEFAULT_DURATION + siren_level = WARNING_DEVICE_SOUND_HIGH + if (duration := kwargs.get(ATTR_DURATION)) is not None: + siren_duration = duration + if (tone := kwargs.get(ATTR_TONE)) is not None: + siren_tone = tone + if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: + siren_level = int(level) + await self._channel.issue_start_warning( + mode=siren_tone, warning_duration=siren_duration, siren_level=siren_level + ) + self._attr_is_on = True + self._off_listener = async_call_later( + self._zha_device.hass, siren_duration, self.async_set_off + ) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off siren.""" + await self._channel.issue_start_warning( + mode=WARNING_DEVICE_MODE_STOP, strobe=WARNING_DEVICE_STROBE_NO + ) + self._attr_is_on = False + self.async_write_ha_state() + + @callback + def async_set_off(self, _) -> None: + """Set is_on to False and write HA state.""" + self._attr_is_on = False + if self._off_listener: + self._off_listener() + self._off_listener = None + self.async_write_ha_state() diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py new file mode 100644 index 00000000000..9a10f55f25a --- /dev/null +++ b/tests/components/zha/test_siren.py @@ -0,0 +1,139 @@ +"""Test zha siren.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest +from zigpy.const import SIG_EP_PROFILE +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.siren import DOMAIN +from homeassistant.components.siren.const import ( + ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, +) +from homeassistant.components.zha.core.const import ( + WARNING_DEVICE_MODE_EMERGENCY_PANIC, + WARNING_DEVICE_SOUND_MEDIUM, +) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +import homeassistant.util.dt as dt_util + +from .common import async_enable_traffic, find_entity_id +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE + +from tests.common import async_fire_time_changed, mock_coro + + +@pytest.fixture +async def siren(hass, zigpy_device_mock, zha_device_joined_restored): + """Siren fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, security.IasWd.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device.endpoints[1].ias_wd + + +async def test_siren(hass, siren): + """Test zha siren platform.""" + + zha_device, cluster = siren + assert cluster is not None + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + assert hass.states.get(entity_id).state == STATE_OFF + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the switch was created and that its state is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + with patch( + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ): + # turn on via UI + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == 0 + assert cluster.request.call_args[0][3] == 54 # bitmask for default args + assert cluster.request.call_args[0][4] == 5 # duration in seconds + assert cluster.request.call_args[0][5] == 0 + assert cluster.request.call_args[0][6] == 2 + + # test that the state has changed to on + assert hass.states.get(entity_id).state == STATE_ON + + # turn off from HA + with patch( + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), + ): + # turn off via UI + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == 0 + assert cluster.request.call_args[0][3] == 2 # bitmask for default args + assert cluster.request.call_args[0][4] == 5 # duration in seconds + assert cluster.request.call_args[0][5] == 0 + assert cluster.request.call_args[0][6] == 2 + + # test that the state has changed to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + with patch( + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ): + # turn on via UI + await hass.services.async_call( + DOMAIN, + "turn_on", + { + "entity_id": entity_id, + ATTR_DURATION: 10, + ATTR_TONE: WARNING_DEVICE_MODE_EMERGENCY_PANIC, + ATTR_VOLUME_LEVEL: WARNING_DEVICE_SOUND_MEDIUM, + }, + blocking=True, + ) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == 0 + assert cluster.request.call_args[0][3] == 101 # bitmask for passed args + assert cluster.request.call_args[0][4] == 10 # duration in seconds + assert cluster.request.call_args[0][5] == 0 + assert cluster.request.call_args[0][6] == 2 + + # test that the state has changed to on + assert hass.states.get(entity_id).state == STATE_ON + + now = dt_util.utcnow() + timedelta(seconds=15) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 41d4b0d1bee..06d3f10556c 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -593,13 +593,21 @@ DEVICES = [ SIG_EP_PROFILE: 260, } }, - DEV_SIG_ENTITIES: ["binary_sensor.heiman_warningdevice_77665544_ias_zone"], + DEV_SIG_ENTITIES: [ + "binary_sensor.heiman_warningdevice_77665544_ias_zone", + "siren.heiman_warningdevice_77665544_ias_wd", + ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", - } + }, + ("siren", "00:11:22:33:44:55:66:77-1"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHASiren", + DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_77665544_ias_wd", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Heiman", From 74d1c340d73be3b3fa999895940099e6145650bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 19:30:12 +0100 Subject: [PATCH 1295/1452] Allow use of relative_time as a filter (#60923) --- homeassistant/helpers/template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 26a8add3de5..d460e7ab42b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1858,6 +1858,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["is_number"] = is_number self.filters["float"] = forgiving_float_filter self.filters["int"] = forgiving_int_filter + self.filters["relative_time"] = relative_time self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine From b65b25c1bb35debe8af73023df22284af5343340 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Dec 2021 19:34:48 +0100 Subject: [PATCH 1296/1452] Move MqttServiceInfo to init.py (#60905) Co-authored-by: epenet Co-authored-by: Paulus Schoutsen --- homeassistant/components/mqtt/__init__.py | 34 ++++++++++++++++ homeassistant/components/mqtt/discovery.py | 39 +------------------ .../components/tasmota/config_flow.py | 4 +- homeassistant/config_entries.py | 2 +- homeassistant/helpers/config_entry_flow.py | 3 +- tests/components/mqtt/test_discovery.py | 30 +------------- tests/components/mqtt/test_init.py | 24 ++++++++++++ tests/components/tasmota/test_config_flow.py | 2 +- 8 files changed, 66 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 59f64972f6c..cdf37cd6381 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass +import datetime as dt from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -38,9 +40,11 @@ from homeassistant.core import ( ServiceCall, callback, ) +from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -246,6 +250,36 @@ MQTT_PUBLISH_SCHEMA = vol.All( SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None +@dataclass +class MqttServiceInfo(BaseServiceInfo): + """Prepared info from mqtt entries.""" + + topic: str + payload: ReceivePayloadType + qos: int + retain: bool + subscribed_topic: str + timestamp: dt.datetime + + # Used to prevent log flooding. To be removed in 2022.6 + _warning_logged: bool = False + + def __getitem__(self, name: str) -> Any: + """ + Allow property access by name for compatibility reason. + + Deprecated, and will be removed in version 2022.6. + """ + if not self._warning_logged: + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={"mqtt"}, + error_if_core=False, + ) + self._warning_logged = True + return getattr(self, name) + + def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: """Build the arguments for the publish service without the payload.""" data = {ATTR_TOPIC: topic} diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3826ce81d92..c9b0b816c4e 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,24 +1,20 @@ """Support for MQTT discovery.""" import asyncio from collections import deque -from dataclasses import dataclass -import datetime as dt import functools import json import logging import re import time -from typing import Any from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.frame import report from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -31,7 +27,6 @@ from .const import ( CONF_TOPIC, DOMAIN, ) -from .models import ReceivePayloadType _LOGGER = logging.getLogger(__name__) @@ -91,36 +86,6 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -@dataclass -class MqttServiceInfo(BaseServiceInfo): - """Prepared info from mqtt entries.""" - - topic: str - payload: ReceivePayloadType - qos: int - retain: bool - subscribed_topic: str - timestamp: dt.datetime - - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - - def __getitem__(self, name: str) -> Any: - """ - Allow property access by name for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"mqtt"}, - error_if_core=False, - ) - self._warning_logged = True - return getattr(self, name) - - async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None ) -> None: @@ -323,7 +288,7 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return - data = MqttServiceInfo( + data = mqtt.MqttServiceInfo( topic=msg.topic, payload=msg.payload, qos=msg.qos, diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index df1c5f17de2..e2109a16afe 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import discovery as mqtt, valid_subscribe_topic +from homeassistant.components.mqtt import MqttServiceInfo, valid_subscribe_topic from homeassistant.data_entry_flow import FlowResult from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN @@ -21,7 +21,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize flow.""" self._prefix = DEFAULT_PREFIX - async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: + async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Handle a flow initialized by MQTT discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c6ad8d0a587..45bc10f5774 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -35,7 +35,7 @@ import homeassistant.util.uuid as uuid_util if TYPE_CHECKING: from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.hassio import HassioServiceInfo - from homeassistant.components.mqtt.discovery import MqttServiceInfo + from homeassistant.components.mqtt import MqttServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 66bb36b7dc6..0a565b3b9eb 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,8 +5,7 @@ import logging from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries -from homeassistant.components import dhcp, ssdp, zeroconf -from homeassistant.components.mqtt import discovery as mqtt +from homeassistant.components import dhcp, mqtt, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index faa9f714ae0..a97047195fb 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -11,11 +11,7 @@ from homeassistant.components.mqtt.abbreviations import ( ABBREVIATIONS, DEVICE_ABBREVIATIONS, ) -from homeassistant.components.mqtt.discovery import ( - ALREADY_DISCOVERED, - MqttServiceInfo, - async_start, -) +from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -909,27 +905,3 @@ async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock await hass.async_block_till_done() await hass.async_block_till_done() mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#") - - -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = MqttServiceInfo( - topic="tasmota/discovery/DC4F220848A2/config", - payload="", - qos=0, - retain=False, - subscribed_topic="tasmota/discovery/#", - timestamp=None, - ) - - # Ensure first call get logged - assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" - assert "Detected code that accessed discovery_info['topic']" in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" - assert "Detected code that accessed discovery_info['topic']" not in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9b862e38a7c..acc25b59442 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1812,3 +1812,27 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.called assert mqtt_mock.async_publish.call_args[0][1] == test_str + + +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = mqtt.MqttServiceInfo( + topic="tasmota/discovery/DC4F220848A2/config", + payload="", + qos=0, + retain=False, + subscribed_topic="tasmota/discovery/#", + timestamp=None, + ) + + # Ensure first call get logged + assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" + assert "Detected code that accessed discovery_info['topic']" in caplog.text + + # Ensure second call doesn't get logged + caplog.clear() + assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" + assert "Detected code that accessed discovery_info['topic']" not in caplog.text diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 5f1c45ce138..3413817892b 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" from homeassistant import config_entries -from homeassistant.components.mqtt import discovery as mqtt +from homeassistant.components import mqtt from tests.common import MockConfigEntry From fdb13726f601e55f082d9ac1a163414cf68de61e Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 3 Dec 2021 15:38:32 -0300 Subject: [PATCH 1297/1452] Add more Tuya Vacuum sensors and select entities (#60927) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/const.py | 15 +++++ homeassistant/components/tuya/select.py | 28 +++++++++ homeassistant/components/tuya/sensor.py | 58 +++++++++++++++++++ .../tuya/translations/select.en.json | 34 ++++++++++- 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 66142998691..fa69b76695c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -60,6 +60,9 @@ DEVICE_CLASS_TUYA_RECORD_MODE = "tuya__record_mode" DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" DEVICE_CLASS_TUYA_STATUS = "tuya__status" DEVICE_CLASS_TUYA_FINGERBOT_MODE = "tuya__fingerbot_mode" +DEVICE_CLASS_TUYA_VACUUM_CISTERN = "tuya__vacuum_cistern" +DEVICE_CLASS_TUYA_VACUUM_COLLECTION = "tuya__vacuum_collection" +DEVICE_CLASS_TUYA_VACUUM_MODE = "tuya__vacuum_mode" TUYA_DISCOVERY_NEW = "tuya_discovery_new" TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" @@ -145,12 +148,16 @@ class DPCode(str, Enum): CH4_SENSOR_STATE = "ch4_sensor_state" CH4_SENSOR_VALUE = "ch4_sensor_value" CHILD_LOCK = "child_lock" # Child lock + CISTERN = "cistern" + CLEAN_AREA = "clean_area" + CLEAN_TIME = "clean_time" CLICK_SUSTAIN_TIME = "click_sustain_time" CO_STATE = "co_state" CO_STATUS = "co_status" CO_VALUE = "co_value" CO2_STATE = "co2_state" CO2_VALUE = "co2_value" # CO2 concentration + COLLECTION_MODE = "collection_mode" COLOR_DATA_V2 = "color_data_v2" COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode @@ -171,11 +178,15 @@ class DPCode(str, Enum): DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor DOORCONTACT_STATE_2 = "doorcontact_state_2" DOORCONTACT_STATE_3 = "doorcontact_state_3" + DUSTER_CLOTH = "duster_cloth" + EDGE_BRUSH = "edge_brush" ELECTRICITY_LEFT = "electricity_left" FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed FAR_DETECTION = "far_detection" + FAULT = "fault" + FILTER_LIFE = "filter" FILTER_RESET = "filter_reset" # Filter (cartridge) reset FLOODLIGHT_LIGHTNESS = "floodlight_lightness" FLOODLIGHT_SWITCH = "floodlight_switch" @@ -232,6 +243,7 @@ class DPCode(str, Enum): RESET_FILTER = "reset_filter" RESET_MAP = "reset_map" RESET_ROLL_BRUSH = "reset_roll_brush" + ROLL_BRUSH = "roll_brush" SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity SENSOR_HUMIDITY = "sensor_humidity" @@ -288,6 +300,9 @@ class DPCode(str, Enum): TEMP_VALUE = "temp_value" # Color temperature TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm + TOTAL_CLEAN_AREA = "total_clean_area" + TOTAL_CLEAN_COUNT = "total_clean_count" + TOTAL_CLEAN_TIME = "total_clean_time" UV = "uv" # UV sterilization VA_BATTERY = "va_battery" VA_HUMIDITY = "va_humidity" diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 9d5012c6578..ca05acbb3e5 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -26,6 +26,9 @@ from .const import ( DEVICE_CLASS_TUYA_MOTION_SENSITIVITY, DEVICE_CLASS_TUYA_RECORD_MODE, DEVICE_CLASS_TUYA_RELAY_STATUS, + DEVICE_CLASS_TUYA_VACUUM_CISTERN, + DEVICE_CLASS_TUYA_VACUUM_COLLECTION, + DEVICE_CLASS_TUYA_VACUUM_MODE, DOMAIN, TUYA_DISCOVERY_NEW, DPCode, @@ -219,6 +222,31 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Robot Vacuum + # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo + "sd": ( + SelectEntityDescription( + key=DPCode.CISTERN, + name="Water Tank Adjustment", + entity_category=EntityCategory.CONFIG, + device_class=DEVICE_CLASS_TUYA_VACUUM_CISTERN, + icon="mdi:water-opacity", + ), + SelectEntityDescription( + key=DPCode.COLLECTION_MODE, + name="Dust Collection Mode", + entity_category=EntityCategory.CONFIG, + device_class=DEVICE_CLASS_TUYA_VACUUM_COLLECTION, + icon="mdi:air-filter", + ), + SelectEntityDescription( + key=DPCode.MODE, + name="Mode", + entity_category=EntityCategory.CONFIG, + device_class=DEVICE_CLASS_TUYA_VACUUM_MODE, + icon="mdi:layers-outline", + ), + ), } diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 62a330cbefb..d6870d4b9bb 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -579,6 +579,64 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { subkey="voltage", ), ), + # Robot Vacuum + # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo + "sd": ( + TuyaSensorEntityDescription( + key=DPCode.CLEAN_AREA, + name="Cleaning Area", + icon="mdi:texture-box", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.CLEAN_TIME, + name="Cleaning Time", + icon="mdi:progress-clock", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_CLEAN_AREA, + name="Total Cleaning Area", + icon="mdi:texture-box", + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_CLEAN_TIME, + name="Total Cleaning Time", + icon="mdi:history", + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_CLEAN_COUNT, + name="Total Cleaning Times", + icon="mdi:counter", + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.DUSTER_CLOTH, + name="Duster Cloth Life", + icon="mdi:ticket-percent-outline", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.EDGE_BRUSH, + name="Side Brush Life", + icon="mdi:ticket-percent-outline", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.FILTER_LIFE, + name="Filter Life", + icon="mdi:ticket-percent-outline", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.ROLL_BRUSH, + name="Rolling Brush Life", + icon="mdi:ticket-percent-outline", + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index 55c8d3f2f90..22127d17f8a 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -48,6 +48,38 @@ "on": "On", "power_off": "Off", "power_on": "On" + }, + "tuya__vacuum_cistern": { + "low": "Low", + "middle": "Middle", + "high": "High", + "closed": "Closed" + }, + "tuya__vacuum_collection": { + "small": "Small", + "middle": "Middle", + "large": "Large" + }, + "tuya__vacuum_mode": { + "standby": "Standby", + "random": "Random", + "smart": "Smart", + "wall_follow": "Follow Wall", + "mop": "Mop", + "spiral": "Spiral", + "left_spiral": "Spiral Left", + "right_spiral": "Spiral Right", + "bow": "Bow", + "left_bow": "Bow Lef", + "right_bow": "Bow Right", + "partial_bow": "Bow Partially", + "chargego": "Return to dock", + "single": "Single", + "zone": "Zone", + "pose": "Pose", + "point": "Point", + "part": "Part", + "pick_zone": "Pick Zone" } } -} \ No newline at end of file +} From 40f1b0d3a5f7a45a17460b45537be122d9ad093f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 3 Dec 2021 19:43:01 +0100 Subject: [PATCH 1298/1452] Add quality scale for Fronius (#60531) --- homeassistant/components/fronius/manifest.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index edce9bab944..d2f3fc2e0f3 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,14 +1,15 @@ { - "domain": "fronius", + "codeowners": ["@nielstron", "@farmio"], + "config_flow": true, "dhcp": [ { "macaddress": "0003AC*" } ], - "name": "Fronius", - "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.7.1"], - "codeowners": ["@nielstron", "@farmio"], - "iot_class": "local_polling" + "domain": "fronius", + "iot_class": "local_polling", + "name": "Fronius", + "quality_scale": "platinum", + "requirements": ["pyfronius==0.7.1"] } From ba99dc3af9db5191e05987267ea57eb717421d75 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Dec 2021 10:53:05 -0800 Subject: [PATCH 1299/1452] Add Nest Battery Cam event clip support with a Nest MediaSource (#60073) --- homeassistant/components/nest/__init__.py | 65 +- homeassistant/components/nest/events.py | 8 + homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 259 ++++++++ tests/components/nest/test_media_source.py | 569 ++++++++++++++++++ 5 files changed, 901 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/nest/media_source.py create mode 100644 tests/components/nest/test_media_source.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0933f10e6ce..af3757d31da 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,7 +1,10 @@ """Support for Nest devices.""" +from __future__ import annotations +from http import HTTPStatus import logging +from aiohttp import web from google_nest_sdm.event import EventMessage from google_nest_sdm.exceptions import ( AuthException, @@ -10,6 +13,9 @@ from google_nest_sdm.exceptions import ( ) import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.components.http.const import KEY_HASS_USER +from homeassistant.components.http.view import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -20,8 +26,14 @@ from homeassistant.const import ( CONF_STRUCTURE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, + Unauthorized, +) from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType from . import api, config_flow @@ -38,6 +50,7 @@ from .const import ( ) from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry +from .media_source import get_media_source_devices _LOGGER = logging.getLogger(__name__) @@ -226,6 +239,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.http.register_view(NestEventMediaView(hass)) + return True @@ -264,3 +279,51 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: ) finally: subscriber.stop_async() + + +class NestEventMediaView(HomeAssistantView): + """Returns media for related to events for a specific device. + + This is primarily used to render media for events for MediaSource. The media type + depends on the specific device e.g. an image, or a movie clip preview. + """ + + url = "/api/nest/event_media/{device_id}/{event_id}" + name = "api:nest:event_media" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize NestEventMediaView.""" + self.hass = hass + + async def get( + self, request: web.Request, device_id: str, event_id: str + ) -> web.StreamResponse: + """Start a GET request.""" + user = request[KEY_HASS_USER] + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + for entry in async_entries_for_device(entity_registry, device_id): + if not user.permissions.check_entity(entry.entity_id, POLICY_READ): + raise Unauthorized(entity_id=entry.entity_id) + + devices = await get_media_source_devices(self.hass) + if not (nest_device := devices.get(device_id)): + return self._json_error( + f"No Nest Device found for '{device_id}'", HTTPStatus.NOT_FOUND + ) + try: + event_media = await nest_device.event_media_manager.get_media(event_id) + except GoogleNestException as err: + raise HomeAssistantError("Unable to fetch media for event") from err + if not event_media: + return self._json_error( + f"No event found for event_id '{event_id}'", HTTPStatus.NOT_FOUND + ) + media = event_media.media + return web.Response( + body=media.contents, content_type=media.event_image_type.content_type + ) + + def _json_error(self, message: str, status: HTTPStatus) -> web.StreamResponse: + """Return a json error message with additional logging.""" + _LOGGER.debug(message) + return self.json_message(message, status) diff --git a/homeassistant/components/nest/events.py b/homeassistant/components/nest/events.py index 6802a98cc40..10983768e17 100644 --- a/homeassistant/components/nest/events.py +++ b/homeassistant/components/nest/events.py @@ -57,3 +57,11 @@ EVENT_NAME_MAP = { CameraPersonEvent.NAME: EVENT_CAMERA_PERSON, CameraSoundEvent.NAME: EVENT_CAMERA_SOUND, } + +# Names for event types shown in the media source +MEDIA_SOURCE_EVENT_TITLE_MAP = { + DoorbellChimeEvent.NAME: "Doorbell", + CameraMotionEvent.NAME: "Motion", + CameraPersonEvent.NAME: "Person", + CameraSoundEvent.NAME: "Sound", +} diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 488aeb9d053..94b2c338528 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http"], + "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py new file mode 100644 index 00000000000..e21be20380a --- /dev/null +++ b/homeassistant/components/nest/media_source.py @@ -0,0 +1,259 @@ +"""Nest Media Source implementation. + +The Nest MediaSource implementation provides a directory tree of devices and +events and associated media (e.g. an image or clip). Camera device events +publish an event message, received by the subscriber library. Media for an +event, such as camera image or clip, may be fetched from the cloud during a +short time window after the event happens. + +The actual management of associating events to devices, fetching media for +events, caching, and the overall lifetime of recent events are managed outside +of the Nest MediaSource. + +Users may also record clips to local storage, unrelated to this MediaSource. + +For additional background on Nest Camera events see: +https://developers.google.com/nest/device-access/api/camera#handle_camera_events +""" + +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Mapping +from dataclasses import dataclass +import logging + +from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait +from google_nest_sdm.device import Device +from google_nest_sdm.event import ImageEventBase + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_VIDEO, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN +from homeassistant.components.nest.device_info import NestDeviceInfo +from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +MEDIA_SOURCE_TITLE = "Nest" +DEVICE_TITLE_FORMAT = "{device_name}: Recent Events" +CLIP_TITLE_FORMAT = "{event_name} @ {event_time}" +EVENT_MEDIA_API_URL_FORMAT = "/api/nest/event_media/{device_id}/{event_id}" + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up Nest media source.""" + return NestMediaSource(hass) + + +async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: + """Return a mapping of device id to eligible Nest event media devices.""" + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + device_manager = await subscriber.async_get_device_manager() + device_registry = await hass.helpers.device_registry.async_get_registry() + devices = {} + for device in device_manager.devices.values(): + if not ( + CameraEventImageTrait.NAME in device.traits + or CameraClipPreviewTrait.NAME in device.traits + ): + continue + if device_entry := device_registry.async_get_device({(DOMAIN, device.name)}): + devices[device_entry.id] = device + return devices + + +@dataclass +class MediaId: + """Media identifier for a node in the Media Browse tree. + + A MediaId can refer to either a device, or a specific event for a device + that is associated with media (e.g. image or video clip). + """ + + device_id: str + event_id: str | None = None + + @property + def identifier(self) -> str: + """Media identifier represented as a string.""" + if self.event_id: + return f"{self.device_id}/{self.event_id}" + return self.device_id + + +def parse_media_id(identifier: str | None = None) -> MediaId | None: + """Parse the identifier path string into a MediaId.""" + if identifier is None or identifier == "": + return None + parts = identifier.split("/") + if len(parts) > 1: + return MediaId(parts[0], parts[1]) + return MediaId(parts[0]) + + +class NestMediaSource(MediaSource): + """Provide Nest Media Sources for Nest Cameras. + + The media source generates a directory tree of devices and media associated + with events for each device (e.g. motion, person, etc). Each node in the + tree has a unique MediaId. + + The lifecycle for event media is handled outside of NestMediaSource, and + instead it just asks the device for all events it knows about. + """ + + name: str = MEDIA_SOURCE_TITLE + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize NestMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media identifier to a url.""" + media_id: MediaId | None = parse_media_id(item.identifier) + if not media_id: + raise Unresolvable("No identifier specified for MediaSourceItem") + if not media_id.event_id: + raise Unresolvable("Identifier missing an event_id: %s" % item.identifier) + devices = await self.devices() + if not (device := devices.get(media_id.device_id)): + raise Unresolvable( + "Unable to find device with identifier: %s" % item.identifier + ) + events = _get_events(device) + if media_id.event_id not in events: + raise Unresolvable( + "Unable to find event with identifier: %s" % item.identifier + ) + event = events[media_id.event_id] + return PlayMedia( + EVENT_MEDIA_API_URL_FORMAT.format( + device_id=media_id.device_id, event_id=media_id.event_id + ), + event.event_image_type.content_type, + ) + + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: + """Return media for the specified level of the directory tree. + + The top level is the root that contains devices. Inside each device are + media for events for that device. + """ + media_id: MediaId | None = parse_media_id(item.identifier) + _LOGGER.debug( + "Browsing media for identifier=%s, media_id=%s", item.identifier, media_id + ) + devices = await self.devices() + if media_id is None: + # Browse the root and return child devices + browse_root = _browse_root() + browse_root.children = [] + for device_id, child_device in devices.items(): + browse_root.children.append( + _browse_device(MediaId(device_id), child_device) + ) + return browse_root + + # Browse either a device or events within a device + if not (device := devices.get(media_id.device_id)): + raise BrowseError( + "Unable to find device with identiifer: %s" % item.identifier + ) + if media_id.event_id is None: + # Browse a specific device and return child events + browse_device = _browse_device(media_id, device) + browse_device.children = [] + events = _get_events(device) + for child_event in events.values(): + event_id = MediaId(media_id.device_id, child_event.event_id) + browse_device.children.append( + _browse_event(event_id, device, child_event) + ) + return browse_device + + # Browse a specific event + events = _get_events(device) + if not (event := events.get(media_id.event_id)): + raise BrowseError( + "Unable to find event with identiifer: %s" % item.identifier + ) + return _browse_event(media_id, device, event) + + async def devices(self) -> Mapping[str, Device]: + """Return all event media related devices.""" + return await get_media_source_devices(self.hass) + + +def _get_events(device: Device) -> Mapping[str, ImageEventBase]: + """Return relevant events for the specified device.""" + return OrderedDict({e.event_id: e for e in device.event_media_manager.events}) + + +def _browse_root() -> BrowseMediaSource: + """Return devices in the root.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_VIDEO, + children_media_class=MEDIA_CLASS_VIDEO, + title=MEDIA_SOURCE_TITLE, + can_play=False, + can_expand=True, + thumbnail=None, + children=[], + ) + + +def _browse_device(device_id: MediaId, device: Device) -> BrowseMediaSource: + """Return details for the specified device.""" + device_info = NestDeviceInfo(device) + return BrowseMediaSource( + domain=DOMAIN, + identifier=device_id.identifier, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_VIDEO, + children_media_class=MEDIA_CLASS_VIDEO, + title=DEVICE_TITLE_FORMAT.format(device_name=device_info.device_name), + can_play=False, + can_expand=True, + thumbnail=None, + children=[], + ) + + +def _browse_event( + event_id: MediaId, device: Device, event: ImageEventBase +) -> BrowseMediaSource: + """Build a BrowseMediaSource for a specific event.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier=event_id.identifier, + media_class=MEDIA_CLASS_IMAGE, + media_content_type=MEDIA_TYPE_IMAGE, + title=CLIP_TITLE_FORMAT.format( + event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), + event_time=dt_util.as_local(event.timestamp), + ), + can_play=True, + can_expand=False, + thumbnail=None, + children=[], + ) diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py new file mode 100644 index 00000000000..4cda781ebeb --- /dev/null +++ b/tests/components/nest/test_media_source.py @@ -0,0 +1,569 @@ +"""Test for Nest Media Source. + +These tests simulate recent camera events received by the subscriber exposed +as media in the media source. +""" + +import datetime +from http import HTTPStatus + +import aiohttp +from google_nest_sdm.device import Device +from google_nest_sdm.event import EventMessage +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source import const +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.helpers import device_registry as dr +import homeassistant.util.dt as dt_util + +from .common import async_setup_sdm_platform + +DOMAIN = "nest" +DEVICE_ID = "example/api/device/id" +DEVICE_NAME = "Front" +PLATFORM = "camera" +NEST_EVENT = "nest_event" +EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." +CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" +CAMERA_TRAITS = { + "sdm.devices.traits.Info": { + "customName": DEVICE_NAME, + }, + "sdm.devices.traits.CameraImage": {}, + "sdm.devices.traits.CameraEventImage": {}, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraMotion": {}, +} +BATTERY_CAMERA_TRAITS = { + "sdm.devices.traits.Info": { + "customName": DEVICE_NAME, + }, + "sdm.devices.traits.CameraClipPreview": {}, + "sdm.devices.traits.CameraLiveStream": {}, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraMotion": {}, +} +PERSON_EVENT = "sdm.devices.events.CameraPerson.Person" +MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion" + +TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." +GENERATE_IMAGE_URL_RESPONSE = { + "results": { + "url": TEST_IMAGE_URL, + "token": "g.0.eventToken", + }, +} +IMAGE_BYTES_FROM_EVENT = b"test url image bytes" +IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} + + +async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): + """Set up the platform and prerequisites.""" + devices = { + DEVICE_ID: Device.MakeDevice( + { + "name": DEVICE_ID, + "type": device_type, + "traits": traits, + }, + auth=auth, + ), + } + subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + if events: + for event in events: + await subscriber.async_receive_event(event) + await hass.async_block_till_done() + return subscriber + + +def create_event(event_id, event_type, timestamp=None): + """Create an EventMessage for a single event type.""" + if not timestamp: + timestamp = dt_util.now() + event_data = { + event_type: { + "eventSessionId": EVENT_SESSION_ID, + "eventId": event_id, + }, + } + return create_event_message(event_id, event_data, timestamp) + + +def create_event_message(event_id, event_data, timestamp): + """Create an EventMessage for a single event type.""" + return EventMessage( + { + "eventId": f"{event_id}-{timestamp}", + "timestamp": timestamp.isoformat(timespec="seconds"), + "resourceUpdate": { + "name": DEVICE_ID, + "events": event_data, + }, + }, + auth=None, + ) + + +async def test_no_eligible_devices(hass, auth): + """Test a media source with no eligible camera devices.""" + await async_setup_devices( + hass, + auth, + "sdm.devices.types.THERMOSTAT", + { + "sdm.devices.traits.Temperature": {}, + }, + ) + + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.identifier == "" + assert browse.title == "Nest" + assert not browse.children + + +async def test_supported_device(hass, auth): + """Test a media source with a supported camera.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.title == "Nest" + assert browse.identifier == "" + assert browse.can_expand + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == device.id + assert browse.children[0].title == "Front: Recent Events" + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + assert len(browse.children) == 0 + + +async def test_camera_event(hass, auth, hass_client): + """Test a media source and image created for an event.""" + event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_timestamp = dt_util.now() + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + events=[ + create_event( + event_id, + PERSON_EVENT, + timestamp=event_timestamp, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + # Media root directory + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.title == "Nest" + assert browse.identifier == "" + assert browse.can_expand + # A device is represented as a child directory + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == device.id + assert browse.children[0].title == "Front: Recent Events" + assert browse.children[0].can_expand + # Expanding the root does not expand the device + assert len(browse.children[0].children) == 0 + + # Browse to the device + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + assert browse.can_expand + # The device expands recent events + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == f"{device.id}/{event_id}" + event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + assert browse.children[0].title == f"Person @ {event_timestamp_string}" + assert not browse.children[0].can_expand + assert len(browse.children[0].children) == 0 + + # Browse to the event + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == f"{device.id}/{event_id}" + assert "Person" in browse.title + assert not browse.can_expand + assert not browse.children + + # Resolving the event links to the media + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.mime_type == "image/jpeg" + + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + client = await hass_client() + response = await client.get(media.url) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_event_order(hass, auth): + """Test multiple events are in descending timestamp order.""" + event_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_timestamp1 = dt_util.now() + event_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." + event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + events=[ + create_event( + event_id1, + PERSON_EVENT, + timestamp=event_timestamp1, + ), + create_event( + event_id2, + MOTION_EVENT, + timestamp=event_timestamp2, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + assert browse.can_expand + + # Motion event is most recent + assert len(browse.children) == 2 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == f"{device.id}/{event_id2}" + event_timestamp_string = event_timestamp2.isoformat(timespec="seconds", sep=" ") + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" + assert not browse.children[0].can_expand + + # Person event is next + assert browse.children[1].domain == DOMAIN + + assert browse.children[1].identifier == f"{device.id}/{event_id1}" + event_timestamp_string = event_timestamp1.isoformat(timespec="seconds", sep=" ") + assert browse.children[1].title == f"Person @ {event_timestamp_string}" + assert not browse.children[1].can_expand + + +async def test_browse_invalid_device_id(hass, auth): + """Test a media source request for an invalid device id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + with pytest.raises(BrowseError): + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id" + ) + + with pytest.raises(BrowseError): + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/invalid-event-id" + ) + + +async def test_browse_invalid_event_id(hass, auth): + """Test a media source browsing for an invalid event id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + + with pytest.raises(BrowseError): + await media_source.async_browse_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + ) + + +async def test_resolve_missing_event_id(hass, auth): + """Test a media source request missing an event id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + with pytest.raises(Unresolvable): + await media_source.async_resolve_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{device.id}", + ) + + +async def test_resolve_invalid_device_id(hass, auth): + """Test resolving media for an invalid event id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + with pytest.raises(Unresolvable): + await media_source.async_resolve_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + ) + + +async def test_resolve_invalid_event_id(hass, auth): + """Test resolving media for an invalid event id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + with pytest.raises(Unresolvable): + await media_source.async_resolve_media( + hass, + f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + ) + + +async def test_camera_event_clip_preview(hass, auth, hass_client): + """Test an event for a battery camera video clip.""" + event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_timestamp = dt_util.now() + event_data = { + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "https://127.0.0.1/example", + }, + } + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + BATTERY_CAMERA_TRAITS, + events=[ + create_event_message( + event_id, + event_data, + timestamp=event_timestamp, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + # Browse to the device + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + assert browse.can_expand + # The device expands recent events + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + actual_event_id = browse.children[0].identifier + event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + assert browse.children[0].title == f"Event @ {event_timestamp_string}" + assert not browse.children[0].can_expand + assert len(browse.children[0].children) == 0 + + # Resolving the event links to the media + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{actual_event_id}" + ) + assert media.url == f"/api/nest/event_media/{actual_event_id}" + assert media.mime_type == "video/mp4" + + auth.responses = [ + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + client = await hass_client() + response = await client.get(media.url) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_event_media_render_invalid_device_id(hass, auth, hass_client): + """Test event media API called with an invalid device id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + client = await hass_client() + response = await client.get("/api/nest/event_media/invalid-device-id") + assert response.status == HTTPStatus.NOT_FOUND, ( + "Response not matched: %s" % response + ) + + +async def test_event_media_render_invalid_event_id(hass, auth, hass_client): + """Test event media API called with an invalid device id.""" + await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + client = await hass_client() + response = await client.get("/api/nest/event_media/{device.id}/invalid-event-id") + assert response.status == HTTPStatus.NOT_FOUND, ( + "Response not matched: %s" % response + ) + + +async def test_event_media_failure(hass, auth, hass_client): + """Test event media fetch sees a failure from the server.""" + event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_timestamp = dt_util.now() + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + events=[ + create_event( + event_id, + PERSON_EVENT, + timestamp=event_timestamp, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + # Resolving the event links to the media + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.mime_type == "image/jpeg" + + auth.responses = [ + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), + ] + + client = await hass_client() + response = await client.get(media.url) + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR, ( + "Response not matched: %s" % response + ) + + +async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): + """Test case where user does not have permissions to view media.""" + event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_timestamp = dt_util.now() + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + events=[ + create_event( + event_id, + PERSON_EVENT, + timestamp=event_timestamp, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + media_url = f"/api/nest/event_media/{device.id}/{event_id}" + + # Empty policy with no access to the entity + hass_admin_user.mock_policy({}) + + client = await hass_client() + response = await client.get(media_url) + assert response.status == HTTPStatus.UNAUTHORIZED, ( + "Response not matched: %s" % response + ) From 171b57bf3210da9ec9a44b102e497d41efd0670b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Dec 2021 12:57:19 -0600 Subject: [PATCH 1300/1452] Use _attrs where possible in Sonos (#60931) --- .../components/sonos/binary_sensor.py | 20 ++++--------- .../components/sonos/media_player.py | 29 ++++++------------- homeassistant/components/sonos/number.py | 12 ++------ homeassistant/components/sonos/sensor.py | 26 +++++------------ homeassistant/components/sonos/switch.py | 13 ++------- 5 files changed, 26 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 488f29a7be8..615ad24e655 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -33,21 +33,13 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Representation of a Sonos power entity.""" _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return f"{self.soco.uid}-power" - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self.speaker.zone_name} Power" - - @property - def device_class(self) -> str: - """Return the entity's device class.""" - return DEVICE_CLASS_BATTERY_CHARGING + def __init__(self, speaker: SonosSpeaker) -> None: + """Initialize the power entity binary sensor.""" + super().__init__(speaker) + self._attr_unique_id = f"{self.soco.uid}-power" + self._attr_name = f"{self.speaker.zone_name} Power" async def _async_poll(self) -> None: """Poll the device for the current state.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 664245a1c99..90c33d7a4e6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -238,25 +238,24 @@ async def async_setup_entry( class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" + _attr_supported_features = SUPPORT_SONOS + _attr_media_content_type = MEDIA_TYPE_MUSIC + + def __init__(self, speaker: SonosSpeaker) -> None: + """Initialize the media player entity.""" + super().__init__(speaker) + self._attr_unique_id = self.soco.uid + self._attr_name = self.speaker.zone_name + @property def coordinator(self) -> SonosSpeaker: """Return the current coordinator SonosSpeaker.""" return self.speaker.coordinator or self.speaker - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self.soco.uid # type: ignore[no-any-return] - def __hash__(self) -> int: """Return a hash of self.""" return hash(self.unique_id) - @property - def name(self) -> str: - """Return the name of the entity.""" - return self.speaker.zone_name # type: ignore[no-any-return] - @property # type: ignore[misc] def state(self) -> str: """Return the state of the entity.""" @@ -322,11 +321,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Content id of current playing media.""" return self.media.uri - @property - def media_content_type(self) -> str: - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property # type: ignore[misc] def media_duration(self) -> float | None: """Duration of current playing media in seconds.""" @@ -377,11 +371,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Name of the current input source.""" return self.media.source_name or None - @property # type: ignore[misc] - def supported_features(self) -> int: - """Flag media player features that are supported.""" - return SUPPORT_SONOS - @soco_error() def volume_up(self) -> None: """Volume up media player.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 574eba95137..2bcfe5cd5ec 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -39,18 +39,10 @@ class SonosLevelEntity(SonosEntity, NumberEntity): def __init__(self, speaker: SonosSpeaker, level_type: str) -> None: """Initialize the level entity.""" super().__init__(speaker) + self._attr_unique_id = f"{self.soco.uid}-{level_type}" + self._attr_name = f"{self.speaker.zone_name} {level_type.capitalize()}" self.level_type = level_type - @property - def unique_id(self) -> str: - """Return the unique ID.""" - return f"{self.soco.uid}-{self.level_type}" - - @property - def name(self) -> str: - """Return the name.""" - return f"{self.speaker.zone_name} {self.level_type.capitalize()}" - async def _async_poll(self) -> None: """Poll the value if subscriptions are not working.""" # Handled by SonosSpeaker diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 7c9235cf4af..62017f4d541 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -45,27 +45,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosBatteryEntity(SonosEntity, SensorEntity): """Representation of a Sonos Battery entity.""" + _attr_device_class = DEVICE_CLASS_BATTERY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_native_unit_of_measurement = PERCENTAGE - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return f"{self.soco.uid}-battery" - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self.speaker.zone_name} Battery" - - @property - def device_class(self) -> str: - """Return the entity's device class.""" - return DEVICE_CLASS_BATTERY - - @property - def native_unit_of_measurement(self) -> str: - """Get the unit of measurement.""" - return PERCENTAGE + def __init__(self, speaker: SonosSpeaker) -> None: + """Initialize the battery sensor.""" + super().__init__(speaker) + self._attr_unique_id = f"{self.soco.uid}-battery" + self._attr_name = f"{self.speaker.zone_name} Battery" async def _async_poll(self) -> None: """Poll the device for the current state.""" diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index f6fe81953f5..e92263991ab 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -195,11 +195,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): """Representation of a Sonos Alarm entity.""" _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_icon = "mdi:alarm" def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None: """Initialize the switch.""" super().__init__(speaker) - + self._attr_unique_id = f"{SONOS_DOMAIN}-{alarm_id}" self.alarm_id = alarm_id self.household_id = speaker.household_id self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") @@ -220,16 +221,6 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id) - @property - def unique_id(self) -> str: - """Return the unique ID of the switch.""" - return f"{SONOS_DOMAIN}-{self.alarm_id}" - - @property - def icon(self): - """Return icon of Sonos alarm switch.""" - return "mdi:alarm" - @property def name(self) -> str: """Return the name of the sensor.""" From 88b93546f3c646587ea1a2c7a9a3934e8e6f92c0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 3 Dec 2021 20:04:05 +0100 Subject: [PATCH 1301/1452] Re-add-tests with new filters removed with #60854 (#60895) --- tests/components/mqtt/test_binary_sensor.py | 33 ++++++++ tests/components/mqtt/test_discovery.py | 94 +++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index ba601fd094d..aebdc8b692e 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -373,6 +373,39 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( assert "template output: 'ILLEGAL'" in caplog.text +async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( + hass, mqtt_mock, caplog +): + """Test processing a raw value via MQTT.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "encoding": "", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{%if value|unpack('b')-%}ON{%else%}OFF{%-endif-%}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test-topic", b"\x01") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test-topic", b"\x00") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + async def test_setting_sensor_value_via_mqtt_message_empty_template( hass, mqtt_mock, caplog ): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index a97047195fb..de9150de1a2 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -733,6 +733,100 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): ) +async def test_discovery_expansion_without_encoding_and_value_template_1( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template as list.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "encoding":"",' + ' "availability": [{' + ' "topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "value_template":"{{value|unpack(\'b\')}}"' + " }]," + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_expansion_without_encoding_and_value_template_2( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template directly.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability_topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "encoding":"",' + ' "availability_template":"{{ value | unpack(\'b\') }}",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE", From c915aa1493635d84797a91678f9da678ddd2d074 Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Fri, 3 Dec 2021 11:07:03 -0800 Subject: [PATCH 1302/1452] Update volvooncall, add hybrid plug status (#58919) --- CODEOWNERS | 1 + .../components/volvooncall/__init__.py | 22 ++++++++++++++----- .../components/volvooncall/device_tracker.py | 4 ++-- .../components/volvooncall/manifest.json | 4 ++-- requirements_all.txt | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7b872ade7ff..513115a9953 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -585,6 +585,7 @@ homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund +homeassistant/components/volvooncall/* @molobrakos @decompil3d homeassistant/components/wake_on_lan/* @ntilley905 homeassistant/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 556a5f25114..1d4f95b9341 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -156,7 +156,12 @@ async def async_setup(hass, config): hass, PLATFORMS[instrument.component], DOMAIN, - (vehicle.vin, instrument.component, instrument.attr), + ( + vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ), config, ) ) @@ -192,7 +197,7 @@ class VolvoData: self.config = config[DOMAIN] self.names = self.config.get(CONF_NAME) - def instrument(self, vin, component, attr): + def instrument(self, vin, component, attr, slug_attr): """Return corresponding instrument.""" return next( ( @@ -201,6 +206,7 @@ class VolvoData: if instrument.vehicle.vin == vin and instrument.component == component and instrument.attr == attr + and instrument.slug_attr == slug_attr ), None, ) @@ -223,12 +229,13 @@ class VolvoData: class VolvoEntity(Entity): """Base class for all VOC entities.""" - def __init__(self, data, vin, component, attribute): + def __init__(self, data, vin, component, attribute, slug_attr): """Initialize the entity.""" self.data = data self.vin = vin self.component = component self.attribute = attribute + self.slug_attr = slug_attr async def async_added_to_hass(self): """Register update dispatcher.""" @@ -241,7 +248,9 @@ class VolvoEntity(Entity): @property def instrument(self): """Return corresponding instrument.""" - return self.data.instrument(self.vin, self.component, self.attribute) + return self.data.instrument( + self.vin, self.component, self.attribute, self.slug_attr + ) @property def icon(self): @@ -287,4 +296,7 @@ class VolvoEntity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - return f"{self.vin}-{self.component}-{self.attribute}" + slug_override = "" + if self.instrument.slug_override is not None: + slug_override = f"-{self.instrument.slug_override}" + return f"{self.vin}-{self.component}-{self.attribute}{slug_override}" diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index ebc4990db55..15615c9d807 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -11,9 +11,9 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): if discovery_info is None: return - vin, component, attr = discovery_info + vin, component, attr, slug_attr = discovery_info data = hass.data[DATA_KEY] - instrument = data.instrument(vin, component, attr) + instrument = data.instrument(vin, component, attr, slug_attr) async def see_vehicle(): """Handle the reporting of the vehicle position.""" diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 5201614ab8b..eac179efa8d 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.8.12"], - "codeowners": [], + "requirements": ["volvooncall==0.9.1"], + "codeowners": ["@molobrakos", "@decompil3d"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0160ed64b6b..e49e0aae85d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2393,7 +2393,7 @@ vilfo-api-client==0.3.2 volkszaehler==0.2.1 # homeassistant.components.volvooncall -volvooncall==0.8.12 +volvooncall==0.9.1 # homeassistant.components.verisure vsure==1.7.3 From b60b38c6f676b34f831fe8a052c098bd1281e7ed Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 3 Dec 2021 20:14:21 +0100 Subject: [PATCH 1303/1452] Fix amcrest pylint issue (#60932) --- homeassistant/components/amcrest/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py index 67dc551fcb9..876deeacf91 100644 --- a/homeassistant/components/amcrest/switch.py +++ b/homeassistant/components/amcrest/switch.py @@ -58,7 +58,7 @@ class AmcrestSwitch(SwitchEntity): name: str, device: AmcrestDevice, entity_description: SwitchEntityDescription, - ): + ) -> None: """Initialize switch.""" self._api = device.api self.entity_description = entity_description From 788a9bd9f7d2d951563d498f9d1d9e6f85b526d8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:17:00 -0500 Subject: [PATCH 1304/1452] Clean up eight_sleep code (#58508) --- .../components/eight_sleep/__init__.py | 88 +++-- .../components/eight_sleep/binary_sensor.py | 51 +-- .../components/eight_sleep/sensor.py | 343 +++++++----------- 3 files changed, 213 insertions(+), 269 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 7413e5009de..09229ce767e 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -38,6 +38,7 @@ DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" USER_ENTITY = "user" + HEAT_SCAN_INTERVAL = timedelta(seconds=60) USER_SCAN_INTERVAL = timedelta(seconds=300) @@ -48,18 +49,9 @@ NAME_MAP = { "left_current_sleep": "Left Sleep Session", "left_current_sleep_fitness": "Left Sleep Fitness", "left_last_sleep": "Left Previous Sleep Session", - "left_bed_state": "Left Bed State", - "left_presence": "Left Bed Presence", - "left_bed_temp": "Left Bed Temperature", - "left_sleep_stage": "Left Sleep Stage", "right_current_sleep": "Right Sleep Session", "right_current_sleep_fitness": "Right Sleep Fitness", "right_last_sleep": "Right Previous Sleep Session", - "right_bed_state": "Right Bed State", - "right_presence": "Right Bed Presence", - "right_bed_temp": "Right Bed Temperature", - "right_sleep_stage": "Right Sleep Stage", - "room_temp": "Room Temperature", } SENSORS = [ @@ -67,7 +59,7 @@ SENSORS = [ "current_sleep_fitness", "last_sleep", "bed_state", - "bed_temp", + "bed_temperature", "sleep_stage", ] @@ -104,6 +96,14 @@ CONFIG_SCHEMA = vol.Schema( ) +def _get_device_unique_id(eight: EightSleep, user_obj: EightUser = None) -> str: + """Get the device's unique ID.""" + unique_id = eight.deviceid + if user_obj: + unique_id = f"{unique_id}.{user_obj.userid}.{user_obj.side}" + return unique_id + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Eight Sleep component.""" @@ -143,11 +143,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sensors = [] binary_sensors = [] if eight.users: - for obj in eight.users.values(): + for user, obj in eight.users.items(): for sensor in SENSORS: - sensors.append(f"{obj.side}_{sensor}") - binary_sensors.append(f"{obj.side}_presence") - sensors.append("room_temp") + sensors.append((obj.side, sensor)) + binary_sensors.append((obj.side, "bed_presence")) + sensors.append((None, "room_temperature")) else: # No users, cannot continue return False @@ -173,9 +173,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: duration = params.pop(ATTR_HEAT_DURATION, 0) for sens in sensor: - side = sens.split("_")[1] + side = sens[0] userid = eight.fetch_userid(side) - usrobj: EightUser = eight.users[userid] + usrobj = eight.users[userid] await usrobj.set_heating_level(target, duration) await heat_coordinator.async_request_refresh() @@ -199,9 +199,12 @@ class EightSleepHeatDataCoordinator(DataUpdateCoordinator): _LOGGER, name=f"{DOMAIN}_heat", update_interval=HEAT_SCAN_INTERVAL, - update_method=self.api.update_device_data, + update_method=self._async_update_data, ) + async def _async_update_data(self) -> None: + await self.api.update_device_data() + class EightSleepUserDataCoordinator(DataUpdateCoordinator): """Class to retrieve user data from Eight Sleep.""" @@ -214,14 +217,57 @@ class EightSleepUserDataCoordinator(DataUpdateCoordinator): _LOGGER, name=f"{DOMAIN}_user", update_interval=USER_SCAN_INTERVAL, - update_method=self.api.update_user_data, + update_method=self._async_update_data, ) + async def _async_update_data(self) -> None: + await self.api.update_user_data() -class EightSleepEntity(CoordinatorEntity): - """The Eight Sleep device entity.""" - def __init__(self, coordinator: DataUpdateCoordinator, eight: EightSleep) -> None: +class EightSleepBaseEntity(CoordinatorEntity): + """The base Eight Sleep entity class.""" + + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator | EightSleepHeatDataCoordinator, + eight: EightSleep, + side: str | None, + sensor: str, + ) -> None: """Initialize the data object.""" super().__init__(coordinator) self._eight = eight + self._side = side + self._sensor = sensor + self._usrobj: EightUser = None + if self._side: + self._usrobj = self._eight.users[self._eight.fetch_userid(self._side)] + full_sensor_name = self._sensor + if self._side is not None: + full_sensor_name = f"{self._side}_{full_sensor_name}" + mapped_name = NAME_MAP.get( + full_sensor_name, full_sensor_name.replace("_", " ").title() + ) + + self._attr_name = f"{name} {mapped_name}" + self._attr_unique_id = ( + f"{_get_device_unique_id(eight, self._usrobj)}.{self._sensor}" + ) + + +class EightSleepUserEntity(EightSleepBaseEntity): + """The Eight Sleep user entity.""" + + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator, + eight: EightSleep, + side: str | None, + sensor: str, + units: str, + ) -> None: + """Initialize the data object.""" + super().__init__(name, coordinator, eight, side, sensor) + self._units = units diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 5b6e1f6a9c3..7240d65d262 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,25 +1,25 @@ """Support for Eight Sleep binary sensors.""" +from __future__ import annotations + import logging from pyeight.eight import EightSleep -from pyeight.user import EightUser from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ( CONF_BINARY_SENSORS, DATA_API, DATA_EIGHT, DATA_HEAT, - NAME_MAP, - EightSleepEntity, + EightSleepBaseEntity, + EightSleepHeatDataCoordinator, ) _LOGGER = logging.getLogger(__name__) @@ -37,55 +37,40 @@ async def async_setup_platform( name = "Eight" sensors = discovery_info[CONF_BINARY_SENSORS] - eight = hass.data[DATA_EIGHT][DATA_API] - heat_coordinator = hass.data[DATA_EIGHT][DATA_HEAT] + eight: EightSleep = hass.data[DATA_EIGHT][DATA_API] + heat_coordinator: EightSleepHeatDataCoordinator = hass.data[DATA_EIGHT][DATA_HEAT] - all_sensors = [] + all_sensors = [ + EightHeatSensor(name, heat_coordinator, eight, side, sensor) + for side, sensor in sensors + ] - for sensor in sensors: - all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) - - async_add_entities(all_sensors, True) + async_add_entities(all_sensors) -class EightHeatSensor(EightSleepEntity, BinarySensorEntity): +class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" def __init__( self, name: str, - coordinator: DataUpdateCoordinator, + coordinator: EightSleepHeatDataCoordinator, eight: EightSleep, + side: str | None, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight) - - self._sensor = sensor - self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._state = None - - self._side = self._sensor.split("_")[0] - self._userid = self._eight.fetch_userid(self._side) - self._usrobj: EightUser = self._eight.users[self._userid] - - self._attr_name = f"{name} {self._mapped_name}" + super().__init__(name, coordinator, eight, side, sensor) self._attr_device_class = DEVICE_CLASS_OCCUPANCY _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", self._sensor, self._side, - self._userid, + self._usrobj.userid, ) @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return bool(self._state) - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._state = self._usrobj.bed_presence - super()._handle_coordinator_update() + return bool(self._usrobj.bed_presence) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index c7c58c05e7d..42270ad4fc4 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,23 +1,16 @@ """Support for Eight Sleep sensors.""" from __future__ import annotations -from collections.abc import Mapping import logging from typing import Any from pyeight.eight import EightSleep -from pyeight.user import EightUser from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_SENSORS, @@ -25,10 +18,10 @@ from . import ( DATA_EIGHT, DATA_HEAT, DATA_USER, - NAME_MAP, - EightSleepEntity, + EightSleepBaseEntity, EightSleepHeatDataCoordinator, EightSleepUserDataCoordinator, + EightSleepUserEntity, ) ATTR_ROOM_TEMP = "Room Temperature" @@ -63,7 +56,7 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType = None, + discovery_info: dict[str, list[tuple[str, str]]] = None, ) -> None: """Set up the eight sleep sensors.""" if discovery_info is None: @@ -71,7 +64,7 @@ async def async_setup_platform( name = "Eight" sensors = discovery_info[CONF_SENSORS] - eight = hass.data[DATA_EIGHT][DATA_API] + eight: EightSleep = hass.data[DATA_EIGHT][DATA_API] heat_coordinator: EightSleepHeatDataCoordinator = hass.data[DATA_EIGHT][DATA_HEAT] user_coordinator: EightSleepUserDataCoordinator = hass.data[DATA_EIGHT][DATA_USER] @@ -80,24 +73,26 @@ async def async_setup_platform( else: units = "us" - all_sensors: list[EightSleepEntity] = [] + all_sensors: list[SensorEntity] = [] - for sensor in sensors: - if "bed_state" in sensor: - all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) - elif "room_temp" in sensor: + for side, sensor in sensors: + if sensor == "bed_state": all_sensors.append( - EightRoomSensor(name, user_coordinator, eight, sensor, units) + EightHeatSensor(name, heat_coordinator, eight, side, sensor) + ) + elif sensor == "room_temperature": + all_sensors.append( + EightRoomSensor(name, user_coordinator, eight, side, sensor, units) ) else: all_sensors.append( - EightUserSensor(name, user_coordinator, eight, sensor, units) + EightUserSensor(name, user_coordinator, eight, side, sensor, units) ) - async_add_entities(all_sensors, True) + async_add_entities(all_sensors) -class EightHeatSensor(EightSleepEntity, SensorEntity): +class EightHeatSensor(EightSleepBaseEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" def __init__( @@ -105,51 +100,27 @@ class EightHeatSensor(EightSleepEntity, SensorEntity): name: str, coordinator: EightSleepHeatDataCoordinator, eight: EightSleep, + side: str | None, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight) - - self._sensor = sensor - self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" - self._state = None - - self._side = self._sensor.split("_")[0] - self._userid = self._eight.fetch_userid(self._side) - self._usrobj: EightUser = self._eight.users[self._userid] + super().__init__(name, coordinator, eight, side, sensor) + self._attr_native_unit_of_measurement = PERCENTAGE _LOGGER.debug( "Heat Sensor: %s, Side: %s, User: %s", self._sensor, self._side, - self._userid, + self._usrobj.userid, ) @property - def name(self) -> str: - """Return the name of the sensor, if any.""" - return self._name - - @property - def native_value(self) -> str | None: + def native_value(self) -> int: """Return the state of the sensor.""" - return self._state + return self._usrobj.heating_level @property - def native_unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - return PERCENTAGE - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating Heat sensor: %s", self._sensor) - self._state = self._usrobj.heating_level - super()._handle_coordinator_update() - - @property - def extra_state_attributes(self) -> Mapping[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return { ATTR_TARGET_HEAT: self._usrobj.target_heating_level, @@ -158,7 +129,17 @@ class EightHeatSensor(EightSleepEntity, SensorEntity): } -class EightUserSensor(EightSleepEntity, SensorEntity): +def _get_breakdown_percent( + attr: dict[str, Any], key: str, denominator: int | float +) -> int | float: + """Get a breakdown percent.""" + try: + return round((attr["breakdown"][key] / denominator) * 100, 2) + except ZeroDivisionError: + return 0 + + +class EightUserSensor(EightSleepUserEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" def __init__( @@ -166,180 +147,138 @@ class EightUserSensor(EightSleepEntity, SensorEntity): name: str, coordinator: EightSleepUserDataCoordinator, eight: EightSleep, + side: str | None, sensor: str, units: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight) + super().__init__(name, coordinator, eight, side, sensor, units) - self._sensor = sensor - self._sensor_root = self._sensor.split("_", 1)[1] - self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" - self._state = None - self._attr = None - self._units = units - - self._side = self._sensor.split("_", 1)[0] - self._userid = self._eight.fetch_userid(self._side) - self._usrobj: EightUser = self._eight.users[self._userid] + if self._sensor == "bed_temperature": + self._attr_icon = "mdi:thermometer" _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", self._sensor, self._side, - self._userid, + self._usrobj.userid if self._usrobj else None, ) @property - def name(self) -> str: - """Return the name of the sensor, if any.""" - return self._name - - @property - def native_value(self) -> str | None: + def native_value(self) -> str | int | float | None: """Return the state of the sensor.""" - return self._state + if "current" in self._sensor: + if "fitness" in self._sensor: + return self._usrobj.current_sleep_fitness_score + return self._usrobj.current_sleep_score + + if "last" in self._sensor: + return self._usrobj.last_sleep_score + + if self._sensor == "bed_temperature": + temp = self._usrobj.current_values["bed_temp"] + try: + if self._units == "si": + return round(temp, 2) + return round((temp * 1.8) + 32, 2) + except TypeError: + return None + + if self._sensor == "sleep_stage": + return self._usrobj.current_values["stage"] + + return None @property def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - if ( - "current_sleep" in self._sensor - or "last_sleep" in self._sensor - or "current_sleep_fitness" in self._sensor - ): + if self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): return "Score" - if "bed_temp" in self._sensor: + if self._sensor == "bed_temperature": if self._units == "si": return TEMP_CELSIUS return TEMP_FAHRENHEIT return None - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - if "bed_temp" in self._sensor: - return DEVICE_CLASS_TEMPERATURE - return None + def _get_rounded_value( + self, attr: dict[str, Any], key: str, use_units: bool = True + ) -> int | float | None: + """Get rounded value based on units for given key.""" + try: + if self._units == "si" or not use_units: + return round(attr["room_temp"], 2) + return round((attr["room_temp"] * 1.8) + 32, 2) + except TypeError: + return None - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating User sensor: %s", self._sensor) + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return device state attributes.""" + attr = None if "current" in self._sensor: if "fitness" in self._sensor: - self._state = self._usrobj.current_sleep_fitness_score - self._attr = self._usrobj.current_fitness_values + attr = self._usrobj.current_fitness_values else: - self._state = self._usrobj.current_sleep_score - self._attr = self._usrobj.current_values + attr = self._usrobj.current_values elif "last" in self._sensor: - self._state = self._usrobj.last_sleep_score - self._attr = self._usrobj.last_values - elif "bed_temp" in self._sensor: - temp = self._usrobj.current_values["bed_temp"] - try: - if self._units == "si": - self._state = round(temp, 2) - else: - self._state = round((temp * 1.8) + 32, 2) - except TypeError: - self._state = None - elif "sleep_stage" in self._sensor: - self._state = self._usrobj.current_values["stage"] + attr = self._usrobj.last_values - super()._handle_coordinator_update() - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return device state attributes.""" - if self._attr is None: + if attr is None: # Skip attributes if sensor type doesn't support return None - if "fitness" in self._sensor_root: + if "fitness" in self._sensor: state_attr = { - ATTR_FIT_DATE: self._attr["date"], - ATTR_FIT_DURATION_SCORE: self._attr["duration"], - ATTR_FIT_ASLEEP_SCORE: self._attr["asleep"], - ATTR_FIT_OUT_SCORE: self._attr["out"], - ATTR_FIT_WAKEUP_SCORE: self._attr["wakeup"], + ATTR_FIT_DATE: attr["date"], + ATTR_FIT_DURATION_SCORE: attr["duration"], + ATTR_FIT_ASLEEP_SCORE: attr["asleep"], + ATTR_FIT_OUT_SCORE: attr["out"], + ATTR_FIT_WAKEUP_SCORE: attr["wakeup"], } return state_attr - state_attr = {ATTR_SESSION_START: self._attr["date"]} - state_attr[ATTR_TNT] = self._attr["tnt"] - state_attr[ATTR_PROCESSING] = self._attr["processing"] + state_attr = {ATTR_SESSION_START: attr["date"]} + state_attr[ATTR_TNT] = attr["tnt"] + state_attr[ATTR_PROCESSING] = attr["processing"] - sleep_time = ( - sum(self._attr["breakdown"].values()) - self._attr["breakdown"]["awake"] - ) - state_attr[ATTR_SLEEP_DUR] = sleep_time - try: - state_attr[ATTR_LIGHT_PERC] = round( - (self._attr["breakdown"]["light"] / sleep_time) * 100, 2 + if attr.get("breakdown") is not None: + sleep_time = sum(attr["breakdown"].values()) - attr["breakdown"]["awake"] + state_attr[ATTR_SLEEP_DUR] = sleep_time + state_attr[ATTR_LIGHT_PERC] = _get_breakdown_percent( + attr, "light", sleep_time ) - except ZeroDivisionError: - state_attr[ATTR_LIGHT_PERC] = 0 - try: - state_attr[ATTR_DEEP_PERC] = round( - (self._attr["breakdown"]["deep"] / sleep_time) * 100, 2 + state_attr[ATTR_DEEP_PERC] = _get_breakdown_percent( + attr, "deep", sleep_time ) - except ZeroDivisionError: - state_attr[ATTR_DEEP_PERC] = 0 + state_attr[ATTR_REM_PERC] = _get_breakdown_percent(attr, "rem", sleep_time) - try: - state_attr[ATTR_REM_PERC] = round( - (self._attr["breakdown"]["rem"] / sleep_time) * 100, 2 + room_temp = self._get_rounded_value(attr, "room_temp") + bed_temp = self._get_rounded_value(attr, "bed_temp") + + if "current" in self._sensor: + state_attr[ATTR_RESP_RATE] = self._get_rounded_value( + attr, "resp_rate", False ) - except ZeroDivisionError: - state_attr[ATTR_REM_PERC] = 0 - - try: - if self._units == "si": - room_temp = round(self._attr["room_temp"], 2) - else: - room_temp = round((self._attr["room_temp"] * 1.8) + 32, 2) - except TypeError: - room_temp = None - - try: - if self._units == "si": - bed_temp = round(self._attr["bed_temp"], 2) - else: - bed_temp = round((self._attr["bed_temp"] * 1.8) + 32, 2) - except TypeError: - bed_temp = None - - if "current" in self._sensor_root: - try: - state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2) - except TypeError: - state_attr[ATTR_RESP_RATE] = None - try: - state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2) - except TypeError: - state_attr[ATTR_HEART_RATE] = None - state_attr[ATTR_SLEEP_STAGE] = self._attr["stage"] + state_attr[ATTR_HEART_RATE] = self._get_rounded_value( + attr, "heart_rate", False + ) + state_attr[ATTR_SLEEP_STAGE] = attr["stage"] state_attr[ATTR_ROOM_TEMP] = room_temp state_attr[ATTR_BED_TEMP] = bed_temp - elif "last" in self._sensor_root: - try: - state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2) - except TypeError: - state_attr[ATTR_AVG_RESP_RATE] = None - try: - state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2) - except TypeError: - state_attr[ATTR_AVG_HEART_RATE] = None + elif "last" in self._sensor: + state_attr[ATTR_AVG_RESP_RATE] = self._get_rounded_value( + attr, "resp_rate", False + ) + state_attr[ATTR_AVG_HEART_RATE] = self._get_rounded_value( + attr, "heart_rate", False + ) state_attr[ATTR_AVG_ROOM_TEMP] = room_temp state_attr[ATTR_AVG_BED_TEMP] = bed_temp return state_attr -class EightRoomSensor(EightSleepEntity, SensorEntity): +class EightRoomSensor(EightSleepUserEntity, SensorEntity): """Representation of an eight sleep room sensor.""" def __init__( @@ -347,51 +286,25 @@ class EightRoomSensor(EightSleepEntity, SensorEntity): name: str, coordinator: EightSleepUserDataCoordinator, eight: EightSleep, + side: str | None, sensor: str, units: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight) + super().__init__(name, coordinator, eight, side, sensor, units) - self._sensor = sensor - self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" - self._state = None - self._attr = None - self._units = units + self._attr_icon = "mdi:thermometer" + self._attr_native_unit_of_measurement: str = ( + TEMP_CELSIUS if self._units == "si" else TEMP_FAHRENHEIT + ) @property - def name(self) -> str: - """Return the name of the sensor, if any.""" - return self._name - - @property - def native_value(self) -> str | None: + def native_value(self) -> int | float | None: """Return the state of the sensor.""" - return self._state - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() try: if self._units == "si": - self._state = round(temp, 2) - else: - self._state = round((temp * 1.8) + 32, 2) + return round(temp, 2) + return round((temp * 1.8) + 32, 2) except TypeError: - self._state = None - super()._handle_coordinator_update() - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - if self._units == "si": - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TEMPERATURE + return None From d6c27809dc0e11f60269aa4114d59004925247da Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Fri, 3 Dec 2021 14:18:53 -0500 Subject: [PATCH 1305/1452] Gree update device ips when changed (#57876) --- homeassistant/components/gree/bridge.py | 7 +++ tests/components/gree/common.py | 32 ++++++++++-- tests/components/gree/test_bridge.py | 67 +++++++++++++++++++++++++ tests/components/gree/test_climate.py | 18 ++----- 4 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 tests/components/gree/test_bridge.py diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 9a927d13d29..41ba4bd9842 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -103,3 +103,10 @@ class DiscoveryService(Listener): await coordo.async_refresh() async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo) + + async def device_update(self, device_info: DeviceInfo) -> None: + """Handle updates in device information, update if ip has changed.""" + for coordinator in self.hass.data[DOMAIN][COORDINATORS]: + if coordinator.device.device_info.mac == device_info.mac: + coordinator.device.device_info.ip = device_info.ip + await coordinator.async_refresh() diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 40403377957..c7db03b118f 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -5,7 +5,10 @@ from unittest.mock import AsyncMock, Mock from greeclimate.discovery import Listener -from homeassistant.components.gree.const import DISCOVERY_TIMEOUT +from homeassistant.components.gree.const import DISCOVERY_TIMEOUT, DOMAIN as GREE_DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) @@ -16,6 +19,7 @@ class FakeDiscovery: def __init__(self, timeout: int = DISCOVERY_TIMEOUT) -> None: """Initialize the class.""" self.mock_devices = [build_device_mock()] + self.last_mock_infos = [] self.timeout = timeout self._listeners = [] self.scan_count = 0 @@ -29,14 +33,27 @@ class FakeDiscovery: self.scan_count += 1 _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) - infos = [x.device_info for x in self.mock_devices] + mock_infos = [x.device_info for x in self.mock_devices] + + new_infos = [] + updated_infos = [] + for info in mock_infos: + if not [i for i in self.last_mock_infos if info.mac == i.mac]: + new_infos.append(info) + else: + last_info = next(i for i in self.last_mock_infos if info.mac == i.mac) + if info.ip != last_info.ip: + updated_infos.append(info) + + self.last_mock_infos = mock_infos for listener in self._listeners: - [await listener.device_found(x) for x in infos] + [await listener.device_found(x) for x in new_infos] + [await listener.device_update(x) for x in updated_infos] if wait_for: await asyncio.sleep(wait_for) - return infos + return new_infos def build_device_info_mock( @@ -71,3 +88,10 @@ def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc1122 steady_heat=False, ) return mock + + +async def async_setup_gree(hass): + """Set up the gree platform.""" + MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass) + await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {"climate": {}}}) + await hass.async_block_till_done() diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py new file mode 100644 index 00000000000..13522b1216b --- /dev/null +++ b/tests/components/gree/test_bridge.py @@ -0,0 +1,67 @@ +"""Tests for gree component.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.climate.const import DOMAIN +from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE +import homeassistant.util.dt as dt_util + +from .common import async_setup_gree, build_device_mock + +from tests.common import async_fire_time_changed + +ENTITY_ID_1 = f"{DOMAIN}.fake_device_1" +ENTITY_ID_2 = f"{DOMAIN}.fake_device_2" + + +@pytest.fixture +def mock_now(): + """Fixture for dtutil.now.""" + return dt_util.utcnow() + + +async def test_discovery_after_setup(hass, discovery, device, mock_now): + """Test gree devices don't change after multiple discoveries.""" + mock_device_1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + mock_device_2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" + ) + + discovery.return_value.mock_devices = [mock_device_1, mock_device_2] + device.side_effect = [mock_device_1, mock_device_2] + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 1 + assert len(hass.states.async_all(DOMAIN)) == 2 + + device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] + assert device_infos[0].ip == "1.1.1.1" + assert device_infos[1].ip == "2.2.2.2" + + # rediscover the same devices with new ip addresses should update + mock_device_1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.2", mac="aabbcc112233" + ) + mock_device_2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.1", mac="bbccdd223344" + ) + discovery.return_value.mock_devices = [mock_device_1, mock_device_2] + device.side_effect = [mock_device_1, mock_device_2] + + next_update = mock_now + timedelta(minutes=6) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 2 + assert len(hass.states.async_all(DOMAIN)) == 2 + + device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] + assert device_infos[0].ip == "1.1.1.2" + assert device_infos[1].ip == "2.2.2.1" diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index d88f6a6fbf0..ce1d8f3c705 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -43,11 +43,7 @@ from homeassistant.components.gree.climate import ( HVAC_MODES_REVERSE, SUPPORTED_FEATURES, ) -from homeassistant.components.gree.const import ( - DOMAIN as GREE_DOMAIN, - FAN_MEDIUM_HIGH, - FAN_MEDIUM_LOW, -) +from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -59,12 +55,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import build_device_mock +from .common import async_setup_gree, build_device_mock -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" @@ -75,13 +70,6 @@ def mock_now(): return dt_util.utcnow() -async def async_setup_gree(hass): - """Set up the gree platform.""" - MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass) - await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {"climate": {}}}) - await hass.async_block_till_done() - - async def test_discovery_called_once(hass, discovery, device): """Test discovery is only ever called once.""" await async_setup_gree(hass) From 0dfc86956bacab67860bfcc4f813ab8860f3548f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 09:26:15 -1000 Subject: [PATCH 1306/1452] Reduce flux_led dhcp matching complexity (#60934) --- .../components/flux_led/manifest.json | 38 +------------- homeassistant/generated/dhcp.py | 50 +------------------ 2 files changed, 2 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 91de8cb5b7d..60efb52934c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -33,43 +33,7 @@ "hostname": "lwip*" }, { - "hostname": "zengge_0[6789b]_*" - }, - { - "hostname": "zengge_1[06789abc]_*" - }, - { - "hostname": "zengge_2[15]_*" - }, - { - "hostname": "zengge_3[35]_*" - }, - { - "hostname": "zengge_4[14]_*" - }, - { - "hostname": "zengge_5[24]_*" - }, - { - "hostname": "zengge_62_*" - }, - { - "hostname": "zengge_81_*" - }, - { - "hostname": "zengge_0[0e]_*" - }, - { - "hostname": "zengge_9[34567]_*" - }, - { - "hostname": "zengge_a[123]_*" - }, - { - "hostname": "zengge_d1_*" - }, - { - "hostname": "zengge_e[12]_*" + "hostname": "zengge_[0-9a-f][0-9a-f]_*" }, { "macaddress": "C82E47*", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index bdef30cf201..4313aa3f486 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -103,55 +103,7 @@ DHCP = [ }, { "domain": "flux_led", - "hostname": "zengge_0[6789b]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_1[06789abc]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_2[15]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_3[35]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_4[14]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_5[24]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_62_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_81_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_0[0e]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_9[34567]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_a[123]_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_d1_*" - }, - { - "domain": "flux_led", - "hostname": "zengge_e[12]_*" + "hostname": "zengge_[0-9a-f][0-9a-f]_*" }, { "domain": "flux_led", From ac26c2378badf0f7bb13099924214a00f8a51879 Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Fri, 3 Dec 2021 21:27:17 +0200 Subject: [PATCH 1307/1452] Add temperature sensors for Asuswrt (#58303) Co-authored-by: Paulus Schoutsen --- homeassistant/components/asuswrt/const.py | 1 + homeassistant/components/asuswrt/router.py | 35 ++++++++++++++++++ homeassistant/components/asuswrt/sensor.py | 41 +++++++++++++++++++++- tests/components/asuswrt/test_sensor.py | 39 ++++++++++++++++++-- 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index e41d683a7df..95e93e0ff25 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -25,3 +25,4 @@ SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"] SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"] SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"] SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"] +SENSORS_TEMPERATURES = ["2.4GHz", "5.0GHz", "CPU"] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 986ba828466..da314b12b65 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -47,6 +47,7 @@ from .const import ( SENSORS_CONNECTED_DEVICE, SENSORS_LOAD_AVG, SENSORS_RATES, + SENSORS_TEMPERATURES, ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] @@ -60,6 +61,7 @@ SENSORS_TYPE_BYTES = "sensors_bytes" SENSORS_TYPE_COUNT = "sensors_count" SENSORS_TYPE_LOAD_AVG = "sensors_load_avg" SENSORS_TYPE_RATES = "sensors_rates" +SENSORS_TYPE_TEMPERATURES = "sensors_temperatures" _LOGGER = logging.getLogger(__name__) @@ -114,6 +116,15 @@ class AsusWrtSensorDataHandler: return _get_dict(SENSORS_LOAD_AVG, avg) + async def _get_temperatures(self): + """Fetch temperatures information from the router.""" + try: + temperatures = await self._api.async_get_temperature() + except (OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return temperatures + def update_device_count(self, conn_devices: int): """Update connected devices attribute.""" if self._connected_devices == conn_devices: @@ -131,6 +142,8 @@ class AsusWrtSensorDataHandler: method = self._get_load_avg elif sensor_type == SENSORS_TYPE_RATES: method = self._get_rates + elif sensor_type == SENSORS_TYPE_TEMPERATURES: + method = self._get_temperatures else: raise RuntimeError(f"Invalid sensor type: {sensor_type}") @@ -349,9 +362,14 @@ class AsusWrtRouter: SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG, SENSORS_TYPE_RATES: SENSORS_RATES, + SENSORS_TYPE_TEMPERATURES: SENSORS_TEMPERATURES, } for sensor_type, sensor_names in sensors_types.items(): + if sensor_type == SENSORS_TYPE_TEMPERATURES: + sensor_names = await self._get_available_temperature_sensors() + if not sensor_names: + continue coordinator = await self._sensors_data_handler.get_coordinator( sensor_type, sensor_type != SENSORS_TYPE_COUNT ) @@ -370,6 +388,23 @@ class AsusWrtRouter: if self._sensors_data_handler.update_device_count(self._connected_devices): await coordinator.async_refresh() + async def _get_available_temperature_sensors(self): + """Check which temperature information is available on the router.""" + try: + availability = await self._api.async_find_temperature_commands() + available_sensors = [ + SENSORS_TEMPERATURES[i] for i in range(3) if availability[i] + ] + except Exception as exc: # pylint: disable=broad-except + _LOGGER.debug( + "Failed checking temperature sensor availability for ASUS router %s. Exception: %s", + self._host, + exc, + ) + return [] + + return available_sensors + async def close(self) -> None: """Close the connection.""" if self._api is not None and self._protocol == PROTOCOL_TELNET: diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7c375ed3c20..2c3b022cb9d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -5,12 +5,17 @@ from dataclasses import dataclass from numbers import Real from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_RATE_MEGABITS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.update_coordinator import ( @@ -25,6 +30,7 @@ from .const import ( SENSORS_CONNECTED_DEVICE, SENSORS_LOAD_AVG, SENSORS_RATES, + SENSORS_TEMPERATURES, ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter @@ -114,6 +120,39 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( factor=1, precision=1, ), + AsusWrtSensorEntityDescription( + key=SENSORS_TEMPERATURES[0], + name="2.4GHz Temperature", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_TEMPERATURES[1], + name="5GHz Temperature", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_TEMPERATURES[2], + name="CPU Temperature", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), ) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 19c27777c2a..b8537a5e6a6 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -40,6 +40,7 @@ CONFIG_DATA = { MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] MOCK_LOAD_AVG = [1.1, 1.2, 1.3] +MOCK_TEMPERATURES = {"2.4GHz": 40, "5.0GHz": 0, "CPU": 71.2} SENSOR_NAMES = [ "Devices Connected", @@ -50,6 +51,9 @@ SENSOR_NAMES = [ "Load Avg (1m)", "Load Avg (5m)", "Load Avg (15m)", + "2.4GHz Temperature", + "5GHz Temperature", + "CPU Temperature", ] @@ -62,8 +66,16 @@ def mock_devices_fixture(): } +@pytest.fixture(name="mock_available_temps") +def mock_available_temps_list(): + """Mock a list of available temperature sensors.""" + + # Only length of 3 booleans is valid. First checking the exception handling. + return [True, False] + + @pytest.fixture(name="connect") -def mock_controller_connect(mock_devices): +def mock_controller_connect(mock_devices, mock_available_temps): """Mock a successful connection.""" with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: service_mock.return_value.connection.async_connect = AsyncMock() @@ -88,10 +100,16 @@ def mock_controller_connect(mock_devices): service_mock.return_value.async_get_loadavg = AsyncMock( return_value=MOCK_LOAD_AVG ) + service_mock.return_value.async_get_temperature = AsyncMock( + return_value=MOCK_TEMPERATURES + ) + service_mock.return_value.async_find_temperature_commands = AsyncMock( + return_value=mock_available_temps + ) yield service_mock -async def test_sensors(hass, connect, mock_devices): +async def test_sensors(hass, connect, mock_devices, mock_available_temps): """Test creating an AsusWRT sensor.""" entity_reg = er.async_get(hass) @@ -137,6 +155,11 @@ async def test_sensors(hass, connect, mock_devices): assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3" assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2" + # assert temperature availability exception is handled correctly + assert not hass.states.get(f"{sensor_prefix}_2_4ghz_temperature") + assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature") + assert not hass.states.get(f"{sensor_prefix}_cpu_temperature") + # add one device and remove another mock_devices.pop("a1:b1:c1:d1:e1:f1") mock_devices["a3:b3:c3:d3:e3:f3"] = Device( @@ -161,3 +184,15 @@ async def test_sensors(hass, connect, mock_devices): # consider home option not set, device "test" not home assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_NOT_HOME + + # checking temperature sensors without exceptions + mock_available_temps.append(True) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3" + assert hass.states.get(f"{sensor_prefix}_2_4ghz_temperature").state == "40.0" + assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature") + assert hass.states.get(f"{sensor_prefix}_cpu_temperature").state == "71.2" From 215d0ac612632b50449aeb66d33ce6a5ca021fc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 20:29:15 +0100 Subject: [PATCH 1308/1452] Bumped version to 2021.12.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9da00de9a9a..73261963ce5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f78e59842d0a2ed57ed92aa74ee71168b665a5cb Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:20:40 +0100 Subject: [PATCH 1309/1452] Fix BMW Connected Drive (#60938) * Bump bimmer_connected to 0.8.5 * Always update HA states after service execution * Fix BMW device tracker & vehicle_finder service * Add charging_end_time sensor * Fix pylint & pytest * Remove unneeded DEFAULT_OPTION * Revert adding charging_end_time & state_attributes * Don't delete option data for CONF_USE_LOCATION * Remove stale string Co-authored-by: rikroe --- .../components/bmw_connected_drive/__init__.py | 14 ++++++++------ .../components/bmw_connected_drive/config_flow.py | 6 +----- .../bmw_connected_drive/device_tracker.py | 4 ++-- .../components/bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/strings.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/test_config_flow.py | 10 +++------- 8 files changed, 18 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index a520214ca6a..e681cac8223 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -35,7 +35,6 @@ from .const import ( CONF_ACCOUNT, CONF_ALLOWED_REGIONS, CONF_READ_ONLY, - CONF_USE_LOCATION, DATA_ENTRIES, DATA_HASS_CONFIG, ) @@ -65,7 +64,6 @@ SERVICE_SCHEMA = vol.Schema( DEFAULT_OPTIONS = { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } PLATFORMS = [ @@ -215,13 +213,10 @@ def setup_account( password: str = entry.data[CONF_PASSWORD] region: str = entry.data[CONF_REGION] read_only: bool = entry.options[CONF_READ_ONLY] - use_location: bool = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) - pos = ( - (hass.config.latitude, hass.config.longitude) if use_location else (None, None) - ) + pos = (hass.config.latitude, hass.config.longitude) cd_account = BMWConnectedDriveAccount( username, password, region, name, read_only, *pos ) @@ -258,6 +253,13 @@ def setup_account( function_call = getattr(vehicle.remote_services, function_name) function_call() + if call.service in [ + "find_vehicle", + "activate_air_conditioning", + "deactivate_air_conditioning", + ]: + cd_account.update() + if not read_only: # register the remote services for service in _SERVICE_MAP: diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 838c991edb3..3b07830c077 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from . import DOMAIN -from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY DATA_SCHEMA = vol.Schema( { @@ -115,10 +115,6 @@ class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): CONF_READ_ONLY, default=self.config_entry.options.get(CONF_READ_ONLY, False), ): bool, - vol.Optional( - CONF_USE_LOCATION, - default=self.config_entry.options.get(CONF_USE_LOCATION, False), - ): bool, } ), ) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index d17920fef0c..0ba2d5012a1 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) - if not vehicle.status.is_vehicle_tracking_enabled: + if not vehicle.is_vehicle_tracking_enabled: _LOGGER.info( "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", vehicle.name, @@ -83,6 +83,6 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.status.gps_position - if self._vehicle.status.is_vehicle_tracking_enabled + if self._vehicle.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 95ea2061fb4..fc641548aff 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.8.2"], + "requirements": ["bimmer_connected==0.8.5"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index c0c45b814a4..3e93cccb8c6 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", - "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)" } } } diff --git a/requirements_all.txt b/requirements_all.txt index e49e0aae85d..840be1149fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82d1aefb1bc..ae996a17fb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.blebox blebox_uniapi==1.3.3 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 6a0bd210387..b0bc3ce292c 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -3,10 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN -from homeassistant.components.bmw_connected_drive.const import ( - CONF_READ_ONLY, - CONF_USE_LOCATION, -) +from homeassistant.components.bmw_connected_drive.const import CONF_READ_ONLY from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from tests.common import MockConfigEntry @@ -28,7 +25,7 @@ FIXTURE_CONFIG_ENTRY = { CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, - "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } @@ -137,14 +134,13 @@ async def test_options_flow_implementation(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + user_input={CONF_READ_ONLY: False}, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } assert len(mock_setup.mock_calls) == 1 From 6af9471710fdad43bf4b6686c3c5db93ab95dfa1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:01:48 -0500 Subject: [PATCH 1310/1452] Fix nzbget datetime return value (#60953) --- homeassistant/components/nzbget/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 5bfde7e7c2b..9f94d458f42 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -127,6 +127,6 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): if "UpTimeSec" in sensor_type and value > 0: uptime = utcnow() - timedelta(seconds=value) - return uptime.replace(microsecond=0).isoformat() + return uptime.replace(microsecond=0) return value From 0b9efc2a06398d597b7a52218e6b84ddb70f3335 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 4 Dec 2021 09:34:24 -0700 Subject: [PATCH 1311/1452] Add missing SimpliSafe service information (#60958) --- homeassistant/components/simplisafe/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 273aa02c300..bdd7939a209 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,4 +1,16 @@ # Describes the format for available SimpliSafe services +clear_notifications: + name: Clear notifications + description: Clear any active SimpliSafe notificiations + fields: + device_id: + name: System + description: The system to remove the PIN from + required: true + selector: + device: + integration: simplisafe + model: alarm_control_panel remove_pin: name: Remove PIN description: Remove a PIN by its label or value. From 823e46ea26fd3ddb644bfc1becb0cd6ffbb179fc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 3 Dec 2021 21:05:01 -0700 Subject: [PATCH 1312/1452] Ensure that inactive RainMachine switch that is toggled on is toggled back off (#60959) --- homeassistant/components/rainmachine/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ab39ca1a669..5a178718c9b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -277,6 +277,8 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if not self.coordinator.data[self.entity_description.uid]["active"]: + self._attr_is_on = False + self.async_write_ha_state() raise HomeAssistantError( f"Cannot turn on an inactive program/zone: {self.name}" ) From 4023d55229e8b9cee4ed1317472438f49fd7d04c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:17:13 -0800 Subject: [PATCH 1313/1452] Fix statistics registering at start callback (#60963) --- homeassistant/components/statistics/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 23a4a31d936..a931a4cf806 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -243,8 +243,7 @@ class StatisticsSensor(SensorEntity): self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) - @callback - def async_stats_sensor_startup(_): + async def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) From 53e2ebc688eb144ba5c2c971b9bcb3833702e845 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Dec 2021 23:24:22 -0800 Subject: [PATCH 1314/1452] Correctly type the SSDP callback function (#60964) --- homeassistant/components/yeelight/scanner.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 648168ff84a..1756fbe865c 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv6Address import logging from urllib.parse import urlparse -from async_upnp_client.search import SsdpSearchListener +from async_upnp_client.search import SsdpHeaders, SsdpSearchListener from homeassistant import config_entries from homeassistant.components import network, ssdp @@ -174,10 +174,9 @@ class YeelightScanner: # of another discovery async_call_later(self._hass, 1, _async_start_flow) - async def _async_process_entry(self, response: ssdp.SsdpServiceInfo): + async def _async_process_entry(self, headers: SsdpHeaders): """Process a discovery.""" - _LOGGER.debug("Discovered via SSDP: %s", response) - headers = response.ssdp_headers + _LOGGER.debug("Discovered via SSDP: %s", headers) unique_id = headers["id"] host = urlparse(headers["location"]).hostname current_entry = self._unique_id_capabilities.get(unique_id) From fe46b2664ad2579f1bf9c35b24168550190a6c2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:20:12 -0800 Subject: [PATCH 1315/1452] Handle invalid device registry entry type (#60966) Co-authored-by: Franck Nijhof --- homeassistant/helpers/device_registry.py | 6 ++++- tests/helpers/test_device_registry.py | 32 ++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c8ae7fd148c..e31b77d3ae2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -172,7 +172,11 @@ class DeviceRegistryStore(storage.Store): # From version 1.1 for device in old_data["devices"]: # Introduced in 0.110 - device["entry_type"] = device.get("entry_type") + try: + device["entry_type"] = DeviceEntryType(device.get("entry_type")) + except ValueError: + device["entry_type"] = None + # Introduced in 0.79 # renamed in 0.95 device["via_device_id"] = device.get("via_device_id") or device.get( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a689cc9ac3d..455c90b8f65 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -252,7 +252,19 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", - } + }, + # Invalid entry type + { + "config_entries": [None], + "connections": [], + "entry_type": "INVALID_VALUE", + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name": None, + "sw_version": None, + }, ], }, } @@ -300,7 +312,23 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "name_by_user": None, "sw_version": "new_version", "via_device_id": None, - } + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + }, ], "deleted_devices": [], }, From 11b343a513ef7cdc2fdfe85133b39129dce098d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 21:41:58 -1000 Subject: [PATCH 1316/1452] Fix yeelight name changing to ip address if discovery fails (#60967) --- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/yeelight/device.py | 4 +++- tests/components/yeelight/test_init.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 79249ae8c44..d1b8e9d4f46 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -244,7 +244,7 @@ async def _async_get_device( # Set up device bulb = AsyncBulb(host, model=model or None) - device = YeelightDevice(hass, host, entry.options, bulb) + device = YeelightDevice(hass, host, {**entry.options, **entry.data}, bulb) # start listening for local pushes await device.bulb.async_listen(device.async_update_callback) diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 5f70866b229..02228e5d9fc 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -7,7 +7,7 @@ import logging from yeelight import BulbException from yeelight.aio import KEY_CONNECTED -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -199,6 +199,8 @@ class YeelightDevice: elif self.capabilities: # Generate name from model and id when capabilities is available self._name = _async_unique_name(self.capabilities) + elif self.model and (id_ := self._config.get(CONF_ID)): + self._name = f"Yeelight {async_format_model_id(self.model, id_)}" else: self._name = self._host # Default name is host diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 13c71d656bb..dc3d602edeb 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -588,3 +588,25 @@ async def test_non_oserror_exception_on_first_update( await hass.async_block_till_done() assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE + + +async def test_async_setup_with_discovery_not_working(hass: HomeAssistant): + """Test we can setup even if discovery is broken.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_ID: ID}, + options={}, + unique_id=ID, + ) + 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() + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.yeelight_color_0x15243f").state == STATE_ON From b5bfa728e9f5a05fd89bfd2176adcf755e0b8817 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:16:00 +0100 Subject: [PATCH 1317/1452] Upgrade luftdaten to 0.7.1 (#60970) --- homeassistant/components/luftdaten/__init__.py | 5 +---- homeassistant/components/luftdaten/config_flow.py | 4 +--- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index a87c67620cd..f8a67fff2f3 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -175,11 +174,9 @@ async def async_setup_entry(hass, config_entry): hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) return False - session = async_get_clientsession(hass) - try: luftdaten = LuftDatenData( - Luftdaten(config_entry.data[CONF_SENSOR_ID], hass.loop, session), + Luftdaten(config_entry.data[CONF_SENSOR_ID]), config_entry.data.get(CONF_SENSORS, {}).get( CONF_MONITORED_CONDITIONS, SENSOR_KEYS ), diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index f13fcc831dc..56dee86e9fb 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -69,8 +68,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if sensor_id in configured_sensors(self.hass): return self._show_form({CONF_SENSOR_ID: "already_configured"}) - session = aiohttp_client.async_get_clientsession(self.hass) - luftdaten = Luftdaten(user_input[CONF_SENSOR_ID], self.hass.loop, session) + luftdaten = Luftdaten(user_input[CONF_SENSOR_ID]) try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index f296093b556..fd355bd8d3c 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.6.5"], + "requirements": ["luftdaten==0.7.1"], "codeowners": ["@fabaff"], "quality_scale": "gold", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 840be1149fb..dba86955e03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -966,7 +966,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.lupusec lupupy==0.0.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae996a17fb1..a63eaf354c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ libsoundtouch==0.8 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From 823a4578d777c5e2f098c8182506446257d3fad3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:15:26 +0100 Subject: [PATCH 1318/1452] Upgrade netdata to 1.0.1 (#60971) --- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netdata/sensor.py | 4 +--- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9d79f54450c..34fbf45c529 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -2,7 +2,7 @@ "domain": "netdata", "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", - "requirements": ["netdata==0.2.0"], + "requirements": ["netdata==1.0.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index d1fa87a6e5d..3b1e9a0ed47 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -16,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -61,8 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) resources = config.get(CONF_RESOURCES) - session = async_get_clientsession(hass) - netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + netdata = NetdataData(Netdata(host, port=port)) await netdata.async_update() if netdata.api.metrics is None: diff --git a/requirements_all.txt b/requirements_all.txt index dba86955e03..c3053c32dd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.netdata -netdata==0.2.0 +netdata==1.0.1 # homeassistant.components.discovery netdisco==3.0.0 From 5a3dd71bde73e03f287b200bc88533e2909eae44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:20:56 -1000 Subject: [PATCH 1319/1452] Fix dimmable effects for flux_led model 0x33 v9+ (#60972) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 60efb52934c..565b560b45e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.10"], + "requirements": ["flux_led==0.25.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index c3053c32dd7..82d64dd8dc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a63eaf354c3..7e670e0eabb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 From 2ba7f9c584f908c441796cb0c70d13c6c8652e94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:44:16 -1000 Subject: [PATCH 1320/1452] Fix flood lights not turning on/off with flux_led (#60973) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 565b560b45e..1f7c84e73d6 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.12"], + "requirements": ["flux_led==0.25.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 82d64dd8dc4..dc1c4c8b27d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e670e0eabb..c1e3655f588 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 From 18f36b9c0b8e5e446559fb358f219028e026343f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 4 Dec 2021 10:29:48 +0100 Subject: [PATCH 1321/1452] Revert metoffice weather daytime (#60978) --- homeassistant/components/metoffice/const.py | 2 -- homeassistant/components/metoffice/weather.py | 9 ++------- tests/components/metoffice/test_weather.py | 11 ----------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index d1f48eb4f2c..e413b102898 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -24,8 +24,6 @@ DOMAIN = "metoffice" DEFAULT_NAME = "Met Office" ATTRIBUTION = "Data provided by the Met Office" -ATTR_FORECAST_DAYTIME = "daytime" - DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) METOFFICE_COORDINATES = "metoffice_coordinates" diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index d25df1d2654..79363db3667 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_device_info from .const import ( - ATTR_FORECAST_DAYTIME, ATTRIBUTION, CONDITION_CLASSES, DEFAULT_NAME, @@ -47,7 +46,7 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep, use_3hourly): +def _build_forecast_data(timestep): data = {} data[ATTR_FORECAST_TIME] = timestep.date.isoformat() if timestep.weather: @@ -60,9 +59,6 @@ def _build_forecast_data(timestep, use_3hourly): data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value - if not use_3hourly: - # if it's close to noon, mark as Day, otherwise as Night - data[ATTR_FORECAST_DAYTIME] = abs(timestep.date.hour - 12) < 6 return data @@ -86,7 +82,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): ) self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] - self._use_3hourly = use_3hourly if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @@ -160,7 +155,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): if self.coordinator.data.forecast is None: return None return [ - _build_forecast_data(timestep, self._use_3hourly) + _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 1970217db5b..158e44ca15b 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -181,13 +181,6 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True - - # Check that night entry is correctly marked as Night - assert ( - weather.attributes.get("forecast")[6]["datetime"] == "2020-04-29T00:00:00+00:00" - ) - assert weather.attributes.get("forecast")[6]["daytime"] is False @patch( @@ -263,7 +256,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" - assert "daytime" not in weather.attributes.get("forecast")[18] # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") @@ -287,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -312,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 10 assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" - assert "daytime" not in weather.attributes.get("forecast")[18] # King's Lynn daily weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_daily") @@ -336,4 +326,3 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[5]["temperature"] == 11 assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" - assert weather.attributes.get("forecast")[5]["daytime"] is True From dd2e250c66479c7f6311e269e458802131308d7a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 12:33:34 +0100 Subject: [PATCH 1322/1452] Fix Xiaomi Miio providing strings as timestamps (#60979) --- .../components/xiaomi_miio/sensor.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0d67014ced9..ccf55a04e17 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -49,6 +49,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes from .const import ( @@ -689,14 +690,24 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): def _determine_native_value(self): """Determine native value.""" if self.entity_description.parent_key is not None: - return self._extract_value_from_attribute( + native_value = self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), self.entity_description.key, ) + else: + native_value = self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.key + ) - return self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key - ) + if ( + self.device_class == DEVICE_CLASS_TIMESTAMP + and native_value is not None + and (native_datetime := dt_util.parse_datetime(str(native_value))) + is not None + ): + return native_datetime.astimezone(dt_util.UTC) + + return native_value class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): From af1ad0e6f879eeb4519e2ce39a2ea53026eecc2d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:37:42 +0100 Subject: [PATCH 1323/1452] Only report deprecated device_state_attributes once (#60980) --- homeassistant/helpers/entity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 86c8fa86af5..cd04f9db184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -241,6 +241,9 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False + # If we reported this entity is using deprecated device_state_attributes + _deprecated_device_state_attributes_reported = False + # Protect for multiple updates _update_staged = False @@ -538,7 +541,10 @@ class Entity(ABC): extra_state_attributes = self.extra_state_attributes # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 # Warning added in 2021.12, will be removed in 2022.4 - if self.device_state_attributes is not None: + if ( + self.device_state_attributes is not None + and not self._deprecated_device_state_attributes_reported + ): report_issue = self._suggest_report_issue() _LOGGER.warning( "Entity %s (%s) implements device_state_attributes. Please %s", @@ -546,6 +552,7 @@ class Entity(ABC): type(self), report_issue, ) + self._deprecated_device_state_attributes_reported = True if extra_state_attributes is None: extra_state_attributes = self.device_state_attributes attr.update(extra_state_attributes or {}) From 6a1dce852e18cc89440f5e61f0f0d14e642b4e21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:31:34 +0100 Subject: [PATCH 1324/1452] Fix DSMR Reader providing strings as timestamps (#60988) --- homeassistant/components/dsmr_reader/definitions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 1c719bc890b..4645aef9a7a 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -24,6 +24,7 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util PRICE_EUR_KWH: Final = f"EUR/{ENERGY_KILO_WATT_HOUR}" PRICE_EUR_M3: Final = f"EUR/{VOLUME_CUBIC_METERS}" @@ -202,6 +203,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Telegram timestamp", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/delivered", @@ -222,6 +224,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas meter read", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1", From 62a60f1cf69a31c69277f0da645d12bb2bc1fda6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:46:42 +0100 Subject: [PATCH 1325/1452] Fix str for device registry entry_type warnings caused by core (#60989) --- homeassistant/components/config/entity_registry.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 64cbfd7de1e..d42c5be08fc 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -10,7 +10,10 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry +from homeassistant.helpers.entity_registry import ( + RegistryEntryDisabler, + async_get_registry, +) async def async_setup(hass): @@ -75,7 +78,12 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), + vol.Optional("disabled_by"): vol.Any( + None, + vol.All( + vol.Coerce(RegistryEntryDisabler), RegistryEntryDisabler.USER.value + ), + ), } ) async def websocket_update_entity(hass, connection, msg): From 0e70121a6fde75dbe93fbaefee10e08385a4b06b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:52:42 +0100 Subject: [PATCH 1326/1452] Fix typo in state_characteristic warning (#60990) --- homeassistant/components/statistics/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index a931a4cf806..f9ed6863af8 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -62,9 +62,9 @@ STAT_VARIANCE = "variance" STAT_DEFAULT = "default" DEPRECATION_WARNING = ( - "The configuration parameter 'state_characteristics' will become " + "The configuration parameter 'state_characteristic' will become " "mandatory in a future release of the statistics integration. " - "Please add 'state_characteristics: %s' to the configuration of " + "Please add 'state_characteristic: %s' to the configuration of " 'sensor "%s" to keep the current behavior. Read the documentation ' "for further details: " "https://www.home-assistant.io/integrations/statistics/" From 70814130c3b6040b0db6ab2ad03624b3b257fe8f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 4 Dec 2021 18:38:09 +0100 Subject: [PATCH 1327/1452] Fix translations for binary_sensor tampered device triggers (#60996) --- homeassistant/components/binary_sensor/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index eb97b370105..e2167c24f8a 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -90,8 +90,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", - "is_tampered": "{entity_name} started detecting tampering", - "is_not_tampered": "{entity_name} stopped detecting tampering", + "tampered": "{entity_name} started detecting tampering", + "not_tampered": "{entity_name} stopped detecting tampering", "update": "{entity_name} got an update available", "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", From f81055dc09366064bb12ce9a0a0248060fcc8d07 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 4 Dec 2021 23:07:28 +0100 Subject: [PATCH 1328/1452] Add missing local_ip to KNX config flow and options flow (#61018) * Add missing local_ip to KNX config flow and options flow * Update strings --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/config_flow.py | 8 +++ homeassistant/components/knx/strings.json | 6 +- .../components/knx/translations/en.json | 2 + tests/components/knx/test_config_flow.py | 56 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 3e75c614f1e..ba6689a023d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -390,6 +390,7 @@ class KNXModule: connection_type=ConnectionType.TUNNELING, gateway_ip=self.config[CONF_HOST], gateway_port=self.config[CONF_PORT], + local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, ) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 3fcf4069624..30071752731 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -121,6 +121,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[ ConnectionSchema.CONF_KNX_ROUTE_BACK ], + ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) @@ -134,6 +137,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False ): vol.Coerce(bool), + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, } return self.async_show_form( @@ -243,6 +247,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): **DEFAULT_ENTRY_DATA, CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST], CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT], + ConnectionSchema.CONF_KNX_LOCAL_IP: config[CONF_KNX_TUNNELING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][ ConnectionSchema.CONF_KNX_ROUTE_BACK ], @@ -299,6 +306,7 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) ): cv.port, + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=self.current_config.get( diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index ff191f7a4ce..7f770c25427 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -19,7 +19,8 @@ "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", "individual_address": "Individual address for the connection", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } }, "routing": { @@ -55,7 +56,8 @@ "data": { "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 80890538fbc..5320f0cfb03 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Individual address for the connection", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 2b792044fe5..ff1fc362aa5 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -132,6 +132,57 @@ async def test_tunneling_setup(hass: HomeAssistant) -> None: CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: + """Test tunneling if only one gateway is found.""" + gateway = _gateway_descriptor("192.168.0.2", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "manual_tunnel" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.2" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } assert len(mock_setup_entry.mock_calls) == 1 @@ -188,6 +239,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, } assert len(mock_setup_entry.mock_calls) == 1 @@ -261,6 +313,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: CONF_KNX_TUNNELING: { CONF_HOST: "192.168.1.1", CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_ROUTE_BACK: True, }, } @@ -284,6 +337,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: ConnectionSchema.CONF_KNX_STATE_UPDATER: True, ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, } @@ -509,6 +563,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -526,6 +581,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } From c4fe3d05f222b0fbd4ee708d702281e4271d0dd4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 00:39:18 -0800 Subject: [PATCH 1329/1452] Improve nest media source event timestamp display (#61027) Drop subsecond text from the nest media source event timestamp display, using a common date/time template string. --- homeassistant/components/nest/media_source.py | 4 ++-- tests/components/nest/test_media_source.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index e21be20380a..af51c296e43 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,7 +46,7 @@ from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import DATE_STR_FORMAT _LOGGER = logging.getLogger(__name__) @@ -250,7 +250,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=dt_util.as_local(event.timestamp), + event_time=event.timestamp.strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 4cda781ebeb..67d6ba2f229 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util from .common import async_setup_sdm_platform @@ -211,7 +212,7 @@ async def test_camera_event(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id}" - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -291,7 +292,7 @@ async def test_event_order(hass, auth): assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id2}" - event_timestamp_string = event_timestamp2.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -299,7 +300,7 @@ async def test_event_order(hass, auth): assert browse.children[1].domain == DOMAIN assert browse.children[1].identifier == f"{device.id}/{event_id1}" - event_timestamp_string = event_timestamp1.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -435,7 +436,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Event @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 From 4e957b1dbe4665fc459798ecb4066c2dfe4615b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 06:09:18 -1000 Subject: [PATCH 1330/1452] Fix lutron caseta discovery with newer firmwares (#61029) --- homeassistant/components/lutron_caseta/config_flow.py | 2 +- tests/components/lutron_caseta/test_config_flow.py | 6 +++--- tests/components/lutron_caseta/test_device_trigger.py | 8 -------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index d75fc77c66e..b198d5ddbee 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -68,7 +68,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" hostname = discovery_info.hostname - if hostname is None or not hostname.startswith("lutron-"): + if hostname is None or not hostname.lower().startswith("lutron-"): return self.async_abort(reason="not_lutron_device") self.lutron_id = hostname.split("-")[1].replace(".local.", "") diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index e22d759c1b3..2b947c36982 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -427,7 +427,7 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -454,7 +454,7 @@ async def test_zeroconf_lutron_id_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -504,7 +504,7 @@ async def test_zeroconf(hass, source, tmpdir): context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 32d6eb3dc5f..23faa929574 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -335,11 +335,3 @@ async def test_validate_trigger_invalid_triggers(hass, device_reg): ] }, ) - - assert ( - len(entity_ids := hass.states.async_entity_ids("persistent_notification")) == 1 - ) - assert ( - "The following integrations and platforms could not be set up" - in hass.states.get(entity_ids[0]).attributes["message"] - ) From 576362bfe175b013dfe5d6a0b93fc8e69588757c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 01:50:47 -0800 Subject: [PATCH 1331/1452] Bump nest to version 0.4.2 (#61036) --- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 11 ++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 94b2c338528..3a4f64877d2 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index af51c296e43..140489bd63a 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -137,7 +137,7 @@ class NestMediaSource(MediaSource): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) - events = _get_events(device) + events = await _get_events(device) if media_id.event_id not in events: raise Unresolvable( "Unable to find event with identifier: %s" % item.identifier @@ -180,7 +180,7 @@ class NestMediaSource(MediaSource): # Browse a specific device and return child events browse_device = _browse_device(media_id, device) browse_device.children = [] - events = _get_events(device) + events = await _get_events(device) for child_event in events.values(): event_id = MediaId(media_id.device_id, child_event.event_id) browse_device.children.append( @@ -189,7 +189,7 @@ class NestMediaSource(MediaSource): return browse_device # Browse a specific event - events = _get_events(device) + events = await _get_events(device) if not (event := events.get(media_id.event_id)): raise BrowseError( "Unable to find event with identiifer: %s" % item.identifier @@ -201,9 +201,10 @@ class NestMediaSource(MediaSource): return await get_media_source_devices(self.hass) -def _get_events(device: Device) -> Mapping[str, ImageEventBase]: +async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" - return OrderedDict({e.event_id: e for e in device.event_media_manager.events}) + events = await device.event_media_manager.async_events() + return OrderedDict({e.event_id: e for e in events}) def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index dc1c4c8b27d..74af9b74a0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1e3655f588..2db8378dbfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From eaf53c10edc9468a7f9e69f90019a69dce9669e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 5 Dec 2021 09:44:11 -0800 Subject: [PATCH 1332/1452] Bumped version to 2021.12.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73261963ce5..f9c4b09944f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From c70f833069561b315b2f2333375ab422324f708c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 5 Dec 2021 18:48:25 +0100 Subject: [PATCH 1333/1452] Use STATE_DOCKED for emptying the bin for xiaomi_miio.vacuum (#60513) --- homeassistant/components/xiaomi_miio/vacuum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 60d557837fb..2362fcf8996 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -92,6 +92,7 @@ STATE_CODE_TO_STATE = { 16: STATE_CLEANING, # "Going to target" 17: STATE_CLEANING, # "Zoned cleaning" 18: STATE_CLEANING, # "Segment cleaning" + 22: STATE_DOCKED, # "Emptying the bin" on s7+ 100: STATE_DOCKED, # "Charging complete" 101: STATE_ERROR, # "Device offline" } From 7a4f1c3147dd00c5fc5aed66a76b704924c4417a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Dec 2021 18:51:57 +0100 Subject: [PATCH 1334/1452] Handle unknown/unavailable state for mobile_app (#60974) --- homeassistant/components/mobile_app/entity.py | 13 ++++++++++++- homeassistant/components/mobile_app/sensor.py | 7 +++++-- tests/components/mobile_app/test_sensor.py | 14 ++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 03b6d95c2a2..0c26533b7cb 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,6 +1,12 @@ """A entity class for mobile_app.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID +from homeassistant.const import ( + ATTR_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_WEBHOOK_ID, + STATE_UNAVAILABLE, +) from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -101,6 +107,11 @@ class MobileAppEntity(RestoreEntity): """Return device registry information for this entity.""" return device_info(self._registration) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE + @callback def _handle_update(self, data): """Handle async event updates.""" diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b58beef96ba..0631f8f72aa 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_WEBHOOK_ID, DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, + STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.helpers import entity_registry as er @@ -88,9 +89,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" + if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): + return None + if ( - (state := self._config[ATTR_SENSOR_STATE]) is not None - and self.device_class + self.device_class in ( DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index cfd9efa34c2..295e37ee7d9 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -4,7 +4,7 @@ from http import HTTPStatus import pytest from homeassistant.components.sensor import DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP -from homeassistant.const import PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -89,7 +89,7 @@ async def test_sensor(hass, create_registrations, webhook_client): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() unloaded_entity = hass.states.get("sensor.test_1_battery_state") - assert unloaded_entity.state == "unavailable" + assert unloaded_entity.state == STATE_UNAVAILABLE await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -295,6 +295,16 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client "2021-11-18 20:25:00+01:00", "2021-11-18T19:25:00+00:00", ), + ( + DEVICE_CLASS_TIMESTAMP, + "unavailable", + STATE_UNAVAILABLE, + ), + ( + DEVICE_CLASS_TIMESTAMP, + "unknown", + STATE_UNKNOWN, + ), ], ) async def test_sensor_datetime( From 528d4bc6cecb1762f16386ba60a1cbf0425cde7b Mon Sep 17 00:00:00 2001 From: david reid Date: Sun, 5 Dec 2021 17:50:15 +0000 Subject: [PATCH 1335/1452] Catch ConnectionResetError (#60987) --- homeassistant/components/hassio/ingress.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 620c69f543d..6935bbdc7da 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -255,3 +255,5 @@ async def _websocket_forward(ws_from, ws_to): await ws_to.close(code=ws_to.close_code, message=msg.extra) except RuntimeError: _LOGGER.debug("Ingress Websocket runtime error") + except ConnectionResetError: + _LOGGER.debug("Ingress Websocket Connection Reset") From 974cc94f873604b11a05676b99e0c369642faa9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 07:48:03 -1000 Subject: [PATCH 1336/1452] Update flux_led models database to fix turn on for newer models (#61005) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 1f7c84e73d6..dc60a46d68c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.13"], + "requirements": ["flux_led==0.25.16"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 74af9b74a0c..4f0a63e6848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2db8378dbfe..3943df51383 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 From c67b250be26b0416874418705ebd618043e9991b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:48 +0100 Subject: [PATCH 1337/1452] Fix Hue config flow (#61028) --- homeassistant/components/hue/config_flow.py | 34 +++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9d4bc87889d..a77a2ff101a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -216,16 +216,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not url.hostname: return self.async_abort(reason="not_hue_bridge") - bridge = await self._get_bridge( + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) + await self.async_set_unique_id(bridge_id) + self._abort_if_unique_id_configured( + updates={CONF_HOST: url.hostname}, reload_on_update=True + ) + + self.bridge = await self._get_bridge( url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] ) - - await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False - ) - - self.bridge = bridge return await self.async_step_link() async def async_step_zeroconf( @@ -236,17 +237,18 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the Zeroconf component. It will check if the host is already configured and delegate to the import step if not. """ - bridge = await self._get_bridge( - discovery_info.host, - discovery_info.properties["bridgeid"], - ) - - await self.async_set_unique_id(bridge.id) + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.properties["bridgeid"]) + await self.async_set_unique_id(bridge_id) self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False + updates={CONF_HOST: discovery_info.host}, reload_on_update=True ) - self.bridge = bridge + # we need to query the other capabilities too + self.bridge = await self._get_bridge( + discovery_info.host, discovery_info.properties["bridgeid"] + ) return await self.async_step_link() async def async_step_homekit( From 90442d9e9e26b4b0b5093119590862eb2a14c2ac Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:24 +0100 Subject: [PATCH 1338/1452] Fix Hue migration (#61030) --- homeassistant/components/hue/migration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 5396e646ce1..cf8ff50bfad 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -167,6 +167,9 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N continue v1_id = f"/groups/{ent.unique_id}" hue_group = api.groups.room.get_by_v1_id(v1_id) + if hue_group is None or hue_group.grouped_light is None: + # try again with zone + hue_group = api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( From a976ed2c72e35c59048e26219271174b1a4bd5f0 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:05 +0100 Subject: [PATCH 1339/1452] Add guard for empty mac address in Hue integration (#61037) --- homeassistant/components/hue/migration.py | 7 +++---- homeassistant/components/hue/v2/device.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index cf8ff50bfad..408ba3fc8e0 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -95,13 +95,12 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N # handle entities attached to device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) - if not zigbee: - # not a zigbee device + if not zigbee or not zigbee.mac_address: + # not a zigbee device or invalid mac continue - mac = zigbee.mac_address # get/update existing device by V1 identifier (mac address) # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, mac)} + identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} hass_dev = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers=identifiers ) diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 1608743cc48..64bdcc7a4f2 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -49,7 +49,8 @@ async def async_setup_devices(bridge: "HueBridge"): params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id)) else: params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id) - if zigbee := dev_controller.get_zigbee_connectivity(hue_device.id): + zigbee = dev_controller.get_zigbee_connectivity(hue_device.id) + if zigbee and zigbee.mac_address: params[ATTR_CONNECTIONS] = { (device_registry.CONNECTION_NETWORK_MAC, zigbee.mac_address) } From dd95b9b1e47dd7fd55870d88e87e6c072ad06304 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:44 +0100 Subject: [PATCH 1340/1452] Disable options flow for Hue V2 bridges (#61045) --- homeassistant/components/hue/config_flow.py | 11 +++++------ tests/components/hue/test_config_flow.py | 10 +++------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a77a2ff101a..ceb5a9a1a8e 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,7 +48,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - return HueOptionsFlowHandler(config_entry) + if config_entry.data.get(CONF_API_VERSION, 1) == 1: + # Options for Hue are only applicable to V1 bridges. + return HueOptionsFlowHandler(config_entry) + raise data_entry_flow.UnknownHandler def __init__(self) -> None: """Initialize the Hue flow.""" @@ -292,10 +295,6 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - if self.config_entry.data.get(CONF_API_VERSION, 1) > 1: - # Options for Hue are only applicable to V1 bridges. - return self.async_show_form(step_id="init") - return self.async_show_form( step_id="init", data_schema=vol.Schema( diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 80e1a8909b9..65d3dd696d6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,12 +706,8 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" - # V2 bridge does not have config options - assert result["data_schema"] is None + with pytest.raises(data_entry_flow.UnknownHandler): + await hass.config_entries.options.async_init(entry.entry_id) async def test_bridge_zeroconf(hass, aioclient_mock): From 377046bff57aab617682accfecdcb1d8c96068ec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:45:40 -0800 Subject: [PATCH 1341/1452] Fetch media for events for rendering in the nest media player (#61056) --- homeassistant/components/nest/__init__.py | 8 ++++++++ tests/components/nest/common.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index af3757d31da..fb39188710c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -86,6 +86,11 @@ PLATFORMS = ["sensor", "camera", "climate"] WEB_AUTH_DOMAIN = DOMAIN INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" +# Fetch media for events with an in memory cache. The largest media items +# are mp4 clips at ~90kb each, so this totals a few MB per camera. +# Note: Media for events can only be published within 30 seconds of the event +EVENT_MEDIA_CACHE_SIZE = 64 + class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): """OAuth implementation using OAuth for web applications.""" @@ -206,6 +211,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False + # Keep media for last N events in memory + subscriber.cache_policy.event_cache_size = EVENT_MEDIA_CACHE_SIZE + subscriber.cache_policy.fetch = True callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index eb44b19d540..a0ba813ab28 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -6,6 +6,7 @@ from unittest.mock import patch from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage +from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -98,6 +99,11 @@ class FakeSubscriber(GoogleNestSubscriber): """Return the fake device manager.""" return self._device_manager + @property + def cache_policy(self) -> CachePolicy: + """Return the cache policy.""" + return self._device_manager.cache_policy + def stop_async(self): """No-op to stop the subscriber.""" return None From 34f728e5d22d888edd7bf89e1b78fa91dfeadb8b Mon Sep 17 00:00:00 2001 From: schreyack Date: Sun, 5 Dec 2021 23:56:59 -0800 Subject: [PATCH 1342/1452] Fix previous setting briefly appearing on newer flux_led devices when turning on (#60004) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/light.py | 9 +- tests/components/flux_led/__init__.py | 2 + tests/components/flux_led/test_light.py | 127 +++++++++++++++++++-- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index fafa6a0b22e..d364d8b9581 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -280,10 +280,11 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" - if not self.is_on: - await self._device.async_turn_on() - if not kwargs: - return + if self._device.requires_turn_on or not kwargs: + if not self.is_on: + await self._device.async_turn_on() + if not kwargs: + return if MODE_ATTRS.intersection(kwargs): await self._async_set_mode(**kwargs) diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 5b39c5656f6..764c33686b7 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -66,6 +66,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.data_receive_callback = callback bulb.device_type = DeviceType.Bulb + bulb.requires_turn_on = True bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.effect_list = ["some_effect"] bulb.async_set_custom_pattern = AsyncMock() @@ -115,6 +116,7 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.data_receive_callback = callback switch.device_type = DeviceType.Switch + switch.requires_turn_on = True switch.async_setup = AsyncMock(side_effect=_save_setup_callback) switch.async_stop = AsyncMock() switch.async_update = AsyncMock() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 4f401197173..6f08ae8a307 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -226,22 +226,19 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_levels.reset_mock() bulb.async_turn_on.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - bulb.async_turn_on.assert_called_once() - bulb.async_turn_on.reset_mock() - await async_mock_device_turn_on(hass, bulb) - assert hass.states.get(entity_id).state == STATE_ON - await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) + # If its off and the device requires the turn on + # command before setting brightness we need to make sure its called + bulb.async_turn_on.assert_called_once() bulb.async_set_brightness.assert_called_with(100) bulb.async_set_brightness.reset_mock() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, @@ -284,6 +281,120 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_effect.reset_mock() +async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgb light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 it means we could not read it because + # an effect is in progress so we use 255 + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + async def test_rgb_cct_light(hass: HomeAssistant) -> None: """Test an rgb cct light.""" config_entry = MockConfigEntry( From e4d9d0d83e0ca13e154ab13726d8bb9c9681df0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Mon, 6 Dec 2021 17:49:47 +0100 Subject: [PATCH 1343/1452] Add media player volume control in `fr-FR` with Alexa (#60489) * media player volume control in `fr-FR` with Alexa * Apply suggestions from code review Co-authored-by: Erik Montnemery --- homeassistant/components/alexa/capabilities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ea8a1ed8681..0182d2aa085 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -695,6 +695,7 @@ class AlexaSpeaker(AlexaCapability): "en-US", "es-ES", "es-MX", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", "ja-JP", } @@ -752,6 +753,7 @@ class AlexaStepSpeaker(AlexaCapability): "en-IN", "en-US", "es-ES", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", } From 056575f491358b9c7b7661352dd12228063c5a0a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:30:02 -0800 Subject: [PATCH 1344/1452] Add debug logging for pip install command (#61057) --- homeassistant/util/package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 609d09e4f55..a0b5c2832ad 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -93,6 +93,7 @@ def install_package( # Workaround for incompatible prefix setting # See http://stackoverflow.com/a/4495175 args += ["--prefix="] + _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() if process.returncode != 0: From c159790cafecee06432e81d23db93d3024ca3dee Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Dec 2021 11:38:27 -0700 Subject: [PATCH 1345/1452] Fix mispelling in SimpliSafe service description (#61058) --- homeassistant/components/simplisafe/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index bdd7939a209..3d0965b4b0b 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available SimpliSafe services clear_notifications: name: Clear notifications - description: Clear any active SimpliSafe notificiations + description: Clear any active SimpliSafe notifications fields: device_id: name: System From 0c87885f41ed5e2a2ee10cbdaf8437128b51d9f7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:02:37 -0800 Subject: [PATCH 1346/1452] Fix regression in nest event media player with multiple devices (#61064) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 36 +++----- tests/components/nest/test_config_flow_sdm.py | 12 +-- tests/components/nest/test_media_source.py | 82 ++++++++++++++++++- 6 files changed, 97 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 3a4f64877d2..a82f8395733 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4f0a63e6848..22554d25fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3943df51383..8d08bbbd92d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index a0ba813ab28..35183a441a5 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -53,31 +53,12 @@ def create_config_entry(hass, token_expiration_time=None) -> MockConfigEntry: return config_entry -class FakeDeviceManager(DeviceManager): - """Fake DeviceManager that can supply a list of devices and structures.""" - - def __init__(self, devices: dict, structures: dict): - """Initialize FakeDeviceManager.""" - super().__init__() - self._devices = devices - - @property - def structures(self) -> dict: - """Override structures with fake result.""" - return self._structures - - @property - def devices(self) -> dict: - """Override devices with fake result.""" - return self._devices - - class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" - def __init__(self, device_manager: FakeDeviceManager): + def __init__(self): """Initialize Fake Subscriber.""" - self._device_manager = device_manager + self._device_manager = DeviceManager() def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" @@ -121,8 +102,14 @@ async def async_setup_sdm_platform( """Set up the platform and prerequisites.""" if with_config: create_config_entry(hass) - device_manager = FakeDeviceManager(devices=devices, structures=structures) - subscriber = FakeSubscriber(device_manager) + subscriber = FakeSubscriber() + device_manager = await subscriber.async_get_device_manager() + if devices: + for device in devices.values(): + device_manager.add_device(device) + if structures: + for structure in structures.values(): + device_manager.add_structure(structure) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch("homeassistant.components.nest.PLATFORMS", [platform]), patch( @@ -131,4 +118,7 @@ async def async_setup_sdm_platform( ): assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() + # Disabled to reduce setup burden, and enabled manually by tests that + # need to exercise this + subscriber.cache_policy.fetch = False return subscriber diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 5d6987f94f7..d4af62cb255 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .common import FakeDeviceManager, FakeSubscriber, MockConfigEntry +from .common import FakeSubscriber, MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -43,15 +43,9 @@ APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" @pytest.fixture -def device_manager() -> FakeDeviceManager: - """Create FakeDeviceManager.""" - return FakeDeviceManager(devices={}, structures={}) - - -@pytest.fixture -def subscriber(device_manager: FakeDeviceManager) -> FakeSubscriber: +def subscriber() -> FakeSubscriber: """Create FakeSubscriber.""" - return FakeSubscriber(device_manager) + return FakeSubscriber() def get_config_entry(hass): diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 67d6ba2f229..82c87579525 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -81,7 +81,7 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None): +def create_event(event_id, event_type, timestamp=None, device_id=None): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() @@ -91,17 +91,19 @@ def create_event(event_id, event_type, timestamp=None): "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp) + return create_event_message(event_id, event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp): +def create_event_message(event_id, event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" + if device_id is None: + device_id = DEVICE_ID return EventMessage( { "eventId": f"{event_id}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { - "name": DEVICE_ID, + "name": device_id, "events": event_data, }, }, @@ -568,3 +570,75 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert response.status == HTTPStatus.UNAUTHORIZED, ( "Response not matched: %s" % response ) + + +async def test_multiple_devices(hass, auth, hass_client): + """Test events received for multiple devices.""" + device_id1 = f"{DEVICE_ID}-1" + device_id2 = f"{DEVICE_ID}-2" + + devices = { + device_id1: Device.MakeDevice( + { + "name": device_id1, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + device_id2: Device.MakeDevice( + { + "name": device_id2, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + } + subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + + device_registry = dr.async_get(hass) + device1 = device_registry.async_get_device({(DOMAIN, device_id1)}) + assert device1 + device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) + assert device2 + + # Very no events have been received yet + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 0 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #1 + for i in range(0, 5): + await subscriber.async_receive_event( + create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #2 + for i in range(0, 3): + await subscriber.async_receive_event( + create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 3 From 23ebde58cdfa81a18d106f1a872fb152ac1c69ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 15:56:35 -1000 Subject: [PATCH 1347/1452] Bump flux_led to 0.25.17 to fix missing push messages on 0xA3 models (#61070) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index dc60a46d68c..71d8fd350b7 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.16"], + "requirements": ["flux_led==0.25.17"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 22554d25fcc..63585a3c37b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d08bbbd92d..a37f93b1f87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 From a3ede8f8950561c1a286c8ea0b416c14ba14cc96 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 5 Dec 2021 20:36:05 -0500 Subject: [PATCH 1348/1452] Add 3157100-E model to Centralite thermostat (#61073) --- homeassistant/components/zha/climate.py | 2 +- homeassistant/components/zha/sensor.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 69c1ce35849..9ef7e8fdebc 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -599,7 +599,7 @@ class ZenWithinThermostat(Thermostat): channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Centralite", - models="3157100", + models={"3157100", "3157100-E"}, stop_on_match=True, ) class CentralitePearl(ZenWithinThermostat): diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 304a3d155f5..567d2a6065e 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -61,6 +61,7 @@ from .core import discovery from .core.const import ( CHANNEL_ANALOG_INPUT, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_FAN, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, CHANNEL_LEAF_WETNESS, @@ -636,6 +637,13 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): self.async_write_ha_state() +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Centralite", + models={"3157100", "3157100-E"}, + stop_on_match=True, +) @MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers="Zen Within", From ec88a42948ed0bfadf4546edec17b38354a8be47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 18:41:09 -1000 Subject: [PATCH 1349/1452] Abort flux_led discovery if another device gets the ip (#61074) - If the dhcp reservation expired for the device that was at the ip and a new flux_led device appears we would discover it because the unique_id did not match --- .../components/flux_led/config_flow.py | 5 ++-- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index ab39e5b8ace..87d07bba2b1 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -115,8 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host and not entry.unique_id: - async_update_entry_from_discovery(self.hass, entry, device) + if entry.data[CONF_HOST] == host: + if not entry.unique_id: + async_update_entry_from_discovery(self.hass, entry, device) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index af84d3561f7..4c956358818 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -40,6 +40,8 @@ from . import ( from tests.common import MockConfigEntry +MAC_ADDRESS_DIFFERENT = "ff:bb:ff:dd:ee:ff" + async def test_discovery(hass: HomeAssistant): """Test setting up discovery.""" @@ -472,6 +474,34 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( assert config_entry.unique_id == MAC_ADDRESS +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already_configured( + hass, source, data +): + """Test we abort if the host is already configured but the mac does not match.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_DIFFERENT + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + 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_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT + + async def test_options(hass: HomeAssistant): """Test options flow.""" config_entry = MockConfigEntry( From d5f3e2a761ef5594a1315f8fbc483744bddbc1b9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 00:55:52 -0700 Subject: [PATCH 1350/1452] Deprecate `system_id` parameter in SimpliSafe service calls (#61076) --- .../components/simplisafe/__init__.py | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 250b3e6c3b1..cd04de5d34c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -144,57 +144,86 @@ SERVICES = ( SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) - -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, - } +SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, - } +SERVICE_REMOVE_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ALARM_DURATION): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), - ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), - ), - vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_ENTRY_DELAY_HOME), - ), - vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), - ), - vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_EXIT_DELAY_HOME), - ), - vol.Optional(ATTR_LIGHT): cv.boolean, - vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - } +SERVICE_SET_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL): cv.string, + vol.Required(ATTR_PIN_VALUE): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +) + +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Optional(ATTR_ALARM_DURATION): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), + ), + vol.Optional(ATTR_ALARM_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_CHIME_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), + ), + vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_ENTRY_DELAY_HOME), + ), + vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), + ), + vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_EXIT_DELAY_HOME), + ), + vol.Optional(ATTR_LIGHT): cv.boolean, + vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] @@ -216,6 +245,15 @@ def _async_get_system_for_service_call( hass: HomeAssistant, call: ServiceCall ) -> SystemType: """Get the SimpliSafe system related to a service call (by device ID).""" + if ATTR_SYSTEM_ID in call.data: + for entry in hass.config_entries.async_entries(DOMAIN): + simplisafe = hass.data[DOMAIN][entry.entry_id] + if ( + system := simplisafe.systems.get(int(call.data[ATTR_SYSTEM_ID])) + ) is None: + continue + return cast(SystemType, system) + device_id = call.data[ATTR_DEVICE_ID] device_registry = dr.async_get(hass) @@ -365,7 +403,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service, method, schema in ( - (SERVICE_NAME_CLEAR_NOTIFICATIONS, async_clear_notifications, None), + ( + SERVICE_NAME_CLEAR_NOTIFICATIONS, + async_clear_notifications, + SERVICE_CLEAR_NOTIFICATIONS_SCHEMA, + ), (SERVICE_NAME_REMOVE_PIN, async_remove_pin, SERVICE_REMOVE_PIN_SCHEMA), (SERVICE_NAME_SET_PIN, async_set_pin, SERVICE_SET_PIN_SCHEMA), ( From bd239bcbed005361cd7e8718386834b84d954b81 Mon Sep 17 00:00:00 2001 From: Alexander Pitkin Date: Mon, 6 Dec 2021 11:49:31 +0300 Subject: [PATCH 1351/1452] Fix yandex transport for Belarus (#61080) --- homeassistant/components/yandex_transport/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 680336fe47b..22872259a6f 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "yandex_transport", "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", - "requirements": ["aioymaps==1.2.1"], + "requirements": ["aioymaps==1.2.2"], "codeowners": ["@rishatik92", "@devbis"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 63585a3c37b..384ab0dccdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a37f93b1f87..bc1d226d1dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 From fa33464217b56cb4385a69649ff3c4cbca652451 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:11:44 -0800 Subject: [PATCH 1352/1452] Remove unnecessary explicit use of OrderedDict in nest media source (#61054) Address follow up PR comments from #60073 --- homeassistant/components/nest/media_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 140489bd63a..4f6cd8147d9 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -18,7 +18,6 @@ https://developers.google.com/nest/device-access/api/camera#handle_camera_events from __future__ import annotations -from collections import OrderedDict from collections.abc import Mapping from dataclasses import dataclass import logging @@ -204,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return OrderedDict({e.event_id: e for e in events}) + return {e.event_id: e for e in events} def _browse_root() -> BrowseMediaSource: From f4a38c01902f6cafcc971323cb018981cf080114 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 23:59:24 -0800 Subject: [PATCH 1353/1452] Coalesce nest media source preview clips by session and bump google-nest-sdm (#61081) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 12 +-- tests/components/nest/test_media_source.py | 74 +++++++++++-------- 7 files changed, 56 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fb39188710c..0151fb6a6a5 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -197,7 +197,7 @@ class SignalUpdateCallback: "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, - "nest_event_id": image_event.event_id, + "nest_event_id": image_event.event_session_id, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index a82f8395733..b9f20e92670 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4f6cd8147d9..8fd7d384e36 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -181,7 +181,7 @@ class NestMediaSource(MediaSource): browse_device.children = [] events = await _get_events(device) for child_event in events.values(): - event_id = MediaId(media_id.device_id, child_event.event_id) + event_id = MediaId(media_id.device_id, child_event.event_session_id) browse_device.children.append( _browse_event(event_id, device, child_event) ) @@ -203,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return {e.event_id: e for e in events} + return {e.event_session_id: e for e in events} def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index 384ab0dccdb..53b1db5a9e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc1d226d1dc..cf411b32d26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 6e9dd7dd40d..a2f5c21fdac 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -117,7 +117,7 @@ async def test_doorbell_chime_event(hass): "device_id": entry.device_id, "type": "doorbell_chime", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -145,7 +145,7 @@ async def test_camera_motion_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -173,7 +173,7 @@ async def test_camera_sound_event(hass): "device_id": entry.device_id, "type": "camera_sound", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -201,7 +201,7 @@ async def test_camera_person_event(hass): "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -238,13 +238,13 @@ async def test_camera_multiple_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 82c87579525..22ed0721eb2 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -27,6 +27,7 @@ DEVICE_ID = "example/api/device/id" DEVICE_NAME = "Front" PLATFORM = "camera" NEST_EVENT = "nest_event" +EVENT_ID = "1aXEvi9ajKVTdDsXdJda8fzfCa..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" CAMERA_TRAITS = { @@ -81,26 +82,28 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None, device_id=None): +def create_event( + event_session_id, event_id, event_type, timestamp=None, device_id=None +): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() event_data = { event_type: { - "eventSessionId": EVENT_SESSION_ID, + "eventSessionId": event_session_id, "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp, device_id=device_id) + return create_event_message(event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp, device_id=None): +def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID return EventMessage( { - "eventId": f"{event_id}-{timestamp}", + "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, @@ -163,7 +166,6 @@ async def test_supported_device(hass, auth): async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -172,7 +174,8 @@ async def test_camera_event(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -213,7 +216,7 @@ async def test_camera_event(hass, auth, hass_client): # The device expands recent events assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id}" + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -221,19 +224,19 @@ async def test_camera_event(hass, auth, hass_client): # Browse to the event browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) assert browse.domain == DOMAIN - assert browse.identifier == f"{device.id}/{event_id}" + assert browse.identifier == f"{device.id}/{EVENT_SESSION_ID}" assert "Person" in browse.title assert not browse.can_expand assert not browse.children # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -250,9 +253,9 @@ async def test_camera_event(hass, auth, hass_client): async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" - event_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_session_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp1 = dt_util.now() - event_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." + event_session_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) await async_setup_devices( hass, @@ -261,12 +264,14 @@ async def test_event_order(hass, auth): CAMERA_TRAITS, events=[ create_event( - event_id1, + event_session_id1, + EVENT_ID + "1", PERSON_EVENT, timestamp=event_timestamp1, ), create_event( - event_id2, + event_session_id2, + EVENT_ID + "2", MOTION_EVENT, timestamp=event_timestamp2, ), @@ -293,7 +298,7 @@ async def test_event_order(hass, auth): # Motion event is most recent assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id2}" + assert browse.children[0].identifier == f"{device.id}/{event_session_id2}" event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -301,7 +306,7 @@ async def test_event_order(hass, auth): # Person event is next assert browse.children[1].domain == DOMAIN - assert browse.children[1].identifier == f"{device.id}/{event_id1}" + assert browse.children[1].identifier == f"{device.id}/{event_session_id1}" event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -395,9 +400,12 @@ async def test_resolve_invalid_event_id(hass, auth): async def test_camera_event_clip_preview(hass, auth, hass_client): """Test an event for a battery camera video clip.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() event_data = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, "previewUrl": "https://127.0.0.1/example", @@ -410,7 +418,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): BATTERY_CAMERA_TRAITS, events=[ create_event_message( - event_id, event_data, timestamp=event_timestamp, ), @@ -439,7 +446,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) - assert browse.children[0].title == f"Event @ {event_timestamp_string}" + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -490,7 +497,6 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client): async def test_event_media_failure(hass, auth, hass_client): """Test event media fetch sees a failure from the server.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -499,7 +505,8 @@ async def test_event_media_failure(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -517,9 +524,9 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -535,7 +542,6 @@ async def test_event_media_failure(hass, auth, hass_client): async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): """Test case where user does not have permissions to view media.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -544,7 +550,8 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -560,7 +567,7 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert device assert device.name == DEVICE_NAME - media_url = f"/api/nest/event_media/{device.id}/{event_id}" + media_url = f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" # Empty policy with no access to the entity hass_admin_user.mock_policy({}) @@ -616,7 +623,12 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #1 for i in range(0, 5): await subscriber.async_receive_event( - create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + create_event( + f"event-session-id-{i}", + f"event-id-{i}", + PERSON_EVENT, + device_id=device_id1, + ) ) browse = await media_source.async_browse_media( @@ -631,7 +643,9 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #2 for i in range(0, 3): await subscriber.async_receive_event( - create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + create_event( + f"other-id-{i}", f"event-id{i}", PERSON_EVENT, device_id=device_id2 + ) ) browse = await media_source.async_browse_media( From 20fb06484cbd05d0d55cd35f31ef92070ed7dffc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:48:17 -1000 Subject: [PATCH 1354/1452] Bump enphase_envoy to 0.20.1 (#61082) --- homeassistant/components/enphase_envoy/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9e948eaf842..d7ad10ca062 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.20.0" + "envoy_reader==0.20.1" ], "codeowners": [ "@gtdiehl" @@ -15,4 +15,4 @@ } ], "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 53b1db5a9e4..3f7fa4cbe75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ env_canada==0.5.18 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf411b32d26..615d41b663b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -378,7 +378,7 @@ enocean==0.50 env_canada==0.5.18 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 From e09245eb14dd4a4b0ab7aa2284c757d927f5708b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:53:53 -1000 Subject: [PATCH 1355/1452] Fix missing unique id in enphase_envoy (#61083) --- .../components/enphase_envoy/__init__.py | 8 ++ .../components/enphase_envoy/config_flow.py | 53 ++++++++--- .../enphase_envoy/test_config_flow.py | 88 +++++++++++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 69c488169a6..7b3765bd25c 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -75,6 +75,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() + if not entry.unique_id: + try: + serial = await envoy_reader.get_full_serial_number() + except httpx.HTTPError: + pass + else: + hass.config_entries.async_update_entry(entry, unique_id=serial) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 0b163e331d6..d1e0febe2e6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Enphase Envoy integration.""" from __future__ import annotations +import contextlib import logging from typing import Any @@ -31,7 +32,7 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( data[CONF_HOST], @@ -48,6 +49,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err + return envoy_reader + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Enphase Envoy.""" @@ -59,7 +62,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address = None self.name = None self.username = None - self.serial = None self._reauth_entry = None @callback @@ -104,8 +106,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info.properties["serialnum"] - await self.async_set_unique_id(self.serial) + serial = discovery_info.properties["serialnum"] + await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): @@ -114,9 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): - title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY self.hass.config_entries.async_update_entry( - entry, title=title, unique_id=self.serial + entry, title=title, unique_id=serial ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -132,6 +134,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() + def _async_envoy_name(self) -> str: + """Return the name of the envoy.""" + if self.name: + return self.name + if self.unique_id: + return f"{ENVOY} {self.unique_id}" + return ENVOY + + async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool: + """Set the unique id by fetching it from the envoy.""" + serial = None + with contextlib.suppress(httpx.HTTPError): + serial = await envoy_reader.get_full_serial_number() + if serial: + await self.async_set_unique_id(serial) + return True + return False + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -145,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_configured") try: - await validate_input(self.hass, user_input) + envoy_reader = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -155,21 +175,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: data = user_input.copy() - if self.serial: - data[CONF_NAME] = f"{ENVOY} {self.serial}" - else: - data[CONF_NAME] = self.name or ENVOY + data[CONF_NAME] = self._async_envoy_name() + if self._reauth_entry: self.hass.config_entries.async_update_entry( self._reauth_entry, data=data, ) return self.async_abort(reason="reauth_successful") + + if not self.unique_id and await self._async_set_unique_id_from_envoy( + envoy_reader + ): + data[CONF_NAME] = self._async_envoy_name() + + if self.unique_id: + self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]}) + return self.async_create_entry(title=data[CONF_NAME], data=data) - if self.serial: + if self.unique_id: self.context["title_placeholders"] = { - CONF_SERIAL: self.serial, + CONF_SERIAL: self.unique_id, CONF_HOST: self.ip_address, } return self.async_show_form( diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index fc9a7de188e..41a49a7b245 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -23,6 +23,91 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_no_serial_number(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + 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.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value=None, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + 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.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + side_effect=httpx.HTTPStatusError( + "any", request=MagicMock(), response=MagicMock() + ), ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, @@ -125,6 +210,9 @@ async def test_import(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, From 878700e26f02c70e2b0b2a24e085dcbac28669b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:50:45 -1000 Subject: [PATCH 1356/1452] Provide a hint on which username to use for enphase_envoy (#61084) --- homeassistant/components/enphase_envoy/strings.json | 1 + homeassistant/components/enphase_envoy/translations/en.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index b42f6bfb50f..822ee14fc9e 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -3,6 +3,7 @@ "flow_title": "{serial} ({host})", "step": { "user": { + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password.", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 2cdb75a6b53..5d4617ed9fa 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Password", "username": "Username" - } + }, + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password." } } } From cb371ef27ccb76ad9059770761bca1c39c20716c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:26:31 +0100 Subject: [PATCH 1357/1452] Prevent log flooding in frame helper (#61085) Co-authored-by: epenet --- homeassistant/helpers/frame.py | 9 +++++++++ tests/helpers/test_frame.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 3995f24102d..13ffea48f81 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -12,6 +12,9 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) +# Keep track of integrations already reported to prevent flooding +_REPORTED_INTEGRATIONS: set[str] = set() + CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name @@ -85,6 +88,12 @@ def report_integration( """ found_frame, integration, path = integration_frame + # Keep track of integrations already reported to prevent flooding + key = f"{found_frame.filename}:{found_frame.lineno}" + if key in _REPORTED_INTEGRATIONS: + return + _REPORTED_INTEGRATIONS.add(key) + index = found_frame.filename.index(path) if path == "custom_components/": extra = " to the custom component author" diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 5e48b2aec5f..37f3e7ec95f 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,4 +1,5 @@ """Test the frame helper.""" +# pylint: disable=protected-access from unittest.mock import Mock, patch import pytest @@ -70,3 +71,24 @@ async def test_extract_frame_no_integration(caplog): ], ), pytest.raises(frame.MissingIntegrationFrame): frame.get_integration_frame() + + +@pytest.mark.usefixtures("mock_integration_frame") +@patch.object(frame, "_REPORTED_INTEGRATIONS", set()) +async def test_prevent_flooding(caplog): + """Test to ensure a report is only written once to the log.""" + + what = "accessed hi instead of hello" + key = "/home/paulus/homeassistant/components/hue/light.py:23" + + frame.report(what, error_if_core=False) + assert what in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 + + caplog.clear() + + frame.report(what, error_if_core=False) + assert what not in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 From ef0f3f7ce9f59c78eea93e0c7f75f9a02cff2868 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Dec 2021 17:24:59 +0100 Subject: [PATCH 1358/1452] Fix migration of entities of Hue integration (#61095) * fix device name in log * Fix Hue migration for all id versions * fix tests * typo * change to bit more universal approach * fix test again * formatting --- homeassistant/components/hue/migration.py | 138 +++++++++++++--------- tests/components/hue/test_migration.py | 54 +++++++-- 2 files changed, 125 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 408ba3fc8e0..9891cc65b0c 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -4,6 +4,7 @@ import logging from aiohue import HueBridgeV2 from aiohue.discovery import is_v2_bridge +from aiohue.v2.models.device import DeviceArchetypes from aiohue.v2.models.resource import ResourceTypes from homeassistant import core @@ -18,7 +19,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry as devices_for_config_entries, + async_get as async_get_device_registry, +) from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry as entities_for_config_entry, async_entries_for_device, @@ -82,6 +86,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info("Start of migration of devices and entities to support API schema 2") + + # Create mapping of mac address to HA device id's. + # Identifier in dev reg should be mac-address, + # but in some cases it has a postfix like `-0b` or `-01`. + dev_ids = {} + for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id): + for domain, mac in hass_dev.identifiers: + if domain != DOMAIN: + continue + normalized_mac = mac.split("-")[0] + dev_ids[normalized_mac] = hass_dev.id + # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key, websession) as api: @@ -92,83 +108,93 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, } - # handle entities attached to device + # migrate entities attached to a device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) if not zigbee or not zigbee.mac_address: # not a zigbee device or invalid mac continue - # get/update existing device by V1 identifier (mac address) - # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} - hass_dev = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, identifiers=identifiers - ) - LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id) - # loop through al entities for device and find match - for ent in async_entries_for_device(ent_reg, hass_dev.id, True): - # migrate light - if ent.entity_id.startswith("light"): - # should always return one lightid here - new_unique_id = next(iter(hue_dev.lights)) - if ent.unique_id == new_unique_id: - continue # just in case - LOGGER.info( - "Migrating %s from unique id %s to %s", - ent.entity_id, - ent.unique_id, - new_unique_id, - ) - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=new_unique_id - ) - continue - # migrate sensors - matched_dev_class = sensor_class_mapping.get( - ent.original_device_class or "unknown" + + # get existing device by V1 identifier (mac address) + if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2: + hass_dev_id = dev_ids.get(api.config.bridge_id.upper()) + else: + hass_dev_id = dev_ids.get(zigbee.mac_address) + if hass_dev_id is None: + # can be safely ignored, this device does not exist in current config + LOGGER.debug( + "Ignoring device %s (%s) as it does not (yet) exist in the device registry", + hue_dev.metadata.name, + hue_dev.id, ) - if matched_dev_class is None: + continue + dev_reg.async_update_device( + hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)} + ) + LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id) + + # loop through all entities for device and find match + for ent in async_entries_for_device(ent_reg, hass_dev_id, True): + + if ent.entity_id.startswith("light"): + # migrate light + # should always return one lightid here + new_unique_id = next(iter(hue_dev.lights), None) + else: + # migrate sensors + matched_dev_class = sensor_class_mapping.get( + ent.original_device_class or "unknown" + ) + new_unique_id = next( + ( + sensor.id + for sensor in api.devices.get_sensors(hue_dev.id) + if sensor.type == matched_dev_class + ), + None, + ) + + if new_unique_id is None: # this may happen if we're looking at orphaned or unsupported entity LOGGER.warning( "Skip migration of %s because it no longer exists on the bridge", ent.entity_id, ) continue - for sensor in api.devices.get_sensors(hue_dev.id): - if sensor.type != matched_dev_class: - continue - new_unique_id = sensor.id - if ent.unique_id == new_unique_id: - break # just in case + + try: + ent_reg.async_update_entity( + ent.entity_id, new_unique_id=new_unique_id + ) + except ValueError: + # assume edge case where the entity was already migrated in a previous run + # which got aborted somehow and we do not want + # to crash the entire integration init + LOGGER.warning( + "Skip migration of %s because it already exists", + ent.entity_id, + ) + else: LOGGER.info( - "Migrating %s from unique id %s to %s", + "Migrated entity %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) - try: - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=sensor.id - ) - except ValueError: - # assume edge case where the entity was already migrated in a previous run - # which got aborted somehow and we do not want - # to crash the entire integration init - LOGGER.warning( - "Skip migration of %s because it already exists", - ent.entity_id, - ) - break # migrate entities that are not connected to a device (groups) for ent in entities_for_config_entry(ent_reg, entry.entry_id): if ent.device_id is not None: continue - v1_id = f"/groups/{ent.unique_id}" - hue_group = api.groups.room.get_by_v1_id(v1_id) - if hue_group is None or hue_group.grouped_light is None: - # try again with zone - hue_group = api.groups.zone.get_by_v1_id(v1_id) + if "-" in ent.unique_id: + # handle case where unique id is v2-id of group/zone + hue_group = api.groups.get(ent.unique_id) + else: + # handle case where the unique id is just the v1 id + v1_id = f"/groups/{ent.unique_id}" + hue_group = api.groups.room.get_by_v1_id( + v1_id + ) or api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 8457ed04170..2dc1636d485 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -54,12 +54,12 @@ async def test_light_entity_migration( # create device/entity with V1 schema in registry device = dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65")}, + identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65-0b")}, ) ent_reg.async_get_or_create( "light", hue.DOMAIN, - "00:17:88:01:09:aa:bb:65", + "00:17:88:01:09:aa:bb:65-0b", suggested_object_id="migrated_light_1", device_id=device.id, ) @@ -74,14 +74,13 @@ async def test_light_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50"), - (hue.DOMAIN, "00:17:88:01:09:aa:bb:65"), + (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50") } - # the entity should have the new identifier (guid) + # the entity should have the new unique_id (guid) migrated_entity = ent_reg.async_get("light.migrated_light_1") assert migrated_entity is not None assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1" @@ -131,14 +130,13 @@ async def test_sensor_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6"), - (hue.DOMAIN, device_mac), + (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6") } - # the entities should have the correct V2 identifier (guid) + # the entities should have the correct V2 unique_id (guid) for dev_class, platform, new_id in sensor_mappings: migrated_entity = ent_reg.async_get( f"{platform}.hue_migrated_{dev_class}_sensor" @@ -147,7 +145,7 @@ async def test_sensor_entity_migration( assert migrated_entity.unique_id == new_id -async def test_group_entity_migration( +async def test_group_entity_migration_with_v1_id( hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data ): """Test if entity schema for grouped_lights migrates from v1 to v2.""" @@ -156,6 +154,7 @@ async def test_group_entity_migration( ent_reg = er.async_get(hass) # create (deviceless) entity with V1 schema in registry + # using the legacy style group id as unique id ent_reg.async_get_or_create( "light", hue.DOMAIN, @@ -177,3 +176,36 @@ async def test_group_entity_migration( migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") assert migrated_entity is not None assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" + + +async def test_group_entity_migration_with_v2_group_id( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for grouped_lights migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + + # create (deviceless) entity with V1 schema in registry + # using the V2 group id as unique id + ent_reg.async_get_or_create( + "light", + hue.DOMAIN, + "6ddc9066-7e7d-4a03-a773-c73937968296", + suggested_object_id="hue_migrated_grouped_light", + config_entry=config_entry, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # the entity should have the new identifier (guid) + migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") + assert migrated_entity is not None + assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" From 21463121a747ea4e21fbd0675f38e0232d87cdeb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 6 Dec 2021 14:46:53 +0100 Subject: [PATCH 1359/1452] Improve zwave_js add-on config flow description (#61099) --- homeassistant/components/zwave_js/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 1446c1fc7aa..13f65921cdb 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -22,6 +22,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", @@ -79,6 +80,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", From 3ba07ce395f5a8cb8dc28de930d170e1e5fd69ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 6 Dec 2021 18:55:28 +0100 Subject: [PATCH 1360/1452] Fix CO2 calculation when data is missing (#61106) --- .../components/energy/websocket_api.py | 2 + tests/components/energy/test_websocket_api.py | 193 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index e15713ff8ad..cdc7599b55b 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -303,6 +303,8 @@ async def ws_get_fossil_energy_consumption( """Reduce hourly deltas to daily or monthly deltas.""" result: list[dict[str, Any]] = [] deltas: list[float] = [] + if not stat_list: + return result prev_stat: dict[str, Any] = stat_list[0] # Loop over the hourly deltas + a fake entry to end the period diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index f86e43dd1b2..46c6a5c0fa6 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -472,8 +472,10 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) external_energy_statistics_1 = ( { @@ -575,6 +577,197 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period4.isoformat(): pytest.approx(88.0 - 55.0), } + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx(55.0 - 3.0), + period4_day_start.isoformat(): pytest.approx(88.0 - 55.0), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx((55.0 - 3.0) + (88.0 - 55.0)), + } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_no_data(hass, hass_ws_client): + """Test fossil_energy_consumption when there is no data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": None, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": None, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 80, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") async def test_fossil_energy_consumption(hass, hass_ws_client): From 325aa66b8ca10ddda36d98a630d43bde84c85b2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 09:46:17 -0800 Subject: [PATCH 1361/1452] Bump aiohue to 3.0.2 (#61115) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 02734df1481..c789755c9a3 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.1"], + "requirements": ["aiohue==3.0.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3f7fa4cbe75..8022ad374b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 615d41b663b..88045ab4f19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 725e3046db81a6285f56c700f1da80d5a0f8e4f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 10:08:52 -0800 Subject: [PATCH 1362/1452] Return native timestamps for home connect (#61116) --- homeassistant/components/home_connect/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 373ad6be295..910bec3e6ab 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -63,16 +63,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): elif ( self._state is not None and self._sign == 1 - and dt_util.parse_datetime(self._state) < dt_util.utcnow() + and self._state < dt_util.utcnow() ): # if the date is supposed to be in the future but we're # already past it, set state to None. self._state = None else: seconds = self._sign * float(status[self._key][ATTR_VALUE]) - self._state = ( - dt_util.utcnow() + timedelta(seconds=seconds) - ).isoformat() + self._state = dt_util.utcnow() + timedelta(seconds=seconds) else: self._state = status[self._key].get(ATTR_VALUE) if self._key == BSH_OPERATION_STATE: From d1672a1e9aaa4ddc1eed83d181bf1e6692eeb581 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 00:17:17 +0100 Subject: [PATCH 1363/1452] Remove colon from default entity name in Hue integration (#61118) --- homeassistant/components/hue/v2/entity.py | 2 +- tests/components/hue/test_binary_sensor.py | 2 +- tests/components/hue/test_sensor_v2.py | 6 +++--- tests/components/hue/test_switch.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 368a6cfe9d0..6dbc959fd9c 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -64,7 +64,7 @@ class HueBaseEntity(Entity): type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) - return f"{dev_name}: {type_title}" + return f"{dev_name} {type_title}" async def async_added_to_hass(self) -> None: """Call when entity is added.""" diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index f1d6a1a8087..ba5a58b4be0 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -19,7 +19,7 @@ async def test_binary_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion") assert sensor is not None assert sensor.state == "off" - assert sensor.name == "Hue motion sensor: Motion" + assert sensor.name == "Hue motion sensor Motion" assert sensor.attributes["device_class"] == "motion" assert sensor.attributes["motion_valid"] is True diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 2668922590f..256c323ccce 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -22,7 +22,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_temperature") assert sensor is not None assert sensor.state == "18.1" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Temperature" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Temperature" assert sensor.attributes["device_class"] == "temperature" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "°C" @@ -32,7 +32,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_illuminance") assert sensor is not None assert sensor.state == "63" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Illuminance" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Illuminance" assert sensor.attributes["device_class"] == "illuminance" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "lx" @@ -43,7 +43,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery") assert sensor is not None assert sensor.state == "100" - assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls: Battery" + assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls Battery" assert sensor.attributes["device_class"] == "battery" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "%" diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 30f4d3634b4..257f1a253c3 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -17,7 +17,7 @@ async def test_switch(hass, mock_bridge_v2, v2_resources_test_data): # test config switch to enable/disable motion sensor test_entity = hass.states.get("switch.hue_motion_sensor_motion") assert test_entity is not None - assert test_entity.name == "Hue motion sensor: Motion" + assert test_entity.name == "Hue motion sensor Motion" assert test_entity.state == "on" assert test_entity.attributes["device_class"] == "switch" From 0532c2206923671a1fb3f90d688010a6c0495cde Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:20:59 -0700 Subject: [PATCH 1364/1452] Bump simplisafe-python to 2021.12.0 (#61121) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 81cb5b7febc..954c39efce1 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==2021.11.2"], + "requirements": ["simplisafe-python==2021.12.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8022ad374b4..ac45e00c77c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2149,7 +2149,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88045ab4f19..4ef88e0196f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.slack slackclient==2.5.0 From b6d012222a9e7d898fe0a1c1d31e40355bddfdac Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Dec 2021 17:21:28 -0600 Subject: [PATCH 1365/1452] Improve Sonos activity debug logging (#61122) --- homeassistant/components/sonos/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index e80d16a491b..74897a618ea 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -40,7 +40,7 @@ def soco_error( return None except (OSError, SoCoException, SoCoUPnPException) as err: error_code = getattr(err, "error_code", None) - function = funct.__name__ + function = funct.__qualname__ if errorcodes and error_code in errorcodes: _LOGGER.debug( "Error code %s ignored in call to %s", error_code, function @@ -59,7 +59,9 @@ def soco_error( return None dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", funct.__name__ + self.hass, + f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", + funct.__qualname__, ) return result From 86f5165e4c6a896d0230515afab7afdad3f43f09 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:23:03 -0700 Subject: [PATCH 1366/1452] Deprecate `entity_id` parameter in Guardian service calls (#61129) --- homeassistant/components/guardian/__init__.py | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 892080b9afe..fc7e2dd7be3 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable -from typing import cast +from typing import TYPE_CHECKING, cast from aioguardian import Client from aioguardian.errors import GuardianError @@ -11,6 +11,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, CONF_DEVICE_ID, CONF_FILENAME, CONF_IP_ADDRESS, @@ -18,7 +20,11 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -63,20 +69,41 @@ SERVICES = ( SERVICE_NAME_UPGRADE_FIRMWARE, ) -SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_UID): cv.string, - } +SERVICE_BASE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_UID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) @@ -86,6 +113,14 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" + if ATTR_ENTITY_ID in call.data: + entity_registry = er.async_get(hass) + entity_registry_entry = entity_registry.async_get(call.data[ATTR_ENTITY_ID]) + if TYPE_CHECKING: + assert entity_registry_entry + assert entity_registry_entry.config_entry_id + return entity_registry_entry.config_entry_id + device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -221,15 +256,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service_name, schema, method in ( - (SERVICE_NAME_DISABLE_AP, None, async_disable_ap), - (SERVICE_NAME_ENABLE_AP, None, async_enable_ap), + (SERVICE_NAME_DISABLE_AP, SERVICE_BASE_SCHEMA, async_disable_ap), + (SERVICE_NAME_ENABLE_AP, SERVICE_BASE_SCHEMA, async_enable_ap), ( SERVICE_NAME_PAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, async_pair_sensor, ), - (SERVICE_NAME_REBOOT, None, async_reboot), - (SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics), + (SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot), + ( + SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, + SERVICE_BASE_SCHEMA, + async_reset_valve_diagnostics, + ), ( SERVICE_NAME_UNPAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, From 348079f069534711315f7a867a7021714d031694 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 15:51:03 -0800 Subject: [PATCH 1367/1452] Bump frontend to 20211206.0 (#61133) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1ca97e4cdd8..6d419276029 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211203.0" + "home-assistant-frontend==20211206.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 029f889fd85..5082fe5559d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index ac45e00c77c..1ac6768455d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ef88e0196f..6daf9a62177 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d105e9f99e35f143844fb79241dd7531073c84c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 15:54:09 -0800 Subject: [PATCH 1368/1452] Bumped version to 2021.12.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9c4b09944f..de6fbd815e4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 01adc6a042050a8a0ae532a64fa9dbb9bc634831 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 6 Dec 2021 18:01:46 +0100 Subject: [PATCH 1369/1452] Improve code quality trafikverket_weatherstation (#61044) * Code quality trafikverket_weatherstation * Updates from review * Fix extra attributes settings * Fix for additional review comments --- .../trafikverket_weatherstation/sensor.py | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 46fa3d9a5bd..01b70d5d3c7 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,13 +5,15 @@ import asyncio from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any import aiohttp -from pytrafikverket.trafikverket_weather import TrafikverketWeather +from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -70,6 +72,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="road_temp", @@ -78,12 +81,14 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation", api_key="precipitationtype", name="Precipitation type", icon="mdi:weather-snowy-rainy", + entity_registry_enabled_default=False, ), TrafikverketSensorEntityDescription( key="wind_direction", @@ -91,6 +96,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind direction", native_unit_of_measurement=DEGREE, icon="mdi:flag-triangle", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_direction_text", @@ -104,6 +110,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_speed_max", @@ -111,6 +118,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed max", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy-variant", + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="humidity", @@ -119,6 +128,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amount", @@ -126,18 +137,20 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Precipitation amount", native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:cup-water", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amountname", api_key="precipitation_amountname", name="Precipitation name", icon="mdi:weather-pouring", + entity_registry_enabled_default=False, ), ) SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, @@ -172,17 +185,12 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - sensor_name = entry.data[CONF_STATION] - sensor_api = entry.data[CONF_API_KEY] - sensor_station = entry.data[CONF_STATION] - web_session = async_get_clientsession(hass) - - weather_api = TrafikverketWeather(web_session, sensor_api) + weather_api = TrafikverketWeather(web_session, entry.data[CONF_API_KEY]) entities = [ TrafikverketWeatherStation( - weather_api, sensor_name, sensor_station, description + weather_api, entry.entry_id, entry.data[CONF_STATION], description ) for description in SENSOR_TYPES ] @@ -197,29 +205,36 @@ class TrafikverketWeatherStation(SensorEntity): def __init__( self, - weather_api, - name, - sensor_station, + weather_api: TrafikverketWeather, + entry_id: str, + sensor_station: str, description: TrafikverketSensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = f"{sensor_station} {description.name}" + self._attr_unique_id = f"{entry_id}_{description.key}" self._station = sensor_station self._weather_api = weather_api - self._weather = None + self._weather: WeatherStationInfo | None = None + self._active: bool | None = None + self._measure_time: str | None = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of Trafikverket Weatherstation.""" - return { + _additional_attributes: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_ACTIVE: self._weather.active, - ATTR_MEASURE_TIME: self._weather.measure_time, } + if self._active: + _additional_attributes[ATTR_ACTIVE] = self._active + if self._measure_time: + _additional_attributes[ATTR_MEASURE_TIME] = self._measure_time + + return _additional_attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Trafikverket and updates the states.""" try: self._weather = await self._weather_api.async_get_weather(self._station) @@ -228,3 +243,6 @@ class TrafikverketWeatherStation(SensorEntity): ) except (asyncio.TimeoutError, aiohttp.ClientError, ValueError) as error: _LOGGER.error("Could not fetch weather data: %s", error) + return + self._active = self._weather.active + self._measure_time = self._weather.measure_time From 8da375660296a28479c471d1ebaa219bbdfe99fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 08:07:31 +0100 Subject: [PATCH 1370/1452] Bump hatasmota to 0.3.1 (#61120) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 9ea06bea545..bd30231396f 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.0"], + "requirements": ["hatasmota==0.3.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 1ac6768455d..7a74a9a7575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ hass-nabucasa==0.50.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6daf9a62177..0c881de605e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ hangups==0.4.14 hass-nabucasa==0.50.0 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 54ac192f7c1..c036f490f6d 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -9,6 +9,7 @@ from hatasmota.utils import ( get_topic_tele_sensor, get_topic_tele_will, ) +import pytest from homeassistant.components import cover from homeassistant.components.tasmota.const import DEFAULT_PREFIX @@ -34,6 +35,35 @@ async def test_missing_relay(hass, mqtt_mock, setup_tasmota): """Test no cover is discovered if relays are missing.""" +@pytest.mark.parametrize( + "relay_config, num_covers", + [ + ([3, 3, 3, 3, 3, 3, 1, 1, 3, 3], 4), + ([3, 3, 3, 3, 0, 0, 0, 0], 2), + ([3, 3, 1, 1, 0, 0, 0, 0], 1), + ([3, 3, 3, 1, 0, 0, 0, 0], 0), + ], +) +async def test_multiple_covers( + hass, mqtt_mock, setup_tasmota, relay_config, num_covers +): + """Test discovery of multiple covers.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"] = relay_config + mac = config["mac"] + + assert len(hass.states.async_all("cover")) == 0 + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all("cover")) == num_covers + + async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) From fa447332c61b9600f503046be563ca8fab630225 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 7 Dec 2021 09:00:30 +0100 Subject: [PATCH 1371/1452] Fix point availability (#61144) --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index dee0ae1a492..5cbab59eabf 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -185,7 +185,7 @@ class MinutPointClient: async def _sync(self): """Update local list of devices.""" - if not await self._client.update() and self._is_available: + if not await self._client.update(): self._is_available = False _LOGGER.warning("Device is unavailable") async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) From 4ad904f3b7a140d106f8fd17a301ea8186cd8e83 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 21:50:34 +0100 Subject: [PATCH 1372/1452] Change check for existence of options flow (#61147) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/config/config_entries.py | 10 ++++------ homeassistant/components/hue/config_flow.py | 15 ++++++++++----- homeassistant/config_entries.py | 6 ++++++ tests/components/config/test_config_entries.py | 8 +++++++- tests/components/hue/test_config_flow.py | 5 ++--- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b1f686e23a4..61df9dc190d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -366,13 +366,11 @@ async def ignore_config_flow(hass, connection, msg): def entry_json(entry: config_entries.ConfigEntry) -> dict: """Return JSON value of a config entry.""" handler = config_entries.HANDLERS.get(entry.domain) - supports_options = ( - # Guard in case handler is no longer registered (custom component etc) - handler is not None - # pylint: disable=comparison-with-callable - and handler.async_get_options_flow - != config_entries.ConfigFlow.async_get_options_flow + # work out if handler has support for options flow + supports_options = handler is not None and handler.async_supports_options_flow( + entry ) + return { "entry_id": entry.entry_id, "domain": entry.domain, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ceb5a9a1a8e..49fca2158d5 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,10 +48,15 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - if config_entry.data.get(CONF_API_VERSION, 1) == 1: - # Options for Hue are only applicable to V1 bridges. - return HueOptionsFlowHandler(config_entry) - raise data_entry_flow.UnknownHandler + return HueOptionsFlowHandler(config_entry) + + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return config_entry.data.get(CONF_API_VERSION, 1) == 1 def __init__(self) -> None: """Initialize the Hue flow.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 45bc10f5774..cdea9da2540 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1163,6 +1163,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @classmethod + @callback + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: + """Return options flow support for this handler.""" + return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow + @callback def _async_abort_entries_match( self, match_dict: dict[str, Any] | None = None diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8b890148d51..20a19495597 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -47,10 +47,16 @@ async def test_get_entries(hass, client): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): """Get options flow.""" pass + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True + hass.helpers.config_entry_flow.register_discovery_flow( "comp2", "Comp 2", lambda: None ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 65d3dd696d6..6ce8ff3e1c4 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,8 +706,7 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - with pytest.raises(data_entry_flow.UnknownHandler): - await hass.config_entries.options.async_init(entry.entry_id) + assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False async def test_bridge_zeroconf(hass, aioclient_mock): From 78ada630c002c031bf5647a21f2dbea0e904f194 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 12:44:50 +0100 Subject: [PATCH 1373/1452] Guard against missing states in Alexa state updates (#61152) --- homeassistant/components/alexa/state_report.py | 9 +++++---- tests/components/alexa/test_state_report.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1ab12041e32..767bfa18224 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -182,12 +182,13 @@ async def async_send_add_or_update_message(hass, config, entity_ids): endpoints = [] for entity_id in entity_ids: - domain = entity_id.split(".", 1)[0] - - if domain not in ENTITY_ADAPTERS: + if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: continue - alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) + if (state := hass.states.get(entity_id)) is None: + continue + + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state) endpoints.append(alexa_entity.serialize_discovery()) payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index bd91dc8f846..29624e7d1ff 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -117,10 +117,18 @@ async def test_send_add_or_update_message(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_send_add_or_update_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] + hass.states.async_set( + "zwave.bla", + "wow_such_unsupported", ) + entities = [ + "binary_sensor.test_contact", + "binary_sensor.non_existing", # Supported, but does not exist + "zwave.bla", # Unsupported + ] + await state_report.async_send_add_or_update_message(hass, DEFAULT_CONFIG, entities) + assert len(aioclient_mock.mock_calls) == 1 call = aioclient_mock.mock_calls From 816b5af883a2566fc502134c2c54987d463edfb8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 7 Dec 2021 13:56:31 +0100 Subject: [PATCH 1374/1452] Fix Netatmo climate issue (#61154) Signed-off-by: cgtobi --- homeassistant/components/netatmo/climate.py | 11 +- .../components/netatmo/data_handler.py | 6 +- homeassistant/components/netatmo/select.py | 8 ++ .../netatmo/fixtures/homesdata.json | 102 ++++++++++++++++-- .../homestatus_111111111111111111111401.json | 4 + 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 49e02f566a0..1ead9d7cbdb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -135,9 +135,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + climate_state = data_handler.data[signal_name] climate_topology.register_handler(home_id, climate_state.process_topology) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index c62522a931a..7a97ec3748f 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -194,7 +194,11 @@ class NetatmoDataHandler: self._auth, **kwargs ) - await self.async_fetch_data(data_class_entry) + try: + await self.async_fetch_data(data_class_entry) + except KeyError: + self.data_classes.pop(data_class_entry) + raise self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 9902155be73..98576497f3e 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -48,6 +48,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index fd63a0c200f..8c6587ca973 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -5,7 +5,10 @@ "id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "altitude": 112, - "coordinates": [52.516263, 13.377726], + "coordinates": [ + 52.516263, + 13.377726 + ], "country": "DE", "timezone": "Europe/Berlin", "rooms": [ @@ -13,25 +16,33 @@ "id": "2746182631", "name": "Livingroom", "type": "livingroom", - "module_ids": ["12:34:56:00:01:ae"] + "module_ids": [ + "12:34:56:00:01:ae" + ] }, { "id": "3688132631", "name": "Hall", "type": "custom", - "module_ids": ["12:34:56:00:f1:62"] + "module_ids": [ + "12:34:56:00:f1:62" + ] }, { "id": "2833524037", "name": "Entrada", "type": "lobby", - "module_ids": ["12:34:56:03:a5:54"] + "module_ids": [ + "12:34:56:03:a5:54" + ] }, { "id": "2940411577", "name": "Cocina", "type": "kitchen", - "module_ids": ["12:34:56:03:a0:ac"] + "module_ids": [ + "12:34:56:03:a0:ac" + ] } ], "modules": [ @@ -388,6 +399,85 @@ } ], "therm_mode": "schedule" + }, + { + "id": "111111111111111111111401", + "name": "Home with no modules", + "altitude": 9, + "coordinates": [ + 1.23456789, + 50.0987654 + ], + "country": "BE", + "timezone": "Europe/Brussels", + "rooms": [ + { + "id": "1111111401", + "name": "Livingroom", + "type": "livingroom" + } + ], + "temperature_control_mode": "heating", + "therm_mode": "away", + "therm_setpoint_default_duration": 120, + "cooling_mode": "schedule", + "schedules": [ + { + "away_temp": 14, + "hg_temp": 7, + "name": "Week", + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 6, + "m_offset": 420 + } + ], + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [], + "id": 0, + "rooms": [] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [], + "id": 1, + "rooms": [] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [], + "id": 4, + "rooms": [] + }, + { + "type": 4, + "name": "Tussenin", + "rooms_temp": [], + "id": 5, + "rooms": [] + }, + { + "type": 4, + "name": "Ochtend", + "rooms_temp": [], + "id": 6, + "rooms": [] + } + ], + "id": "700000000000000000000401", + "selected": true, + "type": "therm" + } + ] } ], "user": { @@ -404,4 +494,4 @@ "status": "ok", "time_exec": 0.056135892868042, "time_server": 1559171003 -} +} \ No newline at end of file diff --git a/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json new file mode 100644 index 00000000000..2ae65dc0d21 --- /dev/null +++ b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json @@ -0,0 +1,4 @@ +{ + "status": "ok", + "time_server": 1638873670 +} \ No newline at end of file From 13ce6edc6870f60190e89e30f9643a9aa2cb87d8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Dec 2021 12:29:54 -0700 Subject: [PATCH 1375/1452] Bump py17track to 2021.12.2 (#61166) --- homeassistant/components/seventeentrack/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 05f240043a9..01fdb22395c 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -2,7 +2,7 @@ "domain": "seventeentrack", "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": ["py17track==2021.12.1"], + "requirements": ["py17track==2021.12.2"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a74a9a7575..cf6b78a83b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ py-synologydsm-api==1.0.4 py-zabbix==1.1.7 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.hdmi_cec pyCEC==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c881de605e..38e8fcf29f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -792,7 +792,7 @@ py-nightscout==1.2.2 py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.control4 pyControl4==0.0.6 From fecfbba44242d93ac37f0a679a062318b12b441f Mon Sep 17 00:00:00 2001 From: einarhauks Date: Tue, 7 Dec 2021 19:33:24 +0000 Subject: [PATCH 1376/1452] Display energy in wh instead of kWh (#61169) --- .../components/tesla_wall_connector/sensor.py | 10 +++++----- tests/components/tesla_wall_connector/test_sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index b2353681291..8219d121ae3 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, TEMP_CELSIUS, @@ -120,10 +120,10 @@ WALL_CONNECTOR_SENSORS = [ entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), WallConnectorSensorDescription( - key="total_energy_kWh", - name=prefix_entity_name("Total Energy"), - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh / 1000.0, + key="energy_kWh", + name=prefix_entity_name("Energy"), + native_unit_of_measurement=ENERGY_WATT_HOUR, + value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh, state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, ), diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 6763f685441..0cafc15c6f1 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -24,7 +24,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_total_energy", "988.022", "989.0" + "sensor.tesla_wall_connector_energy", "988022", "989000" ), EntityAndExpectedValues( "sensor.tesla_wall_connector_phase_a_current", "10", "7" From e09c85c5912d10fa7f2eea44dda3372f7d147653 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 7 Dec 2021 11:30:23 -0800 Subject: [PATCH 1377/1452] Bump nest to 0.4.5 to fix media player event expiration (#61174) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b9f20e92670..11a464dbaf1 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index cf6b78a83b8..11392cc4ac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38e8fcf29f0..e8c46b255f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 61545edd963b5116f97bec5ab44d5f0d327ab85f Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 7 Dec 2021 13:47:44 -0500 Subject: [PATCH 1378/1452] Remove loopenergy integration (#61175) * Remove loopenergy integration * Fix requirements_all.txt * Fix requirements_test_all.txt --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/loopenergy/__init__.py | 1 - .../components/loopenergy/manifest.json | 8 - homeassistant/components/loopenergy/sensor.py | 149 ------------------ requirements_all.txt | 3 - 6 files changed, 163 deletions(-) delete mode 100644 homeassistant/components/loopenergy/__init__.py delete mode 100644 homeassistant/components/loopenergy/manifest.json delete mode 100644 homeassistant/components/loopenergy/sensor.py diff --git a/.coveragerc b/.coveragerc index fc033c77369..05bb880c2f2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -597,7 +597,6 @@ omit = homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/lookin/climate.py - homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py homeassistant/components/luftdaten/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 513115a9953..f7333867069 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -293,7 +293,6 @@ homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lookin/* @ANMalko -homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/loopenergy/__init__.py b/homeassistant/components/loopenergy/__init__.py deleted file mode 100644 index 4e963f2828a..00000000000 --- a/homeassistant/components/loopenergy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The loopenergy component.""" diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json deleted file mode 100644 index 01a18dc01db..00000000000 --- a/homeassistant/components/loopenergy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "loopenergy", - "name": "Loop Energy", - "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": ["pyloopenergy==0.2.1"], - "codeowners": ["@pavoni"], - "iot_class": "cloud_push" -} diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py deleted file mode 100644 index 05d7f79ebfd..00000000000 --- a/homeassistant/components/loopenergy/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for Loop Energy sensors.""" -import logging - -import pyloopenergy -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, - EVENT_HOMEASSISTANT_STOP, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_ELEC = "electricity" -CONF_GAS = "gas" - -CONF_ELEC_SERIAL = "electricity_serial" -CONF_ELEC_SECRET = "electricity_secret" - -CONF_GAS_SERIAL = "gas_serial" -CONF_GAS_SECRET = "gas_secret" -CONF_GAS_CALORIFIC = "gas_calorific" - -CONF_GAS_TYPE = "gas_type" - -DEFAULT_CALORIFIC = 39.11 -DEFAULT_UNIT = "kW" - -ELEC_SCHEMA = vol.Schema( - { - vol.Required(CONF_ELEC_SERIAL): cv.string, - vol.Required(CONF_ELEC_SECRET): cv.string, - } -) - -GAS_TYPE_SCHEMA = vol.In([CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]) - -GAS_SCHEMA = vol.Schema( - { - vol.Required(CONF_GAS_SERIAL): cv.string, - vol.Required(CONF_GAS_SECRET): cv.string, - vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA, - vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): vol.Coerce(float), - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ELEC): ELEC_SCHEMA, vol.Optional(CONF_GAS): GAS_SCHEMA} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Loop Energy sensors.""" - elec_config = config.get(CONF_ELEC) - gas_config = config.get(CONF_GAS, {}) - - controller = pyloopenergy.LoopEnergy( - elec_config.get(CONF_ELEC_SERIAL), - elec_config.get(CONF_ELEC_SECRET), - gas_config.get(CONF_GAS_SERIAL), - gas_config.get(CONF_GAS_SECRET), - gas_config.get(CONF_GAS_TYPE), - gas_config.get(CONF_GAS_CALORIFIC), - ) - - def stop_loopenergy(event): - """Shutdown loopenergy thread on exit.""" - _LOGGER.info("Shutting down loopenergy") - controller.terminate() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_loopenergy) - - sensors = [LoopEnergyElec(controller)] - - if gas_config.get(CONF_GAS_SERIAL): - sensors.append(LoopEnergyGas(controller)) - - add_entities(sensors) - - -class LoopEnergySensor(SensorEntity): - """Implementation of an Loop Energy base sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - self._state = None - self._unit_of_measurement = DEFAULT_UNIT - self._controller = controller - self._name = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - def _callback(self): - self.schedule_update_ha_state(True) - - -class LoopEnergyElec(LoopEnergySensor): - """Implementation of an Loop Energy Electricity sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Power Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_elecricity(self._callback) - - def update(self): - """Get the cached Loop energy reading.""" - self._state = round(self._controller.electricity_useage, 2) - - -class LoopEnergyGas(LoopEnergySensor): - """Implementation of an Loop Energy Gas sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Gas Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_gas(self._callback) - - def update(self): - """Get the cached Loop gas reading.""" - self._state = round(self._controller.gas_useage, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 11392cc4ac7..a827ea36164 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,9 +1612,6 @@ pylitejet==0.3.0 # homeassistant.components.litterrobot pylitterbot==2021.11.0 -# homeassistant.components.loopenergy -pyloopenergy==0.2.1 - # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 9f1701f55734fe790aa545f6782a8a978e20c560 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 12:54:28 -0800 Subject: [PATCH 1379/1452] Bumped version to 2021.12.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de6fbd815e4..31c44f04051 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 707e50151105ace7c9e8951bf40f520028487898 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Dec 2021 19:59:26 +0100 Subject: [PATCH 1380/1452] Skip duplicated data when calculating fossil energy consumption (#60599) --- .../components/energy/websocket_api.py | 4 +- tests/components/energy/test_websocket_api.py | 217 +++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index cdc7599b55b..d243faae89f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -274,14 +274,16 @@ async def ws_get_fossil_energy_consumption( ) -> dict[datetime, float]: """Combine multiple statistics, returns a dict indexed by start time.""" result: defaultdict[datetime, float] = defaultdict(float) + seen: defaultdict[datetime, set[str]] = defaultdict(set) for statistics_id, stat in stats.items(): if statistics_id not in statistic_ids: continue for period in stat: - if period["sum"] is None: + if period["sum"] is None or statistics_id in seen[period["start"]]: continue result[period["start"]] += period["sum"] + seen[period["start"]].add(statistics_id) return {key: result[key] for key in sorted(result)} diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 46c6a5c0fa6..c1dc195a63e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,9 +1,10 @@ """Test the Energy websocket API.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.energy import data, is_configured +from homeassistant.components.recorder import statistics from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -963,6 +964,220 @@ async def test_fossil_energy_consumption(hass, hass_ws_client): } +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_duplicate(hass, hass_ws_client): + """Test fossil_energy_consumption with co2 sensor data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + with patch.object( + statistics, "_statistics_exists", return_value=False + ), patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock: + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + async_add_external_statistics( + hass, external_co2_metadata, external_co2_statistics + ) + await async_wait_recording_done_without_instance(hass) + assert insert_statistics_mock.call_count == 14 + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx( + ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) + ), + } + + async def test_fossil_energy_consumption_checks(hass, hass_ws_client): """Test fossil_energy_consumption parameter validation.""" client = await hass_ws_client(hass) From a581095bd070fb3e9f2b34bd66d1fea14c25adbc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 8 Dec 2021 19:32:25 +0100 Subject: [PATCH 1381/1452] Fix pvoutput template use and REST integer parsing (#61171) * Fix pvoutput template use and REST integer parsing * revert accepting templates as input --- homeassistant/components/pvoutput/sensor.py | 8 ++++++-- homeassistant/components/rest/utils.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 8126e00d8e5..512fb75067b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,9 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) -_ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" +_ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" ATTR_ENERGY_GENERATION = "energy_generation" ATTR_POWER_GENERATION = "power_generation" @@ -59,7 +60,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} + headers = { + "X-Pvoutput-Apikey": Template(api_key, hass), + "X-Pvoutput-SystemId": Template(system_id, hass), + } rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 24c58d294e1..35b3c22db31 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -23,5 +23,5 @@ def render_templates(tpl_dict: dict[str, Template] | None): rendered_items = {} for item_name, template_header in tpl_dict.items(): if (value := template_header.async_render()) is not None: - rendered_items[item_name] = value + rendered_items[item_name] = str(value) return rendered_items From bdc37e9353ebe903db96be950f99051535b5d848 Mon Sep 17 00:00:00 2001 From: Robert Blomqvist Date: Wed, 8 Dec 2021 02:19:23 +0100 Subject: [PATCH 1382/1452] Rephrase upgrade notification message to avoid installing Python 3.10 (#61181) --- homeassistant/bootstrap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f111ff6a079..64a6e98aa87 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -252,8 +252,7 @@ async def async_from_config_dict( f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " "Please upgrade Python to " - f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " - "higher." + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( From 04a2e1fd7b7c1c5304774796ff182c65a33a75ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:38 -1000 Subject: [PATCH 1383/1452] Fix uncaught exception in bond config flow (#61184) --- homeassistant/components/bond/config_flow.py | 5 ++- tests/components/bond/test_config_flow.py | 41 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 5fce8477a28..d3a7b4adf72 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -87,7 +87,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._discovered[CONF_ACCESS_TOKEN] = token - _, hub_name = await _validate_input(self.hass, self._discovered) + try: + _, hub_name = await _validate_input(self.hass, self._discovered) + except InputValidationError: + return self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf( diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 4a6efa8f89b..b36637897d8 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Bond config flow.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -304,6 +305,46 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_with_token_available_name_unavailable( + hass: core.HomeAssistant, +): + """Test we get the discovery form when we can get the token but the name is unavailable.""" + + with patch_bond_version( + side_effect=ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST) + ), patch_bond_token(return_value={"token": "discovered-token"}): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_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["title"] == "test-bond-id" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "discovered-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_already_configured(hass: core.HomeAssistant): """Test starting a flow from discovery when already configured.""" From 64c52aecef9bdd9300f1c7310dff43c981fca987 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 7 Dec 2021 18:38:34 -0500 Subject: [PATCH 1384/1452] Bump ZHA dependency zigpy-znp from 0.6.3 to 0.6.4 (#61194) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index aa167fc2df4..daeb90be801 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.42.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.6.3" + "zigpy-znp==0.6.4" ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, diff --git a/requirements_all.txt b/requirements_all.txt index a827ea36164..a4cd61798e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,7 +2504,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8c46b255f6..cc44099b4e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 From 428129cad7c5c08d47a91b4d2a4b136ffc5227c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:55 -1000 Subject: [PATCH 1385/1452] Fix log spam from flux_led 0x08 devices when in music mode (#61196) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 71d8fd350b7..defaa348262 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.17"], + "requirements": ["flux_led==0.26.2"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a4cd61798e0..cdf9fb48011 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc44099b4e6..c5d218a01b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 From 8735395144a5a6cc182caf02c26eb5e74a47e830 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:55:43 -0800 Subject: [PATCH 1386/1452] Fix Rova using strings as timestamp (#61201) --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 54e2c315a4e..ca9f201b302 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_native_value = pickup_date.isoformat() + self._attr_native_value = pickup_date class RovaData: From 2fa2a2e6d4515bcc4c500e1239ce538a15a38118 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:47:38 -0800 Subject: [PATCH 1387/1452] Fix bbox timestamp (#61202) --- homeassistant/components/bbox/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 53a7e8720b1..129803ad1e1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -134,7 +134,7 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0).isoformat() + self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From 67c808bde9c09483b35c759cac951a62e7c7457c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:48:04 -0800 Subject: [PATCH 1388/1452] Fix flipr timestamp sensor (#61203) --- homeassistant/components/flipr/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index e79ba131618..527742539c5 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,8 +1,6 @@ """Sensor platform for the Flipr's pool_sensor.""" from __future__ import annotations -from datetime import datetime - from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, @@ -60,7 +58,4 @@ class FliprSensor(FliprEntity, SensorEntity): @property def native_value(self): """State of the sensor.""" - state = self.coordinator.data[self.entity_description.key] - if isinstance(state, datetime): - return state.isoformat() - return state + return self.coordinator.data[self.entity_description.key] From 2c0e406c1b53cfa7fe0b0a50e524867e969c7c1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:49:43 -0800 Subject: [PATCH 1389/1452] Fix gtfs timestamp sensor (#61204) --- homeassistant/components/gtfs/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9450c717148..9a622e417ad 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -544,7 +544,7 @@ class GTFSDepartureSensor(SensorEntity): self._available = False self._icon = ICON self._name = "" - self._state: str | None = None + self._state: datetime.datetime | None = None self._attributes: dict[str, Any] = {} self._agency = None @@ -563,7 +563,7 @@ class GTFSDepartureSensor(SensorEntity): return self._name @property - def native_value(self) -> str | None: + def native_value(self) -> datetime.datetime | None: """Return the state of the sensor.""" return self._state @@ -619,9 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc( - self._departure["departure_time"] - ).isoformat() + self._state = dt_util.as_utc(self._departure["departure_time"]) # Fetch trip and route details once, unless updated if not self._departure: From dc3ece447b12212df271814e1315cbb87542b5c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:07 -0800 Subject: [PATCH 1390/1452] Fix hvv_departures timestamp sensor (#61205) --- homeassistant/components/hvv_departures/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index da52fd878d8..d82a15cebe9 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -116,7 +116,7 @@ class HVVDepartureSensor(SensorEntity): departure_time + timedelta(minutes=departure["timeOffset"]) + timedelta(seconds=delay) - ).isoformat() + ) self.attr.update( { From 7583d9a4091641fff8cea5e5ef92e6b126444586 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:22 -0800 Subject: [PATCH 1391/1452] Fix hydrawise timestamp sensor (#61206) --- homeassistant/components/hydrawise/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index f8c02309569..ee9e931a351 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -83,4 +83,4 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): _LOGGER.debug("New cycle time: %s", next_cycle) self._attr_native_value = dt.utc_from_timestamp( dt.as_timestamp(dt.now()) + next_cycle - ).isoformat() + ) From 700eaf8794486fd56e28bd906425f1d9cd380326 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:39:27 -0800 Subject: [PATCH 1392/1452] Fix islamic prayer times timestamp sensor (#61207) --- homeassistant/components/islamic_prayer_times/sensor.py | 6 ++---- tests/components/islamic_prayer_times/test_config_flow.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 99cc65bb548..38a95fb803b 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -45,10 +45,8 @@ class IslamicPrayerTimeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return ( - self.client.prayer_times_info.get(self.sensor_type) - .astimezone(dt_util.UTC) - .isoformat() + return self.client.prayer_times_info.get(self.sensor_type).astimezone( + dt_util.UTC ) async def async_added_to_hass(self): diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 842a877e292..18d64842c65 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import islamic_prayer_times +from homeassistant.components.islamic_prayer_times import config_flow # noqa: F401 from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN from tests.common import MockConfigEntry From d533aba4f90a377aa6f3efa1ec08d568870d51e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:38:35 -0800 Subject: [PATCH 1393/1452] Fix litterrobot timestamp sensor (#61208) * Fix litterrobot timestamp sensor * Update type --- homeassistant/components/litterrobot/sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1fab6983249..6b4dc1b3300 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,9 +1,11 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from datetime import datetime + from pylitterbot.robot import Robot -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, StateType from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.core import HomeAssistant @@ -36,7 +38,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): self.sensor_attribute = sensor_attribute @property - def native_value(self) -> str: + def native_value(self) -> StateType | datetime: """Return the state.""" return getattr(self.robot, self.sensor_attribute) @@ -59,10 +61,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensor.""" @property - def native_value(self) -> str | None: + def native_value(self) -> StateType | datetime: """Return the state.""" if self.robot.sleep_mode_enabled: - return super().native_value.isoformat() + return super().native_value return None @property From 66fa6dff93498eccc6fc4db8419dab04676744e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:46:45 -0800 Subject: [PATCH 1394/1452] Fix lyric timestamp sensor (#61209) * Fix lyric timestamp sensor * Update type --- homeassistant/components/lyric/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 6f550813ad8..be156594524 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,7 +47,7 @@ LYRIC_SETPOINT_STATUS_NAMES = { class LyricSensorEntityDescription(SensorEntityDescription): """Class describing Honeywell Lyric sensor entities.""" - value: Callable[[LyricDevice], StateType] = round + value: Callable[[LyricDevice], StateType | datetime] = round def get_datetime_from_future_time(time: str) -> datetime: @@ -133,7 +133,7 @@ async def async_setup_entry( device_class=DEVICE_CLASS_TIMESTAMP, value=lambda device: get_datetime_from_future_time( device.changeableValues.nextPeriodTime - ).isoformat(), + ), ), location, device, From e68dcff3f364731782c07035fdaebb66fd61b254 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:25:42 -0800 Subject: [PATCH 1395/1452] Fix meteo_france timestamp sensor (#61210) --- homeassistant/components/meteo_france/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index ac1ccc13009..7f4e3e0a77b 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -142,11 +142,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1), None, ) - return ( - dt_util.utc_from_timestamp(next_rain["dt"]).isoformat() - if next_rain - else None - ) + return dt_util.utc_from_timestamp(next_rain["dt"]) if next_rain else None @property def extra_state_attributes(self): From d080c31583a2567d1caa294becd0aeb10ccecdd6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:38 -0800 Subject: [PATCH 1396/1452] Fix modern_forms timestmap sensors (#61211) --- homeassistant/components/modern_forms/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 1e51ec9a1ae..5c9e0a18575 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 native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.light_sleep_timer @@ -83,7 +83,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): or (sleep_time - dt_util.utcnow()).total_seconds() < 0 ): return None - return sleep_time.isoformat() + return sleep_time class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): @@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.fan_sleep_timer @@ -115,4 +115,4 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): ): return None - return sleep_time.isoformat() + return sleep_time From e6b784e4f2c99c8e84b4f7b71a2d64c08ce5116e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:23:14 -0800 Subject: [PATCH 1397/1452] Fix nextbus timestamp sensor (#61212) --- homeassistant/components/nextbus/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index f9df0d60412..3756c1853b7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -218,6 +218,4 @@ class NextBusDepartureSensor(SensorEntity): ) latest_prediction = maybe_first(predictions) - self._state = utc_from_timestamp( - int(latest_prediction["epochTime"]) / 1000 - ).isoformat() + self._state = utc_from_timestamp(int(latest_prediction["epochTime"]) / 1000) From 2513347e2746105d1aecfcec57790b76d8b253d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:52 -0800 Subject: [PATCH 1398/1452] Fix oasa_telematics timestamp sensor (#61213) --- homeassistant/components/oasa_telematics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 1a51738cb77..a5a4a98c3d4 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -120,7 +120,7 @@ class OASATelematicsSensor(SensorEntity): self._name_data = self.data.name_data next_arrival_data = self._times[0] if ATTR_NEXT_ARRIVAL in next_arrival_data: - self._state = next_arrival_data[ATTR_NEXT_ARRIVAL].isoformat() + self._state = next_arrival_data[ATTR_NEXT_ARRIVAL] class OASATelematicsData: From 7940aab4c5262451dc8aac4f0637b152133d0e16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:12:26 -0800 Subject: [PATCH 1399/1452] Fix repetier timestamp sensors (#61214) --- homeassistant/components/repetier/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 393d8a16ae3..25c70cc2960 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -160,7 +160,7 @@ class RepetierJobEndSensor(RepetierSensor): print_time = data["print_time"] from_start = data["from_start"] time_end = start + round(print_time, 0) - self._state = datetime.utcfromtimestamp(time_end).isoformat() + self._state = datetime.utcfromtimestamp(time_end) remaining = print_time - from_start remaining_secs = int(round(remaining, 0)) _LOGGER.debug( @@ -182,7 +182,7 @@ class RepetierJobStartSensor(RepetierSensor): job_name = data["job_name"] start = data["start"] from_start = data["from_start"] - self._state = datetime.utcfromtimestamp(start).isoformat() + self._state = datetime.utcfromtimestamp(start) elapsed_secs = int(round(from_start, 0)) _LOGGER.debug( "Job %s elapsed %s", From b5b2c3cc0d315b0331e697831cffdf577edf068f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:13 -0800 Subject: [PATCH 1400/1452] Fix vallox timestamp sensor (#61216) * Fix vallox timestamp sensor * Change old state type --- homeassistant/components/vallox/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 17bcf0e4499..6eee46be737 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -56,7 +56,7 @@ class ValloxSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{uuid}-{description.key}" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" if (metric_key := self.entity_description.metric_key) is None: return None @@ -84,7 +84,7 @@ class ValloxFanSpeedSensor(ValloxSensor): """Child class for fan speed reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" fan_is_on = self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON return super().native_value if fan_is_on else 0 @@ -94,7 +94,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): """Child class for filter remaining time reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" super_native_value = super().native_value @@ -107,7 +107,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - return (now + days_remaining_delta).isoformat() + return now + days_remaining_delta class ValloxCellStateSensor(ValloxSensor): From 030ac3d762e4f1e0b0dbe240b738ca68309d6743 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:28 -0800 Subject: [PATCH 1401/1452] Fix yandex_transport timestamp sensor (#61217) --- homeassistant/components/yandex_transport/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index bd5d85d3ffe..724fca14725 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -129,9 +129,7 @@ class DiscoverYandexTransport(SensorEntity): if closer_time is None: self._state = None else: - self._state = dt_util.utc_from_timestamp(closer_time).isoformat( - timespec="seconds" - ) + self._state = dt_util.utc_from_timestamp(closer_time).replace(microsecond=0) self._attrs = attrs @property From 0b470bb8fb0670da5f788e82a62b2bf9c117de05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:14:21 -0800 Subject: [PATCH 1402/1452] Fix follow-up review comment for bbox (#61219) --- homeassistant/components/bbox/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 129803ad1e1..fc9c8982733 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -131,10 +131,9 @@ class BboxUptimeSensor(SensorEntity): def update(self): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() - uptime = utcnow() - timedelta( + self._attr_native_value = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From 36eca38be2d1d1e1e3263773ee67565885991630 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:26:45 -0800 Subject: [PATCH 1403/1452] don't convert GTFS timestamp to UTC in timestamp sensor (#61221) --- homeassistant/components/gtfs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9a622e417ad..367a45aa073 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -619,7 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc(self._departure["departure_time"]) + self._state = self._departure["departure_time"] # Fetch trip and route details once, unless updated if not self._departure: From 0cb0136b2fbe3eb300e7c812d1094aa66933a9ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 11:02:14 -0800 Subject: [PATCH 1404/1452] Bumped version to 2021.12.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31c44f04051..e50660a841f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 89eed9c31ee166ea28bec554def7794a0651baa8 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 7 Dec 2021 13:16:24 +0100 Subject: [PATCH 1405/1452] Allow to lock SQLite database during backup (#60874) * Allow to set CONF_DB_URL This is useful for test which need a custom DB path. * Introduce write_lock_db helper to lock SQLite database * Introduce Websocket API which allows to lock database during backup * Fix isort * Avoid mutable default arguments * Address pylint issues * Avoid holding executor thread * Set unlock event in case timeout occures This makes sure the database is left unlocked even in case of a race condition. * Add more unit tests * Address new pylint errors * Lower timeout to speedup tests * Introduce queue overflow test * Unlock database if necessary This makes sure that the test runs through in case locking actually succeeds (and the test fails). * Make DB_LOCK_TIMEOUT a global There is no good reason for this to be an argument. The recorder needs to pick a sensible value. * Add Websocket Timeout test * Test lock_database() return * Update homeassistant/components/recorder/__init__.py Co-authored-by: Erik Montnemery * Fix format Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/recorder/__init__.py | 100 ++++++++++++++++-- homeassistant/components/recorder/util.py | 19 ++++ .../components/recorder/websocket_api.py | 40 +++++++ tests/common.py | 5 +- tests/components/recorder/test_init.py | 79 ++++++++++++++ tests/components/recorder/test_util.py | 19 ++++ .../components/recorder/test_websocket_api.py | 59 +++++++++++ 7 files changed, 310 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index da3955cb9b8..8a907a8d9fa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable import concurrent.futures +from dataclasses import dataclass from datetime import datetime, timedelta import logging import queue @@ -76,6 +77,7 @@ from .util import ( session_scope, setup_connection_for_dialect, validate_or_move_away_sqlite_database, + write_lock_db, ) _LOGGER = logging.getLogger(__name__) @@ -123,6 +125,9 @@ KEEPALIVE_TIME = 30 # States and Events objects EXPIRE_AFTER_COMMITS = 120 +DB_LOCK_TIMEOUT = 30 +DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 + CONF_AUTO_PURGE = "auto_purge" CONF_DB_URL = "db_url" CONF_DB_MAX_RETRIES = "db_max_retries" @@ -370,6 +375,15 @@ class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" +@dataclass +class DatabaseLockTask: + """An object to insert into the recorder queue to prevent writes to the database.""" + + database_locked: asyncio.Event + database_unlock: threading.Event + queue_overflow: bool + + class Recorder(threading.Thread): """A threaded recorder class.""" @@ -419,6 +433,7 @@ class Recorder(threading.Thread): self.migration_in_progress = False self._queue_watcher = None self._db_supports_row_number = True + self._database_lock_task: DatabaseLockTask | None = None self.enabled = True @@ -687,6 +702,8 @@ class Recorder(threading.Thread): def _process_one_event_or_recover(self, event): """Process an event, reconnect, or recover a malformed database.""" try: + if self._process_one_task(event): + return self._process_one_event(event) return except exc.DatabaseError as err: @@ -788,34 +805,63 @@ class Recorder(threading.Thread): # Schedule a new statistics task if this one didn't finish self.queue.put(ExternalStatisticsTask(metadata, stats)) - def _process_one_event(self, event): + def _lock_database(self, task: DatabaseLockTask): + @callback + def _async_set_database_locked(task: DatabaseLockTask): + task.database_locked.set() + + with write_lock_db(self): + # Notify that lock is being held, wait until database can be used again. + self.hass.add_job(_async_set_database_locked, task) + while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): + if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: + _LOGGER.warning( + "Database queue backlog reached more than 90% of maximum queue " + "length while waiting for backup to finish; recorder will now " + "resume writing to database. The backup can not be trusted and " + "must be restarted" + ) + task.queue_overflow = True + break + _LOGGER.info( + "Database queue backlog reached %d entries during backup", + self.queue.qsize(), + ) + + def _process_one_task(self, event) -> bool: """Process one event.""" if isinstance(event, PurgeTask): self._run_purge(event.purge_before, event.repack, event.apply_filter) - return + return True if isinstance(event, PurgeEntitiesTask): self._run_purge_entities(event.entity_filter) - return + return True if isinstance(event, PerodicCleanupTask): perodic_db_cleanups(self) - return + return True if isinstance(event, StatisticsTask): self._run_statistics(event.start) - return + return True if isinstance(event, ClearStatisticsTask): statistics.clear_statistics(self, event.statistic_ids) - return + return True if isinstance(event, UpdateStatisticsMetadataTask): statistics.update_statistics_metadata( self, event.statistic_id, event.unit_of_measurement ) - return + return True if isinstance(event, ExternalStatisticsTask): self._run_external_statistics(event.metadata, event.statistics) - return + return True if isinstance(event, WaitTask): self._queue_watch.set() - return + return True + if isinstance(event, DatabaseLockTask): + self._lock_database(event) + return True + return False + + def _process_one_event(self, event): if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -982,6 +1028,42 @@ class Recorder(threading.Thread): self.queue.put(WaitTask()) self._queue_watch.wait() + async def lock_database(self) -> bool: + """Lock database so it can be backed up safely.""" + if self._database_lock_task: + _LOGGER.warning("Database already locked") + return False + + database_locked = asyncio.Event() + task = DatabaseLockTask(database_locked, threading.Event(), False) + self.queue.put(task) + try: + await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + except asyncio.TimeoutError as err: + task.database_unlock.set() + raise TimeoutError( + f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." + ) from err + self._database_lock_task = task + return True + + @callback + def unlock_database(self) -> bool: + """Unlock database. + + Returns true if database lock has been held throughout the process. + """ + if not self._database_lock_task: + _LOGGER.warning("Database currently not locked") + return False + + self._database_lock_task.database_unlock.set() + success = not self._database_lock_task.queue_overflow + + self._database_lock_task = None + + return success + def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c63f6abee3a..3900641db63 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -457,6 +457,25 @@ def perodic_db_cleanups(instance: Recorder): connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) +@contextmanager +def write_lock_db(instance: Recorder): + """Lock database for writes.""" + + if instance.engine.dialect.name == "sqlite": + with instance.engine.connect() as connection: + # Execute sqlite to create a wal checkpoint + # This is optional but makes sure the backup is going to be minimal + connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) + # Create write lock + _LOGGER.debug("Lock database") + connection.execute(text("BEGIN IMMEDIATE;")) + try: + yield + finally: + _LOGGER.debug("Unlock database") + connection.execute(text("END;")) + + def async_migration_in_progress(hass: HomeAssistant) -> bool: """Determine is a migration is in progress. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 5a4f0425919..f6d4d57a7e5 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,6 +1,7 @@ """The Energy websocket API.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING import voluptuous as vol @@ -15,6 +16,8 @@ from .util import async_migration_in_progress if TYPE_CHECKING: from . import Recorder +_LOGGER: logging.Logger = logging.getLogger(__package__) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -23,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_clear_statistics) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_backup_start) + websocket_api.async_register_command(hass, ws_backup_end) @websocket_api.websocket_command( @@ -106,3 +111,38 @@ def ws_info( "thread_running": thread_alive, } connection.send_result(msg["id"], recorder_info) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/start"}) +@websocket_api.async_response +async def ws_backup_start( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup start notification.""" + + _LOGGER.info("Backup start notification, locking database for writes") + instance: Recorder = hass.data[DATA_INSTANCE] + try: + await instance.lock_database() + except TimeoutError as err: + connection.send_error(msg["id"], "timeout_error", str(err)) + return + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/end"}) +@websocket_api.async_response +async def ws_backup_end( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup end notification.""" + + instance: Recorder = hass.data[DATA_INSTANCE] + _LOGGER.info("Backup end notification, releasing write lock") + if not instance.unlock_database(): + connection.send_error( + msg["id"], "database_unlock_failed", "Failed to unlock database." + ) + connection.send_result(msg["id"]) diff --git a/tests/common.py b/tests/common.py index 55c76e953cd..9d4a9cfe366 100644 --- a/tests/common.py +++ b/tests/common.py @@ -902,8 +902,9 @@ def init_recorder_component(hass, add_config=None): async def async_init_recorder_component(hass, add_config=None): """Initialize the recorder asynchronously.""" - config = dict(add_config) if add_config else {} - config[recorder.CONF_DB_URL] = "sqlite://" + config = add_config or {} + if recorder.CONF_DB_URL not in config: + config[recorder.CONF_DB_URL] = "sqlite://" with patch("homeassistant.components.recorder.migration.migrate_schema"): assert await async_setup_component( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e41a0da34ba..7d7c3f27fb6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,5 +1,6 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access +import asyncio from datetime import datetime, timedelta import sqlite3 from unittest.mock import patch @@ -1134,3 +1135,81 @@ def test_entity_id_filter(hass_recorder): db_events = list(session.query(Events).filter_by(event_type="hello")) # Keep referring idx + 1, as no new events are being added assert len(db_events) == idx + 1, data + + +async def test_database_lock_and_unlock(hass: HomeAssistant, tmp_path): + """Test writing events during lock getting written after unlocking.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + assert await instance.lock_database() + + assert not await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + task = asyncio.create_task(async_wait_recording_done(hass, instance)) + + # Recording can't be finished while lock is held + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(asyncio.shield(task), timeout=1) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 0 + + assert instance.unlock_database() + + await task + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + +async def test_database_lock_and_overflow(hass: HomeAssistant, tmp_path): + """Test writing events during lock leading to overflow the queue causes the database to unlock.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + with patch.object(recorder, "MAX_QUEUE_BACKLOG", 1), patch.object( + recorder, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 + ): + await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + + # Check that this causes the queue to overflow and write succeeds + # even before unlocking. + await async_wait_recording_done(hass, instance) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + assert not instance.unlock_database() + + +async def test_database_lock_timeout(hass): + """Test locking database timeout when recorder stopped.""" + await async_init_recorder_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0.1): + try: + with pytest.raises(TimeoutError): + await instance.lock_database() + finally: + instance.unlock_database() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 940925c48ca..fa449aefefc 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -8,6 +8,7 @@ import pytest from sqlalchemy import text from sqlalchemy.sql.elements import TextClause +from homeassistant.components import recorder from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import RecorderRuns @@ -556,3 +557,21 @@ def test_perodic_db_cleanups(hass_recorder): ][0] assert isinstance(text_obj, TextClause) assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" + + +async def test_write_lock_db(hass, tmp_path): + """Test database write lock.""" + from sqlalchemy.exc import OperationalError + + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance = hass.data[DATA_INSTANCE] + + with util.write_lock_db(instance): + # Database should be locked now, try writing SQL command + with instance.engine.connect() as connection: + with pytest.raises(OperationalError): + connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 7a45dea0379..994d1c677af 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -358,3 +358,62 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["migration_in_progress"] is False assert response["result"]["recording"] is True assert response["result"]["thread_running"] is True + + +async def test_backup_start_no_recorder(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_command" + + +async def test_backup_start_timeout(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0): + try: + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "timeout_error" + finally: + await client.send_json({"id": 2, "type": "backup/end"}) + + +async def test_backup_end(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 2, "type": "backup/end"}) + response = await client.receive_json() + assert response["success"] + + +async def test_backup_end_without_start(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/end"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "database_unlock_failed" From 1ddb0d255a6480b2c9cb19d577b705b62aa8b6c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 11:43:48 +0100 Subject: [PATCH 1406/1452] Fix date/datetime support for templates (#61088) Co-authored-by: Paulus Schoutsen --- homeassistant/components/template/sensor.py | 65 +++++++- tests/components/template/test_sensor.py | 166 ++++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a31e49db570..18ae8af8569 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,8 @@ """Allows the creation of a sensor that breaks out state_attributes.""" from __future__ import annotations +from datetime import date, datetime +import logging from typing import Any import voluptuous as vol @@ -12,6 +14,7 @@ from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -32,6 +35,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -85,6 +89,7 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) +_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -179,6 +184,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) +@callback +def _async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None + + class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -227,7 +258,20 @@ class SensorTemplate(TemplateEntity, SensorEntity): @callback def _update_state(self, result): super()._update_state(result) - self._attr_native_value = None if isinstance(result, TemplateError) else result + if isinstance(result, TemplateError): + self._attr_native_value = None + return + + if result is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._attr_native_value = result + return + + self._attr_native_value = _async_parse_date_datetime( + result, self.entity_id, self.device_class + ) class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -237,7 +281,7 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): extra_template_keys = (CONF_STATE,) @property - def native_value(self) -> str | None: + def native_value(self) -> str | datetime | date | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) @@ -245,3 +289,20 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): def state_class(self) -> str | None: """Sensor state class.""" return self._config.get(CONF_STATE_CLASS) + + @callback + def _process_data(self) -> None: + """Process new data.""" + super()._process_data() + + if ( + state := self._rendered.get(CONF_STATE) + ) is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + return + + self._rendered[CONF_STATE] = _async_parse_date_datetime( + state, self.entity_id, self.device_class + ) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 189fe3653f2..0352080bed8 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1094,3 +1094,169 @@ async def test_trigger_entity_available(hass): state = hass.states.get("sensor.maybe_available") assert state.state == "unavailable" + + +async def test_trigger_entity_device_class_parsing_works(hass): + """Test trigger entity device class parsing works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_trigger_entity_device_class_errors_works(hass): + """Test trigger entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN + + +async def test_entity_device_class_parsing_works(hass): + """Test entity device class parsing works.""" + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_entity_device_class_errors_works(hass): + """Test entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN From 7ee148c65082a4dc74d3cfd8b8fbc20c885d8db2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 8 Dec 2021 21:49:40 -0800 Subject: [PATCH 1407/1452] Display nest media events using local time (#61143) --- homeassistant/components/nest/media_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 8fd7d384e36..b02b9b1870e 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,6 +46,7 @@ from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -250,7 +251,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=event.timestamp.strftime(DATE_STR_FORMAT), + event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, From 79501289f0afb4f3bb006389d3c71b551efbfff8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 9 Dec 2021 10:49:19 +0100 Subject: [PATCH 1408/1452] Correct state class for Tasmota sensors (#61236) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/tasmota/sensor.py | 146 +++++++++++++-------- 1 file changed, 94 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 678d3eaf4fa..45ff93b5945 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -10,25 +10,15 @@ from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -63,28 +53,54 @@ STATE_CLASS = "state_class" ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both -SENSOR_DEVICE_CLASS_ICON_MAP = { - hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, - hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_BATTERY: { - DEVICE_CLASS: DEVICE_CLASS_BATTERY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, +SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { + hc.SENSOR_AMBIENT: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_APPARENT_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_BATTERY: { + DEVICE_CLASS: SensorDeviceClass.BATTERY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CCT: { + ICON: "mdi:temperature-kelvin", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CO2: { + DEVICE_CLASS: SensorDeviceClass.CO2, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, - hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2}, hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, - hc.SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"}, - hc.SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"}, - hc.SENSOR_DISTANCE: {ICON: "mdi:leak"}, - hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, - hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, - hc.SENSOR_HUMIDITY: { - DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + hc.SENSOR_CURRENT: { + ICON: "mdi:alpha-a-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DEWPOINT: { + ICON: "mdi:weather-rainy", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DISTANCE: { + ICON: "mdi:leak", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_FREQUENCY: { + DEVICE_CLASS: SensorDeviceClass.FREQUENCY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_HUMIDITY: { + DEVICE_CLASS: SensorDeviceClass.HUMIDITY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ILLUMINANCE: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"}, @@ -95,40 +111,66 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_PB1: {ICON: "mdi:flask"}, hc.SENSOR_PB2_5: {ICON: "mdi:flask"}, hc.SENSOR_PB5: {ICON: "mdi:flask"}, - hc.SENSOR_PM10: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM1: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"}, - hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, - hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_PM10: { + DEVICE_CLASS: SensorDeviceClass.PM10, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM1: { + DEVICE_CLASS: SensorDeviceClass.PM1, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM2_5: { + DEVICE_CLASS: SensorDeviceClass.PM25, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERFACTOR: { + ICON: "mdi:alpha-f-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERUSAGE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_PRESSURE: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PRESSUREATSEALEVEL: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + hc.SENSOR_REACTIVE_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, hc.SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"}, - hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, - hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, + hc.SENSOR_STATUS_SIGNAL: { + DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_RSSI: { + ICON: "mdi:access-point", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, hc.SENSOR_TEMPERATURE: { - DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_TODAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, hc.SENSOR_TOTAL: { - DEVICE_CLASS: DEVICE_CLASS_ENERGY, - STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, - hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, - hc.SENSOR_WEIGHT: {ICON: "mdi:scale"}, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_VOLTAGE: { + ICON: "mdi:alpha-v-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_WEIGHT: {ICON: "mdi:scale", STATE_CLASS: SensorStateClass.MEASUREMENT}, + hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { @@ -208,7 +250,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @callback def sensor_state_updated(self, state: Any, **kwargs: Any) -> None: """Handle state updates.""" - if self.device_class == DEVICE_CLASS_TIMESTAMP: + if self.device_class == SensorDeviceClass.TIMESTAMP: self._state_timestamp = state else: self._state = state @@ -261,7 +303,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" - if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: + if self._state_timestamp and self.device_class == SensorDeviceClass.TIMESTAMP: return self._state_timestamp return self._state From 5c70ddb7cb05e2e544b636a66bb01d6b56f7bc0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 11:44:53 -0800 Subject: [PATCH 1409/1452] Fix smartthings timestamp sensor (#61254) --- homeassistant/components/smartthings/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index eab840bd629..fa749e07dfb 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -656,7 +657,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._device.status.attributes[self._attribute].value + value = self._device.status.attributes[self._attribute].value + + if self._device_class != DEVICE_CLASS_TIMESTAMP: + return value + + return dt_util.parse_datetime(value) @property def device_class(self): From e66f0a68e79f6dd4c9fbda8cd874d28badd29c97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:21:33 -0800 Subject: [PATCH 1410/1452] Guard cannot connect during Tuya init (#61267) --- homeassistant/components/tuya/__init__.py | 44 ++++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4f34d3c31bf..fb5b4d759f3 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import NamedTuple +import requests from tuya_iot import ( AuthType, TuyaDevice, @@ -18,6 +19,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send @@ -60,18 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data.pop(CONF_PROJECT_TYPE) hass.config_entries.async_update_entry(entry, data=data) - success = await _init_tuya_sdk(hass, entry) - - if not success: - hass.data[DOMAIN].pop(entry.entry_id) - - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return bool(success) - - -async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth_type = AuthType(entry.data[CONF_AUTH_TYPE]) api = TuyaOpenAPI( endpoint=entry.data[CONF_ENDPOINT], @@ -82,22 +72,24 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.set_dev_channel("hass") - if auth_type == AuthType.CUSTOM: - response = await hass.async_add_executor_job( - api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] - ) - else: - response = await hass.async_add_executor_job( - api.connect, - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - entry.data[CONF_COUNTRY_CODE], - entry.data[CONF_APP_TYPE], - ) + try: + if auth_type == AuthType.CUSTOM: + response = await hass.async_add_executor_job( + api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + else: + response = await hass.async_add_executor_job( + api.connect, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTRY_CODE], + entry.data[CONF_APP_TYPE], + ) + except requests.exceptions.RequestException as err: + raise ConfigEntryNotReady(err) from err if response.get("success", False) is False: - _LOGGER.error("Tuya login error response: %s", response) - return False + raise ConfigEntryNotReady(response) tuya_mq = TuyaOpenMQ(api) tuya_mq.start() From 793bdebc1399102efdeee4e5e21037643529c9cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:19:22 -0800 Subject: [PATCH 1411/1452] Use correct template parameter in Rest template rendering (#61269) --- homeassistant/components/rest/data.py | 4 ++-- homeassistant/components/rest/switch.py | 8 ++++---- homeassistant/components/rest/utils.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 513f2393127..e9fbb5718a5 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 3448b79979c..1fd04b66559 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 35b3c22db31..f3fdba651ac 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -15,13 +15,13 @@ def inject_hass_in_templates_list( tpl.hass = hass -def render_templates(tpl_dict: dict[str, Template] | None): +def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): """Render a dict of templates.""" if tpl_dict is None: return None rendered_items = {} - for item_name, template_header in tpl_dict.items(): - if (value := template_header.async_render()) is not None: - rendered_items[item_name] = str(value) + for item_name, template in tpl_dict.items(): + if (value := template.async_render(parse_result=parse_result)) is not None: + rendered_items[item_name] = value return rendered_items From 9a4a09b2f2c7a5bdc3205a3a4884ba750a20b729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 10:54:41 -1000 Subject: [PATCH 1412/1452] Bump flux_led to 0.26.3 (#61287) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index defaa348262..0c5e58027ce 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.2"], + "requirements": ["flux_led==0.26.3"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index cdf9fb48011..26461f9c3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5d218a01b3..516cc0ce54f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 From aefd675737d9a3a08628c5ab60edfe2702b74b92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 11:03:07 -1000 Subject: [PATCH 1413/1452] Restore rest integration ability to follow http redirects (#61293) --- homeassistant/components/rest/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index e9fbb5718a5..bc98b0caf68 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -65,6 +65,7 @@ class RestData: auth=self._auth, data=self._request_data, timeout=self._timeout, + follow_redirects=True, ) self.data = response.text self.headers = response.headers From 10a4037ed32f70e11367829869012f26792b70ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 14:35:50 -0800 Subject: [PATCH 1414/1452] Rest fixes (#61296) --- homeassistant/components/pvoutput/sensor.py | 6 +---- homeassistant/components/rest/__init__.py | 6 ++--- homeassistant/components/rest/data.py | 6 ++--- homeassistant/components/rest/switch.py | 16 ++++++------ homeassistant/components/rest/utils.py | 27 --------------------- homeassistant/helpers/template.py | 15 +++++++++--- 6 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 homeassistant/components/rest/utils.py diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 512fb75067b..1d8b3400d8b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) _ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" @@ -60,10 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = { - "X-Pvoutput-Apikey": Template(api_key, hass), - "X-Pvoutput-SystemId": Template(system_id, hass), - } + headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index ba101624673..b55d9c6d844 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, template from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -37,7 +37,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData from .schema import CONFIG_SCHEMA # noqa: F401 -from .utils import inject_hass_in_templates_list _LOGGER = logging.getLogger(__name__) @@ -161,7 +160,8 @@ def create_rest_data_from_config(hass, config): resource_template.hass = hass resource = resource_template.async_render(parse_result=False) - inject_hass_in_templates_list(hass, [headers, params]) + template.attach(hass, headers) + template.attach(hass, params) if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index bc98b0caf68..7c8fd61e688 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,7 +3,7 @@ import logging import httpx -from homeassistant.components.rest.utils import render_templates +from homeassistant.helpers import template from homeassistant.helpers.httpx_client import get_async_client DEFAULT_TIMEOUT = 10 @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 1fd04b66559..3e5fd7e2c68 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -24,10 +24,8 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -from .utils import inject_hass_in_templates_list, render_templates _LOGGER = logging.getLogger(__name__) CONF_BODY_OFF = "body_off" @@ -92,7 +90,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= body_on.hass = hass if body_off is not None: body_off.hass = hass - inject_hass_in_templates_list(hass, [headers, params]) + + template.attach(hass, headers) + template.attach(hass, params) timeout = config.get(CONF_TIMEOUT) try: @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py deleted file mode 100644 index f3fdba651ac..00000000000 --- a/homeassistant/components/rest/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Reusable utilities for the Rest component.""" -from __future__ import annotations - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.template import Template - - -def inject_hass_in_templates_list( - hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None] -): - """Inject hass in a list of dict of templates.""" - for tpl_dict in tpl_dict_list: - if tpl_dict is not None: - for tpl in tpl_dict.values(): - tpl.hass = hass - - -def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): - """Render a dict of templates.""" - if tpl_dict is None: - return None - - rendered_items = {} - for item_name, template in tpl_dict.items(): - if (value := template.async_render(parse_result=parse_result)) is not None: - rendered_items[item_name] = value - return rendered_items diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d460e7ab42b..0ba1d6bfa14 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -110,18 +110,25 @@ def attach(hass: HomeAssistant, obj: Any) -> None: def render_complex( - value: Any, variables: TemplateVarsType = None, limited: bool = False + value: Any, + variables: TemplateVarsType = None, + limited: bool = False, + parse_result: bool = True, ) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): - return [render_complex(item, variables) for item in value] + return [ + render_complex(item, variables, limited, parse_result) for item in value + ] if isinstance(value, collections.abc.Mapping): return { - render_complex(key, variables): render_complex(item, variables) + render_complex(key, variables, limited, parse_result): render_complex( + item, variables, limited, parse_result + ) for key, item in value.items() } if isinstance(value, Template): - return value.async_render(variables, limited=limited) + return value.async_render(variables, limited=limited, parse_result=parse_result) return value From d7708d58ba44b5978c0fe23a797878404536e621 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 9 Dec 2021 01:49:35 +0100 Subject: [PATCH 1415/1452] Introduce only_supervisor for @websocket_api.ws_require_user() (#61298) --- homeassistant/components/hassio/__init__.py | 5 ++-- .../components/recorder/websocket_api.py | 4 ++-- .../components/websocket_api/decorators.py | 6 +++++ homeassistant/const.py | 3 +++ .../components/recorder/test_websocket_api.py | 20 +++++++++------- .../websocket_api/test_decorators.py | 23 +++++++++++++++++++ tests/conftest.py | 22 +++++++++++++++++- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7991c50563c..2c3d61ef584 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, + HASSIO_USER_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, ) @@ -439,11 +440,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Migrate old name if user.name == "Hass.io": - await hass.auth.async_update_user(user, name="Supervisor") + await hass.auth.async_update_user(user, name=HASSIO_USER_NAME) if refresh_token is None: user = await hass.auth.async_create_system_user( - "Supervisor", group_ids=[GROUP_ID_ADMIN] + HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] ) refresh_token = await hass.auth.async_create_refresh_token(user) data["hassio_user"] = user.id diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f6d4d57a7e5..aec7905615f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -113,7 +113,7 @@ def ws_info( connection.send_result(msg["id"], recorder_info) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/start"}) @websocket_api.async_response async def ws_backup_start( @@ -131,7 +131,7 @@ async def ws_backup_start( connection.send_result(msg["id"]) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/end"}) @websocket_api.async_response async def ws_backup_end( diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index eff82a8c71d..296271c7cfd 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,7 @@ from typing import Any import voluptuous as vol +from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized @@ -70,6 +71,7 @@ def ws_require_user( allow_system_user: bool = True, only_active_user: bool = True, only_inactive_user: bool = False, + only_supervisor: bool = False, ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Decorate function validating login user exist in current WS connection. @@ -111,6 +113,10 @@ def ws_require_user( output_error("only_inactive_user", "Not allowed as active user") return + if only_supervisor and connection.user.name != HASSIO_USER_NAME: + output_error("only_supervisor", "Only allowed as Supervisor") + return + return func(hass, connection, msg) return check_current_user diff --git a/homeassistant/const.py b/homeassistant/const.py index e50660a841f..1eae844f7cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -756,3 +756,6 @@ ENTITY_CATEGORIES: Final[list[str]] = [ CAST_APP_ID_HOMEASSISTANT_MEDIA: Final = "B45F4572" # The ID of the Home Assistant Lovelace Cast App CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" + +# User used by Supervisor +HASSIO_USER_NAME = "Supervisor" diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 994d1c677af..2a9f737e9a5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -360,9 +360,11 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["thread_running"] is True -async def test_backup_start_no_recorder(hass, hass_ws_client): +async def test_backup_start_no_recorder( + hass, hass_ws_client, hass_supervisor_access_token +): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await client.send_json({"id": 1, "type": "backup/start"}) response = await client.receive_json() @@ -370,9 +372,9 @@ async def test_backup_start_no_recorder(hass, hass_ws_client): assert response["error"]["code"] == "unknown_command" -async def test_backup_start_timeout(hass, hass_ws_client): +async def test_backup_start_timeout(hass, hass_ws_client, hass_supervisor_access_token): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -388,9 +390,9 @@ async def test_backup_start_timeout(hass, hass_ws_client): await client.send_json({"id": 2, "type": "backup/end"}) -async def test_backup_end(hass, hass_ws_client): +async def test_backup_end(hass, hass_ws_client, hass_supervisor_access_token): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -405,9 +407,11 @@ async def test_backup_end(hass, hass_ws_client): assert response["success"] -async def test_backup_end_without_start(hass, hass_ws_client): +async def test_backup_end_without_start( + hass, hass_ws_client, hass_supervisor_access_token +): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 45d761f6fed..4fbc1ae1a21 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -66,3 +66,26 @@ async def test_async_response_request_context(hass, websocket_client): assert msg["id"] == 7 assert not msg["success"] assert msg["error"]["code"] == "not_found" + + +async def test_supervisor_only(hass, websocket_client): + """Test that only the Supervisor can make requests.""" + + @websocket_api.ws_require_user(only_supervisor=True) + @websocket_api.websocket_command({"type": "test-require-supervisor-user"}) + def require_supervisor_request(hass, connection, msg): + connection.send_result(msg["id"]) + + websocket_api.async_register_command(hass, require_supervisor_request) + + await websocket_client.send_json( + { + "id": 5, + "type": "test-require-supervisor-user", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "only_supervisor" diff --git a/tests/conftest.py b/tests/conftest.py index 10a9dd1627b..0107e218335 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.http import URL -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED, HASSIO_USER_NAME from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -405,6 +405,26 @@ def hass_read_only_access_token(hass, hass_read_only_user, local_auth): return hass.auth.async_create_access_token(refresh_token) +@pytest.fixture +def hass_supervisor_user(hass, local_auth): + """Return the Home Assistant Supervisor user.""" + admin_group = hass.loop.run_until_complete( + hass.auth.async_get_group(GROUP_ID_ADMIN) + ) + return MockUser( + name=HASSIO_USER_NAME, groups=[admin_group], system_generated=True + ).add_to_hass(hass) + + +@pytest.fixture +def hass_supervisor_access_token(hass, hass_supervisor_user, local_auth): + """Return a Home Assistant Supervisor access token.""" + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(hass_supervisor_user) + ) + return hass.auth.async_create_access_token(refresh_token) + + @pytest.fixture def legacy_auth(hass): """Load legacy API password provider.""" From 7387640524974dde756f1d0c362f062fa8fdeac6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:36:41 -0800 Subject: [PATCH 1416/1452] Fix rova timezone (#61302) --- homeassistant/components/rova/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index ca9f201b302..60d0fbe6df0 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import get_time_zone, now # Config for rova requests. CONF_ZIP_CODE = "zip_code" @@ -140,10 +141,12 @@ class RovaData: self.data = {} for item in items: - date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S") + date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace( + tzinfo=get_time_zone("Europe/Amsterdam") + ) code = item["GarbageTypeCode"].lower() - if code not in self.data and date > datetime.now(): + if code not in self.data and date > now(): self.data[code] = date _LOGGER.debug("Updated Rova calendar: %s", self.data) From 24a6e9004272a0251ec00edfd4db9c8856a20ebf Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Thu, 9 Dec 2021 02:40:45 -0500 Subject: [PATCH 1417/1452] Fix regression in Tuya cover is_closed logic (#61303) --- homeassistant/components/tuya/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index fd1f2aae972..275d28bae17 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -297,7 +297,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): is not None ): return self.entity_description.current_state_inverse is not ( - current_state in (False, "fully_close") + current_state in (True, "fully_close") ) if (position := self.current_cover_position) is not None: From fe7521b503e19cc13cdf6df1fbe7abcc43090337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 20:21:11 -1000 Subject: [PATCH 1418/1452] Fix lookin failing to setup during firmware updates (#61305) --- homeassistant/components/lookin/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index f749621beaf..5e603027a50 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -1,6 +1,7 @@ """The lookin integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: lookin_device = await lookin_protocol.get_info() devices = await lookin_protocol.get_devices() - except aiohttp.ClientError as ex: + except (asyncio.TimeoutError, aiohttp.ClientError) as ex: raise ConfigEntryNotReady from ex meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator( From 0203228a11426d8e88e4477ca6d41f8356806401 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 23:58:23 -0800 Subject: [PATCH 1419/1452] Fix hue groups inheritance (#61308) --- homeassistant/components/hue/v2/group.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index ae77ab38539..312fef6629f 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -7,7 +7,6 @@ from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone -from homeassistant.components.group.light import LightGroup from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,6 +17,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_XY, SUPPORT_TRANSITION, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -73,11 +73,12 @@ async def async_setup_entry( ) -class GroupedHueLight(HueBaseEntity, LightGroup): +class GroupedHueLight(HueBaseEntity, LightEntity): """Representation of a Grouped Hue light.""" # Entities for Hue groups are disabled by default _attr_entity_registry_enabled_default = False + _attr_icon = "mdi:lightbulb-group" def __init__( self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone From 07438c07c948b68cb8217c81cd2b8c2c68f0b281 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:50:30 -0800 Subject: [PATCH 1420/1452] Fix CO2signal error handling (#61311) --- homeassistant/components/co2signal/__init__.py | 3 --- homeassistant/components/co2signal/config_flow.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a87aed64564..56be5bca57b 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -134,9 +134,6 @@ def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse: _LOGGER.exception("Unexpected exception") raise UnknownError from err - except Exception as err: - _LOGGER.exception("Unexpected exception") - raise UnknownError from err else: if "error" in data: diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index e7f94e4d603..fb4e48c66e8 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_data +from . import APIRatelimitExceeded, CO2Error, InvalidAuth, get_data from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name @@ -172,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except APIRatelimitExceeded: errors["base"] = "api_ratelimit" - except UnknownError: + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" else: return self.async_create_entry( From 3b2b116c1046ed865c01261c5e1adac7d1c4ca54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 10:17:34 +0100 Subject: [PATCH 1421/1452] Upgrade tailscale to 0.1.4 (#61338) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index e1b2435b989..4d47e397b76 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.3"], + "requirements": ["tailscale==0.1.4"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 26461f9c3d8..95818018796 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2272,7 +2272,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 516cc0ce54f..fbb236aff66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tellduslive tellduslive==0.10.11 From 29aab7ad7acfd80b492a579cfb9b5e8870c9634d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 12:09:53 +0100 Subject: [PATCH 1422/1452] Bumped version to 2021.12.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1eae844f7cd..474d2125689 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 7cc2af2a468fe07c057e1afaeebb331afba65330 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Thu, 9 Dec 2021 22:08:29 +0000 Subject: [PATCH 1423/1452] Update tesla-wall-connector to v1.0.1 (#61392) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index 08d52b3016b..8e86fa3d2f8 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Wall Connector", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", - "requirements": ["tesla-wall-connector==1.0.0"], + "requirements": ["tesla-wall-connector==1.0.1"], "dhcp": [ { "hostname": "teslawallconnector_*", diff --git a/requirements_all.txt b/requirements_all.txt index 95818018796..2f06a548d13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2302,7 +2302,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbb236aff66..ae4395dc699 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1361,7 +1361,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tolo tololib==0.1.0b3 From f8f381afa3c446b2764e4ab42f439767206b655d Mon Sep 17 00:00:00 2001 From: bigbadblunt Date: Thu, 9 Dec 2021 21:12:40 +0000 Subject: [PATCH 1424/1452] Add default value for signal_repetitions in cover (#61393) --- homeassistant/components/rfxtrx/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 0244e3aa8a0..4dc89577542 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -65,7 +65,7 @@ async def async_setup_entry( entity = RfxtrxCover( event.device, device_id, - signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS], + signal_repetitions=entity_info.get(CONF_SIGNAL_REPETITIONS, 1), venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), ) entities.append(entity) From ae26e60740072fa9e6fb2cf10fc186d3d17e5de9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 9 Dec 2021 16:12:19 -0600 Subject: [PATCH 1425/1452] Fix Sonos radio handling during polling (#61401) --- homeassistant/components/sonos/speaker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 05787a354f0..e12166d7795 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -994,9 +994,9 @@ class SonosSpeaker: @soco_error() def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" - variables = event and event.variables + variables = event.variables if event else {} - if variables and "transport_state" in variables: + if "transport_state" in variables: # If the transport has an error then transport_state will # not be set new_status = variables["transport_state"] @@ -1012,7 +1012,7 @@ class SonosSpeaker: update_position = new_status != self.media.playback_status self.media.playback_status = new_status - if variables and "transport_state" in variables: + if "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] track_uri = ( variables["enqueued_transport_uri"] or variables["current_track_uri"] @@ -1060,7 +1060,7 @@ class SonosSpeaker: self.media.title = source self.media.source_name = source - def update_media_radio(self, variables: dict | None) -> None: + def update_media_radio(self, variables: dict) -> None: """Update state when streaming radio.""" self.media.clear_position() radio_title = None From abe6f1ab5b45e1bdc475e89739690deab3496405 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:11:41 -0700 Subject: [PATCH 1426/1452] Consolidate SimpliSafe config flow forms into one (#61402) --- .../components/simplisafe/config_flow.py | 59 +++++++++---------- .../components/simplisafe/strings.json | 8 +-- .../simplisafe/translations/en.json | 25 +------- .../components/simplisafe/test_config_flow.py | 21 ------- 4 files changed, 31 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 8f8ec6cdc16..53fcb92f71a 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -24,7 +24,7 @@ from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" -STEP_INPUT_AUTH_CODE_SCHEMA = vol.Schema( +STEP_USER_SCHEMA = vol.Schema( { vol.Required(CONF_AUTH_CODE): cv.string, } @@ -54,8 +54,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors: dict[str, Any] = {} - self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() + self._oauth_values: SimpliSafeOAuthValues | None = None self._reauth: bool = False self._username: str | None = None @@ -67,19 +66,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_input_auth_code( + def _async_show_form(self, *, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the form.""" + self._oauth_values = async_get_simplisafe_oauth_values() + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors or {}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config.get(CONF_USERNAME) + self._reauth = True + return await self.async_step_user() + + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the input of a SimpliSafe OAuth authorization code.""" + """Handle the start of the config flow.""" if user_input is None: - return self.async_show_form( - step_id="input_auth_code", data_schema=STEP_INPUT_AUTH_CODE_SCHEMA - ) + return self._async_show_form() if TYPE_CHECKING: assert self._oauth_values - self._errors = {} + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) try: @@ -89,13 +103,13 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session=session, ) except InvalidCredentialsError: - self._errors = {"base": "invalid_auth"} + errors = {"base": "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) - self._errors = {"base": "unknown"} + errors = {"base": "unknown"} - if self._errors: - return await self.async_step_user() + if errors: + return self._async_show_form(errors=errors) data = {CONF_USER_ID: simplisafe.user_id, CONF_TOKEN: simplisafe.refresh_token} unique_id = str(simplisafe.user_id) @@ -122,25 +136,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=unique_id, data=data) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: - """Handle configuration by re-auth.""" - self._username = config.get(CONF_USERNAME) - self._reauth = True - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the start of the config flow.""" - if user_input is None: - return self.async_show_form( - step_id="user", - errors=self._errors, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, - ) - - return await self.async_step_input_auth_code() - class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): """Handle a SimpliSafe options flow.""" diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 55a916bfe6b..08632cd754a 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,15 +1,11 @@ { "config": { "step": { - "input_auth_code": { - "title": "Finish Authorization", - "description": "Input the authorization code from the SimpliSafe web app URL:", + "user": { + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } - }, - "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit." } }, "error": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 66843f86d27..3c5dd9261bc 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -12,32 +12,11 @@ "unknown": "Unexpected error" }, "step": { - "input_auth_code": { + "user": { "data": { "auth_code": "Authorization Code" }, - "description": "Input the authorization code from the SimpliSafe web app URL:", - "title": "Finish Authorization" - }, - "mfa": { - "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", - "title": "SimpliSafe Multi-Factor Authentication" - }, - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", - "title": "Reauthenticate Integration" - }, - "user": { - "data": { - "code": "Code (used in Home Assistant UI)", - "password": "Password", - "username": "Email" - }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit.", - "title": "Fill in your information." + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 99943497556..0597ad377cf 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -53,9 +53,6 @@ async def test_duplicate_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -73,9 +70,6 @@ async def test_invalid_credentials(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -131,9 +125,6 @@ async def test_step_reauth_old_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -166,9 +157,6 @@ async def test_step_reauth_new_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -205,9 +193,6 @@ async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -229,9 +214,6 @@ async def test_step_user(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -252,9 +234,6 @@ async def test_unknown_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) From 509ebbc743e1166fdbb906263dc02ae4f4f184ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 14:40:05 -0800 Subject: [PATCH 1427/1452] Bump frontend to 20211209.0 (#61406) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6d419276029..c9739cd0302 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211206.0" + "home-assistant-frontend==20211209.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5082fe5559d..77275b0e6d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2f06a548d13..6775cdc4f00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae4395dc699..d5092d08477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5d65db5168e8383c30ffbb130f80213efb5b8ffd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:41:13 -0700 Subject: [PATCH 1428/1452] Assign docs URL to a placeholder in SimpliSafe config flow (#61410) --- homeassistant/components/simplisafe/config_flow.py | 10 +++++++++- homeassistant/components/simplisafe/strings.json | 2 +- .../components/simplisafe/translations/en.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 53fcb92f71a..3a3d1963e0e 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -23,6 +23,11 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" +CONF_DOCS_URL = "docs_url" + +AUTH_DOCS_URL = ( + "http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code" +) STEP_USER_SCHEMA = vol.Schema( { @@ -74,7 +79,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors or {}, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCS_URL: AUTH_DOCS_URL, + }, ) async def async_step_reauth(self, config: ConfigType) -> FlowResult: diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 08632cd754a..a0ff28fd689 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 3c5dd9261bc..5829c68301f 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -16,7 +16,7 @@ "data": { "auth_code": "Authorization Code" }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, From 5476b23d8b0b014beecfa2aaf686df506c899b2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 14:42:14 -0800 Subject: [PATCH 1429/1452] Bumped version to 2021.12.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 474d2125689..bf14b073cb7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 7208cb49f1ae0c0ec354ba9ba57d3bee4523c5c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 21:24:18 -0800 Subject: [PATCH 1430/1452] Disable lupusec (#61142) --- homeassistant/components/lupusec/__init__.py | 1 + homeassistant/components/lupusec/binary_sensor.py | 1 + homeassistant/components/lupusec/manifest.json | 1 + homeassistant/components/lupusec/switch.py | 1 + requirements_all.txt | 3 --- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index 3ae07bd8105..734c7affe90 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,4 +1,5 @@ """Support for Lupusec Home Security system.""" +# pylint: disable=import-error import logging import lupupy diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index 963c82da5fa..9668b06b0ef 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System binary sensors.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 6541925a5e4..ce200fe196a 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Library has incompatible requirements.", "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index f35322eb773..5321d1b4f25 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System switches.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/requirements_all.txt b/requirements_all.txt index 6775cdc4f00..6a7c60023a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,9 +968,6 @@ london-tube-status==0.2 # homeassistant.components.luftdaten luftdaten==0.7.1 -# homeassistant.components.lupusec -lupupy==0.0.21 - # homeassistant.components.lw12wifi lw12==0.9.2 From 6785e32683302883c84f6b6f55dc21bd4d61d611 Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Fri, 10 Dec 2021 20:07:53 +0100 Subject: [PATCH 1431/1452] Add 2 new CN-Hysen TRVs (#61002) Adding CN-Hysen "_TZE200_pvvbommb" and "_TZE200_4eeyebrt" TRVs --- homeassistant/components/zha/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 9ef7e8fdebc..f7a1d1815db 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -614,6 +614,8 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_cwnjrr72", "_TZE200_b6wax7g0", "_TZE200_2atgpdho", + "_TZE200_pvvbommb", + "_TZE200_4eeyebrt", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From da9fbde83aac1f5140b3e9cb8a91cdc8b1515e07 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:00:23 +0100 Subject: [PATCH 1432/1452] add missing unit of measurement in Smappee (#61365) --- homeassistant/components/smappee/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 276248fd6ae..ccae097e53f 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -250,6 +250,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): description=SmappeeSensorEntityDescription( key="load", name=measurement.name, + native_unit_of_measurement=POWER_WATT, sensor_id=measurement_id, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, From 87b50fff54609574f994caf2f3de1c1064411124 Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Fri, 10 Dec 2021 12:19:33 -0500 Subject: [PATCH 1433/1452] Fix Tuya cover open/close commands (#61369) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/cover.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 275d28bae17..572d440f937 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -315,11 +315,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 0, + "value": round( + self._set_position_type.remap_value_from( + 100, 0, 100, reverse=True + ), + ), } ) @@ -327,7 +334,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): def close_cover(self, **kwargs: Any) -> None: """Close cover.""" - value: bool | str = True + value: bool | str = False if self.device.function[self.entity_description.key].type == "Enum": value = "close" @@ -335,11 +342,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 100, + "value": round( + self._set_position_type.remap_value_from( + 0, 0, 100, reverse=True + ), + ), } ) From e483c16d59abb7927a55bbf3462e796adb64f100 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:42:33 +0100 Subject: [PATCH 1434/1452] Remove energy entity again in Smappee local integration (#61373) --- homeassistant/components/smappee/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index ccae097e53f..595cc4da02d 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -320,7 +320,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ), ) for actuator_id, actuator in service_location.actuators.items() - if actuator.type == "SWITCH" + if actuator.type == "SWITCH" and not service_location.local_polling ] ) From c51c18781db60625b0daf2eaac64a440b730bf34 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 10 Dec 2021 00:35:20 +0100 Subject: [PATCH 1435/1452] Fix unique_id of S0 meters connected to Fronius inverters (#61408) --- homeassistant/components/fronius/sensor.py | 11 +- tests/components/fronius/__init__.py | 28 ++-- .../fixtures/primo_s0/GetAPIVersion.json | 5 + .../fixtures/primo_s0/GetInverterInfo.json | 33 +++++ .../GetInverterRealtimeData_Device_1.json | 64 +++++++++ .../GetInverterRealtimeData_Device_2.json | 64 +++++++++ .../fixtures/primo_s0/GetLoggerInfo.json | 29 ++++ .../primo_s0/GetMeterRealtimeData.json | 31 ++++ .../primo_s0/GetOhmPilotRealtimeData.json | 17 +++ .../primo_s0/GetPowerFlowRealtimeData.json | 45 ++++++ .../primo_s0/GetStorageRealtimeData.json | 14 ++ tests/components/fronius/test_sensor.py | 136 +++++++++++++++++- 12 files changed, 462 insertions(+), 15 deletions(-) create mode 100644 tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index b2c6ecbb820..8a1348bed14 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -784,15 +784,22 @@ class MeterSensor(_FroniusSensorEntity): self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() + # S0 meters connected directly to inverters respond "n.a." as serial number + # `model` contains the inverter id: "S0 Meter at inverter 1" + if (meter_uid := meter_data["serial"]["value"]) == "n.a.": + meter_uid = ( + f"{coordinator.solar_net.solar_net_device_id}:" + f'{meter_data["model"]["value"]}' + ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, meter_data["serial"]["value"])}, + identifiers={(DOMAIN, meter_uid)}, manufacturer=meter_data["manufacturer"]["value"], model=meter_data["model"]["value"], name=meter_data["model"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' + self._attr_unique_id = f"{meter_uid}-{key}" class OhmpilotSensor(_FroniusSensorEntity): diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 683575a11c8..7930f6c01f4 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,4 +1,6 @@ """Tests for the Fronius integration.""" +from __future__ import annotations + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -10,16 +12,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker MOCK_HOST = "http://fronius" -MOCK_UID = "123.4567890" # has to match mocked logger unique_id +MOCK_UID = "123.4567890" async def setup_fronius_integration( - hass: HomeAssistant, is_logger: bool = True + hass: HomeAssistant, is_logger: bool = True, unique_id: str = MOCK_UID ) -> ConfigEntry: """Create the Fronius integration.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_UID, + unique_id=unique_id, # has to match mocked logger unique_id data={ CONF_HOST: MOCK_HOST, "is_logger": is_logger, @@ -35,9 +37,10 @@ def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, fixture_set: str = "symo", + inverter_ids: list[str | int] = [1], night: bool = False, ) -> None: - """Mock responses for Fronius Symo inverter with meter.""" + """Mock responses for Fronius devices.""" aioclient_mock.clear_requests() _night = "_night" if night else "" @@ -45,14 +48,15 @@ def mock_responses( f"{host}/solar_api/GetAPIVersion.cgi", text=load_fixture(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) - aioclient_mock.get( - f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" - "DeviceId=1&DataCollection=CommonInverterData", - text=load_fixture( - f"{fixture_set}/GetInverterRealtimeData_Device_1{_night}.json", - "fronius", - ), - ) + for inverter_id in inverter_ids: + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + f"DeviceId={inverter_id}&DataCollection=CommonInverterData", + text=load_fixture( + f"{fixture_set}/GetInverterRealtimeData_Device_{inverter_id}{_night}.json", + "fronius", + ), + ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), diff --git a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json new file mode 100644 index 00000000000..2051b4d58e3 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.6-3" +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json new file mode 100644 index 00000000000..5ac293653c0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json @@ -0,0 +1,33 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Primo 5.0-1", + "DT" : 76, + "ErrorCode" : 0, + "PVPower" : 5160, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "123456" + }, + "2" : { + "CustomName" : "Primo 3.0-1", + "DT" : 81, + "ErrorCode" : 0, + "PVPower" : 3240, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "234567" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..e54366a5008 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 22504 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60 + }, + "IAC" : { + "Unit" : "A", + "Value" : 3.8500000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 4.2300000000000004 + }, + "PAC" : { + "Unit" : "W", + "Value" : 862 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 17114940 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.90000000000001 + }, + "UDC" : { + "Unit" : "V", + "Value" : 452.30000000000001 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 7532755.5 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:08-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json new file mode 100644 index 00000000000..dd1e22c0a7a --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 14237 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60.009999999999998 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.3200000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.96999999999999997 + }, + "PAC" : { + "Unit" : "W", + "Value" : 296 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 5796010 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.59999999999999 + }, + "UDC" : { + "Unit" : "V", + "Value" : 329.5 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 3596193.25 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "2", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:36:15-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json new file mode 100644 index 00000000000..1fb0bbc8577 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body" : { + "LoggerInfo" : { + "CO2Factor" : 0.52999997138977051, + "CO2Unit" : "kg", + "CashCurrency" : "BRL", + "CashFactor" : 1, + "DefaultLanguage" : "en", + "DeliveryFactor" : 1, + "HWVersion" : "2.4E", + "PlatformID" : "wilma", + "ProductID" : "fronius-datamanager-card", + "SWVersion" : "3.18.7-1", + "TimezoneLocation" : "Sao_Paulo", + "TimezoneName" : "-03", + "UTCOffset" : 4294956496, + "UniqueID" : "123.4567890" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:09-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json new file mode 100644 index 00000000000..aa308bb3b69 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json @@ -0,0 +1,31 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "S0 Meter at inverter 1", + "Serial" : "n.a." + }, + "Enable" : 1, + "EnergyReal_WAC_Minus_Relative" : 191.25, + "Meter_Location_Current" : 1, + "PowerReal_P_Sum" : -2216.7486858112229, + "TimeStamp" : 1639074843, + "Visible" : 1 + } + } + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "Meter", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:04-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..4562b45efb0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "OhmPilot", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..4bbee2aec28 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json @@ -0,0 +1,45 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "DT" : 76, + "E_Day" : 22502, + "E_Total" : 17114930, + "E_Year" : 7532753.5, + "P" : 886 + }, + "2" : { + "DT" : 81, + "E_Day" : 14222, + "E_Total" : 5795989.5, + "E_Year" : 3596179.75, + "P" : 948 + } + }, + "Site" : { + "E_Day" : 36724, + "E_Total" : 22910919.5, + "E_Year" : 11128933.25, + "Meter_Location" : "load", + "Mode" : "vague-meter", + "P_Akku" : null, + "P_Grid" : 384.93491437299008, + "P_Load" : -2218.9349143729901, + "P_PV" : 1834, + "rel_Autonomy" : 82.652266550064084, + "rel_SelfConsumption" : 100 + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json new file mode 100644 index 00000000000..8743a2c6d68 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 255, + "Reason" : "GetStorageRealtimeData request is not supported by this device.", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index cf371a47471..2e48faf606a 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Fronius sensor platform.""" +from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, FroniusMeterUpdateCoordinator, @@ -6,6 +7,7 @@ from homeassistant.components.fronius.coordinator import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt from . import enable_all_entities, mock_responses, setup_fronius_integration @@ -371,7 +373,9 @@ async def test_gen24_storage(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock, fixture_set="gen24_storage") - config_entry = await setup_fronius_integration(hass, is_logger=False) + config_entry = await setup_fronius_integration( + hass, is_logger=False, unique_id="12345678" + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( @@ -469,3 +473,133 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_12345678")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.name == "SolarNet" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "12345678")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Gen24" + assert inverter_1.name == "Gen24 Storage" + + meter = device_registry.async_get_device(identifiers={(DOMAIN, "1234567890")}) + assert meter.manufacturer == "Fronius" + assert meter.model == "Smart Meter TS 65A-3" + assert meter.name == "Smart Meter TS 65A-3" + + ohmpilot = device_registry.async_get_device(identifiers={(DOMAIN, "23456789")}) + assert ohmpilot.manufacturer == "Fronius" + assert ohmpilot.model == "Ohmpilot 6" + assert ohmpilot.name == "Ohmpilot" + assert ohmpilot.sw_version == "1.0.25-3" + + storage = device_registry.async_get_device( + identifiers={(DOMAIN, "P030T020Z2001234567 ")} + ) + assert storage.manufacturer == "BYD" + assert storage.model == "BYD Battery-Box Premium HV" + assert storage.name == "BYD Battery-Box Premium HV" + + +async def test_primo_s0(hass, aioclient_mock): + """Test Fronius Primo dual inverter with S0 meter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) + config_entry = await setup_fronius_integration(hass, is_logger=True) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 41 + # logger + assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) + assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + # inverter 1 + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) + assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + # inverter 2 + assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) + assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) + assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) + assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) + assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) + assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) + assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) + assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) + assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) + assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + # meter + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + # power_flow + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN + ) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") + assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 + ) + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.model == "fronius-datamanager-card" + assert solar_net.name == "SolarNet" + assert solar_net.sw_version == "3.18.7-1" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "123456")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Primo 5.0-1" + assert inverter_1.name == "Primo 5.0-1" + + inverter_2 = device_registry.async_get_device(identifiers={(DOMAIN, "234567")}) + assert inverter_2.manufacturer == "Fronius" + assert inverter_2.model == "Primo 3.0-1" + assert inverter_2.name == "Primo 3.0-1" + + meter = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890:S0 Meter at inverter 1")} + ) + assert meter.manufacturer == "Fronius" + assert meter.model == "S0 Meter at inverter 1" + assert meter.name == "S0 Meter at inverter 1" From d038db01ed9f24c14cc8755017901b1a06f1ed0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:35:07 -1000 Subject: [PATCH 1436/1452] Fix lookin set temperature when device is off (#61411) --- homeassistant/components/lookin/climate.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index cab4b0968eb..356b57453bc 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -10,6 +10,7 @@ from aiolookin import Climate, MeteoSensor, SensorID from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -151,6 +152,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._climate.temp_celsius = int(temperature) + lookin_index = LOOKIN_HVAC_MODE_IDX_TO_HASS + if hvac_mode := kwargs.get(ATTR_HVAC_MODE): + self._climate.hvac_mode = HASS_TO_LOOKIN_HVAC_MODE[hvac_mode] + elif self._climate.hvac_mode == lookin_index.index(HVAC_MODE_OFF): + # + # If the device is off, and the user didn't specify an HVAC mode + # (which is the default when using the HA UI), the device won't turn + # on without having an HVAC mode passed. + # + # We picked the hvac mode based on the current temp if its available + # since only some units support auto, but most support either heat + # or cool otherwise we set auto since we don't have a way to make + # an educated guess. + # + meteo_data: MeteoSensor = self._meteo_coordinator.data + current_temp = meteo_data.temperature + if not current_temp: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_AUTO) + elif current_temp >= self._climate.temp_celsius: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_COOL) + else: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_HEAT) await self._async_update_conditioner() async def async_set_fan_mode(self, fan_mode: str) -> None: From 18768ad8a043ddeb8313df6ba816d133bc03a9dc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 10 Dec 2021 00:30:15 -0700 Subject: [PATCH 1437/1452] Bump simplisafe-python to 2021.12.1 (#61412) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 954c39efce1..0b6cb385be6 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==2021.12.0"], + "requirements": ["simplisafe-python==2021.12.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6a7c60023a1..ba4f7167c10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2143,7 +2143,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5092d08477..89ee14dcc1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.slack slackclient==2.5.0 From 81b1b042101e9835212975da46636fcd0387cc8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:29:27 -1000 Subject: [PATCH 1438/1452] Fix flux_led discovery with older models (#61413) --- .../components/flux_led/config_flow.py | 9 +++++- tests/components/flux_led/__init__.py | 18 ++++++++--- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ tests/components/flux_led/test_light.py | 28 ++++++++--------- tests/components/flux_led/test_number.py | 8 ++--- tests/components/flux_led/test_switch.py | 2 +- 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 87d07bba2b1..cefecf216db 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -238,7 +238,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FluxLEDDiscovery: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: host}) - if device := await async_discover_device(self.hass, host): + if (device := await async_discover_device(self.hass, host)) and device[ + ATTR_MODEL_DESCRIPTION + ]: + # Older models do not return enough information + # to build the model description via UDP so we have + # to fallback to making a tcp connection to avoid + # identifying the device as the chip model number + # AKA `HF-LPB100-ZJ200` return device bulb = async_wifi_bulb_for_host(host) try: diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 764c33686b7..6bfe02990a2 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import contextmanager import datetime from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch @@ -162,11 +163,20 @@ def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: raise OSError - return [FLUX_DISCOVERY] + return [] if no_device else [device or FLUX_DISCOVERY] - return patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery - ) + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.AIOBulbScanner.getBulbInfo", + return_value=[] if no_device else [device or FLUX_DISCOVERY], + ): + yield + + return _patcher() def _patch_wifibulb(device=None, no_device=False): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 4c956358818..a546120ae41 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -31,6 +31,7 @@ from . import ( DEFAULT_ENTRY_TITLE, DHCP_DISCOVERY, FLUX_DISCOVERY, + FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, MODULE, @@ -435,6 +436,35 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert mock_async_setup_entry.called +async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): + """Test we can setup when discovered from dhcp but part of the udp response is missing.""" + + with _patch_discovery(no_device=True), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), 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_NAME: DEFAULT_ENTRY_TITLE, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): """Test we can setup when discovered from dhcp but no udp response or tcp response.""" diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6f08ae8a307..92ea0fd8d39 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -80,7 +80,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -100,7 +100,7 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -294,7 +294,7 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -407,7 +407,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x35) # RGB & CCT model bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -526,7 +526,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGBW} bulb.color_mode = FLUX_COLOR_MODE_RGBW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -628,7 +628,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = FLUX_COLOR_MODES_RGB_W bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -739,7 +739,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(warm_white=1, cool_white=2) bulb.color_modes = {FLUX_COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGBWW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -879,7 +879,7 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = {FLUX_COLOR_MODE_DIM} bulb.color_mode = FLUX_COLOR_MODE_DIM - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -930,7 +930,7 @@ async def test_no_color_modes(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = set() bulb.color_mode = None - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -974,7 +974,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1056,7 +1056,7 @@ async def test_rgb_light_custom_effects_invalid_colors( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1085,7 +1085,7 @@ async def test_rgb_light_custom_effect_via_service( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1230,7 +1230,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_ADDRESSABLE} bulb.color_mode = FLUX_COLOR_MODE_ADDRESSABLE - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 11df6daae4a..325307f1f32 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -41,7 +41,7 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -64,7 +64,7 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -129,7 +129,7 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "7 colors change gradually" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "RBM 1" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index 852e1efd49e..b569d51e13a 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -35,7 +35,7 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) switch = _mocked_switch() - with _patch_discovery(device=switch), _patch_wifibulb(device=switch): + with _patch_discovery(), _patch_wifibulb(device=switch): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() From c14269d09df31a351cc1d00662b06811f8011004 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:28:35 -1000 Subject: [PATCH 1439/1452] Fix older v1 dimmable flux_led bulbs not turning on (#61414) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0c5e58027ce..4e38a00a677 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.3"], + "requirements": ["flux_led==0.26.5"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ba4f7167c10..00877c44edf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89ee14dcc1b..614cd2e9801 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 From 519ec18a047981571cf2207cb59204b71d02152c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 09:12:24 +0100 Subject: [PATCH 1440/1452] Correct device class for Tasmota dewpoint sensor (#61420) --- homeassistant/components/tasmota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 45ff93b5945..961a89cfb31 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -81,6 +81,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_DEWPOINT: { + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ICON: "mdi:weather-rainy", STATE_CLASS: SensorStateClass.MEASUREMENT, }, From 4496aeb3276e5fa7f9c60df6d213e7ab8a79afbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 19:09:29 +0100 Subject: [PATCH 1441/1452] Correct recorder.statistics.get_last_statistics (#61421) --- .../components/recorder/statistics.py | 43 +++++++++++++++---- homeassistant/components/sensor/recorder.py | 4 +- tests/components/recorder/test_statistics.py | 33 ++++++++++---- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2b60e6fbf00..02c00722e72 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -834,8 +834,12 @@ def statistics_during_period( return _reduce_statistics_per_month(result) -def get_last_statistics( - hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +def _get_last_statistics( + hass: HomeAssistant, + number_of_stats: int, + statistic_id: str, + convert_units: bool, + table: type[Statistics | StatisticsShortTerm], ) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a given statistic_id.""" statistic_ids = [statistic_id] @@ -845,16 +849,19 @@ def get_last_statistics( if not metadata: return {} - baked_query = hass.data[STATISTICS_SHORT_TERM_BAKERY]( - lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM) - ) + if table == StatisticsShortTerm: + bakery = STATISTICS_SHORT_TERM_BAKERY + base_query = QUERY_STATISTICS_SHORT_TERM + else: + bakery = STATISTICS_BAKERY + base_query = QUERY_STATISTICS + + baked_query = hass.data[bakery](lambda session: session.query(*base_query)) baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) metadata_id = metadata[statistic_id][0] - baked_query += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc() - ) + baked_query += lambda q: q.order_by(table.metadata_id, table.start.desc()) baked_query += lambda q: q.limit(bindparam("number_of_stats")) @@ -874,11 +881,29 @@ def get_last_statistics( statistic_ids, metadata, convert_units, - StatisticsShortTerm, + table, None, ) +def get_last_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, Statistics + ) + + +def get_last_short_term_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats short term statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm + ) + + def _statistics_at_time( session: scoped_session, metadata_ids: set[int], diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 25cb81ded12..3cfe8d45b70 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -517,7 +517,9 @@ def _compile_statistics( # noqa: C901 last_reset = old_last_reset = None new_state = old_state = None _sum = 0.0 - last_stats = statistics.get_last_statistics(hass, 1, entity_id, False) + last_stats = statistics.get_last_short_term_statistics( + hass, 1, entity_id, False + ) 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"] diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d510d6ef612..c4dd33ce840 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -14,6 +14,7 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + get_last_short_term_statistics, get_last_statistics, get_metadata, list_statistic_ids, @@ -40,7 +41,7 @@ def test_compile_hourly_statistics(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -91,20 +92,20 @@ def test_compile_hourly_statistics(hass_recorder): ) assert stats == {} - # Test get_last_statistics - stats = get_last_statistics(hass, 0, "sensor.test1", True) + # Test get_last_short_term_statistics + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} - stats = get_last_statistics(hass, 1, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test1", True) assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} - stats = get_last_statistics(hass, 2, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 2, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 3, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 3, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 1, "sensor.test3", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} @@ -236,7 +237,7 @@ def test_rename_entity(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -392,6 +393,22 @@ def test_external_statistics(hass_recorder, caplog): }, ) } + last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + assert last_stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } # Update the previously inserted statistics external_statistics = { From d20496a1bcb181933beef46d84e43276ebead333 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 18:59:27 +0100 Subject: [PATCH 1442/1452] Correct rest sensor configured to generate timestamps (#61429) --- homeassistant/components/rest/sensor.py | 13 ++++- homeassistant/components/sensor/helpers.py | 38 +++++++++++++ homeassistant/components/template/sensor.py | 34 +---------- tests/components/rest/test_sensor.py | 63 +++++++++++++++++++++ tests/components/sensor/test_helpers.py | 38 +++++++++++++ tests/components/sensor/test_init.py | 2 +- 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/sensor/helpers.py create mode 100644 tests/components/sensor/test_helpers.py diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 9f8c33ad6df..422ce84cc46 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -11,8 +11,10 @@ from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -186,4 +188,13 @@ class RestSensor(RestEntity, SensorEntity): value, None ) - self._state = value + if value is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._state = value + return + + self._state = async_parse_date_datetime( + value, self.entity_id, self.device_class + ) diff --git a/homeassistant/components/sensor/helpers.py b/homeassistant/components/sensor/helpers.py new file mode 100644 index 00000000000..a3f5e3827bf --- /dev/null +++ b/homeassistant/components/sensor/helpers.py @@ -0,0 +1,38 @@ +"""Helpers for sensor entities.""" +from __future__ import annotations + +from datetime import date, datetime +import logging + +from homeassistant.core import callback +from homeassistant.util import dt as dt_util + +from . import SensorDeviceClass + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime string to a data or datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 18ae8af8569..18d0be616d4 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import date, datetime -import logging from typing import Any import voluptuous as vol @@ -17,6 +16,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -35,7 +35,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -89,7 +88,6 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) -_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -184,32 +182,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -@callback -def _async_parse_date_datetime( - value: str, entity_id: str, device_class: SensorDeviceClass | str | None -) -> datetime | date | None: - """Parse datetime.""" - if device_class == SensorDeviceClass.TIMESTAMP: - if (parsed_timestamp := dt_util.parse_datetime(value)) is None: - _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) - return None - - if parsed_timestamp.tzinfo is None: - _LOGGER.warning( - "%s rendered timestamp without timezone: %s", entity_id, value - ) - return None - - return parsed_timestamp - - # Date device class - if (parsed_date := dt_util.parse_date(value)) is not None: - return parsed_date - - _LOGGER.warning("%s rendered invalid date %s", entity_id, value) - return None - - class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -269,7 +241,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._attr_native_value = result return - self._attr_native_value = _async_parse_date_datetime( + self._attr_native_value = async_parse_date_datetime( result, self.entity_id, self.device_class ) @@ -303,6 +275,6 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): ): return - self._rendered[CONF_STATE] = _async_parse_date_datetime( + self._rendered[CONF_STATE] = async_parse_date_datetime( state, self.entity_id, self.device_class ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d37fb047f8f..fb826eefd78 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, DATA_MEGABYTES, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, SERVICE_RELOAD, STATE_UNKNOWN, TEMP_CELSIUS, @@ -218,6 +219,68 @@ async def test_setup_get(hass): assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT +@respx.mock +async def test_setup_timestamp(hass, caplog): + """Test setup with valid configuration.""" + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"} + ) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "device_class": DEVICE_CLASS_TIMESTAMP, + "state_class": sensor.STATE_CLASS_MEASUREMENT, + } + }, + ) + await async_setup_component(hass, "homeassistant", {}) + + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + state = hass.states.get("sensor.rest_sensor") + assert state.state == "2021-11-11T11:39:00+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text + assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text + + # Bad response: Not a timestamp + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "invalid time stamp"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" in caplog.text + + # Bad response: No timezone + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-10-11 11:39"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered timestamp without timezone" in caplog.text + + @respx.mock async def test_setup_get_templated_headers_params(hass): """Test setup with valid configuration.""" diff --git a/tests/components/sensor/test_helpers.py b/tests/components/sensor/test_helpers.py new file mode 100644 index 00000000000..d43443b85ba --- /dev/null +++ b/tests/components/sensor/test_helpers.py @@ -0,0 +1,38 @@ +"""The test for sensor helpers.""" +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor.helpers import async_parse_date_datetime + + +def test_async_parse_datetime(caplog): + """Test async_parse_date_datetime.""" + entity_id = "sensor.timestamp" + device_class = SensorDeviceClass.TIMESTAMP + assert ( + async_parse_date_datetime( + "2021-12-12 12:12Z", entity_id, device_class + ).isoformat() + == "2021-12-12T12:12:00+00:00" + ) + assert not caplog.text + + # No timezone + assert ( + async_parse_date_datetime("2021-12-12 12:12", entity_id, device_class) is None + ) + assert "sensor.timestamp rendered timestamp without timezone" in caplog.text + + # Invalid timestamp + assert async_parse_date_datetime("12 past 12", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid timestamp: 12 past 12" in caplog.text + + device_class = SensorDeviceClass.DATE + caplog.clear() + assert ( + async_parse_date_datetime("2021-12-12", entity_id, device_class).isoformat() + == "2021-12-12" + ) + assert not caplog.text + + # Invalid date + assert async_parse_date_datetime("December 12th", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid date December 12th" in caplog.text diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 67f750ece96..d5deee41679 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,4 +1,4 @@ -"""The test for sensor device automation.""" +"""The test for sensor entity.""" from datetime import date, datetime, timezone import pytest From 23cb75fe200a35c7170d70e5ec5a90a738f63b68 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 10 Dec 2021 16:40:31 +0100 Subject: [PATCH 1443/1452] Interim fix (#61435) --- homeassistant/components/shelly/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 27f25211a96..4109130ab80 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,12 +68,13 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ "binary_sensor", "button", + "climate", "cover", "light", "sensor", "switch", ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) From 46808b1fc1ccd32ad899bb4c57db3879221d63d1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 10 Dec 2021 14:29:46 -0500 Subject: [PATCH 1444/1452] Bump ZHA quirks to 0.0.65 (#61458) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index daeb90be801..960bb55e004 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.64", + "zha-quirks==0.0.65", "zigpy-deconz==0.14.0", "zigpy==0.42.0", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 00877c44edf..5ab89d07119 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ zengge==0.2 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 614cd2e9801..b4b79ca7e80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1476,7 +1476,7 @@ youless-api==0.15 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zha zigpy-deconz==0.14.0 From 7b64eabde109bd49fb68a4a7769d3a362e2ad3b5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 00:11:34 +0100 Subject: [PATCH 1445/1452] Small fix for device triggers and events on Hue integration (#61462) --- .../components/hue/v2/device_trigger.py | 20 +++++++++++++++++-- .../components/hue/test_device_trigger_v2.py | 14 ++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index b33b7540cb8..7a194bef746 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -40,6 +40,19 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) +DEFAULT_BUTTON_EVENT_TYPES = ( + # all except `DOUBLE_SHORT_RELEASE` + ButtonEvent.INITIAL_PRESS, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_RELEASE, +) + +DEVICE_SPECIFIC_EVENT_TYPES = { + # device specific overrides of specific supported button events + "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), +} + async def async_validate_trigger_config( bridge: "HueBridge", @@ -84,10 +97,13 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): hue_dev_id = get_hue_device_id(device_entry) # extract triggers from all button resources of this Hue device triggers = [] + model_id = api.devices[hue_dev_id].product_data.product_name for resource in api.devices.get_sensors(hue_dev_id): if resource.type != ResourceTypes.BUTTON: continue - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN): + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): triggers.append( { CONF_DEVICE_ID: device_entry.id, @@ -95,7 +111,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_PLATFORM: "device", CONF_TYPE: event_type, CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: device_entry.id, + CONF_UNIQUE_ID: resource.id, } ) return triggers diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index bda963552c7..e155b0adb6d 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -70,12 +70,20 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "platform": "device", "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, - "unique_id": hue_wall_switch_device.id, + "unique_id": resource_id, "type": event_type, "subtype": control_id, } - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN) - for control_id in range(1, 3) + for event_type in ( + ButtonEvent.INITIAL_PRESS, + ButtonEvent.LONG_RELEASE, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ) + for control_id, resource_id in ( + (1, "c658d3d8-a013-4b81-8ac6-78b248537e70"), + (2, "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d"), + ) ), ] From 08eabfd056ddcc0dafde164d04689d0e0261a1c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:20:58 -1000 Subject: [PATCH 1446/1452] Fix non-threadsafe call to async_fire in telegram_bot (#61465) Fixes https://github.com/home-assistant/core/issues/53255#issuecomment-888111478 --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7fd83141b7d..c79b8c5a033 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -575,7 +575,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) + self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data) elif not isinstance(out, bool): _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out From dd47f0b6986abb844d416c515c7a976a01117ef4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Dec 2021 20:19:54 -1000 Subject: [PATCH 1447/1452] Fix exception in color_rgb_to_rgbww (#61466) --- homeassistant/util/color.py | 2 +- tests/util/test_color.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 2daccf28915..3d4f7122ad0 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -450,7 +450,7 @@ def color_rgb_to_rgbww( w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) # Find the ratio of the midpoint white in the input rgb channels - white_level = min(r / w_r, g / w_g, b / w_b) + white_level = min(r / w_r, g / w_g, b / w_b if w_b else 0) # Subtract the white portion from the rgb channels. rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index db9ad988aee..d806a941965 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -366,3 +366,38 @@ def test_get_color_in_voluptuous(): schema("not a color") assert schema("red") == (255, 0, 0) + + +def test_color_rgb_to_rgbww(): + """Test color_rgb_to_rgbww conversions.""" + assert color_util.color_rgb_to_rgbww(255, 255, 255, 154, 370) == ( + 0, + 54, + 98, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 100, 1000) == ( + 255, + 255, + 255, + 0, + 0, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 1000) == ( + 0, + 118, + 241, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(128, 128, 128, 154, 370) == ( + 0, + 27, + 49, + 128, + 128, + ) + assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) + assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) From 1f57c8ed1a809ae91fddad8ff7bf9e01b05c689f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:39:32 -1000 Subject: [PATCH 1448/1452] Fix missing color modes for Magic Home Ceiling Light CCT (0xE1) (#61478) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4e38a00a677..191cdef7c38 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.5"], + "requirements": ["flux_led==0.26.7"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5ab89d07119..81b773c9d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4b79ca7e80..d099f500416 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 From f10bfc961da453bb328e9b811eba9fe6e7027675 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Dec 2021 13:36:48 +0100 Subject: [PATCH 1449/1452] Bumped version to 2021.12.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bf14b073cb7..05541f1e497 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2e989bdfcfe80730826b7890da1b7d3135030ecd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 17:12:33 +0100 Subject: [PATCH 1450/1452] Fix typo in Hue device triggers - use enum value (#61498) --- homeassistant/components/hue/v2/device_trigger.py | 2 +- tests/components/hue/test_device_trigger_v2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 7a194bef746..74863a1897e 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -109,7 +109,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_DEVICE_ID: device_entry.id, CONF_DOMAIN: DOMAIN, CONF_PLATFORM: "device", - CONF_TYPE: event_type, + CONF_TYPE: event_type.value, CONF_SUBTYPE: resource.metadata.control_id, CONF_UNIQUE_ID: resource.id, } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e155b0adb6d..0641281b9fa 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -71,7 +71,7 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, "unique_id": resource_id, - "type": event_type, + "type": event_type.value, "subtype": control_id, } for event_type in ( From 608ce2d5a0de07b3d30ec17d79c13f3eccb5f376 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 11 Dec 2021 18:11:42 +0100 Subject: [PATCH 1451/1452] Update frontend to 20211211.0 (#61499) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c9739cd0302..994ac596527 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20211209.0" - ], + "requirements": ["home-assistant-frontend==20211211.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 77275b0e6d8..18c7c1befda 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 81b773c9d91..42e3237f874 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d099f500416..9c4def0e712 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1042f23a0af68a526b0c7cb5dda0d0e203472633 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Dec 2021 18:15:19 +0100 Subject: [PATCH 1452/1452] Bumped version to 2021.12.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 05541f1e497..f0808c28aaf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)